options.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. import { ClientOptions } from '@elastic/elasticsearch';
  2. import { DeepRequired, ID, LanguageCode, Product, ProductVariant } from '@vendure/core';
  3. import deepmerge from 'deepmerge';
  4. import { CustomMapping, ElasticSearchInput } from './types';
  5. /**
  6. * @description
  7. * Configuration options for the {@link ElasticsearchPlugin}.
  8. *
  9. * @docsCategory ElasticsearchPlugin
  10. * @docsPage ElasticsearchOptions
  11. */
  12. export interface ElasticsearchOptions {
  13. /**
  14. * @description
  15. * The host of the Elasticsearch server. May also be specified in `clientOptions.node`.
  16. *
  17. * @default 'http://localhost'
  18. */
  19. host?: string;
  20. /**
  21. * @description
  22. * The port of the Elasticsearch server. May also be specified in `clientOptions.node`.
  23. *
  24. * @default 9200
  25. */
  26. port?: number;
  27. /**
  28. * @description
  29. * Maximum amount of attempts made to connect to the ElasticSearch server on startup.
  30. *
  31. * @default 10
  32. */
  33. connectionAttempts?: number;
  34. /**
  35. * @description
  36. * Interval in milliseconds between attempts to connect to the ElasticSearch server on startup.
  37. *
  38. * @default 5000
  39. */
  40. connectionAttemptInterval?: number;
  41. /**
  42. * @description
  43. * Options to pass directly to the
  44. * [Elasticsearch Node.js client](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html). For example, to
  45. * set authentication or other more advanced options.
  46. * Note that if the `node` or `nodes` option is specified, it will override the values provided in the `host` and `port` options.
  47. */
  48. clientOptions?: ClientOptions;
  49. /**
  50. * @description
  51. * Prefix for the indices created by the plugin.
  52. *
  53. * @default
  54. * 'vendure-'
  55. */
  56. indexPrefix?: string;
  57. /**
  58. * @description
  59. * [These options](https://www.elastic.co/guide/en/elasticsearch/reference/7.x/index-modules.html#index-modules-settings)
  60. * are directly passed to index settings. To apply some settings indices will be recreated.
  61. *
  62. * @example
  63. * ```TypeScript
  64. * // Configuring an English stemmer
  65. * indexSettings: {
  66. * analysis: {
  67. * analyzer: {
  68. * custom_analyzer: {
  69. * tokenizer: 'standard',
  70. * filter: [
  71. * 'lowercase',
  72. * 'english_stemmer'
  73. * ]
  74. * }
  75. * },
  76. * filter : {
  77. * english_stemmer : {
  78. * type : 'stemmer',
  79. * name : 'english'
  80. * }
  81. * }
  82. * }
  83. * },
  84. * ```
  85. *
  86. * @since 1.2.0
  87. * @default
  88. * {}
  89. */
  90. indexSettings?: object;
  91. /**
  92. * @description
  93. * This option allow to redefine or define new properties in mapping. More about elastic
  94. * [mapping](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html)
  95. * After changing this option indices will be recreated.
  96. *
  97. * @example
  98. * ```TypeScript
  99. * // Configuring custom analyzer for the `productName` field.
  100. * indexMappingProperties: {
  101. * productName: {
  102. * type: 'text',
  103. * analyzer:'custom_analyzer',
  104. * fields: {
  105. * keyword: {
  106. * type: 'keyword',
  107. * ignore_above: 256,
  108. * }
  109. * }
  110. * }
  111. * }
  112. * ```
  113. *
  114. * @since 1.2.0
  115. * @default
  116. * {}
  117. */
  118. indexMappingProperties?: object;
  119. /**
  120. * @description
  121. * Batch size for bulk operations (e.g. when rebuilding the indices).
  122. *
  123. * @default
  124. * 2000
  125. */
  126. batchSize?: number;
  127. /**
  128. * @description
  129. * Configuration of the internal Elasticseach query.
  130. */
  131. searchConfig?: SearchConfig;
  132. /**
  133. * @description
  134. * Custom mappings may be defined which will add the defined data to the
  135. * Elasticsearch index and expose that data via the SearchResult GraphQL type,
  136. * adding a new `customMappings` field.
  137. *
  138. * The `graphQlType` property may be one of `String`, `Int`, `Float`, `Boolean` and
  139. * can be appended with a `!` to indicate non-nullable fields.
  140. *
  141. * This config option defines custom mappings which are accessible when the "groupByProduct"
  142. * input options is set to `true`.
  143. *
  144. * @example
  145. * ```TypeScript
  146. * customProductMappings: {
  147. * variantCount: {
  148. * graphQlType: 'Int!',
  149. * valueFn: (product, variants) => variants.length,
  150. * },
  151. * reviewRating: {
  152. * graphQlType: 'Float',
  153. * valueFn: product => (product.customFields as any).reviewRating,
  154. * },
  155. * }
  156. * ```
  157. *
  158. * @example
  159. * ```SDL
  160. * query SearchProducts($input: SearchInput!) {
  161. * search(input: $input) {
  162. * totalItems
  163. * items {
  164. * productId
  165. * productName
  166. * customMappings {
  167. * ...on CustomProductMappings {
  168. * variantCount
  169. * reviewRating
  170. * }
  171. * }
  172. * }
  173. * }
  174. * }
  175. * ```
  176. */
  177. customProductMappings?: {
  178. [fieldName: string]: CustomMapping<[Product, ProductVariant[], LanguageCode]>;
  179. };
  180. /**
  181. * @description
  182. * This config option defines custom mappings which are accessible when the "groupByProduct"
  183. * input options is set to `false`.
  184. *
  185. * @example
  186. * ```SDL
  187. * query SearchProducts($input: SearchInput!) {
  188. * search(input: $input) {
  189. * totalItems
  190. * items {
  191. * productId
  192. * productName
  193. * customMappings {
  194. * ...on CustomProductVariantMappings {
  195. * weight
  196. * }
  197. * }
  198. * }
  199. * }
  200. * }
  201. * ```
  202. */
  203. customProductVariantMappings?: {
  204. [fieldName: string]: CustomMapping<[ProductVariant, LanguageCode]>;
  205. };
  206. }
  207. /**
  208. * @description
  209. * Configuration options for the internal Elasticsearch query which is generated when performing a search.
  210. *
  211. * @docsCategory ElasticsearchPlugin
  212. * @docsPage ElasticsearchOptions
  213. */
  214. export interface SearchConfig {
  215. /**
  216. * @description
  217. * The maximum number of FacetValues to return from the search query. Internally, this
  218. * value sets the "size" property of an Elasticsearch aggregation.
  219. *
  220. * @default
  221. * 50
  222. */
  223. facetValueMaxSize?: number;
  224. /**
  225. * @description
  226. * The maximum number of Collections to return from the search query. Internally, this
  227. * value sets the "size" property of an Elasticsearch aggregation.
  228. *
  229. * @since 1.1.0
  230. * @default
  231. * 50
  232. */
  233. collectionMaxSize?: number;
  234. /**
  235. * @description
  236. * The maximum number of totalItems to return from the search query. Internally, this
  237. * value sets the "track_total_hits" property of an Elasticsearch query.
  238. * If this parameter is set to "True", accurate count of totalItems will be returned.
  239. * If this parameter is set to "False", totalItems will be returned as 0.
  240. * If this parameter is set to integer, accurate count of totalItems will be returned not bigger than integer.
  241. *
  242. * @since 1.2.0
  243. * @default
  244. * 10000
  245. */
  246. totalItemsMaxSize?: number | boolean;
  247. // prettier-ignore
  248. /**
  249. * @description
  250. * Defines the
  251. * [multi match type](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#multi-match-types)
  252. * used when matching against a search term.
  253. *
  254. * @default
  255. * 'best_fields'
  256. */
  257. multiMatchType?: 'best_fields' | 'most_fields' | 'cross_fields' | 'phrase' | 'phrase_prefix' | 'bool_prefix';
  258. /**
  259. * @description
  260. * Set custom boost values for particular fields when matching against a search term.
  261. */
  262. boostFields?: BoostFieldsConfig;
  263. /**
  264. * @description
  265. * The interval used to group search results into buckets according to price range. For example, setting this to
  266. * `2000` will group into buckets every $20.00:
  267. *
  268. * ```JSON
  269. * {
  270. * "data": {
  271. * "search": {
  272. * "totalItems": 32,
  273. * "priceRange": {
  274. * "buckets": [
  275. * {
  276. * "to": 2000,
  277. * "count": 21
  278. * },
  279. * {
  280. * "to": 4000,
  281. * "count": 7
  282. * },
  283. * {
  284. * "to": 6000,
  285. * "count": 3
  286. * },
  287. * {
  288. * "to": 12000,
  289. * "count": 1
  290. * }
  291. * ]
  292. * }
  293. * }
  294. * }
  295. * }
  296. * ```
  297. */
  298. priceRangeBucketInterval?: number;
  299. /**
  300. * @description
  301. * This config option allows the the modification of the whole (already built) search query. This allows
  302. * for e.g. wildcard / fuzzy searches on the index.
  303. *
  304. * @example
  305. * ```TypeScript
  306. * mapQuery: (query, input, searchConfig, channelId, enabledOnly){
  307. * if(query.bool.must){
  308. * delete query.bool.must;
  309. * }
  310. * query.bool.should = [
  311. * {
  312. * query_string: {
  313. * query: "*" + term + "*",
  314. * fields: [
  315. * `productName^${searchConfig.boostFields.productName}`,
  316. * `productVariantName^${searchConfig.boostFields.productVariantName}`,
  317. * ]
  318. * }
  319. * },
  320. * {
  321. * multi_match: {
  322. * query: term,
  323. * type: searchConfig.multiMatchType,
  324. * fields: [
  325. * `description^${searchConfig.boostFields.description}`,
  326. * `sku^${searchConfig.boostFields.sku}`,
  327. * ],
  328. * },
  329. * },
  330. * ];
  331. *
  332. * return query;
  333. * }
  334. * ```
  335. */
  336. mapQuery?: (
  337. query: any,
  338. input: ElasticSearchInput,
  339. searchConfig: DeepRequired<SearchConfig>,
  340. channelId: ID,
  341. enabledOnly: boolean,
  342. ) => any;
  343. }
  344. /**
  345. * @description
  346. * Configuration for [boosting](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#field-boost)
  347. * the scores of given fields when performing a search against a term.
  348. *
  349. * Boosting a field acts as a score multiplier for matches against that field.
  350. *
  351. * @docsCategory ElasticsearchPlugin
  352. * @docsPage ElasticsearchOptions
  353. */
  354. export interface BoostFieldsConfig {
  355. /**
  356. * @description
  357. * Defines the boost factor for the productName field.
  358. *
  359. * @default 1
  360. */
  361. productName?: number;
  362. /**
  363. * @description
  364. * Defines the boost factor for the productVariantName field.
  365. *
  366. * @default 1
  367. */
  368. productVariantName?: number;
  369. /**
  370. * @description
  371. * Defines the boost factor for the description field.
  372. *
  373. * @default 1
  374. */
  375. description?: number;
  376. /**
  377. * @description
  378. * Defines the boost factor for the sku field.
  379. *
  380. * @default 1
  381. */
  382. sku?: number;
  383. }
  384. export type ElasticsearchRuntimeOptions = DeepRequired<Omit<ElasticsearchOptions, 'clientOptions'>> & {
  385. clientOptions?: ClientOptions;
  386. };
  387. export const defaultOptions: ElasticsearchRuntimeOptions = {
  388. host: 'http://localhost',
  389. port: 9200,
  390. connectionAttempts: 10,
  391. connectionAttemptInterval: 5000,
  392. indexPrefix: 'vendure-',
  393. indexSettings: {},
  394. indexMappingProperties: {},
  395. batchSize: 2000,
  396. searchConfig: {
  397. facetValueMaxSize: 50,
  398. collectionMaxSize: 50,
  399. totalItemsMaxSize: 10000,
  400. multiMatchType: 'best_fields',
  401. boostFields: {
  402. productName: 1,
  403. productVariantName: 1,
  404. description: 1,
  405. sku: 1,
  406. },
  407. priceRangeBucketInterval: 1000,
  408. mapQuery: query => query,
  409. },
  410. customProductMappings: {},
  411. customProductVariantMappings: {},
  412. };
  413. export function mergeWithDefaults(userOptions: ElasticsearchOptions): ElasticsearchRuntimeOptions {
  414. const { clientOptions, ...pluginOptions } = userOptions;
  415. const merged = deepmerge(defaultOptions, pluginOptions) as ElasticsearchRuntimeOptions;
  416. return { ...merged, clientOptions };
  417. }