| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743 |
- import { ClientOptions } from '@elastic/elasticsearch';
- import {
- DeepRequired,
- EntityRelationPaths,
- ID,
- Injector,
- LanguageCode,
- Product,
- ProductVariant,
- } from '@vendure/core';
- import deepmerge from 'deepmerge';
- import {
- CustomMapping,
- CustomScriptMapping,
- ElasticSearchInput,
- ElasticSearchSortInput,
- ElasticSearchSortParameter,
- GraphQlPrimitive,
- PrimitiveTypeVariations,
- } from './types';
- /**
- * @description
- * Configuration options for the {@link ElasticsearchPlugin}.
- *
- * @docsCategory core plugins/ElasticsearchPlugin
- * @docsPage ElasticsearchOptions
- */
- export interface ElasticsearchOptions {
- /**
- * @description
- * The host of the Elasticsearch server. May also be specified in `clientOptions.node`.
- *
- * @default 'http://localhost'
- */
- host?: string;
- /**
- * @description
- * The port of the Elasticsearch server. May also be specified in `clientOptions.node`.
- *
- * @default 9200
- */
- port?: number;
- /**
- * @description
- * Maximum amount of attempts made to connect to the ElasticSearch server on startup.
- *
- * @default 10
- */
- connectionAttempts?: number;
- /**
- * @description
- * Interval in milliseconds between attempts to connect to the ElasticSearch server on startup.
- *
- * @default 5000
- */
- connectionAttemptInterval?: number;
- /**
- * @description
- * Options to pass directly to the
- * [Elasticsearch Node.js client](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html). For example, to
- * set authentication or other more advanced options.
- * Note that if the `node` or `nodes` option is specified, it will override the values provided in the `host` and `port` options.
- */
- clientOptions?: ClientOptions;
- /**
- * @description
- * Prefix for the indices created by the plugin.
- *
- * @default
- * 'vendure-'
- */
- indexPrefix?: string;
- /**
- * @description
- * [These options](https://www.elastic.co/guide/en/elasticsearch/reference/7.x/index-modules.html#index-modules-settings)
- * are directly passed to index settings. To apply some settings indices will be recreated.
- *
- * @example
- * ```ts
- * // Configuring an English stemmer
- * indexSettings: {
- * analysis: {
- * analyzer: {
- * custom_analyzer: {
- * tokenizer: 'standard',
- * filter: [
- * 'lowercase',
- * 'english_stemmer'
- * ]
- * }
- * },
- * filter : {
- * english_stemmer : {
- * type : 'stemmer',
- * name : 'english'
- * }
- * }
- * }
- * },
- * ```
- * A more complete example can be found in the discussion thread
- * [How to make elastic plugin to search by substring with stemming](https://github.com/vendure-ecommerce/vendure/discussions/1066).
- *
- * @since 1.2.0
- * @default
- * {}
- */
- indexSettings?: object;
- /**
- * @description
- * This option allow to redefine or define new properties in mapping. More about elastic
- * [mapping](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html)
- * After changing this option indices will be recreated.
- *
- * @example
- * ```ts
- * // Configuring custom analyzer for the `productName` field.
- * indexMappingProperties: {
- * productName: {
- * type: 'text',
- * analyzer:'custom_analyzer',
- * fields: {
- * keyword: {
- * type: 'keyword',
- * ignore_above: 256,
- * }
- * }
- * }
- * }
- * ```
- *
- * To reference a field defined by `customProductMappings` or `customProductVariantMappings`, you will
- * need to prefix the name with `'product-<name>'` or `'variant-<name>'` respectively, e.g.:
- *
- * @example
- * ```ts
- * customProductMappings: {
- * variantCount: {
- * graphQlType: 'Int!',
- * valueFn: (product, variants) => variants.length,
- * },
- * },
- * indexMappingProperties: {
- * 'product-variantCount': {
- * type: 'integer',
- * }
- * }
- * ```
- *
- * @since 1.2.0
- * @default
- * {}
- */
- indexMappingProperties?: {
- [indexName: string]: object;
- };
- /**
- * @description
- * Batch size for bulk operations (e.g. when rebuilding the indices).
- *
- * @default
- * 2000
- */
- batchSize?: number;
- /**
- * @description
- * Configuration of the internal Elasticsearch query.
- */
- searchConfig?: SearchConfig;
- /**
- * @description
- * Custom mappings may be defined which will add the defined data to the
- * Elasticsearch index and expose that data via the SearchResult GraphQL type,
- * adding a new `customMappings`, `customProductMappings` & `customProductVariantMappings` fields.
- *
- * The `graphQlType` property may be one of `String`, `Int`, `Float`, `Boolean`, `ID` or list
- * versions thereof (`[String!]` etc) and can be appended with a `!` to indicate non-nullable fields.
- *
- * The `public` (default = `true`) property is used to reveal or hide the property in the GraphQL API schema.
- * If this property is set to `false` it's not accessible in the `customMappings` field but it's still getting
- * parsed to the elasticsearch index.
- *
- * This config option defines custom mappings which are accessible when the "groupByProduct"
- * input options is set to `true`. In addition, custom variant mappings can be accessed by using
- * the `customProductVariantMappings` field, which is always available.
- *
- * @example
- * ```ts
- * customProductMappings: {
- * variantCount: {
- * graphQlType: 'Int!',
- * valueFn: (product, variants) => variants.length,
- * },
- * reviewRating: {
- * graphQlType: 'Float',
- * public: true,
- * valueFn: product => (product.customFields as any).reviewRating,
- * },
- * priority: {
- * graphQlType: 'Int!',
- * public: false,
- * valueFn: product => (product.customFields as any).priority,
- * },
- * }
- * ```
- *
- * @example
- * ```graphql
- * query SearchProducts($input: SearchInput!) {
- * search(input: $input) {
- * totalItems
- * items {
- * productId
- * productName
- * customProductMappings {
- * variantCount
- * reviewRating
- * }
- * customMappings {
- * ...on CustomProductMappings {
- * variantCount
- * reviewRating
- * }
- * }
- * }
- * }
- * }
- * ```
- */
- customProductMappings?: {
- [fieldName: string]: CustomMapping<[Product, ProductVariant[], LanguageCode, Injector]>;
- };
- /**
- * @description
- * This config option defines custom mappings which are accessible when the "groupByProduct"
- * input options is set to `false`. In addition, custom product mappings can be accessed by using
- * the `customProductMappings` field, which is always available.
- *
- * @example
- * ```graphql
- * query SearchProducts($input: SearchInput!) {
- * search(input: $input) {
- * totalItems
- * items {
- * productId
- * productName
- * customProductVariantMappings {
- * weight
- * }
- * customMappings {
- * ...on CustomProductVariantMappings {
- * weight
- * }
- * }
- * }
- * }
- * }
- * ```
- */
- customProductVariantMappings?: {
- [fieldName: string]: CustomMapping<[ProductVariant, LanguageCode, Injector]>;
- };
- /**
- * @description
- * If set to `true`, updates to Products, ProductVariants and Collections will not immediately
- * trigger an update to the search index. Instead, all these changes will be buffered and will
- * only be run via a call to the `runPendingSearchIndexUpdates` mutation in the Admin API.
- *
- * This is very useful for installations with a large number of ProductVariants and/or
- * Collections, as the buffering allows better control over when these expensive jobs are run,
- * and also performs optimizations to minimize the amount of work that needs to be performed by
- * the worker.
- *
- * @since 1.3.0
- * @default false
- */
- bufferUpdates?: boolean;
- /**
- * @description
- * Additional product relations that will be fetched from DB while reindexing. This can be used
- * in combination with `customProductMappings` to ensure that the required relations are joined
- * before the `product` object is passed to the `valueFn`.
- *
- * @example
- * ```ts
- * {
- * hydrateProductRelations: ['assets.asset'],
- * customProductMappings: {
- * assetPreviews: {
- * graphQlType: '[String!]',
- * // Here we can be sure that the `product.assets` array is populated
- * // with an Asset object
- * valueFn: (product) => product.assets.map(a => a.asset.preview),
- * }
- * }
- * }
- * ```
- *
- * @default []
- * @since 1.3.0
- */
- hydrateProductRelations?: Array<EntityRelationPaths<Product>>;
- /**
- * @description
- * Additional variant relations that will be fetched from DB while reindexing. See
- * `hydrateProductRelations` for more explanation and a usage example.
- *
- * @default []
- * @since 1.3.0
- */
- hydrateProductVariantRelations?: Array<EntityRelationPaths<ProductVariant>>;
- /**
- * @description
- * Allows the `SearchInput` type to be extended with new input fields. This allows arbitrary
- * data to be passed in, which can then be used e.g. in the `mapQuery()` function or
- * custom `scriptFields` functions.
- *
- * @example
- * ```ts
- * extendSearchInputType: {
- * longitude: 'Float',
- * latitude: 'Float',
- * radius: 'Float',
- * }
- * ```
- *
- * This allows the search query to include these new fields:
- *
- * @example
- * ```graphql
- * query {
- * search(input: {
- * longitude: 101.7117,
- * latitude: 3.1584,
- * radius: 50.00
- * }) {
- * items {
- * productName
- * }
- * }
- * }
- * ```
- *
- * @default {}
- * @since 1.3.0
- */
- 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
- * ```ts
- * extendSearchSortType: ["distance"]
- * ```
- *
- * will extend the `SearchResultSortParameter` input type like this:
- *
- * @example
- * ```GraphQl
- * extend input SearchResultSortParameter {
- * distance: SortOrder
- * }
- * ```
- *
- * @default []
- * @since 1.4.0
- */
- extendSearchSortType?: string[];
- }
- /**
- * @description
- * Configuration options for the internal Elasticsearch query which is generated when performing a search.
- *
- * @docsCategory core plugins/ElasticsearchPlugin
- * @docsPage ElasticsearchOptions
- */
- export interface SearchConfig {
- /**
- * @description
- * The maximum number of FacetValues to return from the search query. Internally, this
- * value sets the "size" property of an Elasticsearch aggregation.
- *
- * @default
- * 50
- */
- facetValueMaxSize?: number;
- /**
- * @description
- * The maximum number of Collections to return from the search query. Internally, this
- * value sets the "size" property of an Elasticsearch aggregation.
- *
- * @since 1.1.0
- * @default
- * 50
- */
- collectionMaxSize?: number;
- /**
- * @description
- * The maximum number of totalItems to return from the search query. Internally, this
- * value sets the "track_total_hits" property of an Elasticsearch query.
- * If this parameter is set to "True", accurate count of totalItems will be returned.
- * If this parameter is set to "False", totalItems will be returned as 0.
- * If this parameter is set to integer, accurate count of totalItems will be returned not bigger than integer.
- *
- * @since 1.2.0
- * @default
- * 10000
- */
- totalItemsMaxSize?: number | boolean;
- // prettier-ignore
- /**
- * @description
- * Defines the
- * [multi match type](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#multi-match-types)
- * used when matching against a search term.
- *
- * @default
- * 'best_fields'
- */
- multiMatchType?: 'best_fields' | 'most_fields' | 'cross_fields' | 'phrase' | 'phrase_prefix' | 'bool_prefix';
- /**
- * @description
- * Set custom boost values for particular fields when matching against a search term.
- */
- boostFields?: BoostFieldsConfig;
- /**
- * @description
- * The interval used to group search results into buckets according to price range. For example, setting this to
- * `2000` will group into buckets every $20.00:
- *
- * ```json
- * {
- * "data": {
- * "search": {
- * "totalItems": 32,
- * "priceRange": {
- * "buckets": [
- * {
- * "to": 2000,
- * "count": 21
- * },
- * {
- * "to": 4000,
- * "count": 7
- * },
- * {
- * "to": 6000,
- * "count": 3
- * },
- * {
- * "to": 12000,
- * "count": 1
- * }
- * ]
- * }
- * }
- * }
- * }
- * ```
- */
- priceRangeBucketInterval?: number;
- /**
- * @description
- * This config option allows the the modification of the whole (already built) search query. This allows
- * for e.g. wildcard / fuzzy searches on the index.
- *
- * @example
- * ```ts
- * mapQuery: (query, input, searchConfig, channelId, enabledOnly){
- * if(query.bool.must){
- * delete query.bool.must;
- * }
- * query.bool.should = [
- * {
- * query_string: {
- * query: "*" + term + "*",
- * fields: [
- * `productName^${searchConfig.boostFields.productName}`,
- * `productVariantName^${searchConfig.boostFields.productVariantName}`,
- * ]
- * }
- * },
- * {
- * multi_match: {
- * query: term,
- * type: searchConfig.multiMatchType,
- * fields: [
- * `description^${searchConfig.boostFields.description}`,
- * `sku^${searchConfig.boostFields.sku}`,
- * ],
- * },
- * },
- * ];
- *
- * return query;
- * }
- * ```
- */
- mapQuery?: (
- query: any,
- input: ElasticSearchInput,
- searchConfig: DeepRequired<SearchConfig>,
- channelId: ID,
- enabledOnly: boolean,
- ) => any;
- /**
- * @description
- * Sets `script_fields` inside the elasticsearch body which allows returning a script evaluation for each hit.
- *
- * The script field definition consists of three properties:
- *
- * * `graphQlType`: This is the type that will be returned when this script field is queried
- * via the GraphQL API. It may be one of `String`, `Int`, `Float`, `Boolean`, `ID` or list
- * versions thereof (`[String!]` etc) and can be appended with a `!` to indicate non-nullable fields.
- * * `context`: determines whether this script field is available when grouping by product. Can be
- * `product`, `variant` or `both`.
- * * `scriptFn`: This is the function to run on each hit. Should return an object with a `script` property,
- * as covered in the
- * [Elasticsearch script fields docs](https://www.elastic.co/guide/en/elasticsearch/reference/7.15/search-fields.html#script-fields)
- *
- * @example
- * ```ts
- * extendSearchInputType: {
- * latitude: 'Float',
- * longitude: 'Float',
- * },
- * 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', // contains function arcDistance
- * },
- * },
- * 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: {
- * scriptFields: {
- * distance: {
- * graphQlType: 'Float!',
- * // Run this script only when grouping results by product
- * context: 'product',
- * scriptFn: (input) => {
- * // The SearchInput was extended with latitude and longitude
- * // via the `extendSearchInputType` option above.
- * const lat = input.latitude;
- * const lon = input.longitude;
- * return {
- * script: `doc['product-location'].arcDistance(${lat}, ${lon})`,
- * }
- * }
- * }
- * }
- * }
- * ```
- *
- * @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)
- *
- * The `sort` input parameter contains the ElasticSearchSortInput generated for the default sort parameters "name" and "price".
- * If neither of those are applied it will be empty.
- *
- * @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;
- }
- /**
- * @description
- * Configuration for [boosting](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#field-boost)
- * the scores of given fields when performing a search against a term.
- *
- * Boosting a field acts as a score multiplier for matches against that field.
- *
- * @docsCategory core plugins/ElasticsearchPlugin
- * @docsPage ElasticsearchOptions
- */
- export interface BoostFieldsConfig {
- /**
- * @description
- * Defines the boost factor for the productName field.
- *
- * @default 1
- */
- productName?: number;
- /**
- * @description
- * Defines the boost factor for the productVariantName field.
- *
- * @default 1
- */
- productVariantName?: number;
- /**
- * @description
- * Defines the boost factor for the description field.
- *
- * @default 1
- */
- description?: number;
- /**
- * @description
- * Defines the boost factor for the sku field.
- *
- * @default 1
- */
- sku?: number;
- }
- export type ElasticsearchRuntimeOptions = DeepRequired<Omit<ElasticsearchOptions, 'clientOptions'>> & {
- clientOptions?: ClientOptions;
- };
- export const defaultOptions: ElasticsearchRuntimeOptions = {
- host: 'http://localhost',
- port: 9200,
- connectionAttempts: 10,
- connectionAttemptInterval: 5000,
- indexPrefix: 'vendure-',
- indexSettings: {},
- indexMappingProperties: {},
- batchSize: 2000,
- searchConfig: {
- facetValueMaxSize: 50,
- collectionMaxSize: 50,
- totalItemsMaxSize: 10000,
- multiMatchType: 'best_fields',
- boostFields: {
- productName: 1,
- productVariantName: 1,
- description: 1,
- sku: 1,
- },
- priceRangeBucketInterval: 1000,
- mapQuery: query => query,
- mapSort: sort => sort,
- scriptFields: {},
- },
- customProductMappings: {},
- customProductVariantMappings: {},
- bufferUpdates: false,
- hydrateProductRelations: [],
- hydrateProductVariantRelations: [],
- extendSearchInputType: {},
- extendSearchSortType: [],
- };
- export function mergeWithDefaults(userOptions: ElasticsearchOptions): ElasticsearchRuntimeOptions {
- const { clientOptions, ...pluginOptions } = userOptions;
- const merged = deepmerge(defaultOptions, pluginOptions) as ElasticsearchRuntimeOptions;
- return { ...merged, clientOptions };
- }
|