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.

204 lines
5.1 KiB

  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({
  48. query: q,
  49. hitsPerPage: 50
  50. })
  51. return {
  52. results: _.map(results.hits, r => ({
  53. id: r.objectID,
  54. locale: r.locale,
  55. path: r.path,
  56. title: r.title,
  57. description: r.description
  58. })),
  59. suggestions: [],
  60. totalHits: results.nbHits
  61. }
  62. } catch (err) {
  63. WIKI.logger.warn('Search Engine Error:')
  64. WIKI.logger.warn(err)
  65. }
  66. },
  67. /**
  68. * CREATE
  69. *
  70. * @param {Object} page Page to create
  71. */
  72. async created(page) {
  73. await this.index.addObject({
  74. objectID: page.hash,
  75. locale: page.localeCode,
  76. path: page.path,
  77. title: page.title,
  78. description: page.description,
  79. content: page.safeContent
  80. })
  81. },
  82. /**
  83. * UPDATE
  84. *
  85. * @param {Object} page Page to update
  86. */
  87. async updated(page) {
  88. await this.index.partialUpdateObject({
  89. objectID: page.hash,
  90. title: page.title,
  91. description: page.description,
  92. content: page.safeContent
  93. })
  94. },
  95. /**
  96. * DELETE
  97. *
  98. * @param {Object} page Page to delete
  99. */
  100. async deleted(page) {
  101. await this.index.deleteObject(page.hash)
  102. },
  103. /**
  104. * RENAME
  105. *
  106. * @param {Object} page Page to rename
  107. */
  108. async renamed(page) {
  109. await this.index.deleteObject(page.sourceHash)
  110. await this.index.addObject({
  111. objectID: page.destinationHash,
  112. locale: page.localeCode,
  113. path: page.destinationPath,
  114. title: page.title,
  115. description: page.description,
  116. content: page.safeContent
  117. })
  118. },
  119. /**
  120. * REBUILD INDEX
  121. */
  122. async rebuild() {
  123. WIKI.logger.info(`(SEARCH/ALGOLIA) Rebuilding Index...`)
  124. await this.index.clearIndex()
  125. const MAX_DOCUMENT_BYTES = 10 * Math.pow(2, 10) // 10 KB
  126. const MAX_INDEXING_BYTES = 10 * Math.pow(2, 20) - Buffer.from('[').byteLength - Buffer.from(']').byteLength // 10 MB
  127. const MAX_INDEXING_COUNT = 1000
  128. const COMMA_BYTES = Buffer.from(',').byteLength
  129. let chunks = []
  130. let bytes = 0
  131. const processDocument = async (cb, doc) => {
  132. try {
  133. if (doc) {
  134. const docBytes = Buffer.from(JSON.stringify(doc)).byteLength
  135. // -> Document too large
  136. if (docBytes >= MAX_DOCUMENT_BYTES) {
  137. throw new Error('Document exceeds maximum size allowed by Algolia.')
  138. }
  139. // -> Current batch exceeds size hard limit, flush
  140. if (docBytes + COMMA_BYTES + bytes >= MAX_INDEXING_BYTES) {
  141. await flushBuffer()
  142. }
  143. if (chunks.length > 0) {
  144. bytes += COMMA_BYTES
  145. }
  146. bytes += docBytes
  147. chunks.push(doc)
  148. // -> Current batch exceeds count soft limit, flush
  149. if (chunks.length >= MAX_INDEXING_COUNT) {
  150. await flushBuffer()
  151. }
  152. } else {
  153. // -> End of stream, flush
  154. await flushBuffer()
  155. }
  156. cb()
  157. } catch (err) {
  158. cb(err)
  159. }
  160. }
  161. const flushBuffer = async () => {
  162. WIKI.logger.info(`(SEARCH/ALGOLIA) Sending batch of ${chunks.length}...`)
  163. try {
  164. await this.index.addObjects(
  165. _.map(chunks, doc => ({
  166. objectID: doc.id,
  167. locale: doc.locale,
  168. path: doc.path,
  169. title: doc.title,
  170. description: doc.description,
  171. content: WIKI.models.pages.cleanHTML(doc.render)
  172. }))
  173. )
  174. } catch (err) {
  175. WIKI.logger.warn('(SEARCH/ALGOLIA) Failed to send batch to Algolia: ', err)
  176. }
  177. chunks.length = 0
  178. bytes = 0
  179. }
  180. await pipeline(
  181. WIKI.models.knex.column({ id: 'hash' }, 'path', { locale: 'localeCode' }, 'title', 'description', 'render').select().from('pages').where({
  182. isPublished: true,
  183. isPrivate: false
  184. }).stream(),
  185. new stream.Transform({
  186. objectMode: true,
  187. transform: async (chunk, enc, cb) => processDocument(cb, chunk),
  188. flush: async (cb) => processDocument(cb)
  189. })
  190. )
  191. WIKI.logger.info(`(SEARCH/ALGOLIA) Index rebuilt successfully.`)
  192. }
  193. }