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.

204 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('[SERVER.Search] 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('[SERVER.Search] 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. return self._isReady.then(() => {
  48. return self.delete(content._id).then(() => {
  49. return self._si.concurrentAddAsync({
  50. fieldOptions: [{
  51. fieldName: 'entryPath',
  52. searchable: true,
  53. weight: 2
  54. },
  55. {
  56. fieldName: 'title',
  57. nGramLength: [1, 2],
  58. searchable: true,
  59. weight: 3
  60. },
  61. {
  62. fieldName: 'subtitle',
  63. searchable: true,
  64. weight: 1,
  65. storeable: false
  66. },
  67. {
  68. fieldName: 'parent',
  69. searchable: false
  70. },
  71. {
  72. fieldName: 'content',
  73. searchable: true,
  74. weight: 0,
  75. storeable: false
  76. }]
  77. }, [{
  78. entryPath: content._id,
  79. title: content.title,
  80. subtitle: content.subtitle || '',
  81. parent: content.parent || '',
  82. content: content.content || ''
  83. }]).then(() => {
  84. winston.log('verbose', '[SERVER.Search] Entry ' + content._id + ' added/updated to index.')
  85. return true
  86. }).catch((err) => {
  87. winston.error(err)
  88. })
  89. }).catch((err) => {
  90. winston.error(err)
  91. })
  92. })
  93. },
  94. /**
  95. * Delete an entry from the index
  96. *
  97. * @param {String} The entry path
  98. * @return {Promise} Promise of the operation
  99. */
  100. delete (entryPath) {
  101. let self = this
  102. return self._isReady.then(() => {
  103. return streamToPromise(self._si.search({
  104. query: [{
  105. AND: { 'entryPath': [entryPath] }
  106. }]
  107. })).then((results) => {
  108. if (results && results.length > 0) {
  109. let delIds = _.map(results, 'id')
  110. return self._si.delAsync(delIds)
  111. } else {
  112. return true
  113. }
  114. }).catch((err) => {
  115. if (err.type === 'NotFoundError') {
  116. return true
  117. } else {
  118. winston.error(err)
  119. }
  120. })
  121. })
  122. },
  123. /**
  124. * Flush the index
  125. *
  126. * @returns {Promise} Promise of the flush operation
  127. */
  128. flush () {
  129. let self = this
  130. return self._isReady.then(() => {
  131. return self._si.flushAsync()
  132. })
  133. },
  134. /**
  135. * Search the index
  136. *
  137. * @param {Array<String>} terms
  138. * @returns {Promise<Object>} Hits and suggestions
  139. */
  140. find (terms) {
  141. let self = this
  142. terms = _.chain(terms)
  143. .deburr()
  144. .toLower()
  145. .trim()
  146. .replace(/[^a-z0-9 ]/g, '')
  147. .value()
  148. let arrTerms = _.chain(terms)
  149. .split(' ')
  150. .filter((f) => { return !_.isEmpty(f) })
  151. .value()
  152. return streamToPromise(self._si.search({
  153. query: [{
  154. AND: { '*': arrTerms }
  155. }],
  156. pageSize: 10
  157. })).then((hits) => {
  158. if (hits.length > 0) {
  159. hits = _.map(_.sortBy(hits, ['score']), h => {
  160. return h.document
  161. })
  162. }
  163. if (hits.length < 5) {
  164. return streamToPromise(self._si.match({
  165. beginsWith: terms,
  166. threshold: 3,
  167. limit: 5,
  168. type: 'simple'
  169. })).then((matches) => {
  170. return {
  171. match: hits,
  172. suggest: matches
  173. }
  174. })
  175. } else {
  176. return {
  177. match: hits,
  178. suggest: []
  179. }
  180. }
  181. }).catch((err) => {
  182. if (err.type === 'NotFoundError') {
  183. return {
  184. match: [],
  185. suggest: []
  186. }
  187. } else {
  188. winston.error(err)
  189. }
  190. })
  191. }
  192. }