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.

211 lines
4.8 KiB

  1. 'use strict'
  2. /* global winston */
  3. const Promise = require('bluebird')
  4. const _ = require('lodash')
  5. const searchIndex = require('./search-index')
  6. const stopWord = require('stopword')
  7. const streamToPromise = require('stream-to-promise')
  8. const searchAllowedChars = new RegExp('[^a-z0-9' + appdata.regex.cjk + appdata.regex.arabic + ' ]', 'g')
  9. module.exports = {
  10. _si: null,
  11. _isReady: false,
  12. /**
  13. * Initialize search index
  14. *
  15. * @return {undefined} Void
  16. */
  17. init () {
  18. let self = this
  19. self._isReady = new Promise((resolve, reject) => {
  20. searchIndex({
  21. deletable: true,
  22. fieldedSearch: true,
  23. indexPath: 'wiki',
  24. logLevel: 'error',
  25. stopwords: _.get(stopWord, appconfig.lang, [])
  26. }, (err, si) => {
  27. if (err) {
  28. winston.error('Failed to initialize search index.', err)
  29. reject(err)
  30. } else {
  31. self._si = Promise.promisifyAll(si)
  32. self._si.flushAsync().then(() => {
  33. winston.info('Search index flushed and ready.')
  34. resolve(true)
  35. })
  36. }
  37. })
  38. })
  39. return self
  40. },
  41. /**
  42. * Add a document to the index
  43. *
  44. * @param {Object} content Document content
  45. * @return {Promise} Promise of the add operation
  46. */
  47. add (content) {
  48. let self = this
  49. if (!content.isEntry) {
  50. return Promise.resolve(true)
  51. }
  52. return self._isReady.then(() => {
  53. return self.delete(content._id).then(() => {
  54. return self._si.concurrentAddAsync({
  55. fieldOptions: [{
  56. fieldName: 'entryPath',
  57. searchable: true,
  58. weight: 2
  59. },
  60. {
  61. fieldName: 'title',
  62. nGramLength: [1, 2],
  63. searchable: true,
  64. weight: 3
  65. },
  66. {
  67. fieldName: 'subtitle',
  68. searchable: true,
  69. weight: 1,
  70. storeable: false
  71. },
  72. {
  73. fieldName: 'parent',
  74. searchable: false
  75. },
  76. {
  77. fieldName: 'content',
  78. searchable: true,
  79. weight: 0,
  80. storeable: false
  81. }]
  82. }, [{
  83. entryPath: content._id,
  84. title: content.title,
  85. subtitle: content.subtitle || '',
  86. parent: content.parent || '',
  87. content: content.text || ''
  88. }]).then(() => {
  89. winston.log('verbose', 'Entry ' + content._id + ' added/updated to search index.')
  90. return true
  91. }).catch((err) => {
  92. winston.error(err)
  93. })
  94. }).catch((err) => {
  95. winston.error(err)
  96. })
  97. })
  98. },
  99. /**
  100. * Delete an entry from the index
  101. *
  102. * @param {String} The entry path
  103. * @return {Promise} Promise of the operation
  104. */
  105. delete (entryPath) {
  106. let self = this
  107. return self._isReady.then(() => {
  108. return streamToPromise(self._si.search({
  109. query: [{
  110. AND: { 'entryPath': [entryPath] }
  111. }]
  112. })).then((results) => {
  113. if (results && results.length > 0) {
  114. let delIds = _.map(results, 'id')
  115. return self._si.delAsync(delIds)
  116. } else {
  117. return true
  118. }
  119. }).catch((err) => {
  120. if (err.type === 'NotFoundError') {
  121. return true
  122. } else {
  123. winston.error(err)
  124. }
  125. })
  126. })
  127. },
  128. /**
  129. * Flush the index
  130. *
  131. * @returns {Promise} Promise of the flush operation
  132. */
  133. flush () {
  134. let self = this
  135. return self._isReady.then(() => {
  136. return self._si.flushAsync()
  137. })
  138. },
  139. /**
  140. * Search the index
  141. *
  142. * @param {Array<String>} terms
  143. * @returns {Promise<Object>} Hits and suggestions
  144. */
  145. find (terms) {
  146. let self = this
  147. terms = _.chain(terms)
  148. .deburr()
  149. .toLower()
  150. .trim()
  151. .replace(searchAllowedChars, ' ')
  152. .value()
  153. let arrTerms = _.chain(terms)
  154. .split(' ')
  155. .filter((f) => { return !_.isEmpty(f) })
  156. .value()
  157. return streamToPromise(self._si.search({
  158. query: [{
  159. AND: { '*': arrTerms }
  160. }],
  161. pageSize: 10
  162. })).then((hits) => {
  163. if (hits.length > 0) {
  164. hits = _.map(_.sortBy(hits, ['score']), h => {
  165. return h.document
  166. })
  167. }
  168. if (hits.length < 5) {
  169. return streamToPromise(self._si.match({
  170. beginsWith: terms,
  171. threshold: 3,
  172. limit: 5,
  173. type: 'simple'
  174. })).then((matches) => {
  175. return {
  176. match: hits,
  177. suggest: matches
  178. }
  179. })
  180. } else {
  181. return {
  182. match: hits,
  183. suggest: []
  184. }
  185. }
  186. }).catch((err) => {
  187. if (err.type === 'NotFoundError') {
  188. return {
  189. match: [],
  190. suggest: []
  191. }
  192. } else {
  193. winston.error(err)
  194. }
  195. })
  196. }
  197. }