build-elastic-body.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import { LanguageCode, LogicalOperator, PriceRange, SortOrder } from '@vendure/common/lib/generated-types';
  2. import { DeepRequired, ID, RequestContext, UserInputError } from '@vendure/core';
  3. import { SearchConfig } from './options';
  4. import { CustomScriptMapping, ElasticSearchInput, ElasticSearchSortInput, SearchRequestBody } from './types';
  5. /**
  6. * Given a SearchInput object, returns the corresponding Elasticsearch body.
  7. */
  8. export function buildElasticBody(
  9. input: ElasticSearchInput,
  10. searchConfig: DeepRequired<SearchConfig>,
  11. channelId: ID,
  12. languageCode: LanguageCode,
  13. enabledOnly: boolean = false,
  14. ctx: RequestContext,
  15. ): SearchRequestBody {
  16. const {
  17. term,
  18. facetValueIds,
  19. facetValueOperator,
  20. collectionId,
  21. collectionSlug,
  22. groupByProduct,
  23. skip,
  24. take,
  25. sort,
  26. priceRangeWithTax,
  27. priceRange,
  28. facetValueFilters,
  29. inStock,
  30. } = input;
  31. const query: any = {
  32. bool: {},
  33. };
  34. ensureBoolFilterExists(query);
  35. query.bool.filter.push({ term: { channelId } });
  36. query.bool.filter.push({ term: { languageCode } });
  37. if (term) {
  38. query.bool.must = [
  39. {
  40. multi_match: {
  41. query: term,
  42. fuzziness: 'AUTO',
  43. type: searchConfig.multiMatchType,
  44. fields: [
  45. `productName^${searchConfig.boostFields.productName}`,
  46. `productVariantName^${searchConfig.boostFields.productVariantName}`,
  47. `description^${searchConfig.boostFields.description}`,
  48. `sku^${searchConfig.boostFields.sku}`,
  49. ],
  50. },
  51. },
  52. ];
  53. }
  54. if (facetValueIds && facetValueIds.length) {
  55. ensureBoolFilterExists(query);
  56. const operator = facetValueOperator === LogicalOperator.AND ? 'must' : 'should';
  57. query.bool.filter = query.bool.filter.concat([
  58. {
  59. bool: { [operator]: facetValueIds.map(id => ({ term: { facetValueIds: id } })) },
  60. },
  61. ]);
  62. }
  63. if (facetValueFilters && facetValueFilters.length) {
  64. ensureBoolFilterExists(query);
  65. facetValueFilters.forEach(facetValueFilter => {
  66. if (facetValueFilter.and && facetValueFilter.or && facetValueFilter.or.length) {
  67. throw new UserInputError('error.facetfilterinput-invalid-input');
  68. }
  69. if (facetValueFilter.and) {
  70. query.bool.filter.push({ term: { facetValueIds: facetValueFilter.and } });
  71. }
  72. if (facetValueFilter.or && facetValueFilter.or.length) {
  73. query.bool.filter.push({
  74. bool: { ['should']: facetValueFilter.or.map(id => ({ term: { facetValueIds: id } })) },
  75. });
  76. }
  77. });
  78. }
  79. if (collectionId) {
  80. ensureBoolFilterExists(query);
  81. query.bool.filter.push({ term: { collectionIds: collectionId } });
  82. }
  83. if (collectionSlug) {
  84. ensureBoolFilterExists(query);
  85. query.bool.filter.push({ term: { collectionSlugs: collectionSlug } });
  86. }
  87. if (enabledOnly) {
  88. ensureBoolFilterExists(query);
  89. query.bool.filter.push({ term: { enabled: true } });
  90. }
  91. if (priceRange) {
  92. ensureBoolFilterExists(query);
  93. query.bool.filter = query.bool.filter.concat(createPriceFilters(priceRange, false));
  94. }
  95. if (priceRangeWithTax) {
  96. ensureBoolFilterExists(query);
  97. query.bool.filter = query.bool.filter.concat(createPriceFilters(priceRangeWithTax, true));
  98. }
  99. if (inStock !== undefined) {
  100. ensureBoolFilterExists(query);
  101. if (groupByProduct) {
  102. query.bool.filter.push({ term: { productInStock: inStock } });
  103. } else {
  104. query.bool.filter.push({ term: { inStock } });
  105. }
  106. }
  107. const sortArray: ElasticSearchSortInput = [];
  108. if (sort) {
  109. if (sort.name) {
  110. sortArray.push({
  111. 'productName.keyword': { order: sort.name === SortOrder.ASC ? 'asc' : 'desc' },
  112. });
  113. }
  114. if (sort.price) {
  115. const priceField = 'price';
  116. sortArray.push({ [priceField]: { order: sort.price === SortOrder.ASC ? 'asc' : 'desc' } });
  117. }
  118. }
  119. const scriptFields: any | undefined = createScriptFields(
  120. searchConfig.scriptFields,
  121. input,
  122. groupByProduct,
  123. );
  124. const body: SearchRequestBody = {
  125. query: searchConfig.mapQuery
  126. ? searchConfig.mapQuery(query, input, searchConfig, channelId, enabledOnly, ctx)
  127. : query,
  128. sort: searchConfig.mapSort ? searchConfig.mapSort(sortArray, input) : sortArray,
  129. from: skip || 0,
  130. size: take || 10,
  131. track_total_hits: searchConfig.totalItemsMaxSize,
  132. ...(scriptFields !== undefined
  133. ? {
  134. _source: true,
  135. script_fields: scriptFields,
  136. }
  137. : undefined),
  138. };
  139. if (groupByProduct) {
  140. body.collapse = { field: 'productId' };
  141. }
  142. return body;
  143. }
  144. function ensureBoolFilterExists(query: { bool: { filter?: any } }) {
  145. if (!query.bool.filter) {
  146. query.bool.filter = [];
  147. }
  148. }
  149. function createScriptFields(
  150. scriptFields: { [fieldName: string]: CustomScriptMapping<[ElasticSearchInput]> },
  151. input: ElasticSearchInput,
  152. groupByProduct?: boolean,
  153. ): any | undefined {
  154. if (scriptFields) {
  155. const fields = Object.keys(scriptFields);
  156. if (fields.length) {
  157. const result: any = {};
  158. for (const name of fields) {
  159. const scriptField = scriptFields[name];
  160. if (scriptField.context === 'product' && groupByProduct === true) {
  161. result[name] = scriptField.scriptFn(input);
  162. }
  163. if (scriptField.context === 'variant' && groupByProduct === false) {
  164. result[name] = scriptField.scriptFn(input);
  165. }
  166. if (scriptField.context === 'both' || scriptField.context === undefined) {
  167. result[name] = scriptField.scriptFn(input);
  168. }
  169. }
  170. return result;
  171. }
  172. }
  173. return undefined;
  174. }
  175. function createPriceFilters(range: PriceRange, withTax: boolean): any[] {
  176. const withTaxFix = withTax ? 'WithTax' : '';
  177. return [
  178. {
  179. range: {
  180. ['price' + withTaxFix]: {
  181. gte: range.min,
  182. lte: range.max,
  183. },
  184. },
  185. },
  186. ];
  187. }