engine.js 5.15 KB
Newer Older
Nick's avatar
Nick committed
1 2
const _ = require('lodash')
const algoliasearch = require('algoliasearch')
3 4 5
const stream = require('stream')
const Promise = require('bluebird')
const pipeline = Promise.promisify(stream.pipeline)
6

Nick's avatar
Nick committed
7
/* global WIKI */
8

Nick's avatar
Nick committed
9 10 11
module.exports = {
  async activate() {
    // not used
12
  },
Nick's avatar
Nick committed
13 14
  async deactivate() {
    // not used
15
  },
Nick's avatar
Nick committed
16 17 18 19 20 21 22
  /**
   * INIT
   */
  async init() {
    WIKI.logger.info(`(SEARCH/ALGOLIA) Initializing...`)
    this.client = algoliasearch(this.config.appId, this.config.apiKey)
    this.index = this.client.initIndex(this.config.indexName)
23

Nick's avatar
Nick committed
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
    // -> Create Search Index
    WIKI.logger.info(`(SEARCH/ALGOLIA) Setting index configuration...`)
    await this.index.setSettings({
      searchableAttributes: [
        'title',
        'description',
        'content'
      ],
      attributesToRetrieve: [
        'locale',
        'path',
        'title',
        'description'
      ],
      advancedSyntax: true
    })
    WIKI.logger.info(`(SEARCH/ALGOLIA) Initialization completed.`)
41
  },
Nick's avatar
Nick committed
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
  /**
   * QUERY
   *
   * @param {String} q Query
   * @param {Object} opts Additional options
   */
  async query(q, opts) {
    try {
      const results = await this.index.search({
        query: q,
        hitsPerPage: 50
      })
      return {
        results: _.map(results.hits, r => ({
          id: r.objectID,
          locale: r.locale,
          path: r.path,
          title: r.title,
          description: r.description
        })),
        suggestions: [],
        totalHits: results.nbHits
      }
    } catch (err) {
      WIKI.logger.warn('Search Engine Error:')
      WIKI.logger.warn(err)
    }
69
  },
Nick's avatar
Nick committed
70 71 72 73 74 75 76 77 78 79 80 81
  /**
   * CREATE
   *
   * @param {Object} page Page to create
   */
  async created(page) {
    await this.index.addObject({
      objectID: page.hash,
      locale: page.localeCode,
      path: page.path,
      title: page.title,
      description: page.description,
82
      content: page.safeContent
Nick's avatar
Nick committed
83
    })
84
  },
Nick's avatar
Nick committed
85 86 87 88 89 90 91 92 93 94
  /**
   * UPDATE
   *
   * @param {Object} page Page to update
   */
  async updated(page) {
    await this.index.partialUpdateObject({
      objectID: page.hash,
      title: page.title,
      description: page.description,
95
      content: page.safeContent
Nick's avatar
Nick committed
96 97 98 99 100 101 102 103 104
    })
  },
  /**
   * DELETE
   *
   * @param {Object} page Page to delete
   */
  async deleted(page) {
    await this.index.deleteObject(page.hash)
105
  },
Nick's avatar
Nick committed
106 107 108 109 110 111 112 113 114 115 116 117 118
  /**
   * RENAME
   *
   * @param {Object} page Page to rename
   */
  async renamed(page) {
    await this.index.deleteObject(page.sourceHash)
    await this.index.addObject({
      objectID: page.destinationHash,
      locale: page.localeCode,
      path: page.destinationPath,
      title: page.title,
      description: page.description,
119
      content: page.safeContent
Nick's avatar
Nick committed
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
    })
  },
  /**
   * REBUILD INDEX
   */
  async rebuild() {
    WIKI.logger.info(`(SEARCH/ALGOLIA) Rebuilding Index...`)
    await this.index.clearIndex()

    const MAX_DOCUMENT_BYTES = 10 * Math.pow(2, 10) // 10 KB
    const MAX_INDEXING_BYTES = 10 * Math.pow(2, 20) - Buffer.from('[').byteLength - Buffer.from(']').byteLength // 10 MB
    const MAX_INDEXING_COUNT = 1000
    const COMMA_BYTES = Buffer.from(',').byteLength

    let chunks = []
    let bytes = 0

    const processDocument = async (cb, doc) => {
      try {
        if (doc) {
          const docBytes = Buffer.from(JSON.stringify(doc)).byteLength
          // -> Document too large
          if (docBytes >= MAX_DOCUMENT_BYTES) {
            throw new Error('Document exceeds maximum size allowed by Algolia.')
          }

          // -> Current batch exceeds size hard limit, flush
          if (docBytes + COMMA_BYTES + bytes >= MAX_INDEXING_BYTES) {
            await flushBuffer()
          }

          if (chunks.length > 0) {
            bytes += COMMA_BYTES
          }
          bytes += docBytes
          chunks.push(doc)

          // -> Current batch exceeds count soft limit, flush
          if (chunks.length >= MAX_INDEXING_COUNT) {
            await flushBuffer()
          }
        } else {
          // -> End of stream, flush
          await flushBuffer()
        }
        cb()
      } catch (err) {
        cb(err)
      }
    }

    const flushBuffer = async () => {
      WIKI.logger.info(`(SEARCH/ALGOLIA) Sending batch of ${chunks.length}...`)
      try {
        await this.index.addObjects(
          _.map(chunks, doc => ({
            objectID: doc.id,
            locale: doc.locale,
            path: doc.path,
            title: doc.title,
            description: doc.description,
181
            content: WIKI.models.pages.cleanHTML(doc.render)
Nick's avatar
Nick committed
182 183 184 185 186 187 188 189
          }))
        )
      } catch (err) {
        WIKI.logger.warn('(SEARCH/ALGOLIA) Failed to send batch to Algolia: ', err)
      }
      chunks.length = 0
      bytes = 0
    }
190

Nick's avatar
Nick committed
191
    await pipeline(
192
      WIKI.models.knex.column({ id: 'hash' }, 'path', { locale: 'localeCode' }, 'title', 'description', 'render').select().from('pages').where({
Nick's avatar
Nick committed
193 194 195
        isPublished: true,
        isPrivate: false
      }).stream(),
196
      new stream.Transform({
Nick's avatar
Nick committed
197 198 199 200 201 202
        objectMode: true,
        transform: async (chunk, enc, cb) => processDocument(cb, chunk),
        flush: async (cb) => processDocument(cb)
      })
    )
    WIKI.logger.info(`(SEARCH/ALGOLIA) Index rebuilt successfully.`)
203 204
  }
}