options.ts 23 KB

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