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.

202 lines
5.0 KiB

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