瀏覽代碼

feat(server): Search query now returns the FacetValue count

Michael Bromley 6 年之前
父節點
當前提交
646bc4d476

File diff suppressed because it is too large
+ 0 - 0
schema-admin.json


File diff suppressed because it is too large
+ 0 - 0
schema-shop.json


+ 44 - 1
schema.json

@@ -12162,7 +12162,7 @@
                   "name": null,
                   "ofType": {
                     "kind": "OBJECT",
-                    "name": "FacetValue",
+                    "name": "FacetValueResult",
                     "ofType": null
                   }
                 }
@@ -12452,6 +12452,49 @@
         "enumValues": null,
         "possibleTypes": null
       },
+      {
+        "kind": "OBJECT",
+        "name": "FacetValueResult",
+        "description": null,
+        "fields": [
+          {
+            "name": "facetValue",
+            "description": null,
+            "args": [],
+            "type": {
+              "kind": "NON_NULL",
+              "name": null,
+              "ofType": {
+                "kind": "OBJECT",
+                "name": "FacetValue",
+                "ofType": null
+              }
+            },
+            "isDeprecated": false,
+            "deprecationReason": null
+          },
+          {
+            "name": "count",
+            "description": null,
+            "args": [],
+            "type": {
+              "kind": "NON_NULL",
+              "name": null,
+              "ofType": {
+                "kind": "SCALAR",
+                "name": "Int",
+                "ofType": null
+              }
+            },
+            "isDeprecated": false,
+            "deprecationReason": null
+          }
+        ],
+        "inputFields": null,
+        "interfaces": [],
+        "enumValues": null,
+        "possibleTypes": null
+      },
       {
         "kind": "INPUT_OBJECT",
         "name": "ProductListOptions",

+ 17 - 14
server/e2e/default-search-plugin.e2e-spec.ts

@@ -126,12 +126,12 @@ describe('Default search plugin', () => {
                 },
             });
             expect(result.search.facetValues).toEqual([
-                { id: 'T_1', name: 'electronics' },
-                { id: 'T_2', name: 'computers' },
-                { id: 'T_3', name: 'photo' },
-                { id: 'T_4', name: 'sports equipment' },
-                { id: 'T_5', name: 'home & garden' },
-                { id: 'T_6', name: 'plants' },
+                { count: 21, facetValue: { id: 'T_1', name: 'electronics' } },
+                { count: 17, facetValue: { id: 'T_2', name: 'computers' } },
+                { count: 4, facetValue: { id: 'T_3', name: 'photo' } },
+                { count: 10, facetValue: { id: 'T_4', name: 'sports equipment' } },
+                { count: 3, facetValue: { id: 'T_5', name: 'home & garden' } },
+                { count: 3, facetValue: { id: 'T_6', name: 'plants' } },
             ]);
         });
 
@@ -142,12 +142,12 @@ describe('Default search plugin', () => {
                 },
             });
             expect(result.search.facetValues).toEqual([
-                { id: 'T_1', name: 'electronics' },
-                { id: 'T_2', name: 'computers' },
-                { id: 'T_3', name: 'photo' },
-                { id: 'T_4', name: 'sports equipment' },
-                { id: 'T_5', name: 'home & garden' },
-                { id: 'T_6', name: 'plants' },
+                { count: 10, facetValue: { id: 'T_1', name: 'electronics' } },
+                { count: 6, facetValue: { id: 'T_2', name: 'computers' } },
+                { count: 4, facetValue: { id: 'T_3', name: 'photo' } },
+                { count: 7, facetValue: { id: 'T_4', name: 'sports equipment' } },
+                { count: 3, facetValue: { id: 'T_5', name: 'home & garden' } },
+                { count: 3, facetValue: { id: 'T_6', name: 'plants' } },
             ]);
         });
     });
