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.

197 lines
3.4 KiB

  1. "use strict";
  2. var Promise = require('bluebird'),
  3. _ = require('lodash'),
  4. path = require('path'),
  5. searchIndex = require('search-index'),
  6. stopWord = require('stopword');
  7. /**
  8. * Search Model
  9. */
  10. module.exports = {
  11. _si: null,
  12. /**
  13. * Initialize Search model
  14. *
  15. * @param {Object} appconfig The application config
  16. * @return {Object} Search model instance
  17. */
  18. init(appconfig) {
  19. let self = this;
  20. let dbPath = path.resolve(ROOTPATH, appconfig.datadir.db, 'search');
  21. searchIndex({
  22. deletable: true,
  23. fieldedSearch: true,
  24. indexPath: dbPath,
  25. logLevel: 'error',
  26. stopwords: stopWord.getStopwords(appconfig.lang).sort()
  27. }, (err, si) => {
  28. if(err) {
  29. winston.error('Failed to initialize search-index.', err);
  30. } else {
  31. self._si = Promise.promisifyAll(si);
  32. }
  33. });
  34. return self;
  35. },
  36. find(terms) {
  37. let self = this;
  38. terms = _.chain(terms)
  39. .deburr()
  40. .toLower()
  41. .trim()
  42. .replace(/[^a-z0-9 ]/g, '')
  43. .value();
  44. let arrTerms = _.chain(terms)
  45. .split(' ')
  46. .filter((f) => { return !_.isEmpty(f); })
  47. .value();
  48. return self._si.searchAsync({
  49. query: {
  50. AND: [{ '*': arrTerms }]
  51. },
  52. pageSize: 10
  53. }).get('hits').then((hits) => {
  54. if(hits.length < 5) {
  55. return self._si.matchAsync({
  56. beginsWith: terms,
  57. threshold: 3,
  58. limit: 5,
  59. type: 'simple'
  60. }).then((matches) => {
  61. return {
  62. match: hits,
  63. suggest: matches
  64. };
  65. });
  66. } else {
  67. return {
  68. match: hits,
  69. suggest: []
  70. };
  71. }
  72. }).catch((err) => {
  73. if(err.type === 'NotFoundError') {
  74. return {
  75. match: [],
  76. suggest: []
  77. };
  78. } else {
  79. winston.error(err);
  80. }
  81. });
  82. },
  83. /**
  84. * Add a document to the index
  85. *
  86. * @param {Object} content Document content
  87. * @return {Promise} Promise of the add operation
  88. */
  89. add(content) {
  90. let self = this;
  91. return self.delete(content.entryPath).then(() => {
  92. return self._si.addAsync({
  93. entryPath: content.entryPath,
  94. title: content.meta.title,
  95. subtitle: content.meta.subtitle || '',
  96. parent: content.parent.title || '',
  97. content: content.text || ''
  98. }, {
  99. fieldOptions: [{
  100. fieldName: 'entryPath',
  101. searchable: true,
  102. weight: 2
  103. },
  104. {
  105. fieldName: 'title',
  106. nGramLength: [1, 2],
  107. searchable: true,
  108. weight: 3
  109. },
  110. {
  111. fieldName: 'subtitle',
  112. searchable: true,
  113. weight: 1,
  114. store: false
  115. },
  116. {
  117. fieldName: 'parent',
  118. searchable: false,
  119. },
  120. {
  121. fieldName: 'content',
  122. searchable: true,
  123. weight: 0,
  124. store: false
  125. }]
  126. }).then(() => {
  127. winston.info('Entry ' + content.entryPath + ' added/updated to index.');
  128. return true;
  129. }).catch((err) => {
  130. winston.error(err);
  131. });
  132. }).catch((err) => {
  133. winston.error(err);
  134. });
  135. },
  136. /**
  137. * Delete an entry from the index
  138. *
  139. * @param {String} The entry path
  140. * @return {Promise} Promise of the operation
  141. */
  142. delete(entryPath) {
  143. let self = this;
  144. return self._si.searchAsync({
  145. query: {
  146. AND: [{ 'entryPath': [entryPath] }]
  147. }
  148. }).then((results) => {
  149. if(results.totalHits > 0) {
  150. let delIds = _.map(results.hits, 'id');
  151. return self._si.delAsync(delIds);
  152. } else {
  153. return true;
  154. }
  155. }).catch((err) => {
  156. if(err.type === 'NotFoundError') {
  157. return true;
  158. } else {
  159. winston.error(err);
  160. }
  161. });
  162. }
  163. };