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.

208 lines
4.7 KiB

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