| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 | 'use strict'/* global winston */const Promise = require('bluebird')const _ = require('lodash')const searchIndex = require('./search-index')const stopWord = require('stopword')const streamToPromise = require('stream-to-promise')const searchAllowedChars = new RegExp('[^a-z0-9' + appdata.regex.cjk + appdata.regex.arabic + ' ]', 'g')module.exports = {  _si: null,  _isReady: false,  /**   * Initialize search index   *   * @return {undefined} Void   */  init () {    let self = this    self._isReady = new Promise((resolve, reject) => {      searchIndex({        deletable: true,        fieldedSearch: true,        indexPath: 'wiki',        logLevel: 'error',        stopwords: _.get(stopWord, appconfig.lang, [])      }, (err, si) => {        if (err) {          winston.error('Failed to initialize search index.', err)          reject(err)        } else {          self._si = Promise.promisifyAll(si)          self._si.flushAsync().then(() => {            winston.info('Search index flushed and ready.')            resolve(true)          })        }      })    })    return self  },  /**   * Add a document to the index   *   * @param      {Object}   content  Document content   * @return     {Promise}  Promise of the add operation   */  add (content) {    let self = this    if (!content.isEntry) {      return Promise.resolve(true)    }    return self._isReady.then(() => {      return self.delete(content._id).then(() => {        return self._si.concurrentAddAsync({          fieldOptions: [{            fieldName: 'entryPath',            searchable: true,            weight: 2          },          {            fieldName: 'title',            nGramLength: [1, 2],            searchable: true,            weight: 3          },          {            fieldName: 'subtitle',            searchable: true,            weight: 1,            storeable: false          },          {            fieldName: 'parent',            searchable: false          },          {            fieldName: 'content',            searchable: true,            weight: 0,            storeable: false          }]        }, [{          entryPath: content._id,          title: content.title,          subtitle: content.subtitle || '',          parent: content.parent || '',          content: content.text || ''        }]).then(() => {          winston.log('verbose', 'Entry ' + content._id + ' added/updated to search index.')          return true        }).catch((err) => {          winston.error(err)        })      }).catch((err) => {        winston.error(err)      })    })  },  /**   * Delete an entry from the index   *   * @param      {String}   The     entry path   * @return     {Promise}  Promise of the operation   */  delete (entryPath) {    let self = this    return self._isReady.then(() => {      return streamToPromise(self._si.search({        query: [{          AND: { 'entryPath': [entryPath] }        }]      })).then((results) => {        if (results && results.length > 0) {          let delIds = _.map(results, 'id')          return self._si.delAsync(delIds)        } else {          return true        }      }).catch((err) => {        if (err.type === 'NotFoundError') {          return true        } else {          winston.error(err)        }      })    })  },  /**   * Flush the index   *   * @returns {Promise} Promise of the flush operation   */  flush () {    let self = this    return self._isReady.then(() => {      return self._si.flushAsync()    })  },  /**   * Search the index   *   * @param {Array<String>} terms   * @returns {Promise<Object>} Hits and suggestions   */  find (terms) {    let self = this    terms = _.chain(terms)      .deburr()      .toLower()      .trim()      .replace(searchAllowedChars, ' ')      .value()    let arrTerms = _.chain(terms)      .split(' ')      .filter((f) => { return !_.isEmpty(f) })      .value()    return streamToPromise(self._si.search({      query: [{        AND: { '*': arrTerms }      }],      pageSize: 10    })).then((hits) => {      if (hits.length > 0) {        hits = _.map(_.sortBy(hits, ['score']), h => {          return h.document        })      }      if (hits.length < 5) {        return streamToPromise(self._si.match({          beginsWith: terms,          threshold: 3,          limit: 5,          type: 'simple'        })).then((matches) => {          return {            match: hits,            suggest: matches          }        })      } else {        return {          match: hits,          suggest: []        }      }    }).catch((err) => {      if (err.type === 'NotFoundError') {        return {          match: [],          suggest: []        }      } else {        winston.error(err)      }    })  }}
 |