search.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import loggerFactory from '~/utils/logger';
  2. // eslint-disable-next-line no-unused-vars
  3. const logger = loggerFactory('growi:service:search');
  4. const xss = require('xss');
  5. // options for filtering xss
  6. const filterXssOptions = {
  7. whiteList: {
  8. em: ['class'],
  9. },
  10. };
  11. const filterXss = new xss.FilterXSS(filterXssOptions);
  12. class SearchService {
  13. constructor(crowi) {
  14. this.crowi = crowi;
  15. this.configManager = crowi.configManager;
  16. this.isErrorOccuredOnHealthcheck = null;
  17. this.isErrorOccuredOnSearching = null;
  18. try {
  19. this.delegator = this.generateDelegator();
  20. }
  21. catch (err) {
  22. logger.error(err);
  23. }
  24. if (this.isConfigured) {
  25. this.delegator.init();
  26. this.registerUpdateEvent();
  27. }
  28. }
  29. get isConfigured() {
  30. return this.delegator != null;
  31. }
  32. get isReachable() {
  33. return this.isConfigured && !this.isErrorOccuredOnHealthcheck && !this.isErrorOccuredOnSearching;
  34. }
  35. get isSearchboxEnabled() {
  36. const uri = this.configManager.getConfig('crowi', 'app:searchboxSslUrl');
  37. return uri != null && uri.length > 0;
  38. }
  39. get isElasticsearchEnabled() {
  40. const uri = this.configManager.getConfig('crowi', 'app:elasticsearchUri');
  41. return uri != null && uri.length > 0;
  42. }
  43. generateDelegator() {
  44. logger.info('Initializing search delegator');
  45. if (this.isSearchboxEnabled) {
  46. const SearchboxDelegator = require('./search-delegator/searchbox');
  47. logger.info('Searchbox is enabled');
  48. return new SearchboxDelegator(this.configManager, this.crowi.socketIoService);
  49. }
  50. if (this.isElasticsearchEnabled) {
  51. const ElasticsearchDelegator = require('./search-delegator/elasticsearch');
  52. logger.info('Elasticsearch (not Searchbox) is enabled');
  53. return new ElasticsearchDelegator(this.configManager, this.crowi.socketIoService);
  54. }
  55. logger.info('No elasticsearch URI is specified so that full text search is disabled.');
  56. }
  57. registerUpdateEvent() {
  58. const pageEvent = this.crowi.event('page');
  59. pageEvent.on('create', this.delegator.syncPageUpdated.bind(this.delegator));
  60. pageEvent.on('update', this.delegator.syncPageUpdated.bind(this.delegator));
  61. pageEvent.on('deleteCompletely', this.delegator.syncPagesDeletedCompletely.bind(this.delegator));
  62. pageEvent.on('delete', this.delegator.syncPageDeleted.bind(this.delegator));
  63. pageEvent.on('updateMany', this.delegator.syncPagesUpdated.bind(this.delegator));
  64. pageEvent.on('syncDescendants', this.delegator.syncDescendantsPagesUpdated.bind(this.delegator));
  65. pageEvent.on('addSeenUsers', this.delegator.syncPageUpdated.bind(this.delegator));
  66. const bookmarkEvent = this.crowi.event('bookmark');
  67. bookmarkEvent.on('create', this.delegator.syncBookmarkChanged.bind(this.delegator));
  68. bookmarkEvent.on('delete', this.delegator.syncBookmarkChanged.bind(this.delegator));
  69. const tagEvent = this.crowi.event('tag');
  70. tagEvent.on('update', this.delegator.syncTagChanged.bind(this.delegator));
  71. }
  72. resetErrorStatus() {
  73. this.isErrorOccuredOnHealthcheck = false;
  74. this.isErrorOccuredOnSearching = false;
  75. }
  76. async reconnectClient() {
  77. logger.info('Try to reconnect...');
  78. this.delegator.initClient();
  79. try {
  80. await this.getInfoForHealth();
  81. logger.info('Reconnecting succeeded.');
  82. this.resetErrorStatus();
  83. }
  84. catch (err) {
  85. throw err;
  86. }
  87. }
  88. async getInfo() {
  89. try {
  90. return await this.delegator.getInfo();
  91. }
  92. catch (err) {
  93. logger.error(err);
  94. throw err;
  95. }
  96. }
  97. async getInfoForHealth() {
  98. try {
  99. const result = await this.delegator.getInfoForHealth();
  100. this.isErrorOccuredOnHealthcheck = false;
  101. return result;
  102. }
  103. catch (err) {
  104. logger.error(err);
  105. // switch error flag, `isErrorOccuredOnHealthcheck` to be `false`
  106. this.isErrorOccuredOnHealthcheck = true;
  107. throw err;
  108. }
  109. }
  110. async getInfoForAdmin() {
  111. return this.delegator.getInfoForAdmin();
  112. }
  113. async normalizeIndices() {
  114. return this.delegator.normalizeIndices();
  115. }
  116. async rebuildIndex() {
  117. return this.delegator.rebuildIndex();
  118. }
  119. async searchKeyword(keyword, user, userGroups, searchOpts) {
  120. try {
  121. return await this.delegator.searchKeyword(keyword, user, userGroups, searchOpts);
  122. }
  123. catch (err) {
  124. logger.error(err);
  125. // switch error flag, `isReachable` to be `false`
  126. this.isErrorOccuredOnSearching = true;
  127. throw err;
  128. }
  129. }
  130. /**
  131. * formatting result
  132. */
  133. formatResult(esResult) {
  134. esResult.data.forEach((data) => {
  135. const highlightData = data._highlight;
  136. const snippet = highlightData['body.en'] || highlightData['body.ja'] || '';
  137. const pathMatch = highlightData['path.en'] || highlightData['path.ja'] || '';
  138. data.elasticSearchResult = {
  139. snippet: filterXss.process(snippet),
  140. highlightedPath: filterXss.process(pathMatch),
  141. };
  142. });
  143. return esResult;
  144. }
  145. }
  146. module.exports = SearchService;