elasticlink 0.2.2-beta → 0.4.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +223 -177
  2. package/dist/__tests__/aggregation-builder.test.d.ts +2 -0
  3. package/dist/__tests__/aggregation-builder.test.d.ts.map +1 -0
  4. package/dist/__tests__/aggregation-builder.test.js +622 -0
  5. package/dist/__tests__/bulk.test.d.ts +2 -0
  6. package/dist/__tests__/bulk.test.d.ts.map +1 -0
  7. package/dist/__tests__/bulk.test.js +679 -0
  8. package/dist/__tests__/examples.test.d.ts +2 -0
  9. package/dist/__tests__/examples.test.d.ts.map +1 -0
  10. package/dist/__tests__/examples.test.js +2123 -0
  11. package/dist/__tests__/fixtures/finance.d.ts +58 -0
  12. package/dist/__tests__/fixtures/finance.d.ts.map +1 -0
  13. package/dist/__tests__/fixtures/finance.js +73 -0
  14. package/dist/__tests__/fixtures/legal.d.ts +14 -0
  15. package/dist/__tests__/fixtures/legal.d.ts.map +1 -0
  16. package/dist/__tests__/fixtures/legal.js +27 -0
  17. package/dist/__tests__/fixtures/real-estate.d.ts +31 -0
  18. package/dist/__tests__/fixtures/real-estate.d.ts.map +1 -0
  19. package/dist/__tests__/fixtures/real-estate.js +39 -0
  20. package/dist/__tests__/index-management.test.d.ts +2 -0
  21. package/dist/__tests__/index-management.test.d.ts.map +1 -0
  22. package/dist/__tests__/index-management.test.js +1699 -0
  23. package/dist/__tests__/integration/aggregation.integration.test.d.ts +2 -0
  24. package/dist/__tests__/integration/aggregation.integration.test.d.ts.map +1 -0
  25. package/dist/__tests__/integration/aggregation.integration.test.js +188 -0
  26. package/dist/__tests__/integration/bulk.integration.test.d.ts +2 -0
  27. package/dist/__tests__/integration/bulk.integration.test.d.ts.map +1 -0
  28. package/dist/__tests__/integration/bulk.integration.test.js +90 -0
  29. package/dist/__tests__/integration/fixtures/finance.d.ts +37 -0
  30. package/dist/__tests__/integration/fixtures/finance.d.ts.map +1 -0
  31. package/dist/__tests__/integration/fixtures/finance.js +58 -0
  32. package/dist/__tests__/integration/fixtures/legal.d.ts +38 -0
  33. package/dist/__tests__/integration/fixtures/legal.d.ts.map +1 -0
  34. package/dist/__tests__/integration/fixtures/legal.js +65 -0
  35. package/dist/__tests__/integration/fixtures/real-estate.d.ts +17 -0
  36. package/dist/__tests__/integration/fixtures/real-estate.d.ts.map +1 -0
  37. package/dist/__tests__/integration/fixtures/real-estate.js +28 -0
  38. package/dist/__tests__/integration/helpers.d.ts +15 -0
  39. package/dist/__tests__/integration/helpers.d.ts.map +1 -0
  40. package/dist/__tests__/integration/helpers.js +21 -0
  41. package/dist/__tests__/integration/index-management.integration.test.d.ts +2 -0
  42. package/dist/__tests__/integration/index-management.integration.test.d.ts.map +1 -0
  43. package/dist/__tests__/integration/index-management.integration.test.js +67 -0
  44. package/dist/__tests__/integration/multi-search.integration.test.d.ts +2 -0
  45. package/dist/__tests__/integration/multi-search.integration.test.d.ts.map +1 -0
  46. package/dist/__tests__/integration/multi-search.integration.test.js +49 -0
  47. package/dist/__tests__/integration/query.integration.test.d.ts +2 -0
  48. package/dist/__tests__/integration/query.integration.test.d.ts.map +1 -0
  49. package/dist/__tests__/integration/query.integration.test.js +101 -0
  50. package/dist/__tests__/integration/suggester.integration.test.d.ts +2 -0
  51. package/dist/__tests__/integration/suggester.integration.test.d.ts.map +1 -0
  52. package/dist/__tests__/integration/suggester.integration.test.js +42 -0
  53. package/dist/__tests__/multi-search.test.d.ts +2 -0
  54. package/dist/__tests__/multi-search.test.d.ts.map +1 -0
  55. package/dist/__tests__/multi-search.test.js +325 -0
  56. package/dist/__tests__/query-builder-extensions.test.d.ts +2 -0
  57. package/dist/__tests__/query-builder-extensions.test.d.ts.map +1 -0
  58. package/dist/__tests__/query-builder-extensions.test.js +436 -0
  59. package/dist/__tests__/query-builder.test.d.ts +2 -0
  60. package/dist/__tests__/query-builder.test.d.ts.map +1 -0
  61. package/dist/__tests__/query-builder.test.js +5482 -0
  62. package/dist/__tests__/settings-presets.test.d.ts +2 -0
  63. package/dist/__tests__/settings-presets.test.d.ts.map +1 -0
  64. package/dist/__tests__/settings-presets.test.js +183 -0
  65. package/dist/__tests__/suggester.test.d.ts +2 -0
  66. package/dist/__tests__/suggester.test.d.ts.map +1 -0
  67. package/dist/__tests__/suggester.test.js +1006 -0
  68. package/dist/aggregation.builder.d.ts +4 -2
  69. package/dist/aggregation.builder.d.ts.map +1 -1
  70. package/dist/aggregation.builder.js +102 -95
  71. package/dist/aggregation.types.d.ts +16 -14
  72. package/dist/aggregation.types.d.ts.map +1 -1
  73. package/dist/bulk.builder.d.ts +126 -2
  74. package/dist/bulk.builder.d.ts.map +1 -1
  75. package/dist/bulk.builder.js +11 -12
  76. package/dist/bulk.types.d.ts.map +1 -1
  77. package/dist/field.helpers.d.ts +92 -42
  78. package/dist/field.helpers.d.ts.map +1 -1
  79. package/dist/field.helpers.js +99 -20
  80. package/dist/field.types.d.ts +113 -6
  81. package/dist/field.types.d.ts.map +1 -1
  82. package/dist/field.types.js +3 -3
  83. package/dist/index-management.builder.d.ts +19 -14
  84. package/dist/index-management.builder.d.ts.map +1 -1
  85. package/dist/index-management.builder.js +36 -34
  86. package/dist/index-management.types.d.ts +32 -62
  87. package/dist/index-management.types.d.ts.map +1 -1
  88. package/dist/index-management.types.js +0 -1
  89. package/dist/index.d.ts +18 -13
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +12 -8
  92. package/dist/mapping.builder.d.ts +37 -0
  93. package/dist/mapping.builder.d.ts.map +1 -0
  94. package/dist/mapping.builder.js +38 -0
  95. package/dist/mapping.types.d.ts +105 -0
  96. package/dist/mapping.types.d.ts.map +1 -0
  97. package/dist/mapping.types.js +6 -0
  98. package/dist/multi-search.builder.d.ts +5 -3
  99. package/dist/multi-search.builder.d.ts.map +1 -1
  100. package/dist/multi-search.builder.js +6 -4
  101. package/dist/multi-search.types.d.ts +7 -6
  102. package/dist/multi-search.types.d.ts.map +1 -1
  103. package/dist/query.builder.d.ts +3 -11
  104. package/dist/query.builder.d.ts.map +1 -1
  105. package/dist/query.builder.js +118 -259
  106. package/dist/query.types.d.ts +140 -236
  107. package/dist/query.types.d.ts.map +1 -1
  108. package/dist/query.types.js +2 -1
  109. package/dist/settings.presets.d.ts +98 -0
  110. package/dist/settings.presets.d.ts.map +1 -0
  111. package/dist/settings.presets.js +115 -0
  112. package/dist/suggester.builder.d.ts +5 -3
  113. package/dist/suggester.builder.d.ts.map +1 -1
  114. package/dist/suggester.builder.js +3 -2
  115. package/dist/suggester.types.d.ts +5 -4
  116. package/dist/suggester.types.d.ts.map +1 -1
  117. package/dist/vector.types.d.ts.map +1 -1
  118. package/package.json +12 -9