@@ -283,8 +283,11 @@ export const SEARCH_GET_FACET_VALUES = gql`
         search(input: $input) {
             totalItems
             facetValues {
-                id
-                name
+                count
+                facetValue {
+                    id
+                    name
+                }
             }
         }
     }

+ 1 - 1
server/src/api/resolvers/admin/search.resolver.ts

@@ -16,7 +16,7 @@ export class SearchResolver {
     }
 
     @ResolveProperty()
-    async facetValues(...args: any[]): Promise<Array<Translated<FacetValue>>> {
+    async facetValues(...args: any[]): Promise<Array<{ facetValue: FacetValue; count: number }>> {
         throw new InternalServerError(`error.no-search-plugin-configured`);
     }
 

+ 12 - 1
server/src/api/schema/type/product-search.type.graphql

@@ -5,7 +5,16 @@ type SearchReindexResponse {
 type SearchResponse {
     items: [SearchResult!]!
     totalItems: Int!
-    facetValues: [FacetValue!]!
+    facetValues: [FacetValueResult!]!
+}
+
+"""
+Which FacetValues are present in the products returned
+by the search, and in what quantity.
+"""
+type FacetValueResult {
+    facetValue: FacetValue!
+    count: Int!
 }
 
 type SearchResult {
@@ -22,6 +31,8 @@ type SearchResult {
     description: String!
     facetIds: [String!]!
     facetValueIds: [String!]!
+    "An array of ids of the Collections in which this result appears"
     collectionIds: [String!]!
+    "A relevence score for the result. Differs between database implementations."
     score: Float!
 }

+ 2 - 2
server/src/plugin/default-search-plugin/fulltext-search.resolver.ts

@@ -31,7 +31,7 @@ export class ShopFulltextSearchResolver implements Omit<BaseSearchResolver, 'rei
     async facetValues(
         @Ctx() ctx: RequestContext,
         @Context() context: any,
-    ): Promise<Array<Translated<FacetValue>>> {
+    ): Promise<Array<{ facetValue: FacetValue; count: number }>> {
         return this.fulltextSearchService.facetValues(ctx, context.req.body.variables.input);
     }
 }
@@ -54,7 +54,7 @@ export class AdminFulltextSearchResolver implements BaseSearchResolver {
     async facetValues(
         @Ctx() ctx: RequestContext,
         @Context() context: any,
-    ): Promise<Array<Translated<FacetValue>>> {
+    ): Promise<Array<{ facetValue: FacetValue; count: number }>> {
         return this.fulltextSearchService.facetValues(ctx, context.req.body.variables.input);
     }
 

+ 15 - 4
server/src/plugin/default-search-plugin/fulltext-search.service.ts

@@ -9,7 +9,6 @@ import { ID } from '../../../../shared/shared-types';
 import { unique } from '../../../../shared/unique';
 import { RequestContext } from '../../api/common/request-context';
 import { InternalServerError } from '../../common/error/errors';
-import { Translated } from '../../common/types/locale-types';
 import { FacetValue, Product, ProductVariant } from '../../entity';
 import { EventBus } from '../../event-bus/event-bus';
 import { translateDeep } from '../../service/helpers/utils/translate-entity';
@@ -65,9 +64,21 @@ export class FulltextSearchService implements SearchService {
     /**
      * Return a list of all FacetValues which appear in the result set.
      */
-    async facetValues(ctx: RequestContext, input: SearchInput): Promise<Array<Translated<FacetValue>>> {
-        const facetValueIds = await this.searchStrategy.getFacetValueIds(ctx, input);
-        return this.facetValueService.findByIds(facetValueIds, ctx.languageCode);
+    async facetValues(
+        ctx: RequestContext,
+        input: SearchInput,
+    ): Promise<Array<{ facetValue: FacetValue; count: number }>> {
+        const facetValueIdsMap = await this.searchStrategy.getFacetValueIds(ctx, input);
+        const facetValues = await this.facetValueService.findByIds(
+            Array.from(facetValueIdsMap.keys()),
+            ctx.languageCode,
+        );
+        return facetValues.map((facetValue, index) => {
+            return {
+                facetValue,
+                count: facetValueIdsMap.get(facetValue.id.toString()) as number,
+            };
+        });
     }
 
     /**

+ 10 - 9
server/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts

@@ -6,8 +6,8 @@ import { unique } from '../../../../../shared/unique';
 import { RequestContext } from '../../../api/common/request-context';
 import { SearchIndexItem } from '../search-index-item.entity';
 
-import { mapToSearchResult } from './map-to-search-result';
 import { SearchStrategy } from './search-strategy';
+import { createFacetIdCountMap, mapToSearchResult } from './search-strategy-utils';
 
 /**
  * A weighted fulltext search for MySQL / MariaDB.
@@ -17,18 +17,19 @@ export class MysqlSearchStrategy implements SearchStrategy {
 
     constructor(private connection: Connection) {}
 
-    async getFacetValueIds(ctx: RequestContext, input: SearchInput): Promise<ID[]> {
+    async getFacetValueIds(ctx: RequestContext, input: SearchInput): Promise<Map<ID, number>> {
         const facetValuesQb = this.connection
             .getRepository(SearchIndexItem)
             .createQueryBuilder('si')
-            .select('GROUP_CONCAT(si.facetValueIds)', 'allFacetValues');
+            .select(['productId', 'productVariantId'])
+            .addSelect('GROUP_CONCAT(facetValueIds)', 'facetValues');
 
-        const facetValuesResult = await this.applyTermAndFilters(facetValuesQb, {
-            ...input,
-            groupByProduct: false,
-        }).getRawOne();
-        const allFacetValues = facetValuesResult ? facetValuesResult.allFacetValues || '' : '';
-        return unique(allFacetValues.split(',').filter(x => x !== ''));
+        this.applyTermAndFilters(facetValuesQb, input);
+        if (!input.groupByProduct) {
+            facetValuesQb.groupBy('productVariantId');
+        }
+        const facetValuesResult = await facetValuesQb.getRawMany();
+        return createFacetIdCountMap(facetValuesResult);
     }
 
     async getSearchResults(ctx: RequestContext, input: SearchInput): Promise<SearchResult[]> {

+ 10 - 10
server/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts

@@ -6,8 +6,8 @@ import { unique } from '../../../../../shared/unique';
 import { RequestContext } from '../../../api/common/request-context';
 import { SearchIndexItem } from '../search-index-item.entity';
 
-import { mapToSearchResult } from './map-to-search-result';
 import { SearchStrategy } from './search-strategy';
+import { createFacetIdCountMap, mapToSearchResult } from './search-strategy-utils';
 
 /**
  * A weighted fulltext search for PostgeSQL.
@@ -17,19 +17,19 @@ export class PostgresSearchStrategy implements SearchStrategy {
 
     constructor(private connection: Connection) {}
 
-    async getFacetValueIds(ctx: RequestContext, input: SearchInput): Promise<ID[]> {
+    async getFacetValueIds(ctx: RequestContext, input: SearchInput): Promise<Map<ID, number>> {
         const facetValuesQb = this.connection
             .getRepository(SearchIndexItem)
             .createQueryBuilder('si')
-            .select(`string_agg(si.facetValueIds,',')`, 'allFacetValues');
+            .select(['"si.productId"', '"si.productVariantId"'])
+            .addSelect(`string_agg(si.facetValueIds,',')`, 'facetValues');
 
-        const facetValuesResult = await this.applyTermAndFilters(
-            facetValuesQb,
-            { ...input, groupByProduct: false },
-            true,
-        ).getRawOne();
-        const allFacetValues = facetValuesResult ? facetValuesResult.allFacetValues || '' : '';
-        return unique(allFacetValues.split(',').filter(x => x !== ''));
+        this.applyTermAndFilters(facetValuesQb, input, true);
+        if (!input.groupByProduct) {
+            facetValuesQb.groupBy('si.productVariantId');
+        }
+        const facetValuesResult = await facetValuesQb.getRawMany();
+        return createFacetIdCountMap(facetValuesResult);
     }
 
     async getSearchResults(ctx: RequestContext, input: SearchInput): Promise<SearchResult[]> {

+ 19 - 0
server/src/plugin/default-search-plugin/search-strategy/map-to-search-result.ts → server/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.ts

@@ -1,4 +1,6 @@
 import { CurrencyCode, SearchResult } from '../../../../../shared/generated-types';
+import { ID } from '../../../../../shared/shared-types';
+import { unique } from '../../../../../shared/unique';
 
 /**
  * Maps a raw database result to a SearchResult.
@@ -22,3 +24,20 @@ export function mapToSearchResult(raw: any, currencyCode: CurrencyCode): SearchR
         score: raw.score || 0,
     };
 }
+
+/**
+ * Given the raw query results containing rows with a `facetValues` property line "1,2,1,2",
+ * this function returns a map of FacetValue ids => count of how many times they occur.
+ */
+export function createFacetIdCountMap(facetValuesResult: Array<{ facetValues: string }>) {
+    const result = new Map<ID, number>();
+    for (const res of facetValuesResult) {
+        const facetValueIds: ID[] = unique(res.facetValues.split(',').filter(x => x !== ''));
+        for (const id of facetValueIds) {
+            const count = result.get(id);
+            const newCount = count ? count + 1 : 1;
+            result.set(id, newCount);
+        }
+    }
+    return result;
+}

+ 5 - 1
server/src/plugin/default-search-plugin/search-strategy/search-strategy.ts

@@ -9,5 +9,9 @@ import { RequestContext } from '../../../api';
 export interface SearchStrategy {
     getSearchResults(ctx: RequestContext, input: SearchInput): Promise<SearchResult[]>;
     getTotalCount(ctx: RequestContext, input: SearchInput): Promise<number>;
-    getFacetValueIds(ctx: RequestContext, input: SearchInput): Promise<ID[]>;
+    /**
+     * Returns a map of `facetValueId` => `count`, providing the number of times that
+     * facetValue occurs in the result set.
+     */
+    getFacetValueIds(ctx: RequestContext, input: SearchInput): Promise<Map<ID, number>>;
 }

+ 10 - 9
server/src/plugin/default-search-plugin/search-strategy/sqlite-search-strategy.ts

@@ -6,8 +6,8 @@ import { unique } from '../../../../../shared/unique';
 import { RequestContext } from '../../../api/common/request-context';
 import { SearchIndexItem } from '../search-index-item.entity';
 
-import { mapToSearchResult } from './map-to-search-result';
 import { SearchStrategy } from './search-strategy';
+import { createFacetIdCountMap, mapToSearchResult } from './search-strategy-utils';
 
 /**
  * A rather naive search for SQLite / SQL.js. Rather than proper
@@ -18,18 +18,19 @@ export class SqliteSearchStrategy implements SearchStrategy {
 
     constructor(private connection: Connection) {}
 
-    async getFacetValueIds(ctx: RequestContext, input: SearchInput): Promise<ID[]> {
+    async getFacetValueIds(ctx: RequestContext, input: SearchInput): Promise<Map<ID, number>> {
         const facetValuesQb = this.connection
             .getRepository(SearchIndexItem)
             .createQueryBuilder('si')
-            .select('GROUP_CONCAT(si.facetValueIds)', 'allFacetValues');
+            .select(['productId', 'productVariantId'])
+            .addSelect('GROUP_CONCAT(si.facetValueIds)', 'facetValues');
 
-        const facetValuesResult = await this.applyTermAndFilters(facetValuesQb, {
-            ...input,
-            groupByProduct: false,
-        }).getRawOne();
-        const allFacetValues = facetValuesResult ? facetValuesResult.allFacetValues || '' : '';
-        return unique(allFacetValues.split(',').filter(x => x !== ''));
+        this.applyTermAndFilters(facetValuesQb, input);
+        if (!input.groupByProduct) {
+            facetValuesQb.groupBy('productVariantId');
+        }
+        const facetValuesResult = await facetValuesQb.getRawMany();
+        return createFacetIdCountMap(facetValuesResult);
     }
 
     async getSearchResults(ctx: RequestContext, input: SearchInput): Promise<SearchResult[]> {

+ 8 - 2
shared/generated-shop-types.ts

@@ -1,5 +1,5 @@
 // tslint:disable
-// Generated in 2019-03-21T09:29:54+01:00
+// Generated in 2019-03-21T15:11:51+01:00
 export type Maybe<T> = T | null;
 
 export interface OrderListOptions {
@@ -1534,7 +1534,7 @@ export interface SearchResponse {
 
     totalItems: number;
 
-    facetValues: FacetValue[];
+    facetValues: FacetValueResult[];
 }
 
 export interface SearchResult {
@@ -1569,6 +1569,12 @@ export interface SearchResult {
     score: number;
 }
 
+export interface FacetValueResult {
+    facetValue: FacetValue;
+
+    count: number;
+}
+
 export interface Mutation {
     addItemToOrder?: Maybe<Order>;
 

+ 10 - 2
shared/generated-types.ts

@@ -1,5 +1,5 @@
 // tslint:disable
-// Generated in 2019-03-21T09:29:56+01:00
+// Generated in 2019-03-21T15:11:52+01:00
 export type Maybe<T> = T | null;
 
 
@@ -5440,7 +5440,7 @@ export interface SearchResponse {
   
   totalItems: number;
   
-  facetValues: FacetValue[];
+  facetValues: FacetValueResult[];
 }
 
 
@@ -5478,6 +5478,14 @@ export interface SearchResult {
 }
 
 
+export interface FacetValueResult {
+  
+  facetValue: FacetValue;
+  
+  count: number;
+}
+
+
 export interface ProductList extends PaginatedList {
   
   items: Product[];

Some files were not shown because too many files changed in this diff