You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

203 lines
5.1 KiB

5 years ago
5 years ago
  1. const _ = require('lodash')
  2. const algoliasearch = require('algoliasearch')
  3. const stream = require('stream')
  4. const Promise = require('bluebird')
  5. const pipeline = Promise.promisify(stream.pipeline)
  6. /* global WIKI */
  7. module.exports = {
  8. async activate() {
  9. // not used
  10. },
  11. async deactivate() {
  12. // not used
  13. },
  14. /**
  15. * INIT
  16. */
  17. async init() {
  18. WIKI.logger.info(`(SEARCH/ALGOLIA) Initializing...`)
  19. this.client = algoliasearch(this.config.appId, this.config.apiKey)
  20. this.index = this.client.initIndex(this.config.indexName)
  21. // -> Create Search Index
  22. WIKI.logger.info(`(SEARCH/ALGOLIA) Setting index configuration...`)
  23. await this.index.setSettings({
  24. searchableAttributes: [
  25. 'title',
  26. 'description',
  27. 'content'
  28. ],
  29. attributesToRetrieve: [
  30. 'locale',
  31. 'path',
  32. 'title',
  33. 'description'
  34. ],
  35. advancedSyntax: true
  36. })
  37. WIKI.logger.info(`(SEARCH/ALGOLIA) Initialization completed.`)
  38. },
  39. /**
  40. * QUERY
  41. *
  42. * @param {String} q Query
  43. * @param {Object} opts Additional options
  44. */
  45. async query(q, opts) {
  46. try {
  47. const results = await this.index.search(q, {
  48. hitsPerPage: 50
  49. })
  50. return {
  51. results: _.map(results.hits, r => ({
  52. id: r.objectID,
  53. locale: r.locale,
  54. path: r.path,
  55. title: r.title,
  56. description: r.description
  57. })),
  58. suggestions: [],
  59. totalHits: results.nbHits
  60. }
  61. } catch (err) {
  62. WIKI.logger.warn('Search Engine Error:')
  63. WIKI.logger.warn(err)
  64. }
  65. },
  66. /**
  67. * CREATE
  68. *
  69. * @param {Object} page Page to create
  70. */
  71. async created(page) {
  72. await this.index.saveObject({
  73. objectID: page.hash,
  74. locale: page.localeCode,
  75. path: page.path,
  76. title: page.title,
  77. description: page.description,
  78. content: page.safeContent
  79. })
  80. },
  81. /**
  82. * UPDATE
  83. *
  84. * @param {Object} page Page to update
  85. */
  86. async updated(page) {
  87. await this.index.partialUpdateObject({
  88. objectID: page.hash,
  89. title: page.title,
  90. description: page.description,
  91. content: page.safeContent
  92. })
  93. },
  94. /**
  95. * DELETE
  96. *
  97. * @param {Object} page Page to delete
  98. */
  99. async deleted(page) {
  100. await this.index.deleteObject(page.hash)
  101. },
  102. /**
  103. * RENAME
  104. *
  105. * @param {Object} page Page to rename
  106. */
  107. async renamed(page) {
  108. await this.index.deleteObject(page.hash)
  109. await this.index.saveObject({
  110. objectID: page.destinationHash,
  111. locale: page.destinationLocaleCode,
  112. path: page.destinationPath,
  113. title: page.title,
  114. description: page.description,
  115. content: page.safeContent
  116. })
  117. },
  118. /**
  119. * REBUILD INDEX
  120. */
  121. async rebuild() {
  122. WIKI.logger.info(`(SEARCH/ALGOLIA) Rebuilding Index...`)
  123. await this.index.clearObjects()
  124. const MAX_DOCUMENT_BYTES = 10 * Math.pow(2, 10) // 10 KB
  125. const MAX_INDEXING_BYTES = 10 * Math.pow(2, 20) - Buffer.from('[').byteLength - Buffer.from(']').byteLength // 10 MB
  126. const MAX_INDEXING_COUNT = 1000
  127. const COMMA_BYTES = Buffer.from(',').byteLength
  128. let chunks = []
  129. let bytes = 0
  130. const processDocument = async (cb, doc) => {
  131. try {
  132. if (doc) {
  133. const docBytes = Buffer.from(JSON.stringify(doc)).byteLength
  134. // -> Document too large
  135. if (docBytes >= MAX_DOCUMENT_BYTES) {
  136. throw new Error('Document exceeds maximum size allowed by Algolia.')
  137. }
  138. // -> Current batch exceeds size hard limit, flush
  139. if (docBytes + COMMA_BYTES + bytes >= MAX_INDEXING_BYTES) {
  140. await flushBuffer()
  141. }
  142. if (chunks.length > 0) {
  143. bytes += COMMA_BYTES
  144. }
  145. bytes += docBytes
  146. chunks.push(doc)
  147. // -> Current batch exceeds count soft limit, flush
  148. if (chunks.length >= MAX_INDEXING_COUNT) {
  149. await flushBuffer()
  150. }
  151. } else {
  152. // -> End of stream, flush
  153. await flushBuffer()
  154. }
  155. cb()
  156. } catch (err) {
  157. cb(err)
  158. }
  159. }
  160. const flushBuffer = async () => {
  161. WIKI.logger.info(`(SEARCH/ALGOLIA) Sending batch of ${chunks.length}...`)
  162. try {
  163. await this.index.saveObjects(
  164. _.map(chunks, doc => ({
  165. objectID: doc.id,
  166. locale: doc.locale,
  167. path: doc.path,
  168. title: doc.title,
  169. description: doc.description,
  170. content: WIKI.models.pages.cleanHTML(doc.render)
  171. }))
  172. )
  173. } catch (err) {
  174. WIKI.logger.warn('(SEARCH/ALGOLIA) Failed to send batch to Algolia: ', err)
  175. }
  176. chunks.length = 0
  177. bytes = 0
  178. }
  179. await pipeline(
  180. WIKI.models.knex.column({ id: 'hash' }, 'path', { locale: 'localeCode' }, 'title', 'description', 'render').select().from('pages').where({
  181. isPublished: true,
  182. isPrivate: false
  183. }).stream(),
  184. new stream.Transform({
  185. objectMode: true,
  186. transform: async (chunk, enc, cb) => processDocument(cb, chunk),
  187. flush: async (cb) => processDocument(cb)
  188. })
  189. )
  190. WIKI.logger.info(`(SEARCH/ALGOLIA) Index rebuilt successfully.`)
  191. }
  192. }