options.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743
  1. import { ClientOptions } from '@elastic/elasticsearch';
  2. import {
  3. DeepRequired,
  4. EntityRelationPaths,
  5. ID,
  6. Injector,
  7. LanguageCode,
  8. Product,
  9. ProductVariant,
  10. } from '@vendure/core';
  11. import deepmerge from 'deepmerge';
  12. import {
  13. CustomMapping,
  14. CustomScriptMapping,
  15. ElasticSearchInput,
  16. ElasticSearchSortInput,
  17. ElasticSearchSortParameter,
  18. GraphQlPrimitive,
  19. PrimitiveTypeVariations,
  20. } from './types';
  21. /**
  22. * @description
  23. * Configuration options for the {@link ElasticsearchPlugin}.
  24. *
  25. * @docsCategory core plugins/ElasticsearchPlugin
  26. * @docsPage ElasticsearchOptions
  27. */
  28. export interface ElasticsearchOptions {
  29. /**
  30. * @description
  31. * The host of the Elasticsearch server. May also be specified in `clientOptions.node`.
  32. *
  33. * @default 'http://localhost'
  34. */
  35. host?: string;
  36. /**
  37. * @description
  38. * The port of the Elasticsearch server. May also be specified in `clientOptions.node`.
  39. *
  40. * @default 9200
  41. */
  42. port?: number;
  43. /**
  44. * @description
  45. * Maximum amount of attempts made to connect to the ElasticSearch server on startup.
  46. *
  47. * @default 10
  48. */
  49. connectionAttempts?: number;
  50. /**
  51. * @description
  52. * Interval in milliseconds between attempts to connect to the ElasticSearch server on startup.
  53. *
  54. * @default 5000
  55. */
  56. connectionAttemptInterval?: number;
  57. /**
  58. * @description
  59. * Options to pass directly to the
  60. * [Elasticsearch Node.js client](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html). For example, to
  61. * set authentication or other more advanced options.
  62. * Note that if the `node` or `nodes` option is specified, it will override the values provided in the `host` and `port` options.
  63. */
  64. clientOptions?: ClientOptions;
  65. /**
  66. * @description
  67. * Prefix for the indices created by the plugin.
  68. *
  69. * @default
  70. * 'vendure-'
  71. */
  72. indexPrefix?: string;
  73. /**
  74. * @description
  75. * [These options](https://www.elastic.co/guide/en/elasticsearch/reference/7.x/index-modules.html#index-modules-settings)
  76. * are directly passed to index settings. To apply some settings indices will be recreated.
  77. *
  78. * @example
  79. * ```ts
  80. * // Configuring an English stemmer
  81. * indexSettings: {
  82. * analysis: {
  83. * analyzer: {
  84. * custom_analyzer: {
  85. * tokenizer: 'standard',
  86. * filter: [
  87. * 'lowercase',
  88. * 'english_stemmer'
  89. * ]
  90. * }
  91. * },
  92. * filter : {
  93. * english_stemmer : {
  94. * type : 'stemmer',
  95. * name : 'english'
  96. * }
  97. * }
  98. * }
  99. * },
  100. * ```
  101. * A more complete example can be found in the discussion thread
  102. * [How to make elastic plugin to search by substring with stemming](https://github.com/vendure-ecommerce/vendure/discussions/1066).
  103. *
  104. * @since 1.2.0
  105. * @default
  106. * {}
  107. */
  108. indexSettings?: object;
  109. /**
  110. * @description
  111. * This option allow to redefine or define new properties in mapping. More about elastic
  112. * [mapping](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html)
  113. * After changing this option indices will be recreated.
  114. *
  115. * @example
  116. * ```ts
  117. * // Configuring custom analyzer for the `productName` field.
  118. * indexMappingProperties: {
  119. * productName: {
  120. * type: 'text',
  121. * analyzer:'custom_analyzer',
  122. * fields: {
  123. * keyword: {
  124. * type: 'keyword',
  125. * ignore_above: 256,
  126. * }
  127. * }
  128. * }
  129. * }
  130. * ```
  131. *
  132. * To reference a field defined by `customProductMappings` or `customProductVariantMappings`, you will
  133. * need to prefix the name with `'product-<name>'` or `'variant-<name>'` respectively, e.g.:
  134. *
  135. * @example
  136. * ```ts
  137. * customProductMappings: {
  138. * variantCount: {
  139. * graphQlType: 'Int!',
  140. * valueFn: (product, variants) => variants.length,
  141. * },
  142. * },
  143. * indexMappingProperties: {
  144. * 'product-variantCount': {
  145. * type: 'integer',
  146. * }
  147. * }
  148. * ```
  149. *
  150. * @since 1.2.0
  151. * @default
  152. * {}
  153. */
  154. indexMappingProperties?: {
  155. [indexName: string]: object;
  156. };
  157. /**
  158. * @description
  159. * Batch size for bulk operations (e.g. when rebuilding the indices).
  160. *
  161. * @default
  162. * 2000
  163. */
  164. batchSize?: number;
  165. /**
  166. * @description
  167. * Configuration of the internal Elasticsearch query.
  168. */
  169. searchConfig?: SearchConfig;
  170. /**
  171. * @description
  172. * Custom mappings may be defined which will add the defined data to the
  173. * Elasticsearch index and expose that data via the SearchResult GraphQL type,
  174. * adding a new `customMappings`, `customProductMappings` & `customProductVariantMappings` fields.
  175. *
  176. * The `graphQlType` property may be one of `String`, `Int`, `Float`, `Boolean`, `ID` or list
  177. * versions thereof (`[String!]` etc) and can be appended with a `!` to indicate non-nullable fields.
  178. *
  179. * The `public` (default = `true`) property is used to reveal or hide the property in the GraphQL API schema.
  180. * If this property is set to `false` it's not accessible in the `customMappings` field but it's still getting
  181. * parsed to the elasticsearch index.
  182. *
  183. * This config option defines custom mappings which are accessible when the "groupByProduct"
  184. * input options is set to `true`. In addition, custom variant mappings can be accessed by using
  185. * the `customProductVariantMappings` field, which is always available.
  186. *
  187. * @example
  188. * ```ts
  189. * customProductMappings: {
  190. * variantCount: {
  191. * graphQlType: 'Int!',
  192. * valueFn: (product, variants) => variants.length,
  193. * },
  194. * reviewRating: {
  195. * graphQlType: 'Float',
  196. * public: true,
  197. * valueFn: product => (product.customFields as any).reviewRating,
  198. * },
  199. * priority: {
  200. * graphQlType: 'Int!',
  201. * public: false,
  202. * valueFn: product => (product.customFields as any).priority,
  203. * },
  204. * }
  205. * ```
  206. *
  207. * @example
  208. * ```graphql
  209. * query SearchProducts($input: SearchInput!) {
  210. * search(input: $input) {
  211. * totalItems
  212. * items {
  213. * productId
  214. * productName
  215. * customProductMappings {
  216. * variantCount
  217. * reviewRating
  218. * }
  219. * customMappings {
  220. * ...on CustomProductMappings {
  221. * variantCount
  222. * reviewRating
  223. * }
  224. * }
  225. * }
  226. * }
  227. * }
  228. * ```
  229. */
  230. customProductMappings?: {
  231. [fieldName: string]: CustomMapping<[Product, ProductVariant[], LanguageCode, Injector]>;
  232. };
  233. /**
  234. * @description
  235. * This config option defines custom mappings which are accessible when the "groupByProduct"
  236. * input options is set to `false`. In addition, custom product mappings can be accessed by using
  237. * the `customProductMappings` field, which is always available.
  238. *
  239. * @example
  240. * ```graphql
  241. * query SearchProducts($input: SearchInput!) {
  242. * search(input: $input) {
  243. * totalItems
  244. * items {
  245. * productId
  246. * productName
  247. * customProductVariantMappings {
  248. * weight
  249. * }
  250. * customMappings {
  251. * ...on CustomProductVariantMappings {
  252. * weight
  253. * }
  254. * }
  255. * }
  256. * }
  257. * }
  258. * ```
  259. */
  260. customProductVariantMappings?: {
  261. [fieldName: string]: CustomMapping<[ProductVariant, LanguageCode, Injector]>;
  262. };
  263. /**
  264. * @description
  265. * If set to `true`, updates to Products, ProductVariants and Collections will not immediately
  266. * trigger an update to the search index. Instead, all these changes will be buffered and will
  267. * only be run via a call to the `runPendingSearchIndexUpdates` mutation in the Admin API.
  268. *
  269. * This is very useful for installations with a large number of ProductVariants and/or
  270. * Collections, as the buffering allows better control over when these expensive jobs are run,
  271. * and also performs optimizations to minimize the amount of work that needs to be performed by
  272. * the worker.
  273. *
  274. * @since 1.3.0
  275. * @default false
  276. */
  277. bufferUpdates?: boolean;
  278. /**
  279. * @description
  280. * Additional product relations that will be fetched from DB while reindexing. This can be used
  281. * in combination with `customProductMappings` to ensure that the required relations are joined
  282. * before the `product` object is passed to the `valueFn`.
  283. *
  284. * @example
  285. * ```ts
  286. * {
  287. * hydrateProductRelations: ['assets.asset'],
  288. * customProductMappings: {
  289. * assetPreviews: {
  290. * graphQlType: '[String!]',
  291. * // Here we can be sure that the `product.assets` array is populated
  292. * // with an Asset object
  293. * valueFn: (product) => product.assets.map(a => a.asset.preview),
  294. * }
  295. * }
  296. * }
  297. * ```
  298. *
  299. * @default []
  300. * @since 1.3.0
  301. */
  302. hydrateProductRelations?: Array<EntityRelationPaths<Product>>;
  303. /**
  304. * @description
  305. * Additional variant relations that will be fetched from DB while reindexing. See
  306. * `hydrateProductRelations` for more explanation and a usage example.
  307. *
  308. * @default []
  309. * @since 1.3.0
  310. */
  311. hydrateProductVariantRelations?: Array<EntityRelationPaths<ProductVariant>>;
  312. /**
  313. * @description
  314. * Allows the `SearchInput` type to be extended with new input fields. This allows arbitrary
  315. * data to be passed in, which can then be used e.g. in the `mapQuery()` function or
  316. * custom `scriptFields` functions.
  317. *
  318. * @example
  319. * ```ts
  320. * extendSearchInputType: {
  321. * longitude: 'Float',
  322. * latitude: 'Float',
  323. * radius: 'Float',
  324. * }
  325. * ```
  326. *
  327. * This allows the search query to include these new fields:
  328. *
  329. * @example
  330. * ```graphql
  331. * query {
  332. * search(input: {
  333. * longitude: 101.7117,
  334. * latitude: 3.1584,
  335. * radius: 50.00
  336. * }) {
  337. * items {
  338. * productName
  339. * }
  340. * }
  341. * }
  342. * ```
  343. *
  344. * @default {}
  345. * @since 1.3.0
  346. */
  347. extendSearchInputType?: {
  348. [name: string]: PrimitiveTypeVariations<GraphQlPrimitive>;
  349. };
  350. /**
  351. * @description
  352. * Adds a list of sort parameters. This is mostly important to make the
  353. * correct sort order values available inside `input` parameter of the `mapSort` option.
  354. *
  355. * @example
  356. * ```ts
  357. * extendSearchSortType: ["distance"]
  358. * ```
  359. *
  360. * will extend the `SearchResultSortParameter` input type like this:
  361. *
  362. * @example
  363. * ```GraphQl
  364. * extend input SearchResultSortParameter {
  365. * distance: SortOrder
  366. * }
  367. * ```
  368. *
  369. * @default []
  370. * @since 1.4.0
  371. */
  372. extendSearchSortType?: string[];
  373. }
  374. /**
  375. * @description
  376. * Configuration options for the internal Elasticsearch query which is generated when performing a search.
  377. *
  378. * @docsCategory core plugins/ElasticsearchPlugin
  379. * @docsPage ElasticsearchOptions
  380. */
  381. export interface SearchConfig {
  382. /**
  383. * @description
  384. * The maximum number of FacetValues to return from the search query. Internally, this
  385. * value sets the "size" property of an Elasticsearch aggregation.
  386. *
  387. * @default
  388. * 50
  389. */
  390. facetValueMaxSize?: number;
  391. /**
  392. * @description
  393. * The maximum number of Collections to return from the search query. Internally, this
  394. * value sets the "size" property of an Elasticsearch aggregation.
  395. *
  396. * @since 1.1.0
  397. * @default
  398. * 50
  399. */
  400. collectionMaxSize?: number;
  401. /**
  402. * @description
  403. * The maximum number of totalItems to return from the search query. Internally, this
  404. * value sets the "track_total_hits" property of an Elasticsearch query.
  405. * If this parameter is set to "True", accurate count of totalItems will be returned.
  406. * If this parameter is set to "False", totalItems will be returned as 0.
  407. * If this parameter is set to integer, accurate count of totalItems will be returned not bigger than integer.
  408. *
  409. * @since 1.2.0
  410. * @default
  411. * 10000
  412. */
  413. totalItemsMaxSize?: number | boolean;
  414. // prettier-ignore
  415. /**
  416. * @description
  417. * Defines the
  418. * [multi match type](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#multi-match-types)
  419. * used when matching against a search term.
  420. *
  421. * @default
  422. * 'best_fields'
  423. */
  424. multiMatchType?: 'best_fields' | 'most_fields' | 'cross_fields' | 'phrase' | 'phrase_prefix' | 'bool_prefix';
  425. /**
  426. * @description
  427. * Set custom boost values for particular fields when matching against a search term.
  428. */
  429. boostFields?: BoostFieldsConfig;
  430. /**
  431. * @description
  432. * The interval used to group search results into buckets according to price range. For example, setting this to
  433. * `2000` will group into buckets every $20.00:
  434. *
  435. * ```json
  436. * {
  437. * "data": {
  438. * "search": {
  439. * "totalItems": 32,
  440. * "priceRange": {
  441. * "buckets": [
  442. * {
  443. * "to": 2000,
  444. * "count": 21
  445. * },
  446. * {
  447. * "to": 4000,
  448. * "count": 7
  449. * },
  450. * {
  451. * "to": 6000,
  452. * "count": 3
  453. * },
  454. * {
  455. * "to": 12000,
  456. * "count": 1
  457. * }
  458. * ]
  459. * }
  460. * }
  461. * }
  462. * }
  463. * ```
  464. */
  465. priceRangeBucketInterval?: number;
  466. /**
  467. * @description
  468. * This config option allows the the modification of the whole (already built) search query. This allows
  469. * for e.g. wildcard / fuzzy searches on the index.
  470. *
  471. * @example
  472. * ```ts
  473. * mapQuery: (query, input, searchConfig, channelId, enabledOnly){
  474. * if(query.bool.must){
  475. * delete query.bool.must;
  476. * }
  477. * query.bool.should = [
  478. * {
  479. * query_string: {
  480. * query: "*" + term + "*",
  481. * fields: [
  482. * `productName^${searchConfig.boostFields.productName}`,
  483. * `productVariantName^${searchConfig.boostFields.productVariantName}`,
  484. * ]
  485. * }
  486. * },
  487. * {
  488. * multi_match: {
  489. * query: term,
  490. * type: searchConfig.multiMatchType,
  491. * fields: [
  492. * `description^${searchConfig.boostFields.description}`,
  493. * `sku^${searchConfig.boostFields.sku}`,
  494. * ],
  495. * },
  496. * },
  497. * ];
  498. *
  499. * return query;
  500. * }
  501. * ```
  502. */
  503. mapQuery?: (
  504. query: any,
  505. input: ElasticSearchInput,
  506. searchConfig: DeepRequired<SearchConfig>,
  507. channelId: ID,
  508. enabledOnly: boolean,
  509. ) => any;
  510. /**
  511. * @description
  512. * Sets `script_fields` inside the elasticsearch body which allows returning a script evaluation for each hit.
  513. *
  514. * The script field definition consists of three properties:
  515. *
  516. * * `graphQlType`: This is the type that will be returned when this script field is queried
  517. * via the GraphQL API. It may be one of `String`, `Int`, `Float`, `Boolean`, `ID` or list
  518. * versions thereof (`[String!]` etc) and can be appended with a `!` to indicate non-nullable fields.
  519. * * `context`: determines whether this script field is available when grouping by product. Can be
  520. * `product`, `variant` or `both`.
  521. * * `scriptFn`: This is the function to run on each hit. Should return an object with a `script` property,
  522. * as covered in the
  523. * [Elasticsearch script fields docs](https://www.elastic.co/guide/en/elasticsearch/reference/7.15/search-fields.html#script-fields)
  524. *
  525. * @example
  526. * ```ts
  527. * extendSearchInputType: {
  528. * latitude: 'Float',
  529. * longitude: 'Float',
  530. * },
  531. * indexMappingProperties: {
  532. * // The `product-location` field corresponds to the `location` customProductMapping
  533. * // defined below. Here we specify that it would be index as a `geo_point` type,
  534. * // which will allow us to perform geo-spacial calculations on it in our script field.
  535. * 'product-location': {
  536. * type: 'geo_point', // contains function arcDistance
  537. * },
  538. * },
  539. * customProductMappings: {
  540. * location: {
  541. * graphQlType: 'String',
  542. * valueFn: (product: Product) => {
  543. * // Assume that the Product entity has this customField defined
  544. * const custom = product.customFields.location;
  545. * return `${custom.latitude},${custom.longitude}`;
  546. * },
  547. * }
  548. * },
  549. * searchConfig: {
  550. * scriptFields: {
  551. * distance: {
  552. * graphQlType: 'Float!',
  553. * // Run this script only when grouping results by product
  554. * context: 'product',
  555. * scriptFn: (input) => {
  556. * // The SearchInput was extended with latitude and longitude
  557. * // via the `extendSearchInputType` option above.
  558. * const lat = input.latitude;
  559. * const lon = input.longitude;
  560. * return {
  561. * script: `doc['product-location'].arcDistance(${lat}, ${lon})`,
  562. * }
  563. * }
  564. * }
  565. * }
  566. * }
  567. * ```
  568. *
  569. * @since 1.3.0
  570. */
  571. scriptFields?: { [fieldName: string]: CustomScriptMapping<[ElasticSearchInput]> };
  572. /**
  573. * @description
  574. * Allows extending the `sort` input of the elasticsearch body as covered in
  575. * [Elasticsearch sort docs](https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html)
  576. *
  577. * The `sort` input parameter contains the ElasticSearchSortInput generated for the default sort parameters "name" and "price".
  578. * If neither of those are applied it will be empty.
  579. *
  580. * @example
  581. * ```ts
  582. * mapSort: (sort, input) => {
  583. * // Assuming `extendSearchSortType: ["priority"]`
  584. * // Assuming priority is never undefined
  585. * const { priority } = input.sort;
  586. * return [
  587. * ...sort,
  588. * {
  589. * // The `product-priority` field corresponds to the `priority` customProductMapping
  590. * // Depending on the index type, this field might require a
  591. * // more detailed input (example: 'productName.keyword')
  592. * ["product-priority"]: {
  593. * order: priority === SortOrder.ASC ? 'asc' : 'desc'
  594. * }
  595. * }
  596. * ];
  597. * }
  598. * ```
  599. *
  600. * A more generic example would be a sort function based on a product location like this:
  601. * @example
  602. * ```ts
  603. * extendSearchInputType: {
  604. * latitude: 'Float',
  605. * longitude: 'Float',
  606. * },
  607. * extendSearchSortType: ["distance"],
  608. * indexMappingProperties: {
  609. * // The `product-location` field corresponds to the `location` customProductMapping
  610. * // defined below. Here we specify that it would be index as a `geo_point` type,
  611. * // which will allow us to perform geo-spacial calculations on it in our script field.
  612. * 'product-location': {
  613. * type: 'geo_point',
  614. * },
  615. * },
  616. * customProductMappings: {
  617. * location: {
  618. * graphQlType: 'String',
  619. * valueFn: (product: Product) => {
  620. * // Assume that the Product entity has this customField defined
  621. * const custom = product.customFields.location;
  622. * return `${custom.latitude},${custom.longitude}`;
  623. * },
  624. * }
  625. * },
  626. * searchConfig: {
  627. * mapSort: (sort, input) => {
  628. * // Assuming distance is never undefined
  629. * const { distance } = input.sort;
  630. * return [
  631. * ...sort,
  632. * {
  633. * ["_geo_distance"]: {
  634. * "product-location": [
  635. * input.longitude,
  636. * input.latitude
  637. * ],
  638. * order: distance === SortOrder.ASC ? 'asc' : 'desc',
  639. * unit: "km"
  640. * }
  641. * }
  642. * ];
  643. * }
  644. * }
  645. * ```
  646. *
  647. * @default {}
  648. * @since 1.4.0
  649. */
  650. mapSort?: (sort: ElasticSearchSortInput, input: ElasticSearchInput) => ElasticSearchSortInput;
  651. }
  652. /**
  653. * @description
  654. * Configuration for [boosting](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#field-boost)
  655. * the scores of given fields when performing a search against a term.
  656. *
  657. * Boosting a field acts as a score multiplier for matches against that field.
  658. *
  659. * @docsCategory core plugins/ElasticsearchPlugin
  660. * @docsPage ElasticsearchOptions
  661. */
  662. export interface BoostFieldsConfig {
  663. /**
  664. * @description
  665. * Defines the boost factor for the productName field.
  666. *
  667. * @default 1
  668. */
  669. productName?: number;
  670. /**
  671. * @description
  672. * Defines the boost factor for the productVariantName field.
  673. *
  674. * @default 1
  675. */
  676. productVariantName?: number;
  677. /**
  678. * @description
  679. * Defines the boost factor for the description field.
  680. *
  681. * @default 1
  682. */
  683. description?: number;
  684. /**
  685. * @description
  686. * Defines the boost factor for the sku field.
  687. *
  688. * @default 1
  689. */
  690. sku?: number;
  691. }
  692. export type ElasticsearchRuntimeOptions = DeepRequired<Omit<ElasticsearchOptions, 'clientOptions'>> & {
  693. clientOptions?: ClientOptions;
  694. };
  695. export const defaultOptions: ElasticsearchRuntimeOptions = {
  696. host: 'http://localhost',
  697. port: 9200,
  698. connectionAttempts: 10,
  699. connectionAttemptInterval: 5000,
  700. indexPrefix: 'vendure-',
  701. indexSettings: {},
  702. indexMappingProperties: {},
  703. batchSize: 2000,
  704. searchConfig: {
  705. facetValueMaxSize: 50,
  706. collectionMaxSize: 50,
  707. totalItemsMaxSize: 10000,
  708. multiMatchType: 'best_fields',
  709. boostFields: {
  710. productName: 1,
  711. productVariantName: 1,
  712. description: 1,
  713. sku: 1,
  714. },
  715. priceRangeBucketInterval: 1000,
  716. mapQuery: query => query,
  717. mapSort: sort => sort,
  718. scriptFields: {},
  719. },
  720. customProductMappings: {},
  721. customProductVariantMappings: {},
  722. bufferUpdates: false,
  723. hydrateProductRelations: [],
  724. hydrateProductVariantRelations: [],
  725. extendSearchInputType: {},
  726. extendSearchSortType: [],
  727. };
  728. export function mergeWithDefaults(userOptions: ElasticsearchOptions): ElasticsearchRuntimeOptions {
  729. const { clientOptions, ...pluginOptions } = userOptions;
  730. const merged = deepmerge(defaultOptions, pluginOptions) as ElasticsearchRuntimeOptions;
  731. return { ...merged, clientOptions };
  732. }