Taichi Masuyama 4 năm trước cách đây
mục cha
commit
b317b299fa

+ 8 - 4
packages/app/src/server/interfaces/search.ts

@@ -15,7 +15,6 @@ export type QueryTerms = {
 
 export type ParsedQuery = {
   queryString: string // original query string in request
-  terms: QueryTerms // terms found in query string
   nqNames: string[] // possible NamedQuery names found in query string
 }
 
@@ -23,13 +22,13 @@ export interface SearchQueryParser {
   parseSearchQuery(queryString: string): Promise<ParsedQuery>
 }
 
-export interface SearchResolver {
-  resolve(parsedQuery: ParsedQuery): Promise<SearchDelegator>
+export interface SearchResolver{
+  resolve(parsedQuery: ParsedQuery): Promise<[SearchDelegator, SearchableData]>
 }
 
 export interface SearchDelegator<T = unknown> {
   name?: SearchDelegatorName
-  search(queryString: string | null, user, userGroups, option): Promise<Result<T> & MetaData>
+  search(data: SearchableData | null, user, userGroups, option): Promise<Result<T> & MetaData>
 }
 
 export type Result<T> = {
@@ -39,3 +38,8 @@ export type Result<T> = {
 export type MetaData = {
   meta: { [key:string]: any }
 }
+
+export type SearchableData = {
+  queryString: string
+  terms: QueryTerms
+}

+ 8 - 7
packages/app/src/server/service/search-delegator/elasticsearch.ts

@@ -9,7 +9,9 @@ import streamToPromise from 'stream-to-promise';
 import { createBatchStream } from '../../util/batch-stream';
 import loggerFactory from '~/utils/logger';
 import { PageDocument, PageModel } from '../../models/page';
-import { MetaData, SearchDelegator, Result } from '../../interfaces/search';
+import {
+  MetaData, SearchDelegator, Result, SearchableData, QueryTerms,
+} from '../../interfaces/search';
 
 const logger = loggerFactory('growi:service:search-delegator:elasticsearch');
 
@@ -638,12 +640,9 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
     return query;
   }
 
-  appendCriteriaForQueryString(query, queryString) {
+  appendCriteriaForQueryString(query, parsedKeywords: QueryTerms) {
     query = this.initializeBoolQuery(query); // eslint-disable-line no-param-reassign
 
-    // parse
-    const parsedKeywords = {} as any; // TODO: pass parsedQuery to this method
-
     if (parsedKeywords.match.length > 0) {
       const q = {
         multi_match: {
@@ -835,11 +834,13 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
     };
   }
 
-  async search(queryString: string, user, userGroups, option): Promise<Result<Data> & MetaData> {
+  async search(data: SearchableData, user, userGroups, option): Promise<Result<Data> & MetaData> {
+    const { queryString, terms } = data;
+
     const from = option.offset || null;
     const size = option.limit || null;
     const query = this.createSearchQuerySortedByScore();
-    this.appendCriteriaForQueryString(query, queryString);
+    this.appendCriteriaForQueryString(query, terms);
 
     await this.filterPagesByViewer(query, user, userGroups);
 

+ 66 - 51
packages/app/src/server/service/search.ts

@@ -3,7 +3,7 @@ import RE2 from 're2';
 
 import { NamedQueryModel, NamedQueryDocument } from '../models/named-query';
 import {
-  SearchDelegator, SearchQueryParser, SearchResolver, ParsedQuery, Result, MetaData, QueryTerms,
+  SearchDelegator, SearchQueryParser, SearchResolver, ParsedQuery, Result, MetaData, SearchableData, QueryTerms,
 } from '../interfaces/search';
 
 import loggerFactory from '~/utils/logger';
@@ -12,6 +12,13 @@ import { SearchDelegatorName } from '~/interfaces/named-query';
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:service:search');
 
+const normalizeQueryString = (_queryString: string): string => {
+  let queryString = _queryString.trim();
+  queryString = queryString.replace(/\s+/g, ' ');
+
+  return queryString;
+};
+
 class SearchService implements SearchQueryParser, SearchResolver {
 
   crowi!: any
@@ -147,6 +154,62 @@ class SearchService implements SearchQueryParser, SearchResolver {
   }
 
   async parseSearchQuery(_queryString: string): Promise<ParsedQuery> {
+    const nqNames: string[] = [];
+    const regexp = new RE2(/^\[nq:.+\]$/g); // https://regex101.com/r/FzDUvT/1
+
+    const queryString = normalizeQueryString(_queryString);
+
+    queryString.split(' ').forEach((word) => {
+      const isNamedQuery = regexp.test(word);
+
+      if (isNamedQuery) {
+        nqNames.push(word.replace(/\[nq:|\]/g, '')); // remove '[nq:' and ']'
+      }
+    });
+
+    return { queryString: _queryString, nqNames };
+  }
+
+  async resolve(parsedQuery: ParsedQuery): Promise<[SearchDelegator, SearchableData | null]> {
+    const { queryString, nqNames } = parsedQuery;
+
+    if (nqNames.length === 0) {
+      return this.delegator.search(queryString);
+    }
+
+    // find NamedQuery
+    const NamedQuery: NamedQueryModel = mongoose.model('NamedQuery');
+    const namedQueries = await NamedQuery.find({ name: { $in: nqNames } });
+
+    const delegatableNamedQuery = namedQueries.filter(nq => nq.delegatorName != null)[0]; // only the first named query is valid
+
+    if (delegatableNamedQuery == null) {
+      // expand aliasOf
+      // search with new qs
+    }
+
+    const nqDelegator = this.nqDelegators[delegatableNamedQuery.delegatorName as SearchDelegatorName];
+    if (nqDelegator != null) {
+      return [nqDelegator, null];
+    }
+
+    return [this.delegator, null];
+
+    // TODO: impl resolve
+    return [{}, {}] as [SearchDelegator, SearchableData];
+  }
+
+  async searchKeyword(keyword: string, user, userGroups, searchOpts): Promise<Result<any> & MetaData> {
+    // parse
+    const parsedQuery = await this.parseSearchQuery(keyword);
+    // resolve
+    const delegator = await this.resolve(parsedQuery);
+
+    // TODO: search
+    return {} as Result<any> & MetaData;
+  }
+
+  async parseQueryString(_queryString: string): Promise<QueryTerms> {
     // terms
     const matchWords: string[] = [];
     const notMatchWords: string[] = [];
@@ -156,8 +219,6 @@ class SearchService implements SearchQueryParser, SearchResolver {
     const notPrefixPaths: string[] = [];
     const tags: string[] = [];
     const notTags: string[] = [];
-    // nqNames
-    const nqNames: string[] = [];
 
     let queryString = _queryString.trim();
     queryString = queryString.replace(/\s+/g, ' '); // eslint-disable-line no-param-reassign
@@ -190,13 +251,8 @@ class SearchService implements SearchQueryParser, SearchResolver {
       const matchNegative = word.match(/^-(prefix:|tag:)?(.+)$/);
       // https://regex101.com/r/3qw9FQ/1
       const matchPositive = word.match(/^(prefix:|tag:)?(.+)$/);
-      // https://regex101.com/r/FzDUvT/1
-      const isNamedQuery = (new RE2(/^\[nq:.+\]$/g)).test(word);
 
-      if (isNamedQuery) {
-        nqNames.push(word.replace(/\[nq:|\]/g, '')); // remove '[nq:' and ']'
-      }
-      else if (matchNegative != null) {
+      if (matchNegative != null) {
         if (matchNegative[1] === 'prefix:') {
           notPrefixPaths.push(matchNegative[2]);
         }
@@ -231,48 +287,7 @@ class SearchService implements SearchQueryParser, SearchResolver {
       not_tag: notTags,
     };
 
-    return { queryString: _queryString, terms, nqNames };
-  }
-
-  async resolve(parsedQuery: ParsedQuery): Promise<SearchDelegator> {
-    const { queryString, nqNames } = parsedQuery;
-
-    if (nqNames.length === 0) {
-      return this.delegator.search(queryString);
-    }
-
-    // find NamedQuery
-    const NamedQuery: NamedQueryModel = mongoose.model('NamedQuery');
-    const namedQueries = await NamedQuery.find({ name: { $in: nqNames } });
-
-    const delegatableNamedQuery = namedQueries.filter(nq => nq.delegatorName != null)[0]; // only the first named query is valid
-
-    if (delegatableNamedQuery == null) {
-      // expand aliasOf
-      // search with new qs
-    }
-
-    const nqDelegator = this.nqDelegators[delegatableNamedQuery.delegatorName as SearchDelegatorName];
-    if (nqDelegator != null) {
-      return nqDelegator.search(queryString);
-    }
-
-    return this.delegator.search();
-  }
-
-  async searchKeyword(keyword: string, user, userGroups, searchOpts): Promise<Result<any> & MetaData> {
-    // parse
-    const parsedQuery = await this.parseSearchQuery(keyword);
-    // resolve
-    const delegator = await this.resolve(parsedQuery);
-
-    // TODO: search
-    return {} as Result<any> & MetaData;
-  }
-
-  async parseAliasOf(namedQueries: NamedQueryDocument[]): QueryTerms {
-    const expandedAliasOf = namedQueries.map(nq => nq.aliasOf as string).reduce((a, b) => a + b);
-
+    return terms;
   }
 
 }