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.

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