Skip to content

Commit

Permalink
feat(elasticsearch-plugin): Added custom sort parameter mapping (vend…
Browse files Browse the repository at this point in the history
  • Loading branch information
Kevin committed Nov 17, 2021
1 parent 2cb9765 commit 7b53cf4
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 5 deletions.
65 changes: 65 additions & 0 deletions packages/elasticsearch-plugin/e2e/elasticsearch-plugin.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ describe('Elasticsearch plugin', () => {
return 'World';
},
},
priority: {
graphQlType: 'Int!',
valueFn: args => {
return ((args.id as number) % 2) + 1; // only 1 or 2
},
},
},
searchConfig: {
scriptFields: {
Expand All @@ -155,10 +161,25 @@ describe('Elasticsearch plugin', () => {
},
},
},
mapSort: (sort, input) => {
const priority = (input.sort as any)?.priority;
if (priority) {
return [
...sort,
{
['product-priority']: {
order: priority === SortOrder.ASC ? 'asc' : 'desc',
},
},
];
}
return sort;
},
},
extendSearchInputType: {
factor: 'Int',
},
extendSearchSortType: ['priority'],
}),
DefaultJobQueuePlugin,
],
Expand Down Expand Up @@ -1417,6 +1438,50 @@ describe('Elasticsearch plugin', () => {
});
});
});

describe('sort', () => {
it('sort ASC', async () => {
const query = `{
search(input: { take: 1, groupByProduct: true, sort: { priority: ASC } }) {
items {
customMappings {
...on CustomProductMappings {
priority
}
}
}
}
}`;
const { search } = await shopClient.query(gql(query));

expect(search.items[0]).toEqual({
customMappings: {
priority: 1,
},
});
});

it('sort DESC', async () => {
const query = `{
search(input: { take: 1, groupByProduct: true, sort: { priority: DESC } }) {
items {
customMappings {
...on CustomProductMappings {
priority
}
}
}
}
}`;
const { search } = await shopClient.query(gql(query));

expect(search.items[0]).toEqual({
customMappings: {
priority: 2,
},
});
});
});
});

export const SEARCH_PRODUCTS = gql`
Expand Down
9 changes: 9 additions & 0 deletions packages/elasticsearch-plugin/src/api/api-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import { ElasticsearchOptions } from '../options';
export function generateSchemaExtensions(options: ElasticsearchOptions): DocumentNode {
const customMappingTypes = generateCustomMappingTypes(options);
const inputExtensions = Object.entries(options.extendSearchInputType || {});
const sortExtensions = options.extendSearchSortType || [];

const sortExtensionGql = `
extend input SearchResultSortParameter {
${sortExtensions.map(key => `${key}: SortOrder`).join('\n ')}
}`;

return gql`
extend type SearchResponse {
prices: SearchResponsePriceData!
Expand Down Expand Up @@ -34,6 +41,8 @@ export function generateSchemaExtensions(options: ElasticsearchOptions): Documen
${inputExtensions.map(([name, type]) => `${name}: ${type}`).join('\n ')}
}
${sortExtensions.length > 0 ? sortExtensionGql : ''}
input PriceRangeInput {
min: Int!
max: Int!
Expand Down
9 changes: 4 additions & 5 deletions packages/elasticsearch-plugin/src/build-elastic-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { LanguageCode, LogicalOperator, PriceRange, SortOrder } from '@vendure/c
import { DeepRequired, ID, UserInputError } from '@vendure/core';

import { SearchConfig } from './options';
import { CustomScriptMapping, ElasticSearchInput, SearchRequestBody } from './types';
import { CustomScriptMapping, ElasticSearchInput, ElasticSearchSortInput, SearchRequestBody } from './types';

/**
* Given a SearchInput object, returns the corresponding Elasticsearch body.
Expand Down Expand Up @@ -109,14 +109,13 @@ export function buildElasticBody(
}
}

const sortArray = [];
const sortArray: ElasticSearchSortInput = [];
if (sort) {
if (sort.name) {
sortArray.push({
'productName.keyword': { order: sort.name === SortOrder.ASC ? 'asc' : 'desc' },
});
}
if (sort.price) {
} else if (sort.price) {
const priceField = 'price';
sortArray.push({ [priceField]: { order: sort.price === SortOrder.ASC ? 'asc' : 'desc' } });
}
Expand All @@ -131,7 +130,7 @@ export function buildElasticBody(
query: searchConfig.mapQuery
? searchConfig.mapQuery(query, input, searchConfig, channelId, enabledOnly)
: query,
sort: sortArray,
sort: searchConfig.mapSort ? searchConfig.mapSort(sortArray, input) : sortArray,
from: skip || 0,
size: take || 10,
track_total_hits: searchConfig.totalItemsMaxSize,
Expand Down
104 changes: 104 additions & 0 deletions packages/elasticsearch-plugin/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
CustomMapping,
CustomScriptMapping,
ElasticSearchInput,
ElasticSearchSortInput,
ElasticSearchSortParameter,
GraphQlPrimitive,
PrimitiveTypeVariations,
} from './types';
Expand Down Expand Up @@ -328,6 +330,30 @@ export interface ElasticsearchOptions {
extendSearchInputType?: {
[name: string]: PrimitiveTypeVariations<GraphQlPrimitive>;
};

/**
* @description
* Adds a list of sort parameters. This is mostly important to make the
* correct sort order values available inside `input` parameter of the `mapSort` option.
*
* @example
* ```TypeScript
* extendSearchSortType: ["distance"]
* ```
*
* will extend the `SearchResultSortParameter` input type like this:
*
* @example
* ```GraphQl
* extend input SearchResultSortParameter {
* distance: SortOrder
* }
* ```
*
* @default []
* @since 1.4.0
*/
extendSearchSortType?: string[];
}

