search.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  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. });
  73. },
  74. /**
  75. * Add a document to the index
  76. *
  77. * @param {Object} content Document content
  78. * @return {Promise} Promise of the add operation
  79. */
  80. add(content) {
  81. let self = this;
  82. return self._si.searchAsync({
  83. query: {
  84. AND: [{ 'entryPath': [content.entryPath] }]
  85. }
  86. }).then((results) => {
  87. if(results.totalHits > 0) {
  88. let delIds = _.map(results.hits, 'id');
  89. return self._si.delAsync(delIds);
  90. } else {
  91. return true;
  92. }
  93. }).then(() => {
  94. return self._si.addAsync({
  95. entryPath: content.entryPath,
  96. title: content.meta.title,
  97. subtitle: content.meta.subtitle || '',
  98. parent: content.parent.title || '',
  99. content: content.text || ''
  100. }, {
  101. fieldOptions: [{
  102. fieldName: 'entryPath',
  103. searchable: true,
  104. weight: 2
  105. },
  106. {
  107. fieldName: 'title',
  108. nGramLength: [1, 2],
  109. searchable: true,
  110. weight: 3
  111. },
  112. {
  113. fieldName: 'subtitle',
  114. searchable: true,
  115. weight: 1,
  116. store: false
  117. },
  118. {
  119. fieldName: 'parent',
  120. searchable: false,
  121. },
  122. {
  123. fieldName: 'content',
  124. searchable: true,
  125. weight: 0,
  126. store: false
  127. }]
  128. }).then(() => {
  129. winston.info('Entry ' + content.entryPath + ' added/updated to index.');
  130. return true;
  131. }).catch((err) => {
  132. winston.error(err);
  133. });
  134. }).catch((err) => {
  135. winston.error(err);
  136. });
  137. }
  138. };