فهرست منبع

Commonized search result schema

Taichi Masuyama 4 سال پیش
والد
کامیت
04a78dbaf6

+ 1 - 1
packages/app/src/components/SearchPage/SearchResult.jsx

@@ -27,7 +27,7 @@ class SearchResult extends React.Component {
   }
 
   isNotSearchedYet() {
-    return !this.props.searchResultMeta.took;
+    return this.props.searchResultMeta == null;
   }
 
   isNotFound() {

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

@@ -33,7 +33,10 @@ export type Result<T> = {
 }
 
 export type MetaData = {
-  meta?: { [key:string]: any }
+  meta?: {
+    [key:string]: any,
+    total: number,
+  }
 }
 
 export type SearchableData = {

+ 1 - 1
packages/app/src/server/routes/index.js

@@ -146,7 +146,7 @@ module.exports = function(crowi, app) {
   app.get('/download/:id([0-9a-z]{24})'    , loginRequired, attachment.api.download);
 
   app.get('/_search'                 , loginRequired , search.searchPage);
-  app.get('/_api/search'             , accessTokenParser , loginRequired , search.api.search);
+  app.get('/_api/search'             , accessTokenParser , loginRequired, search.api.search);
 
   app.get('/_api/check_username'           , user.api.checkUsername);
   app.get('/_api/me/user-group-relations'  , accessTokenParser , loginRequiredStrictly , me.api.userGroupRelations);

+ 64 - 33
packages/app/src/server/routes/search.js

@@ -1,5 +1,10 @@
+import { SearchDelegatorName } from '~/interfaces/named-query';
+
+const { default: loggerFactory } = require('~/utils/logger');
 const { serializeUserSecurely } = require('../models/serializers/user-serializer');
 
+const logger = loggerFactory('growi:routes:search');
+
 /**
  * @swagger
  *
@@ -36,6 +41,54 @@ module.exports = function(crowi, app) {
   const actions = {};
   const api = {};
 
+  // TODO: optimize the way to check isReshapable e.g. check data schema of searchResult
+  // So far, it determines by delegatorName passed by searchService.searchKeyword
+  const checkIsReshapable = (searchResult, delegatorName) => {
+    return delegatorName === SearchDelegatorName.DEFAULT;
+  };
+
+  const reshapeSearchResult = async(searchResult, delegatorName) => {
+    if (!checkIsReshapable(searchResult, delegatorName)) {
+      return searchResult;
+    }
+
+    const result = {};
+
+    // create score map for sorting
+    // key: id , value: score
+    const scoreMap = {};
+    for (const esPage of searchResult.data) {
+      scoreMap[esPage._id] = esPage._score;
+    }
+
+    const ids = searchResult.data.map((page) => { return page._id });
+    const findResult = await Page.findListByPageIds(ids);
+
+    // add tag data to result pages
+    findResult.pages.map((page) => {
+      const data = searchResult.data.find((data) => { return page.id === data._id });
+      page._doc.tags = data._source.tag_names;
+      return page;
+    });
+
+    result.meta = searchResult.meta;
+    result.totalCount = findResult.totalCount;
+    result.data = findResult.pages
+      .map((page) => {
+        if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
+          page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
+        }
+        page.bookmarkCount = (page._source && page._source.bookmark_count) || 0;
+        return page;
+      })
+      .sort((page1, page2) => {
+        // note: this do not consider NaN
+        return scoreMap[page2._id] - scoreMap[page1._id];
+      });
+
+    return result;
+  };
+
   actions.searchPage = function(req, res) {
     const keyword = req.query.q || null;
 
@@ -137,41 +190,19 @@ module.exports = function(crowi, app) {
 
     const searchOpts = { ...paginateOpts, type };
 
-    const result = {};
+    let searchResult;
+    let delegatorName;
     try {
-      const esResult = await searchService.searchKeyword(keyword, user, userGroups, searchOpts); // TODO: separate when not full-text search
-
-      // create score map for sorting
-      // key: id , value: score
-      const scoreMap = {};
-      for (const esPage of esResult.data) {
-        scoreMap[esPage._id] = esPage._score;
-      }
-
-      const ids = esResult.data.map((page) => { return page._id });
-      const findResult = await Page.findListByPageIds(ids);
-
-      // add tag data to result pages
-      findResult.pages.map((page) => {
-        const data = esResult.data.find((data) => { return page.id === data._id });
-        page._doc.tags = data._source.tag_names;
-        return page;
-      });
+      [searchResult, delegatorName] = await searchService.searchKeyword(keyword, user, userGroups, searchOpts); // TODO: separate when not full-text search
+    }
+    catch (err) {
+      logger.error('Failed to search', err);
+      return res.json(ApiResponse.error(err));
+    }
 
-      result.meta = esResult.meta;
-      result.totalCount = findResult.totalCount;
-      result.data = findResult.pages
-        .map((page) => {
-          if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
-            page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
-          }
-          page.bookmarkCount = (page._source && page._source.bookmark_count) || 0;
-          return page;
-        })
-        .sort((page1, page2) => {
-          // note: this do not consider NaN
-          return scoreMap[page2._id] - scoreMap[page1._id];
-        });
+    let result;
+    try {
+      result = await reshapeSearchResult(searchResult, delegatorName);
     }
     catch (err) {
       return res.json(ApiResponse.error(err));

+ 4 - 0
packages/app/src/server/service/search-delegator/elasticsearch.ts

@@ -12,6 +12,7 @@ import { PageDocument, PageModel } from '../../models/page';
 import {
   MetaData, SearchDelegator, Result, SearchableData, QueryTerms,
 } from '../../interfaces/search';
+import { SearchDelegatorName } from '~/interfaces/named-query';
 
 const logger = loggerFactory('growi:service:search-delegator:elasticsearch');
 
@@ -23,6 +24,8 @@ type Data = any;
 
 class ElasticsearchDelegator implements SearchDelegator<Data> {
 
+  name!: SearchDelegatorName.DEFAULT
+
   configManager!: any
 
   socketIoService!: any
@@ -36,6 +39,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
   esUri: string
 
   constructor(configManager, socketIoService) {
+    this.name = SearchDelegatorName.DEFAULT;
     this.configManager = configManager;
     this.socketIoService = socketIoService;
 

+ 10 - 8
packages/app/src/server/service/search-delegator/private-legacy-pages.ts

@@ -8,15 +8,15 @@ import {
 } from '../../interfaces/search';
 
 
-type Data = {
-  pages: IPage[]
-}
-
-class PrivateLegacyPagesDelegator implements SearchDelegator<Data> {
+class PrivateLegacyPagesDelegator implements SearchDelegator<IPage[]> {
 
   name!: SearchDelegatorName.PRIVATE_LEGACY_PAGES
 
-  async search(data: SearchableData | null, user, userGroups, option): Promise<Result<Data> & MetaData> {
+  constructor() {
+    this.name = SearchDelegatorName.PRIVATE_LEGACY_PAGES;
+  }
+
+  async search(data: SearchableData | null, user, userGroups, option): Promise<Result<IPage[]> & MetaData> {
     const { offset, limit } = option;
 
     if (offset == null || limit == null) {
@@ -38,12 +38,14 @@ class PrivateLegacyPagesDelegator implements SearchDelegator<Data> {
       .addConditionToFilteringByViewer(user, userGroups)
       .addConditionToPagenate(offset, limit)
       .query
+      .populate('lastUpdateUser')
       .lean()
       .exec();
 
     return {
-      data: {
-        pages,
+      data: pages,
+      meta: {
+        total: pages.length,
       },
     };
   }

+ 2 - 2
packages/app/src/server/service/search.ts

@@ -217,7 +217,7 @@ class SearchService implements SearchQueryParser, SearchResolver {
     return [this.nqDelegators[SearchDelegatorName.DEFAULT], data];
   }
 
-  async searchKeyword(keyword: string, user, userGroups, searchOpts): Promise<Result<any> & MetaData> {
+  async searchKeyword(keyword: string, user, userGroups, searchOpts): Promise<[Result<any> & MetaData, string]> {
     let parsedQuery;
     // parse
     try {
@@ -239,7 +239,7 @@ class SearchService implements SearchQueryParser, SearchResolver {
       throw err;
     }
 
-    return delegator.search(data, user, userGroups, searchOpts);
+    return [await delegator.search(data, user, userGroups, searchOpts), delegator.name];
   }
 
   parseQueryString(queryString: string): QueryTerms {

+ 2 - 2
packages/ui/src/components/PagePath/PageListMeta.jsx

@@ -23,12 +23,12 @@ export class PageListMeta extends React.Component {
     }
 
     let commentCount;
-    if (page.commentCount > 0) {
+    if (page.commentCount != null && page.commentCount > 0) {
       commentCount = <span><i className="icon-bubble" />{page.commentCount}</span>;
     }
 
     let likerCount;
-    if (page.liker.length > 0) {
+    if (page.liker != null && page.liker.length > 0) {
       likerCount = <span><i className="icon-like" />{page.liker.length}</span>;
     }