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.

183 lines
3.2 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-index');
  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._si.searchAsync({
  92. query: {
  93. AND: [{ 'entryPath': [content.entryPath] }]
  94. }
  95. }).then((results) => {
  96. if(results.totalHits > 0) {
  97. let delIds = _.map(results.hits, 'id');
  98. return self._si.delAsync(delIds);
  99. } else {
  100. return true;
  101. }
  102. }).catch((err) => {
  103. if(err.type === 'NotFoundError') {
  104. return true;
  105. } else {
  106. winston.error(err);
  107. }
  108. }).then(() => {
  109. return self._si.addAsync({
  110. entryPath: content.entryPath,
  111. title: content.meta.title,
  112. subtitle: content.meta.subtitle || '',
  113. parent: content.parent.title || '',
  114. content: content.text || ''
  115. }, {
  116. fieldOptions: [{
  117. fieldName: 'entryPath',
  118. searchable: true,
  119. weight: 2
  120. },
  121. {
  122. fieldName: 'title',
  123. nGramLength: [1, 2],
  124. searchable: true,
  125. weight: 3
  126. },
  127. {
  128. fieldName: 'subtitle',
  129. searchable: true,
  130. weight: 1,
  131. store: false
  132. },
  133. {
  134. fieldName: 'parent',
  135. searchable: false,
  136. },
  137. {
  138. fieldName: 'content',
  139. searchable: true,
  140. weight: 0,
  141. store: false
  142. }]
  143. }).then(() => {
  144. winston.info('Entry ' + content.entryPath + ' added/updated to index.');
  145. return true;
  146. }).catch((err) => {
  147. winston.error(err);
  148. });
  149. }).catch((err) => {
  150. winston.error(err);
  151. });
  152. }
  153. };