@@ -0,0 +1,2123 @@
1
+ import { query, aggregations, suggest, msearch, bulk, indexBuilder, mappings, text, keyword, integer, float, long, date, scaledFloat, halfFloat, denseVector, geoPoint, nested, completion, percolator } from '..';
2
+ /**
3
+ * Real-world usage examples demonstrating elasticlink's capabilities.
4
+ * These tests showcase common search patterns and can serve as documentation.
5
+ */
6
+ // ---------------------------------------------------------------------------
7
+ // Mappings schemas — each defines field names and their ES types
8
+ // ---------------------------------------------------------------------------
9
+ const instrumentMappings = mappings({
10
+ isin: keyword(),
11
+ name: text(),
12
+ description: text(),
13
+ market_cap: long(),
14
+ asset_class: keyword(),
15
+ tags: keyword(),
16
+ listed_date: date(),
17
+ credit_rating: float()
18
+ });
19
+ const articleMappings = mappings({
20
+ id: keyword(),
21
+ title: text(),
22
+ content: text(),
23
+ author: keyword(),
24
+ published_date: date(),
25
+ updated_date: date(),
26
+ tags: keyword(),
27
+ comments: nested()
28
+ });
29
+ const documentMappings = mappings({
30
+ id: keyword(),
31
+ content: text(),
32
+ title: text(),
33
+ tags: keyword(),
34
+ published_date: date()
35
+ });
36
+ const restaurantMappings = mappings({
37
+ id: keyword(),
38
+ name: text(),
39
+ cuisine: keyword(),
40
+ location: geoPoint(),
41
+ rating: float()
42
+ });
43
+ const storeMappings = mappings({
44
+ id: keyword(),
45
+ name: text(),
46
+ coordinates: geoPoint(),
47
+ district: keyword(),
48
+ rating: float(),
49
+ item_count: integer()
50
+ });
51
+ const instrumentWithEmbeddingMappings = mappings({
52
+ isin: keyword(),
53
+ name: text(),
54
+ description: text(),
55
+ market_cap: long(),
56
+ asset_class: keyword(),
57
+ prospectus_url: keyword(),
58
+ embedding: denseVector({ dims: 384 })
59
+ });
60
+ const contentDocumentMappings = mappings({
61
+ id: keyword(),
62
+ title: text(),
63
+ content: text(),
64
+ author: keyword(),
65
+ published_date: date(),
66
+ tags: keyword(),
67
+ embedding: denseVector({ dims: 384 })
68
+ });
69
+ describe('Real-world Usage Examples', () => {
70
+ describe('E-commerce Product Search', () => {
71
+ it('should build a basic product search query', () => {
72
+ const searchTerm = 'equity';
73
+ const result = query(instrumentMappings)
74
+ .match('name', searchTerm, { operator: 'and', boost: 2 })
75
+ .from(0)
76
+ .size(20)
77
+ .build();
78
+ expect(result).toMatchInlineSnapshot(`
79
+ {
80
+ "from": 0,
81
+ "query": {
82
+ "match": {
83
+ "name": {
84
+ "boost": 2,
85
+ "operator": "and",
86
+ "query": "equity",
87
+ },
88
+ },
89
+ },
90
+ "size": 20,
91
+ }
92
+ `);
93
+ });
94
+ it('should build an advanced product search with filters and highlighting', () => {
95
+ const searchTerm = 'large-cap tech';
96
+ const category = 'technology';
97
+ const minPrice = 1_000_000_000;
98
+ const maxPrice = 5_000_000_000;
99
+ const result = query(instrumentMappings)
100
+ .bool()
101
+ .must((q) => q.match('name', searchTerm, { operator: 'and', boost: 2 }))
102
+ .should((q) => q.fuzzy('description', searchTerm, { fuzziness: 'AUTO' }))
103
+ .filter((q) => q.term('asset_class', category))
104
+ .filter((q) => q.range('market_cap', {
105
+ gte: minPrice,
106
+ lte: maxPrice
107
+ }))
108
+ .minimumShouldMatch(0)
109
+ .highlight(['name', 'description'], {
110
+ fragment_size: 150,
111
+ number_of_fragments: 2,
112
+ pre_tags: ['<mark>'],
113
+ post_tags: ['</mark>']
114
+ })
115
+ .timeout('5s')
116
+ .trackScores(true)
117
+ .from(0)
118
+ .size(20)
119
+ .sort('market_cap', 'asc')
120
+ .build();
121
+ expect(result).toMatchInlineSnapshot(`
122
+ {
123
+ "from": 0,
124
+ "highlight": {
125
+ "fields": {
126
+ "description": {
127
+ "fragment_size": 150,
128
+ "number_of_fragments": 2,
129
+ },
130
+ "name": {
131
+ "fragment_size": 150,
132
+ "number_of_fragments": 2,
133
+ },
134
+ },
135
+ "post_tags": [
136
+ "</mark>",
137
+ ],
138
+ "pre_tags": [
139
+ "<mark>",
140
+ ],
141
+ },
142
+ "query": {
143
+ "bool": {
144
+ "filter": [
145
+ {
146
+ "term": {
147
+ "asset_class": "technology",
148
+ },
149
+ },
150
+ {
151
+ "range": {
152
+ "market_cap": {
153
+ "gte": 1000000000,
154
+ "lte": 5000000000,
155
+ },
156
+ },
157
+ },
158
+ ],
159
+ "minimum_should_match": 0,
160
+ "must": [
161
+ {
162
+ "match": {
163
+ "name": {
164
+ "boost": 2,
165
+ "operator": "and",
166
+ "query": "large-cap tech",
167
+ },
168
+ },
169
+ },
170
+ ],
171
+ "should": [
172
+ {
173
+ "fuzzy": {
174
+ "description": {
175
+ "fuzziness": "AUTO",
176
+ "value": "large-cap tech",
177
+ },
178
+ },
179
+ },
180
+ ],
181
+ },
182
+ },
183
+ "size": 20,
184
+ "sort": [
185
+ {
186
+ "market_cap": "asc",
187
+ },
188
+ ],
189
+ "timeout": "5s",
190
+ "track_scores": true,
191
+ }
192
+ `);
193
+ });
194
+ it('should build a dynamic product search with conditional filters', () => {
195
+ const searchTerm = 'equity';
196
+ const selectedCategory = 'technology';
197
+ const minPrice = undefined;
198
+ const maxPrice = undefined;
199
+ const selectedTags = ['large-cap', 'sp500'];
200
+ const result = query(instrumentMappings)
201
+ .bool()
202
+ .must((q) => q.when(searchTerm, (q2) => q2.match('name', searchTerm, {
203
+ operator: 'and',
204
+ boost: 2
205
+ })) || q.matchAll())
206
+ .filter((q) => q.when(selectedCategory, (q2) => q2.term('asset_class', selectedCategory)) || q.matchAll())
207
+ .filter((q) => q.when(minPrice && maxPrice, (q2) => q2.range('market_cap', {
208
+ gte: minPrice,
209
+ lte: maxPrice
210
+ })) || q.matchAll())
211
+ .filter((q) => q.when(selectedTags && selectedTags.length > 0, (q2) => q2.terms('tags', selectedTags)) || q.matchAll())
212
+ .timeout('5s')
213
+ .from(0)
214
+ .size(20)
215
+ .build();
216
+ expect(result).toMatchInlineSnapshot(`
217
+ {
218
+ "from": 0,
219
+ "query": {
220
+ "bool": {
221
+ "filter": [
222
+ {
223
+ "term": {
224
+ "asset_class": "technology",
225
+ },
226
+ },
227
+ {
228
+ "match_all": {},
229
+ },
230
+ {
231
+ "terms": {
232
+ "tags": [
233
+ "large-cap",
234
+ "sp500",
235
+ ],
236
+ },
237
+ },
238
+ ],
239
+ "must": [
240
+ {
241
+ "match": {
242
+ "name": {
243
+ "boost": 2,
244
+ "operator": "and",
245
+ "query": "equity",
246
+ },
247
+ },
248
+ },
249
+ ],
250
+ },
251
+ },
252
+ "size": 20,
253
+ "timeout": "5s",
254
+ }
255
+ `);
256
+ });
257
+ it('should build an autocomplete-style product search', () => {
258
+ const userInput = 'equ';
259
+ const result = query(instrumentMappings)
260
+ .bool()
261
+ .must((q) => q.matchPhrasePrefix('name', userInput, { max_expansions: 20 }))
262
+ .highlight(['name'], { fragment_size: 100 })
263
+ .trackTotalHits(true)
264
+ .from(0)
265
+ .size(10)
266
+ .build();
267
+ expect(result).toMatchInlineSnapshot(`
268
+ {
269
+ "from": 0,
270
+ "highlight": {
271
+ "fields": {
272
+ "name": {
273
+ "fragment_size": 100,
274
+ },
275
+ },
276
+ },
277
+ "query": {
278
+ "bool": {
279
+ "must": [
280
+ {
281
+ "match_phrase_prefix": {
282
+ "name": {
283
+ "max_expansions": 20,
284
+ "query": "equ",
285
+ },
286
+ },
287
+ },
288
+ ],
289
+ },
290
+ },
291
+ "size": 10,
292
+ "track_total_hits": true,
293
+ }
294
+ `);
295
+ });
296
+ });
297
+ describe('Content/Article Search', () => {
298
+ it('should build a blog article search with full-text and meta filters', () => {
299
+ const searchTerm = 'elasticsearch performance';
300
+ const authorName = 'john';
301
+ const startDate = '2024-01-01';
302
+ const result = query(articleMappings)
303
+ .bool()
304
+ .must((q) => q.multiMatch(['title', 'content'], searchTerm, {
305
+ type: 'best_fields',
306
+ operator: 'and'
307
+ }))
308
+ .should((q) => q.fuzzy('author', authorName, { fuzziness: 'AUTO', boost: 2 }))
309
+ .filter((q) => q.range('published_date', {
310
+ gte: startDate
311
+ }))
312
+ .minimumShouldMatch(0)
313
+ .highlight(['title', 'content'], {
314
+ fragment_size: 200,
315
+ number_of_fragments: 3,
316
+ pre_tags: ['<em>'],
317
+ post_tags: ['</em>']
318
+ })
319
+ .timeout('10s')
320
+ .trackTotalHits(10000)
321
+ .from(0)
322
+ .size(15)
323
+ .sort('published_date', 'desc')
324
+ .build();
325
+ expect(result).toMatchInlineSnapshot(`
326
+ {
327
+ "from": 0,
328
+ "highlight": {
329
+ "fields": {
330
+ "content": {
331
+ "fragment_size": 200,
332
+ "number_of_fragments": 3,
333
+ },
334
+ "title": {
335
+ "fragment_size": 200,
336
+ "number_of_fragments": 3,
337
+ },
338
+ },
339
+ "post_tags": [
340
+ "</em>",
341
+ ],
342
+ "pre_tags": [
343
+ "<em>",
344
+ ],
345
+ },
346
+ "query": {
347
+ "bool": {
348
+ "filter": [
349
+ {
350
+ "range": {
351
+ "published_date": {
352
+ "gte": "2024-01-01",
353
+ },
354
+ },
355
+ },
356
+ ],
357
+ "minimum_should_match": 0,
358
+ "must": [
359
+ {
360
+ "multi_match": {
361
+ "fields": [
362
+ "title",
363
+ "content",
364
+ ],
365
+ "operator": "and",
366
+ "query": "elasticsearch performance",
367
+ "type": "best_fields",
368
+ },
369
+ },
370
+ ],
371
+ "should": [
372
+ {
373
+ "fuzzy": {
374
+ "author": {
375
+ "boost": 2,
376
+ "fuzziness": "AUTO",
377
+ "value": "john",
378
+ },
379
+ },
380
+ },
381
+ ],
382
+ },
383
+ },
384
+ "size": 15,
385
+ "sort": [
386
+ {
387
+ "published_date": "desc",
388
+ },
389
+ ],
390
+ "timeout": "10s",
391
+ "track_total_hits": 10000,
392
+ }
393
+ `);
394
+ });
395
+ it('should build a search for articles with a specific author', () => {
396
+ const authorName = 'jane';
397
+ const result = query(articleMappings)
398
+ .bool()
399
+ .must((q) => q.term('author', authorName))
400
+ .filter((q) => q.range('published_date', {
401
+ gte: '2024-01-01'
402
+ }))
403
+ .highlight(['title'], {
404
+ fragment_size: 150,
405
+ pre_tags: ['<strong>'],
406
+ post_tags: ['</strong>']
407
+ })
408
+ .from(0)
409
+ .size(20)
410
+ .build();
411
+ expect(result).toMatchInlineSnapshot(`
412
+ {
413
+ "from": 0,
414
+ "highlight": {
415
+ "fields": {
416
+ "title": {
417
+ "fragment_size": 150,
418
+ },
419
+ },
420
+ "post_tags": [
421
+ "</strong>",
422
+ ],
423
+ "pre_tags": [
424
+ "<strong>",
425
+ ],
426
+ },
427
+ "query": {
428
+ "bool": {
429
+ "filter": [
430
+ {
431
+ "range": {
432
+ "published_date": {
433
+ "gte": "2024-01-01",
434
+ },
435
+ },
436
+ },
437
+ ],
438
+ "must": [
439
+ {
440
+ "term": {
441
+ "author": "jane",
442
+ },
443
+ },
444
+ ],
445
+ },
446
+ },
447
+ "size": 20,
448
+ }
449
+ `);
450
+ });
451
+ });
452
+ describe('Document Management Search', () => {
453
+ it('should build a document search with ID-based filtering', () => {
454
+ const documentIds = ['doc-123', 'doc-456', 'doc-789'];
455
+ const searchTerm = 'meeting notes';
456
+ const result = query(documentMappings)
457
+ .bool()
458
+ .must((q) => q.ids(documentIds))
459
+ .should((q) => q.when(searchTerm, (q2) => q2.multiMatch(['title', 'content'], searchTerm, {
460
+ operator: 'and'
461
+ })) || q.matchAll())
462
+ .minimumShouldMatch(0)
463
+ .highlight(['title', 'content'], {
464
+ fragment_size: 100,
465
+ number_of_fragments: 2
466
+ })
467
+ .from(0)
468
+ .size(50)
469
+ .build();
470
+ expect(result).toMatchInlineSnapshot(`
471
+ {
472
+ "from": 0,
473
+ "highlight": {
474
+ "fields": {
475
+ "content": {
476
+ "fragment_size": 100,
477
+ "number_of_fragments": 2,
478
+ },
479
+ "title": {
480
+ "fragment_size": 100,
481
+ "number_of_fragments": 2,
482
+ },
483
+ },
484
+ },
485
+ "query": {
486
+ "bool": {
487
+ "minimum_should_match": 0,
488
+ "must": [
489
+ {
490
+ "ids": {
491
+ "values": [
492
+ "doc-123",
493
+ "doc-456",
494
+ "doc-789",
495
+ ],
496
+ },
497
+ },
498
+ ],
499
+ "should": [
500
+ {
501
+ "multi_match": {
502
+ "fields": [
503
+ "title",
504
+ "content",
505
+ ],
506
+ "operator": "and",
507
+ "query": "meeting notes",
508
+ },
509
+ },
510
+ ],
511
+ },
512
+ },
513
+ "size": 50,
514
+ }
515
+ `);
516
+ });
517
+ it('should build a dynamic document search with multiple conditional filters', () => {
518
+ const searchTerm = 'quarterly report';
519
+ const startDate = '2024-01-01';
520
+ const endDate = undefined;
521
+ const result = query(documentMappings)
522
+ .bool()
523
+ .must((q) => q.when(searchTerm, (q2) => q2.match('content', searchTerm, {
524
+ operator: 'and'
525
+ })) || q.matchAll())
526
+ .filter((q) => q.when(true, (q2) => q2.terms('tags', ['finance'])) || q.matchAll())
527
+ .filter((q) => q.when(startDate && endDate, (q2) => q2.range('published_date', {
528
+ gte: startDate,
529
+ lte: endDate
530
+ })) || q.matchAll())
531
+ .filter((q) => q.matchAll())
532
+ .explain(true)
533
+ .trackTotalHits(true)
534
+ .from(0)
535
+ .size(25)
536
+ .sort('published_date', 'desc')
537
+ .build();
538
+ expect(result).toMatchInlineSnapshot(`
539
+ {
540
+ "explain": true,
541
+ "from": 0,
542
+ "query": {
543
+ "bool": {
544
+ "filter": [
545
+ {
546
+ "terms": {
547
+ "tags": [
548
+ "finance",
549
+ ],
550
+ },
551
+ },
552
+ {
553
+ "match_all": {},
554
+ },
555
+ {
556
+ "match_all": {},
557
+ },
558
+ ],
559
+ "must": [
560
+ {
561
+ "match": {
562
+ "content": {
563
+ "operator": "and",
564
+ "query": "quarterly report",
565
+ },
566
+ },
567
+ },
568
+ ],
569
+ },
570
+ },
571
+ "size": 25,
572
+ "sort": [
573
+ {
574
+ "published_date": "desc",
575
+ },
576
+ ],
577
+ "track_total_hits": true,
578
+ }
579
+ `);
580
+ });
581
+ });
582
+ describe('Search UX Patterns', () => {
583
+ it('should build a search-as-you-type query', () => {
584
+ const userTypedPrefix = 'ela';
585
+ const result = query(instrumentMappings)
586
+ .matchPhrasePrefix('name', userTypedPrefix, { max_expansions: 50 })
587
+ .highlight(['name'], {
588
+ fragment_size: 80,
589
+ pre_tags: ['<em>'],
590
+ post_tags: ['</em>']
591
+ })
592
+ .from(0)
593
+ .size(5)
594
+ .timeout('2s')
595
+ .build();
596
+ expect(result).toMatchInlineSnapshot(`
597
+ {
598
+ "from": 0,
599
+ "highlight": {
600
+ "fields": {
601
+ "name": {
602
+ "fragment_size": 80,
603
+ },
604
+ },
605
+ "post_tags": [
606
+ "</em>",
607
+ ],
608
+ "pre_tags": [
609
+ "<em>",
610
+ ],
611
+ },
612
+ "query": {
613
+ "match_phrase_prefix": {
614
+ "name": {
615
+ "max_expansions": 50,
616
+ "query": "ela",
617
+ },
618
+ },
619
+ },
620
+ "size": 5,
621
+ "timeout": "2s",
622
+ }
623
+ `);
624
+ });
625
+ it('should build a faceted search query', () => {
626
+ const searchTerm = 'gaming';
627
+ const facetFilters = {
628
+ categories: ['technology', 'computing'],
629
+ priceRange: { min: 500, max: 3000 },
630
+ minRating: 4
631
+ };
632
+ const result = query(instrumentMappings)
633
+ .bool()
634
+ .must((q) => q.match('name', searchTerm, { boost: 2, operator: 'and' }))
635
+ .filter((q) => q.term('asset_class', facetFilters.categories[0]))
636
+ .filter((q) => q.range('market_cap', {
637
+ gte: facetFilters.priceRange.min,
638
+ lte: facetFilters.priceRange.max
639
+ }))
640
+ .filter((q) => q.range('credit_rating', { gte: facetFilters.minRating }))
641
+ .highlight(['name'], { fragment_size: 100 })
642
+ .timeout('5s')
643
+ .from(0)
644
+ .size(20)
645
+ .sort('market_cap', 'asc')
646
+ .build();
647
+ expect(result).toMatchInlineSnapshot(`
648
+ {
649
+ "from": 0,
650
+ "highlight": {
651
+ "fields": {
652
+ "name": {
653
+ "fragment_size": 100,
654
+ },
655
+ },
656
+ },
657
+ "query": {
658
+ "bool": {
659
+ "filter": [
660
+ {
661
+ "term": {
662
+ "asset_class": "technology",
663
+ },
664
+ },
665
+ {
666
+ "range": {
667
+ "market_cap": {
668
+ "gte": 500,
669
+ "lte": 3000,
670
+ },
671
+ },
672
+ },
673
+ {
674
+ "range": {
675
+ "credit_rating": {
676
+ "gte": 4,
677
+ },
678
+ },
679
+ },
680
+ ],
681
+ "must": [
682
+ {
683
+ "match": {
684
+ "name": {
685
+ "boost": 2,
686
+ "operator": "and",
687
+ "query": "gaming",
688
+ },
689
+ },
690
+ },
691
+ ],
692
+ },
693
+ },
694
+ "size": 20,
695
+ "sort": [
696
+ {
697
+ "market_cap": "asc",
698
+ },
699
+ ],
700
+ "timeout": "5s",
701
+ }
702
+ `);
703
+ });
704
+ it('should build an error-resilient search with typo tolerance', () => {
705
+ const userQuery = 'laptpo'; // Intentional typo
706
+ const result = query(instrumentMappings)
707
+ .bool()
708
+ .must((q) => q.fuzzy('name', userQuery, {
709
+ fuzziness: 'AUTO',
710
+ boost: 2
711
+ }))
712
+ .should((q) => q.fuzzy('description', userQuery, { fuzziness: 'AUTO' }))
713
+ .minimumShouldMatch(1)
714
+ .highlight(['name', 'description'])
715
+ .explain(true)
716
+ .from(0)
717
+ .size(10)
718
+ .build();
719
+ expect(result).toMatchInlineSnapshot(`
720
+ {
721
+ "explain": true,
722
+ "from": 0,
723
+ "highlight": {
724
+ "fields": {
725
+ "description": {},
726
+ "name": {},
727
+ },
728
+ },
729
+ "query": {
730
+ "bool": {
731
+ "minimum_should_match": 1,
732
+ "must": [
733
+ {
734
+ "fuzzy": {
735
+ "name": {
736
+ "boost": 2,
737
+ "fuzziness": "AUTO",
738
+ "value": "laptpo",
739
+ },
740
+ },
741
+ },
742
+ ],
743
+ "should": [
744
+ {
745
+ "fuzzy": {
746
+ "description": {
747
+ "fuzziness": "AUTO",
748
+ "value": "laptpo",
749
+ },
750
+ },
751
+ },
752
+ ],
753
+ },
754
+ },
755
+ "size": 10,
756
+ }
757
+ `);
758
+ });
759
+ });
760
+ describe('Aggregations & Geo Queries', () => {
761
+ it('should aggregate products by category with price statistics', () => {
762
+ const agg = aggregations(instrumentMappings)
763
+ .terms('by_category', 'asset_class', { size: 10 })
764
+ .subAgg((sub) => sub.avg('average_price', 'market_cap').max('highest_price', 'market_cap').min('lowest_price', 'market_cap'))
765
+ .build();
766
+ expect(agg).toMatchInlineSnapshot(`
767
+ {
768
+ "by_category": {
769
+ "aggs": {
770
+ "average_price": {
771
+ "avg": {
772
+ "field": "market_cap",
773
+ },
774
+ },
775
+ "highest_price": {
776
+ "max": {
777
+ "field": "market_cap",
778
+ },
779
+ },
780
+ "lowest_price": {
781
+ "min": {
782
+ "field": "market_cap",
783
+ },
784
+ },
785
+ },
786
+ "terms": {
787
+ "field": "asset_class",
788
+ "size": 10,
789
+ },
790
+ },
791
+ }
792
+ `);
793
+ });
794
+ it('should analyze products sold over time with daily breakdown', () => {
795
+ const agg = aggregations(instrumentMappings)
796
+ .dateHistogram('sales_timeline', 'listed_date', {
797
+ interval: 'day',
798
+ min_doc_count: 1
799
+ })
800
+ .subAgg((sub) => sub.sum('daily_revenue', 'market_cap').cardinality('unique_categories', 'asset_class', {
801
+ precision_threshold: 100
802
+ }))
803
+ .build();
804
+ expect(agg).toMatchInlineSnapshot(`
805
+ {
806
+ "sales_timeline": {
807
+ "aggs": {
808
+ "daily_revenue": {
809
+ "sum": {
810
+ "field": "market_cap",
811
+ },
812
+ },
813
+ "unique_categories": {
814
+ "cardinality": {
815
+ "field": "asset_class",
816
+ "precision_threshold": 100,
817
+ },
818
+ },
819
+ },
820
+ "date_histogram": {
821
+ "field": "listed_date",
822
+ "interval": "day",
823
+ "min_doc_count": 1,
824
+ },
825
+ },
826
+ }
827
+ `);
828
+ });
829
+ it('should find restaurants near a location', () => {
830
+ const result = query(restaurantMappings)
831
+ .geoDistance('location', { lat: 40.7128, lon: -74.006 }, { distance: '5km' })
832
+ .size(20)
833
+ .build();
834
+ expect(result).toMatchInlineSnapshot(`
835
+ {
836
+ "query": {
837
+ "geo_distance": {
838
+ "distance": "5km",
839
+ "location": {
840
+ "lat": 40.7128,
841
+ "lon": -74.006,
842
+ },
843
+ },
844
+ },
845
+ "size": 20,
846
+ }
847
+ `);
848
+ });
849
+ it('should search in a geographic bounding box', () => {
850
+ const result = query(restaurantMappings)
851
+ .geoBoundingBox('location', {
852
+ top_left: { lat: 40.8, lon: -74.1 },
853
+ bottom_right: { lat: 40.7, lon: -74.0 }
854
+ })
855
+ .build();
856
+ expect(result).toMatchInlineSnapshot(`
857
+ {
858
+ "query": {
859
+ "geo_bounding_box": {
860
+ "location": {
861
+ "bottom_right": {
862
+ "lat": 40.7,
863
+ "lon": -74,
864
+ },
865
+ "top_left": {
866
+ "lat": 40.8,
867
+ "lon": -74.1,
868
+ },
869
+ },
870
+ },
871
+ },
872
+ }
873
+ `);
874
+ });
875
+ it('should find products matching a pattern', () => {
876
+ const result = query(instrumentMappings).regexp('asset_class', 'elec.*', { flags: 'CASE_INSENSITIVE' }).build();
877
+ expect(result).toMatchInlineSnapshot(`
878
+ {
879
+ "query": {
880
+ "regexp": {
881
+ "asset_class": {
882
+ "flags": "CASE_INSENSITIVE",
883
+ "value": "elec.*",
884
+ },
885
+ },
886
+ },
887
+ }
888
+ `);
889
+ });
890
+ it('should use constant_score for efficient filtering', () => {
891
+ const result = query(instrumentMappings)
892
+ .constantScore((q) => q.term('asset_class', 'technology'), {
893
+ boost: 1.2
894
+ })
895
+ .build();
896
+ expect(result).toMatchInlineSnapshot(`
897
+ {
898
+ "query": {
899
+ "constant_score": {
900
+ "boost": 1.2,
901
+ "filter": {
902
+ "term": {
903
+ "asset_class": "technology",
904
+ },
905
+ },
906
+ },
907
+ },
908
+ }
909
+ `);
910
+ });
911
+ it('should combine geo search with aggregations for store analytics', () => {
912
+ const queryResult = query(storeMappings)
913
+ .geoDistance('coordinates', { lat: 40.7128, lon: -74.006 }, { distance: '10km' })
914
+ .build();
915
+ const agg = aggregations(storeMappings)
916
+ .terms('by_district', 'district', { size: 5 })
917
+ .subAgg((sub) => sub.avg('avg_rating', 'rating').valueCount('total_items', 'item_count'))
918
+ .build();
919
+ expect(queryResult.query?.geo_distance).toBeDefined();
920
+ expect(agg.by_district).toBeDefined();
921
+ });
922
+ });
923
+ describe('Vector Search & Semantic Search', () => {
924
+ it('should build a basic semantic product search', () => {
925
+ // Simulated embedding vector for "wireless headphones"
926
+ const searchEmbedding = [0.23, 0.45, 0.67, 0.12, 0.89, 0.34, 0.56, 0.78];
927
+ const result = query(instrumentWithEmbeddingMappings)
928
+ .knn('embedding', searchEmbedding, {
929
+ k: 10,
930
+ num_candidates: 100
931
+ })
932
+ .size(10)
933
+ ._source(['name', 'description', 'market_cap', 'prospectus_url'])
934
+ .build();
935
+ expect(result).toMatchInlineSnapshot(`
936
+ {
937
+ "_source": [
938
+ "name",
939
+ "description",
940
+ "market_cap",
941
+ "prospectus_url",
942
+ ],
943
+ "knn": {
944
+ "field": "embedding",
945
+ "k": 10,
946
+ "num_candidates": 100,
947
+ "query_vector": [
948
+ 0.23,
949
+ 0.45,
950
+ 0.67,
951
+ 0.12,
952
+ 0.89,
953
+ 0.34,
954
+ 0.56,
955
+ 0.78,
956
+ ],
957
+ },
958
+ "size": 10,
959
+ }
960
+ `);
961
+ });
962
+ it('should build semantic search with category filtering', () => {
963
+ const queryVector = [0.1, 0.2, 0.3, 0.4, 0.5];
964
+ const result = query(instrumentWithEmbeddingMappings)
965
+ .knn('embedding', queryVector, {
966
+ k: 20,
967
+ num_candidates: 200,
968
+ filter: {
969
+ bool: {
970
+ must: [{ term: { category: 'technology' } }],
971
+ filter: [{ range: { price: { gte: 100, lte: 1000 } } }]
972
+ }
973
+ }
974
+ })
975
+ .size(20)
976
+ .build();
977
+ expect(result.knn?.filter).toBeDefined();
978
+ expect(result.knn?.filter.bool.must).toHaveLength(1);
979
+ expect(result.knn?.filter.bool.filter).toHaveLength(1);
980
+ });
981
+ it('should build image similarity search', () => {
982
+ // Simulated 512-dimensional image embedding (e.g., from ResNet)
983
+ const imageEmbedding = new Array(512).fill(0).map((_, i) => Math.sin(i / 100));
984
+ const result = query(instrumentWithEmbeddingMappings)
985
+ .knn('embedding', imageEmbedding, {
986
+ k: 50,
987
+ num_candidates: 500,
988
+ similarity: 0.7,
989
+ boost: 1.2
990
+ })
991
+ .size(50)
992
+ ._source(['isin', 'name', 'prospectus_url'])
993
+ .build();
994
+ expect(result.knn?.query_vector).toHaveLength(512);
995
+ expect(result.knn?.similarity).toBe(0.7);
996
+ expect(result.knn?.boost).toBe(1.2);
997
+ });
998
+ it('should build product recommendation engine query', () => {
999
+ // Current product's embedding
1000
+ const currentProductEmbedding = [0.45, 0.23, 0.67, 0.89, 0.12];
1001
+ const result = query(instrumentWithEmbeddingMappings)
1002
+ .knn('embedding', currentProductEmbedding, {
1003
+ k: 10,
1004
+ num_candidates: 100,
1005
+ filter: {
1006
+ bool: {
1007
+ must_not: [{ term: { id: 'current-product-123' } }],
1008
+ must: [{ term: { category: 'technology' } }]
1009
+ }
1010
+ }
1011
+ })
1012
+ .size(10)
1013
+ ._source(['isin', 'name', 'market_cap', 'prospectus_url'])
1014
+ .build();
1015
+ expect(result.knn?.filter?.bool?.must_not).toBeDefined();
1016
+ expect(result.size).toBe(10);
1017
+ });
1018
+ it('should build semantic document search with aggregations', () => {
1019
+ // Search embedding for "machine learning best practices"
1020
+ const queryEmbedding = new Array(384).fill(0).map((_, i) => i / 384);
1021
+ const result = query(contentDocumentMappings)
1022
+ .knn('embedding', queryEmbedding, {
1023
+ k: 50,
1024
+ num_candidates: 500,
1025
+ filter: {
1026
+ range: {
1027
+ published_date: { gte: '2023-01-01' }
1028
+ }
1029
+ }
1030
+ })
1031
+ .aggs((agg) => agg.terms('top_authors', 'author', { size: 10 }).terms('popular_tags', 'tags', { size: 20 }))
1032
+ .size(20)
1033
+ .build();
1034
+ expect(result.knn?.query_vector).toHaveLength(384);
1035
+ expect(result.aggs?.top_authors).toBeDefined();
1036
+ expect(result.aggs?.popular_tags).toBeDefined();
1037
+ });
1038
+ it('should build multilingual semantic search', () => {
1039
+ // Embedding from multilingual model (e.g., multilingual-E5)
1040
+ const multilingualEmbedding = new Array(768).fill(0).map(() => Math.random());
1041
+ const result = query(contentDocumentMappings)
1042
+ .knn('embedding', multilingualEmbedding, {
1043
+ k: 30,
1044
+ num_candidates: 300,
1045
+ boost: 1.5
1046
+ })
1047
+ .size(30)
1048
+ ._source(['title', 'content', 'author'])
1049
+ .highlight(['title', 'content'], {
1050
+ fragment_size: 150,
1051
+ number_of_fragments: 3
1052
+ })
1053
+ .build();
1054
+ expect(result.knn?.query_vector).toHaveLength(768);
1055
+ expect(result.highlight).toBeDefined();
1056
+ });
1057
+ it('should build OpenAI embedding search (1536 dimensions)', () => {
1058
+ // OpenAI text-embedding-ada-002 produces 1536-dimensional vectors
1059
+ const openaiEmbedding = new Array(1536).fill(0).map(() => Math.random());
1060
+ const result = query(contentDocumentMappings)
1061
+ .knn('embedding', openaiEmbedding, {
1062
+ k: 10,
1063
+ num_candidates: 100,
1064
+ filter: {
1065
+ bool: {
1066
+ must: [{ term: { author: 'john-doe' } }]
1067
+ }
1068
+ }
1069
+ })
1070
+ .size(10)
1071
+ .from(0)
1072
+ .build();
1073
+ expect(result.knn?.query_vector).toHaveLength(1536);
1074
+ expect(result.knn?.filter).toBeDefined();
1075
+ });
1076
+ it('should build hybrid semantic + price ranking', () => {
1077
+ const productEmbedding = [0.5, 0.3, 0.8, 0.2, 0.6];
1078
+ const result = query(instrumentWithEmbeddingMappings)
1079
+ .knn('embedding', productEmbedding, {
1080
+ k: 100,
1081
+ num_candidates: 1000,
1082
+ filter: {
1083
+ bool: {
1084
+ filter: [{ range: { price: { gte: 50 } } }, { term: { category: 'technology' } }]
1085
+ }
1086
+ }
1087
+ })
1088
+ .size(20)
1089
+ .sort('market_cap', 'asc')
1090
+ ._source(['isin', 'name', 'market_cap'])
1091
+ .build();
1092
+ expect(result.knn?.filter).toBeDefined();
1093
+ expect(result.sort).toMatchInlineSnapshot(`
1094
+ [
1095
+ {
1096
+ "market_cap": "asc",
1097
+ },
1098
+ ]
1099
+ `);
1100
+ });
1101
+ it('should build semantic search with quality thresholding', () => {
1102
+ const queryVector = [0.7, 0.2, 0.5, 0.9, 0.1];
1103
+ const result = query(instrumentWithEmbeddingMappings)
1104
+ .knn('embedding', queryVector, {
1105
+ k: 20,
1106
+ num_candidates: 200,
1107
+ similarity: 0.85 // Only return highly similar results
1108
+ })
1109
+ .size(20)
1110
+ .minScore(0.8) // Additional relevance threshold
1111
+ .build();
1112
+ expect(result.knn?.similarity).toBe(0.85);
1113
+ expect(result.min_score).toBe(0.8);
1114
+ });
1115
+ it('should build "more like this" recommendation query', () => {
1116
+ // Reference item's embedding
1117
+ const referenceEmbedding = [0.33, 0.66, 0.22, 0.88, 0.44];
1118
+ const excludeIds = ['ref-item-1', 'ref-item-2', 'ref-item-3'];
1119
+ const result = query(instrumentWithEmbeddingMappings)
1120
+ .knn('embedding', referenceEmbedding, {
1121
+ k: 15,
1122
+ num_candidates: 150,
1123
+ filter: {
1124
+ bool: {
1125
+ must_not: excludeIds.map((id) => ({ term: { id } }))
1126
+ }
1127
+ }
1128
+ })
1129
+ .size(15)
1130
+ ._source(['isin', 'name', 'description', 'market_cap', 'asset_class'])
1131
+ .build();
1132
+ expect(result.knn?.filter?.bool?.must_not).toHaveLength(3);
1133
+ expect(result.size).toBe(15);
1134
+ });
1135
+ });
1136
+ describe('Script Queries & Custom Scoring', () => {
1137
+ const scoredProductMappings = mappings({
1138
+ id: keyword(),
1139
+ name: text(),
1140
+ price: long(),
1141
+ popularity: long(),
1142
+ quality_score: float(),
1143
+ rating: float(),
1144
+ views: long()
1145
+ });
1146
+ it('should build dynamic price filter with script', () => {
1147
+ const result = query(scoredProductMappings)
1148
+ .bool()
1149
+ .must((q) => q.match('name', 'laptop'))
1150
+ .filter((q) => q.script({
1151
+ source: "doc['price'].value > params.threshold",
1152
+ params: { threshold: 500 }
1153
+ }))
1154
+ .build();
1155
+ expect(result.query?.bool?.filter).toHaveLength(1);
1156
+ expect(result.query?.bool?.filter[0].script).toBeDefined();
1157
+ });
1158
+ it('should build custom popularity scoring', () => {
1159
+ const result = query(scoredProductMappings)
1160
+ .scriptScore((q) => q.match('name', 'smartphone'), {
1161
+ source: "_score * Math.log(2 + doc['popularity'].value)"
1162
+ })
1163
+ .size(20)
1164
+ .build();
1165
+ expect(result.query?.script_score?.query?.match).toBeDefined();
1166
+ expect(result.query?.script_score?.script?.source).toContain('popularity');
1167
+ });
1168
+ it('should build weighted quality + popularity score', () => {
1169
+ const result = query(scoredProductMappings)
1170
+ .scriptScore((q) => q.multiMatch(['name'], 'premium headphones', {
1171
+ type: 'best_fields'
1172
+ }), {
1173
+ source: `
1174
+ double quality = doc['quality_score'].value;
1175
+ double popularity = doc['popularity'].value;
1176
+ return _score * (quality * 0.7 + popularity * 0.3);
1177
+ `.trim(),
1178
+ params: {}
1179
+ }, { min_score: 5.0 })
1180
+ .size(10)
1181
+ .build();
1182
+ expect(result.query?.script_score?.min_score).toBe(5.0);
1183
+ });
1184
+ it('should build personalized recommendation scoring', () => {
1185
+ const userPreferences = {
1186
+ price_weight: 0.3,
1187
+ quality_weight: 0.5,
1188
+ popularity_weight: 0.2
1189
+ };
1190
+ const result = query(scoredProductMappings)
1191
+ .scriptScore((q) => q.term('id', 'prod-123'), {
1192
+ source: `
1193
+ double price_score = 1.0 / (1.0 + doc['price'].value / 1000);
1194
+ double quality_score = doc['quality_score'].value / 10.0;
1195
+ double popularity_score = Math.log(1 + doc['popularity'].value) / 10.0;
1196
+
1197
+ return _score * (
1198
+ price_score * params.price_weight +
1199
+ quality_score * params.quality_weight +
1200
+ popularity_score * params.popularity_weight
1201
+ );
1202
+ `.trim(),
1203
+ params: userPreferences
1204
+ })
1205
+ .size(50)
1206
+ .build();
1207
+ expect(result.query?.script_score?.script?.params).toStrictEqual(userPreferences);
1208
+ });
1209
+ it('should build time-decay scoring for trending products', () => {
1210
+ const result = query(scoredProductMappings)
1211
+ .scriptScore((q) => q.matchAll(), {
1212
+ source: `
1213
+ double views = doc['views'].value;
1214
+ double rating = doc['rating'].value;
1215
+ return Math.log(1 + views) * rating;
1216
+ `.trim()
1217
+ }, { min_score: 1.0, boost: 1.5 })
1218
+ .sort('popularity', 'desc')
1219
+ .size(20)
1220
+ .build();
1221
+ expect(result.query?.script_score?.boost).toBe(1.5);
1222
+ expect(result.sort).toStrictEqual([{ popularity: 'desc' }]);
1223
+ });
1224
+ });
1225
+ describe('Percolate Queries & Alert Matching', () => {
1226
+ const alertRuleMappings = mappings({
1227
+ query: percolator(),
1228
+ name: text(),
1229
+ severity: keyword(),
1230
+ category: keyword()
1231
+ });
1232
+ it('should match log entry against saved alert rules', () => {
1233
+ const logEntry = {
1234
+ level: 'ERROR',
1235
+ message: 'Database connection failed',
1236
+ timestamp: '2024-01-15T10:30:00Z',
1237
+ source: 'api-server'
1238
+ };
1239
+ const result = query(alertRuleMappings)
1240
+ .percolate({
1241
+ field: 'query',
1242
+ document: logEntry
1243
+ })
1244
+ .size(100)
1245
+ .build();
1246
+ expect(result.query?.percolate?.document).toStrictEqual(logEntry);
1247
+ });
1248
+ it('should classify multiple documents', () => {
1249
+ const articles = [
1250
+ { title: 'AI Breakthrough', content: 'Machine learning advances' },
1251
+ { title: 'Market Update', content: 'Stock prices surge' },
1252
+ { title: 'Sports News', content: 'Team wins championship' }
1253
+ ];
1254
+ const result = query(alertRuleMappings)
1255
+ .percolate({
1256
+ field: 'query',
1257
+ documents: articles
1258
+ })
1259
+ ._source(['name', 'category'])
1260
+ .size(50)
1261
+ .build();
1262
+ expect(result.query?.percolate?.documents).toHaveLength(3);
1263
+ expect(result._source).toContain('category');
1264
+ });
1265
+ it('should match against stored document', () => {
1266
+ const result = query(alertRuleMappings)
1267
+ .percolate({
1268
+ field: 'query',
1269
+ index: 'user_content',
1270
+ id: 'content-789',
1271
+ routing: 'user-123'
1272
+ })
1273
+ .size(20)
1274
+ .build();
1275
+ expect(result.query?.percolate?.index).toBe('user_content');
1276
+ expect(result.query?.percolate?.routing).toBe('user-123');
1277
+ });
1278
+ it('should build security alert system', () => {
1279
+ const securityEvent = {
1280
+ event_type: 'unauthorized_access',
1281
+ severity: 'high',
1282
+ ip_address: '192.168.1.100',
1283
+ user_id: 'unknown',
1284
+ timestamp: '2024-01-15T14:00:00Z',
1285
+ attempted_resource: '/admin/users'
1286
+ };
1287
+ const result = query(alertRuleMappings)
1288
+ .percolate({
1289
+ field: 'query',
1290
+ document: securityEvent,
1291
+ name: 'security_event_check'
1292
+ })
1293
+ ._source(['name', 'severity'])
1294
+ .sort('severity', 'desc')
1295
+ .size(100)
1296
+ .build();
1297
+ expect(result.query?.percolate?.name).toBe('security_event_check');
1298
+ expect(result.query?.percolate?.document?.severity).toBe('high');
1299
+ });
1300
+ it('should build content recommendation engine', () => {
1301
+ const userPreferences = {
1302
+ interests: ['technology', 'science', 'programming'],
1303
+ reading_level: 'advanced',
1304
+ preferred_length: 'medium'
1305
+ };
1306
+ const result = query(alertRuleMappings)
1307
+ .percolate({
1308
+ field: 'query',
1309
+ document: userPreferences
1310
+ })
1311
+ ._source(['name', 'category'])
1312
+ .size(50)
1313
+ .build();
1314
+ expect(result.query?.percolate?.document?.interests).toHaveLength(3);
1315
+ });
1316
+ it('should build real-time monitoring system', () => {
1317
+ const metrics = {
1318
+ cpu_usage: 85,
1319
+ memory_usage: 92,
1320
+ disk_usage: 78,
1321
+ response_time_ms: 1500,
1322
+ error_rate: 0.05,
1323
+ timestamp: '2024-01-15T15:00:00Z'
1324
+ };
1325
+ const result = query(alertRuleMappings)
1326
+ .percolate({
1327
+ field: 'query',
1328
+ document: metrics,
1329
+ preference: '_local'
1330
+ })
1331
+ .sort('severity', 'desc')
1332
+ .size(100)
1333
+ .build();
1334
+ expect(result.query?.percolate?.document?.cpu_usage).toBe(85);
1335
+ expect(result.query?.percolate?.preference).toBe('_local');
1336
+ });
1337
+ });
1338
+ describe('Multi-Search', () => {
1339
+ it('should build dashboard with multiple product searches', () => {
1340
+ const dashboardProductMappings = mappings({
1341
+ id: keyword(),
1342
+ name: text(),
1343
+ category: keyword(),
1344
+ price: long(),
1345
+ sales_count: long(),
1346
+ created_at: date()
1347
+ });
1348
+ // Top selling products query
1349
+ const topSelling = query(dashboardProductMappings)
1350
+ .bool()
1351
+ .filter((q) => q.range('created_at', { gte: 'now-30d' }))
1352
+ .sort('sales_count', 'desc')
1353
+ .size(10)
1354
+ .build();
1355
+ // New arrivals query
1356
+ const newArrivals = query(dashboardProductMappings).matchAll().sort('created_at', 'desc').size(10).build();
1357
+ // Electronics deals query
1358
+ const electronicsDeals = query(dashboardProductMappings)
1359
+ .bool()
1360
+ .filter((q) => q.term('category', 'technology'))
1361
+ .filter((q) => q.range('price', { lte: 500 }))
1362
+ .sort('price', 'asc')
1363
+ .size(5)
1364
+ .build();
1365
+ const ndjson = msearch(dashboardProductMappings)
1366
+ .addQuery(topSelling, { index: 'instruments' })
1367
+ .addQuery(newArrivals, { index: 'instruments' })
1368
+ .addQuery(electronicsDeals, { index: 'instruments' })
1369
+ .build();
1370
+ expect(ndjson).toMatchInlineSnapshot(`
1371
+ "{"index":"instruments"}
1372
+ {"query":{"bool":{"filter":[{"range":{"created_at":{"gte":"now-30d"}}}]}},"sort":[{"sales_count":"desc"}],"size":10}
1373
+ {"index":"instruments"}
1374
+ {"query":{"match_all":{}},"sort":[{"created_at":"desc"}],"size":10}
1375
+ {"index":"instruments"}
1376
+ {"query":{"bool":{"filter":[{"term":{"category":"technology"}},{"range":{"price":{"lte":500}}}]}},"sort":[{"price":"asc"}],"size":5}
1377
+ "
1378
+ `);
1379
+ });
1380
+ it('should search across multiple tenant indices', () => {
1381
+ const tenantDocMappings = mappings({
1382
+ id: keyword(),
1383
+ content: text(),
1384
+ tenant_id: keyword()
1385
+ });
1386
+ const searchQuery = query(tenantDocMappings).match('content', 'important').size(20).build();
1387
+ const result = msearch(tenantDocMappings)
1388
+ .addQuery(searchQuery, { index: 'tenant-001-docs' })
1389
+ .addQuery(searchQuery, { index: 'tenant-002-docs' })
1390
+ .addQuery(searchQuery, { index: 'tenant-003-docs' })
1391
+ .buildArray();
1392
+ expect(result).toMatchInlineSnapshot(`
1393
+ [
1394
+ {
1395
+ "index": "tenant-001-docs",
1396
+ },
1397
+ {
1398
+ "query": {
1399
+ "match": {
1400
+ "content": "important",
1401
+ },
1402
+ },
1403
+ "size": 20,
1404
+ },
1405
+ {
1406
+ "index": "tenant-002-docs",
1407
+ },
1408
+ {
1409
+ "query": {
1410
+ "match": {
1411
+ "content": "important",
1412
+ },
1413
+ },
1414
+ "size": 20,
1415
+ },
1416
+ {
1417
+ "index": "tenant-003-docs",
1418
+ },
1419
+ {
1420
+ "query": {
1421
+ "match": {
1422
+ "content": "important",
1423
+ },
1424
+ },
1425
+ "size": 20,
1426
+ },
1427
+ ]
1428
+ `);
1429
+ });
1430
+ });
1431
+ describe('Bulk Operations', () => {
1432
+ it('should build product catalog import', () => {
1433
+ const catalogProductMappings = mappings({
1434
+ sku: keyword(),
1435
+ name: text(),
1436
+ price: long(),
1437
+ category: keyword(),
1438
+ stock: integer()
1439
+ });
1440
+ const products = [
1441
+ {
1442
+ sku: 'LAP-001',
1443
+ name: 'Gaming Laptop',
1444
+ price: 1299,
1445
+ category: 'technology',
1446
+ stock: 15
1447
+ },
1448
+ {
1449
+ sku: 'MOU-002',
1450
+ name: 'Wireless Mouse',
1451
+ price: 29,
1452
+ category: 'accessories',
1453
+ stock: 50
1454
+ },
1455
+ {
1456
+ sku: 'KEY-003',
1457
+ name: 'Mechanical Keyboard',
1458
+ price: 149,
1459
+ category: 'accessories',
1460
+ stock: 30
1461
+ }
1462
+ ];
1463
+ let bulkBuilder = bulk(catalogProductMappings);
1464
+ for (const product of products) {
1465
+ bulkBuilder = bulkBuilder.index(product, {
1466
+ _index: 'instruments',
1467
+ _id: product.sku
1468
+ });
1469
+ }
1470
+ const ndjson = bulkBuilder.build();
1471
+ expect(ndjson).toMatchInlineSnapshot(`
1472
+ "{"index":{"_index":"instruments","_id":"LAP-001"}}
1473
+ {"sku":"LAP-001","name":"Gaming Laptop","price":1299,"category":"technology","stock":15}
1474
+ {"index":{"_index":"instruments","_id":"MOU-002"}}
1475
+ {"sku":"MOU-002","name":"Wireless Mouse","price":29,"category":"accessories","stock":50}
1476
+ {"index":{"_index":"instruments","_id":"KEY-003"}}
1477
+ {"sku":"KEY-003","name":"Mechanical Keyboard","price":149,"category":"accessories","stock":30}
1478
+ "
1479
+ `);
1480
+ });
1481
+ it('should build price update batch with script', () => {
1482
+ const pricedProductMappings = mappings({
1483
+ id: keyword(),
1484
+ price: long()
1485
+ });
1486
+ // Apply 10% discount to specific products
1487
+ const productIds = ['prod-1', 'prod-2', 'prod-3', 'prod-4', 'prod-5'];
1488
+ let bulkBuilder = bulk(pricedProductMappings);
1489
+ for (const id of productIds) {
1490
+ bulkBuilder = bulkBuilder.update({
1491
+ _index: 'instruments',
1492
+ _id: id,
1493
+ script: {
1494
+ source: 'ctx._source.price *= params.discount',
1495
+ params: { discount: 0.9 }
1496
+ }
1497
+ });
1498
+ }
1499
+ const ndjson = bulkBuilder.build();
1500
+ expect(ndjson).toMatchInlineSnapshot(`
1501
+ "{"update":{"_index":"instruments","_id":"prod-1"}}
1502
+ {"script":{"source":"ctx._source.price *= params.discount","params":{"discount":0.9}}}
1503
+ {"update":{"_index":"instruments","_id":"prod-2"}}
1504
+ {"script":{"source":"ctx._source.price *= params.discount","params":{"discount":0.9}}}
1505
+ {"update":{"_index":"instruments","_id":"prod-3"}}
1506
+ {"script":{"source":"ctx._source.price *= params.discount","params":{"discount":0.9}}}
1507
+ {"update":{"_index":"instruments","_id":"prod-4"}}
1508
+ {"script":{"source":"ctx._source.price *= params.discount","params":{"discount":0.9}}}
1509
+ {"update":{"_index":"instruments","_id":"prod-5"}}
1510
+ {"script":{"source":"ctx._source.price *= params.discount","params":{"discount":0.9}}}
1511
+ "
1512
+ `);
1513
+ });
1514
+ it('should build mixed CRUD operations', () => {
1515
+ const inventoryItemMappings = mappings({
1516
+ id: keyword(),
1517
+ name: text(),
1518
+ quantity: integer()
1519
+ });
1520
+ const bulkOp = bulk(inventoryItemMappings)
1521
+ // Add new items
1522
+ .create({ id: 'new-1', name: 'New Product', quantity: 100 }, { _index: 'inventory', _id: 'new-1' })
1523
+ // Update stock levels
1524
+ .update({
1525
+ _index: 'inventory',
1526
+ _id: 'existing-1',
1527
+ doc: { quantity: 50 }
1528
+ })
1529
+ // Replace item entirely
1530
+ .index({ id: 'replace-1', name: 'Replaced Product', quantity: 25 }, { _index: 'inventory', _id: 'replace-1' })
1531
+ // Remove discontinued items
1532
+ .delete({ _index: 'inventory', _id: 'discontinued-1' })
1533
+ .build();
1534
+ expect(bulkOp).toMatchInlineSnapshot(`
1535
+ "{"create":{"_index":"inventory","_id":"new-1"}}
1536
+ {"id":"new-1","name":"New Product","quantity":100}
1537
+ {"update":{"_index":"inventory","_id":"existing-1"}}
1538
+ {"doc":{"quantity":50}}
1539
+ {"index":{"_index":"inventory","_id":"replace-1"}}
1540
+ {"id":"replace-1","name":"Replaced Product","quantity":25}
1541
+ {"delete":{"_index":"inventory","_id":"discontinued-1"}}
1542
+ "
1543
+ `);
1544
+ });
1545
+ it('should build upsert operations for sync', () => {
1546
+ const syncedDocumentMappings = mappings({
1547
+ id: keyword(),
1548
+ data: text(),
1549
+ updated_at: date()
1550
+ });
1551
+ const updates = [
1552
+ { id: 'doc-1', data: 'Updated content', updated_at: '2024-01-15' },
1553
+ { id: 'doc-2', data: 'New content', updated_at: '2024-01-15' }
1554
+ ];
1555
+ let bulkBuilder = bulk(syncedDocumentMappings);
1556
+ for (const update of updates) {
1557
+ bulkBuilder = bulkBuilder.update({
1558
+ _index: 'documents',
1559
+ _id: update.id,
1560
+ doc: update,
1561
+ upsert: update // Insert if doesn't exist
1562
+ });
1563
+ }
1564
+ const ndjson = bulkBuilder.build();
1565
+ expect(ndjson).toMatchInlineSnapshot(`
1566
+ "{"update":{"_index":"documents","_id":"doc-1"}}
1567
+ {"doc":{"id":"doc-1","data":"Updated content","updated_at":"2024-01-15"},"upsert":{"id":"doc-1","data":"Updated content","updated_at":"2024-01-15"}}
1568
+ {"update":{"_index":"documents","_id":"doc-2"}}
1569
+ {"doc":{"id":"doc-2","data":"New content","updated_at":"2024-01-15"},"upsert":{"id":"doc-2","data":"New content","updated_at":"2024-01-15"}}
1570
+ "
1571
+ `);
1572
+ });
1573
+ });
1574
+ describe('Index Management', () => {
1575
+ it('should build e-commerce product index', () => {
1576
+ const ecommerceProductMappings = mappings({
1577
+ sku: keyword(),
1578
+ name: text({
1579
+ analyzer: 'standard',
1580
+ fields: { keyword: { type: 'keyword' } }
1581
+ }),
1582
+ description: text({ analyzer: 'english' }),
1583
+ price: scaledFloat({ scaling_factor: 100 }),
1584
+ category: keyword(),
1585
+ brand: keyword(),
1586
+ tags: keyword(),
1587
+ rating: halfFloat(),
1588
+ reviewCount: integer(),
1589
+ inStock: keyword(), // boolean mapped as keyword for index builder compatibility
1590
+ createdAt: date()
1591
+ });
1592
+ const indexConfig = indexBuilder()
1593
+ .mappings(ecommerceProductMappings)
1594
+ .settings({
1595
+ number_of_shards: 3,
1596
+ number_of_replicas: 2,
1597
+ refresh_interval: '1s'
1598
+ })
1599
+ .alias('instruments')
1600
+ .build();
1601
+ expect(indexConfig).toMatchInlineSnapshot(`
1602
+ {
1603
+ "aliases": {
1604
+ "instruments": {},
1605
+ },
1606
+ "mappings": {
1607
+ "dynamic": "strict",
1608
+ "properties": {
1609
+ "brand": {
1610
+ "type": "keyword",
1611
+ },
1612
+ "category": {
1613
+ "type": "keyword",
1614
+ },
1615
+ "createdAt": {
1616
+ "type": "date",
1617
+ },
1618
+ "description": {
1619
+ "analyzer": "english",
1620
+ "type": "text",
1621
+ },
1622
+ "inStock": {
1623
+ "type": "keyword",
1624
+ },
1625
+ "name": {
1626
+ "analyzer": "standard",
1627
+ "fields": {
1628
+ "keyword": {
1629
+ "type": "keyword",
1630
+ },
1631
+ },
1632
+ "type": "text",
1633
+ },
1634
+ "price": {
1635
+ "scaling_factor": 100,
1636
+ "type": "scaled_float",
1637
+ },
1638
+ "rating": {
1639
+ "type": "half_float",
1640
+ },
1641
+ "reviewCount": {
1642
+ "type": "integer",
1643
+ },
1644
+ "sku": {
1645
+ "type": "keyword",
1646
+ },
1647
+ "tags": {
1648
+ "type": "keyword",
1649
+ },
1650
+ },
1651
+ },
1652
+ "settings": {
1653
+ "number_of_replicas": 2,
1654
+ "number_of_shards": 3,
1655
+ "refresh_interval": "1s",
1656
+ },
1657
+ }
1658
+ `);
1659
+ });
1660
+ it('should build vector search index with HNSW', () => {
1661
+ const vectorDocumentMappings = mappings({
1662
+ title: text(),
1663
+ content: text(),
1664
+ embedding: denseVector({
1665
+ dims: 768,
1666
+ index: true,
1667
+ similarity: 'cosine',
1668
+ index_options: {
1669
+ type: 'hnsw',
1670
+ m: 16,
1671
+ ef_construction: 100
1672
+ }
1673
+ }),
1674
+ category: keyword()
1675
+ });
1676
+ const indexConfig = indexBuilder()
1677
+ .mappings(vectorDocumentMappings)
1678
+ .settings({
1679
+ number_of_shards: 1,
1680
+ number_of_replicas: 0
1681
+ })
1682
+ .build();
1683
+ expect(indexConfig).toMatchInlineSnapshot(`
1684
+ {
1685
+ "mappings": {
1686
+ "dynamic": "strict",
1687
+ "properties": {
1688
+ "category": {
1689
+ "type": "keyword",
1690
+ },
1691
+ "content": {
1692
+ "type": "text",
1693
+ },
1694
+ "embedding": {
1695
+ "dims": 768,
1696
+ "index": true,
1697
+ "index_options": {
1698
+ "ef_construction": 100,
1699
+ "m": 16,
1700
+ "type": "hnsw",
1701
+ },
1702
+ "similarity": "cosine",
1703
+ "type": "dense_vector",
1704
+ },
1705
+ "title": {
1706
+ "type": "text",
1707
+ },
1708
+ },
1709
+ },
1710
+ "settings": {
1711
+ "number_of_replicas": 0,
1712
+ "number_of_shards": 1,
1713
+ },
1714
+ }
1715
+ `);
1716
+ });
1717
+ it('should build time-series index with aliases', () => {
1718
+ const logEntryMappings = mappings({
1719
+ timestamp: date(),
1720
+ level: keyword(),
1721
+ message: text({ analyzer: 'standard' }),
1722
+ service: keyword(),
1723
+ trace_id: keyword()
1724
+ });
1725
+ const indexConfig = indexBuilder()
1726
+ .mappings(logEntryMappings)
1727
+ .settings({
1728
+ number_of_shards: 1,
1729
+ number_of_replicas: 1,
1730
+ refresh_interval: '5s'
1731
+ })
1732
+ .alias('logs-current', { is_write_index: true })
1733
+ .alias('logs-all')
1734
+ .build();
1735
+ expect(indexConfig).toMatchInlineSnapshot(`
1736
+ {
1737
+ "aliases": {
1738
+ "logs-all": {},
1739
+ "logs-current": {
1740
+ "is_write_index": true,
1741
+ },
1742
+ },
1743
+ "mappings": {
1744
+ "dynamic": "strict",
1745
+ "properties": {
1746
+ "level": {
1747
+ "type": "keyword",
1748
+ },
1749
+ "message": {
1750
+ "analyzer": "standard",
1751
+ "type": "text",
1752
+ },
1753
+ "service": {
1754
+ "type": "keyword",
1755
+ },
1756
+ "timestamp": {
1757
+ "type": "date",
1758
+ },
1759
+ "trace_id": {
1760
+ "type": "keyword",
1761
+ },
1762
+ },
1763
+ },
1764
+ "settings": {
1765
+ "number_of_replicas": 1,
1766
+ "number_of_shards": 1,
1767
+ "refresh_interval": "5s",
1768
+ },
1769
+ }
1770
+ `);
1771
+ });
1772
+ it('should build multi-field text index for search', () => {
1773
+ const articleIndexMappings = mappings({
1774
+ title: text({
1775
+ analyzer: 'english',
1776
+ fields: {
1777
+ exact: { type: 'keyword' },
1778
+ raw: { type: 'text', analyzer: 'standard' }
1779
+ }
1780
+ }),
1781
+ author: keyword(),
1782
+ content: text({ analyzer: 'english' }),
1783
+ publishedAt: date()
1784
+ });
1785
+ const indexConfig = indexBuilder()
1786
+ .mappings(articleIndexMappings)
1787
+ .settings({
1788
+ number_of_shards: 2,
1789
+ number_of_replicas: 1
1790
+ })
1791
+ .build();
1792
+ expect(indexConfig).toMatchInlineSnapshot(`
1793
+ {
1794
+ "mappings": {
1795
+ "dynamic": "strict",
1796
+ "properties": {
1797
+ "author": {
1798
+ "type": "keyword",
1799
+ },
1800
+ "content": {
1801
+ "analyzer": "english",
1802
+ "type": "text",
1803
+ },
1804
+ "publishedAt": {
1805
+ "type": "date",
1806
+ },
1807
+ "title": {
1808
+ "analyzer": "english",
1809
+ "fields": {
1810
+ "exact": {
1811
+ "type": "keyword",
1812
+ },
1813
+ "raw": {
1814
+ "analyzer": "standard",
1815
+ "type": "text",
1816
+ },
1817
+ },
1818
+ "type": "text",
1819
+ },
1820
+ },
1821
+ },
1822
+ "settings": {
1823
+ "number_of_replicas": 1,
1824
+ "number_of_shards": 2,
1825
+ },
1826
+ }
1827
+ `);
1828
+ });
1829
+ it('should build a professional domain index using field helpers', () => {
1830
+ const matterMappings = mappings({
1831
+ title: text({ analyzer: 'english' }),
1832
+ practice_area: keyword(),
1833
+ billing_rate: integer(),
1834
+ risk_score: float(),
1835
+ opened_at: date()
1836
+ });
1837
+ const indexConfig = indexBuilder()
1838
+ .mappings(matterMappings)
1839
+ .settings({
1840
+ number_of_shards: 2,
1841
+ number_of_replicas: 1,
1842
+ refresh_interval: '5s'
1843
+ })
1844
+ .alias('matters-current', { is_write_index: true })
1845
+ .alias('matters-all')
1846
+ .build();
1847
+ expect(indexConfig).toMatchInlineSnapshot(`
1848
+ {
1849
+ "aliases": {
1850
+ "matters-all": {},
1851
+ "matters-current": {
1852
+ "is_write_index": true,
1853
+ },
1854
+ },
1855
+ "mappings": {
1856
+ "dynamic": "strict",
1857
+ "properties": {
1858
+ "billing_rate": {
1859
+ "type": "integer",
1860
+ },
1861
+ "opened_at": {
1862
+ "type": "date",
1863
+ },
1864
+ "practice_area": {
1865
+ "type": "keyword",
1866
+ },
1867
+ "risk_score": {
1868
+ "type": "float",
1869
+ },
1870
+ "title": {
1871
+ "analyzer": "english",
1872
+ "type": "text",
1873
+ },
1874
+ },
1875
+ },
1876
+ "settings": {
1877
+ "number_of_replicas": 1,
1878
+ "number_of_shards": 2,
1879
+ "refresh_interval": "5s",
1880
+ },
1881
+ }
1882
+ `);
1883
+ });
1884
+ });
1885
+ describe('Aggregations — Portfolio Analytics', () => {
1886
+ const portfolioMappings = mappings({
1887
+ name: text(),
1888
+ asset_class: keyword(),
1889
+ sector: keyword(),
1890
+ price: long(),
1891
+ yield_rate: float(),
1892
+ listed_date: date()
1893
+ });
1894
+ it('should aggregate fixed-income instruments by sector with yield metrics', () => {
1895
+ const result = query(portfolioMappings)
1896
+ .bool()
1897
+ .filter((q) => q.term('asset_class', 'fixed-income'))
1898
+ .filter((q) => q.range('yield_rate', { gte: 3.0 }))
1899
+ .aggs((agg) => agg
1900
+ .terms('by_sector', 'sector', { size: 10 })
1901
+ .subAgg((sub) => sub.avg('avg_yield', 'yield_rate').max('max_price', 'price')))
1902
+ .size(0)
1903
+ .build();
1904
+ expect(result).toMatchInlineSnapshot(`
1905
+ {
1906
+ "aggs": {
1907
+ "by_sector": {
1908
+ "aggs": {
1909
+ "avg_yield": {
1910
+ "avg": {
1911
+ "field": "yield_rate",
1912
+ },
1913
+ },
1914
+ "max_price": {
1915
+ "max": {
1916
+ "field": "price",
1917
+ },
1918
+ },
1919
+ },
1920
+ "terms": {
1921
+ "field": "sector",
1922
+ "size": 10,
1923
+ },
1924
+ },
1925
+ },
1926
+ "query": {
1927
+ "bool": {
1928
+ "filter": [
1929
+ {
1930
+ "term": {
1931
+ "asset_class": "fixed-income",
1932
+ },
1933
+ },
1934
+ {
1935
+ "range": {
1936
+ "yield_rate": {
1937
+ "gte": 3,
1938
+ },
1939
+ },
1940
+ },
1941
+ ],
1942
+ },
1943
+ },
1944
+ "size": 0,
1945
+ }
1946
+ `);
1947
+ });
1948
+ it('should build a date histogram with percentile sub-aggregation', () => {
1949
+ const result = aggregations(portfolioMappings)
1950
+ .dateHistogram('listings_over_time', 'listed_date', {
1951
+ interval: 'quarter',
1952
+ min_doc_count: 1
1953
+ })
1954
+ .subAgg((sub) => sub.percentiles('yield_percentiles', 'yield_rate', {
1955
+ percents: [25, 50, 75, 95]
1956
+ }))
1957
+ .build();
1958
+ expect(result).toMatchInlineSnapshot(`
1959
+ {
1960
+ "listings_over_time": {
1961
+ "aggs": {
1962
+ "yield_percentiles": {
1963
+ "percentiles": {
1964
+ "field": "yield_rate",
1965
+ "percents": [
1966
+ 25,
1967
+ 50,
1968
+ 75,
1969
+ 95,
1970
+ ],
1971
+ },
1972
+ },
1973
+ },
1974
+ "date_histogram": {
1975
+ "field": "listed_date",
1976
+ "interval": "quarter",
1977
+ "min_doc_count": 1,
1978
+ },
1979
+ },
1980
+ }
1981
+ `);
1982
+ });
1983
+ it('should build a price histogram with stats sub-aggregation', () => {
1984
+ const result = aggregations(portfolioMappings)
1985
+ .histogram('price_buckets', 'price', {
1986
+ interval: 100,
1987
+ min_doc_count: 1
1988
+ })
1989
+ .subAgg((sub) => sub.stats('yield_stats', 'yield_rate'))
1990
+ .build();
1991
+ expect(result).toMatchInlineSnapshot(`
1992
+ {
1993
+ "price_buckets": {
1994
+ "aggs": {
1995
+ "yield_stats": {
1996
+ "stats": {
1997
+ "field": "yield_rate",
1998
+ },
1999
+ },
2000
+ },
2001
+ "histogram": {
2002
+ "field": "price",
2003
+ "interval": 100,
2004
+ "min_doc_count": 1,
2005
+ },
2006
+ },
2007
+ }
2008
+ `);
2009
+ });
2010
+ });
2011
+ describe('Multi-Search with Per-Query Aggregations', () => {
2012
+ it('should batch two aggregation queries in one request', () => {
2013
+ const listingMappings = mappings({
2014
+ address: text(),
2015
+ property_class: keyword(),
2016
+ list_price: long()
2017
+ });
2018
+ const condoSearch = query(listingMappings)
2019
+ .bool()
2020
+ .filter((q) => q.term('property_class', 'condo'))
2021
+ .filter((q) => q.range('list_price', { lte: 2_000_000 }))
2022
+ .aggs((agg) => agg.avg('avg_price', 'list_price'))
2023
+ .size(0)
2024
+ .build();
2025
+ const townhouseSearch = query(listingMappings)
2026
+ .bool()
2027
+ .filter((q) => q.term('property_class', 'townhouse'))
2028
+ .aggs((agg) => agg.avg('avg_price', 'list_price').min('min_price', 'list_price'))
2029
+ .size(0)
2030
+ .build();
2031
+ const ndjson = msearch(listingMappings)
2032
+ .addQuery(condoSearch, { index: 'listings' })
2033
+ .addQuery(townhouseSearch, { index: 'listings' })
2034
+ .build();
2035
+ expect(ndjson).toMatchInlineSnapshot(`
2036
+ "{"index":"listings"}
2037
+ {"query":{"bool":{"filter":[{"term":{"property_class":"condo"}},{"range":{"list_price":{"lte":2000000}}}]}},"aggs":{"avg_price":{"avg":{"field":"list_price"}}},"size":0}
2038
+ {"index":"listings"}
2039
+ {"query":{"bool":{"filter":[{"term":{"property_class":"townhouse"}}]}},"aggs":{"avg_price":{"avg":{"field":"list_price"}},"min_price":{"min":{"field":"list_price"}}},"size":0}
2040
+ "
2041
+ `);
2042
+ });
2043
+ });
2044
+ describe('Suggest — Standalone Builder', () => {
2045
+ const attorneyMappings = mappings({
2046
+ name: text(),
2047
+ practice_area: keyword(),
2048
+ name_suggest: completion()
2049
+ });
2050
+ it('should build a completion autocomplete request', () => {
2051
+ const result = suggest(attorneyMappings)
2052
+ .completion('autocomplete', 'kap', {
2053
+ field: 'name_suggest',
2054
+ size: 5,
2055
+ skip_duplicates: true
2056
+ })
2057
+ .build();
2058
+ expect(result).toMatchInlineSnapshot(`
2059
+ {
2060
+ "suggest": {
2061
+ "autocomplete": {
2062
+ "completion": {
2063
+ "field": "name_suggest",
2064
+ "size": 5,
2065
+ "skip_duplicates": true,
2066
+ },
2067
+ "prefix": "kap",
2068
+ },
2069
+ },
2070
+ }
2071
+ `);
2072
+ });
2073
+ it('should build a term spell-check request', () => {
2074
+ const result = suggest(attorneyMappings)
2075
+ .term('spelling', 'wiliams', {
2076
+ field: 'name',
2077
+ size: 3,
2078
+ suggest_mode: 'popular'
2079
+ })
2080
+ .build();
2081
+ expect(result).toMatchInlineSnapshot(`
2082
+ {
2083
+ "suggest": {
2084
+ "spelling": {
2085
+ "term": {
2086
+ "field": "name",
2087
+ "size": 3,
2088
+ "suggest_mode": "popular",
2089
+ },
2090
+ "text": "wiliams",
2091
+ },
2092
+ },
2093
+ }
2094
+ `);
2095
+ });
2096
+ it('should build a combined autocomplete and spell-check request', () => {
2097
+ const result = suggest(attorneyMappings)
2098
+ .completion('autocomplete', 'kap', { field: 'name_suggest', size: 5 })
2099
+ .term('spelling', 'wiliams', { field: 'name', size: 3 })
2100
+ .build();
2101
+ expect(result).toMatchInlineSnapshot(`
2102
+ {
2103
+ "suggest": {
2104
+ "autocomplete": {
2105
+ "completion": {
2106
+ "field": "name_suggest",
2107
+ "size": 5,
2108
+ },
2109
+ "prefix": "kap",
2110
+ },
2111
+ "spelling": {
2112
+ "term": {
2113
+ "field": "name",
2114
+ "size": 3,
2115
+ },
2116
+ "text": "wiliams",
2117
+ },
2118
+ },
2119
+ }
2120
+ `);
2121
+ });
2122
+ });
2123
+ });