Taichi Masuyama 4 anni fa
parent
commit
8eb02bbb7d
1 ha cambiato i file con 239 aggiunte e 0 eliminazioni
  1. 239 0
      packages/app/src/server/service/search.ts

+ 239 - 0
packages/app/src/server/service/search.ts

@@ -0,0 +1,239 @@
+import mongoose from 'mongoose';
+
+import { HasObjectId } from '~/interfaces/has-object-id';
+import { IPage } from '~/interfaces/page';
+import { NamedQueryModel, NamedQueryDocument } from '../models/named-query';
+
+import loggerFactory from '~/utils/logger';
+
+// eslint-disable-next-line no-unused-vars
+const logger = loggerFactory('growi:service:search');
+
+export type SearchResult = {
+  data: IPage & HasObjectId
+  meta?: any
+}
+
+type ParsedQuery = {
+  queryString: string
+  namedQueries: NamedQueryDocument[]
+  shouldSearchKeyword: boolean
+}
+
+class SearchService {
+
+  crowi!: any
+
+  configManager!: any
+
+  isErrorOccuredOnHealthcheck: boolean | null
+
+  isErrorOccuredOnSearching: boolean | null
+
+  delegator: any
+
+  constructor(crowi) {
+    this.crowi = crowi;
+    this.configManager = crowi.configManager;
+
+    this.isErrorOccuredOnHealthcheck = null;
+    this.isErrorOccuredOnSearching = null;
+
+    try {
+      this.delegator = this.generateDelegator();
+    }
+    catch (err) {
+      logger.error(err);
+    }
+
+    if (this.isConfigured) {
+      this.delegator.init();
+      this.registerUpdateEvent();
+    }
+  }
+
+  get isConfigured() {
+    return this.delegator != null;
+  }
+
+  get isReachable() {
+    return this.isConfigured && !this.isErrorOccuredOnHealthcheck && !this.isErrorOccuredOnSearching;
+  }
+
+  get isSearchboxEnabled() {
+    const uri = this.configManager.getConfig('crowi', 'app:searchboxSslUrl');
+    return uri != null && uri.length > 0;
+  }
+
+  get isElasticsearchEnabled() {
+    const uri = this.configManager.getConfig('crowi', 'app:elasticsearchUri');
+    return uri != null && uri.length > 0;
+  }
+
+  generateDelegator() {
+    logger.info('Initializing search delegator');
+
+    if (this.isSearchboxEnabled) {
+      const SearchboxDelegator = require('./search-delegator/searchbox');
+      logger.info('Searchbox is enabled');
+      return new SearchboxDelegator(this.configManager, this.crowi.socketIoService);
+    }
+    if (this.isElasticsearchEnabled) {
+      const ElasticsearchDelegator = require('./search-delegator/elasticsearch');
+      logger.info('Elasticsearch (not Searchbox) is enabled');
+      return new ElasticsearchDelegator(this.configManager, this.crowi.socketIoService);
+    }
+
+    logger.info('No elasticsearch URI is specified so that full text search is disabled.');
+  }
+
+  registerUpdateEvent() {
+    const pageEvent = this.crowi.event('page');
+    pageEvent.on('create', this.delegator.syncPageUpdated.bind(this.delegator));
+    pageEvent.on('update', this.delegator.syncPageUpdated.bind(this.delegator));
+    pageEvent.on('deleteCompletely', this.delegator.syncPagesDeletedCompletely.bind(this.delegator));
+    pageEvent.on('delete', this.delegator.syncPageDeleted.bind(this.delegator));
+    pageEvent.on('updateMany', this.delegator.syncPagesUpdated.bind(this.delegator));
+    pageEvent.on('syncDescendants', this.delegator.syncDescendantsPagesUpdated.bind(this.delegator));
+
+    const bookmarkEvent = this.crowi.event('bookmark');
+    bookmarkEvent.on('create', this.delegator.syncBookmarkChanged.bind(this.delegator));
+    bookmarkEvent.on('delete', this.delegator.syncBookmarkChanged.bind(this.delegator));
+
+    const tagEvent = this.crowi.event('tag');
+    tagEvent.on('update', this.delegator.syncTagChanged.bind(this.delegator));
+  }
+
+  resetErrorStatus() {
+    this.isErrorOccuredOnHealthcheck = false;
+    this.isErrorOccuredOnSearching = false;
+  }
+
+  async reconnectClient() {
+    logger.info('Try to reconnect...');
+    this.delegator.initClient();
+
+    try {
+      await this.getInfoForHealth();
+
+      logger.info('Reconnecting succeeded.');
+      this.resetErrorStatus();
+    }
+    catch (err) {
+      throw err;
+    }
+  }
+
+  async getInfo() {
+    try {
+      return await this.delegator.getInfo();
+    }
+    catch (err) {
+      logger.error(err);
+      throw err;
+    }
+  }
+
+  async getInfoForHealth() {
+    try {
+      const result = await this.delegator.getInfoForHealth();
+
+      this.isErrorOccuredOnHealthcheck = false;
+      return result;
+    }
+    catch (err) {
+      logger.error(err);
+
+      // switch error flag, `isErrorOccuredOnHealthcheck` to be `false`
+      this.isErrorOccuredOnHealthcheck = true;
+      throw err;
+    }
+  }
+
+  async getInfoForAdmin() {
+    return this.delegator.getInfoForAdmin();
+  }
+
+  async normalizeIndices() {
+    return this.delegator.normalizeIndices();
+  }
+
+  async rebuildIndex() {
+    return this.delegator.rebuildIndex();
+  }
+
+  async parseSearchQuery(_queryString: string): Promise<ParsedQuery> {
+    let shouldSearchKeyword = false;
+
+    // do not reassign queryString
+    let queryString = _queryString.trim();
+    queryString = _queryString.replace(/\s+/, ' ');
+    const namedQueryRegExp = /^\[\.+\]$/g;
+
+    const queryParts = queryString.split(' ');
+    const namedQueryNames = queryParts
+      .filter(str => namedQueryRegExp.test(str)) // filter by regexp
+      .map(str => str.replace(/\[|\]/g, '')); // remove []
+
+    const allCount = queryParts.length;
+    const namedQueriesCount = namedQueryNames.length;
+    if (allCount > 0 && allCount === namedQueriesCount) {
+      shouldSearchKeyword = true;
+    }
+
+    // find NamedQuery
+    const NamedQuery: NamedQueryModel = mongoose.model('NamedQuery');
+    const namedQueries = namedQueryNames.length ? await NamedQuery.find({ name: { $in: namedQueryNames } }) : [];
+    return { queryString, namedQueries, shouldSearchKeyword };
+  }
+
+  async searchKeyword(keyword, user, userGroups, searchOpts): Promise<SearchResult> {
+    // TODO: call parseQueryString then call necessary search methods
+    let parsedQuery: ParsedQuery;
+    try {
+      parsedQuery = await this.parseSearchQuery(keyword);
+    }
+    catch (err) {
+      logger.error('Failed to parse query string', err);
+      throw Error('Failed to parse query string');
+    }
+    const { queryString, namedQueries, shouldSearchKeyword } = parsedQuery;
+
+    const result = await Promise.all(namedQueries.map(async(namedQuery) => {
+      return this.searchByNamedQuery(namedQuery, user, userGroups, searchOpts);
+    }));
+    // if namedQuery.queryString != null { return await this.delegator.searchKeyword(keyword.concat(' ', queryString), user, userGroups, searchOpts) }
+    // else if namedQuery.resolverName != null { return await this.resolveSearch(resolverName, user, userGroups, searchOpts) }
+
+    let searchKeywordResult: SearchResult;
+    try {
+      searchKeywordResult = shouldSearchKeyword ? this.delegator.searchKeyword(keyword, user, userGroups, searchOpts) : {};
+    }
+    catch (err) {
+      logger.error(err);
+
+      // switch error flag, `isReachable` to be `false`
+      this.isErrorOccuredOnSearching = true;
+      throw err;
+    }
+
+    return {
+      data: { ...searchKeywordResult.data },
+      meta: { ...searchKeywordResult.meta },
+    };
+  }
+
+  async searchByNamedQuery(namedQuery: NamedQueryDocument, user, userGroups, searchOpts): Promise<SearchResult> {
+    const { resolverName } = namedQuery;
+
+    switch (resolverName) {
+      default:
+        break;
+    }
+
+    return {} as SearchResult;
+  }
+
+}
+
+export default SearchService;