/**
Expand Down Expand Up @@ -531,6 +557,82 @@ export interface SearchConfig {
* @since 1.3.0
*/
scriptFields?: { [fieldName: string]: CustomScriptMapping<[ElasticSearchInput]> };
/**
* @description
* Allows extending the `sort` input of the elasticsearch body as covered in
* [Elasticsearch sort docs](https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html)
*
* @example
* ```TS
* mapSort: (sort, input) => {
* // Assuming `extendSearchSortType: ["priority"]`
* // Assuming priority is never undefined
* const { priority } = input.sort;
* return [
* ...sort,
* {
* // The `product-priority` field corresponds to the `priority` customProductMapping
* // Depending on the index type, this field might require a
* // more detailed input (example: 'productName.keyword')
* ["product-priority"]: {
* order: priority === SortOrder.ASC ? 'asc' : 'desc'
* }
* }
* ];
* }
* ```
*
* A more generic example would be a sort function based on a product location like this:
* @example
* ```TS
* extendSearchInputType: {
* latitude: 'Float',
* longitude: 'Float',
* },
* extendSearchSortType: ["distance"],
* indexMappingProperties: {
* // The `product-location` field corresponds to the `location` customProductMapping
* // defined below. Here we specify that it would be index as a `geo_point` type,
* // which will allow us to perform geo-spacial calculations on it in our script field.
* 'product-location': {
* type: 'geo_point',
* },
* },
* customProductMappings: {
* location: {
* graphQlType: 'String',
* valueFn: (product: Product) => {
* // Assume that the Product entity has this customField defined
* const custom = product.customFields.location;
* return `${custom.latitude},${custom.longitude}`;
* },
* }
* },
* searchConfig: {
* mapSort: (sort, input) => {
* // Assuming distance is never undefined
* const { distance } = input.sort;
* return [
* ...sort,
* {
* ["_geo_distance"]: {
* "product-location": [
* input.longitude,
* input.latitude
* ],
* order: distance === SortOrder.ASC ? 'asc' : 'desc',
* unit: "km"
* }
* }
* ];
* }
* }
* ```
*
* @default {}
* @since 1.4.0
*/
mapSort?: (sort: ElasticSearchSortInput, input: ElasticSearchInput) => ElasticSearchSortInput;
}

/**
Expand Down Expand Up @@ -600,6 +702,7 @@ export const defaultOptions: ElasticsearchRuntimeOptions = {
},
priceRangeBucketInterval: 1000,
mapQuery: query => query,
mapSort: sort => sort,
scriptFields: {},
},
customProductMappings: {},
Expand All @@ -608,6 +711,7 @@ export const defaultOptions: ElasticsearchRuntimeOptions = {
hydrateProductRelations: [],
hydrateProductVariantRelations: [],
extendSearchInputType: {},
extendSearchSortType: [],
};

export function mergeWithDefaults(userOptions: ElasticsearchOptions): ElasticsearchRuntimeOptions {
Expand Down
21 changes: 21 additions & 0 deletions packages/elasticsearch-plugin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,27 @@ export type PriceRangeBucket = {
count: number;
};

export enum ElasticSearchSortMode {
/** Pick the lowest value */
MIN = 'min',
/** Pick the highest value */
MAX = 'max',
/** Use the sum of all values as sort value. Only applicable for number based array fields */
SUM = 'sum',
/** Use the average of all values as sort value. Only applicable for number based array fields */
AVG = 'avg',
/** Use the median of all values as sort value. Only applicable for number based array fields */
MEDIAN = 'median',
}

export type ElasticSearchSortParameter = {
missing?: '_last' | '_first' | string;
mode?: ElasticSearchSortMode;
order: 'asc' | 'desc';
} & { [key: string]: any };

export type ElasticSearchSortInput = Array<{ [key: string]: ElasticSearchSortParameter }>;

export type IndexItemAssets = {
productAssetId: ID | undefined;
productPreview: string;
Expand Down

0 comments on commit 7b53cf4

Please sign in to comment.