elastiq-ts 0.1.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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1157 -0
  3. package/dist/__tests__/elastic.helpers.d.ts +17 -0
  4. package/dist/__tests__/elastic.helpers.d.ts.map +1 -0
  5. package/dist/__tests__/elastic.helpers.js +46 -0
  6. package/dist/aggregation-builder.d.ts +4 -0
  7. package/dist/aggregation-builder.d.ts.map +1 -0
  8. package/dist/aggregation-builder.js +153 -0
  9. package/dist/aggregation-types.d.ts +234 -0
  10. package/dist/aggregation-types.d.ts.map +1 -0
  11. package/dist/aggregation-types.js +6 -0
  12. package/dist/bulk-types.d.ts +67 -0
  13. package/dist/bulk-types.d.ts.map +1 -0
  14. package/dist/bulk-types.js +6 -0
  15. package/dist/bulk.d.ts +22 -0
  16. package/dist/bulk.d.ts.map +1 -0
  17. package/dist/bulk.js +55 -0
  18. package/dist/index-management.d.ts +30 -0
  19. package/dist/index-management.d.ts.map +1 -0
  20. package/dist/index-management.js +48 -0
  21. package/dist/index-types.d.ts +87 -0
  22. package/dist/index-types.d.ts.map +1 -0
  23. package/dist/index-types.js +6 -0
  24. package/dist/index.d.ts +12 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +25 -0
  27. package/dist/multi-search-types.d.ts +41 -0
  28. package/dist/multi-search-types.d.ts.map +1 -0
  29. package/dist/multi-search-types.js +6 -0
  30. package/dist/multi-search.d.ts +20 -0
  31. package/dist/multi-search.d.ts.map +1 -0
  32. package/dist/multi-search.js +42 -0
  33. package/dist/query-builder.d.ts +12 -0
  34. package/dist/query-builder.d.ts.map +1 -0
  35. package/dist/query-builder.js +396 -0
  36. package/dist/suggester-types.d.ts +130 -0
  37. package/dist/suggester-types.d.ts.map +1 -0
  38. package/dist/suggester-types.js +6 -0
  39. package/dist/suggester.d.ts +21 -0
  40. package/dist/suggester.d.ts.map +1 -0
  41. package/dist/suggester.js +55 -0
  42. package/dist/types.d.ts +378 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +6 -0
  45. package/dist/vector-types.d.ts +42 -0
  46. package/dist/vector-types.d.ts.map +1 -0
  47. package/dist/vector-types.js +6 -0
  48. package/package.json +69 -0
package/README.md ADDED
@@ -0,0 +1,1157 @@
1
+ # elastiq-ts
2
+
3
+ > **โš ๏ธ Pre-Beta Status**: elastiq-ts is still in active development. APIs may change before first stable release.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/elastiq-ts.svg)](https://www.npmjs.com/package/elastiq-ts)
6
+ [![Build Status](https://github.com/misterrodger/elastiq-ts/actions/workflows/ci.yml/badge.svg)](https://github.com/misterrodger/elastiq-ts/actions)
7
+ [![Coverage Status](https://img.shields.io/badge/coverage-98%25-brightgreen)](https://github.com/misterrodger/elastiq-ts)
8
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
9
+
10
+ > **Type-safe, lightweight Elasticsearch query builder with a fluent, chainable API**
11
+
12
+ elastiq-ts simplifies building Elasticsearch queries in TypeScript. Write type-checked queries that compile to valid Elasticsearch DSL with zero runtime overhead.
13
+
14
+ ## Features
15
+
16
+ - โœจ **Type-Safe**: Full TypeScript generics for field autocomplete and type checking
17
+ - ๐Ÿ”— **Fluent API**: Chainable query builder with intuitive method names
18
+ - ๐ŸŽฏ **Zero Runtime Overhead**: Compiles directly to Elasticsearch DSL objects
19
+ - ๐Ÿงช **Well-Tested**: 430+ passing tests with 98%+ coverage
20
+ - ๐Ÿ“ฆ **Lightweight**: ~22KB uncompressed, no external dependencies
21
+ - ๐ŸŽ“ **Great DX**: Excellent IntelliSense and error messages
22
+ - ๐Ÿš€ **Ready to Use**: Core query features working and tested
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install elastiq-ts@latest
28
+ ```
29
+
30
+ Requires Node.js 20+
31
+
32
+ ## Quick Start
33
+
34
+ ```typescript
35
+ import { query } from 'elastiq-ts';
36
+
37
+ type Product = {
38
+ id: string;
39
+ name: string;
40
+ price: number;
41
+ category: string;
42
+ };
43
+
44
+ // Build a type-safe query
45
+ const q = query<Product>()
46
+ .match('name', 'laptop', { operator: 'and', boost: 2 })
47
+ .range('price', { gte: 500, lte: 2000 })
48
+ .from(0)
49
+ .size(20)
50
+ .build();
51
+
52
+ // Send to Elasticsearch
53
+ const response = await client.search({ index: 'products', ...q });
54
+ ```
55
+
56
+ ## API Overview
57
+
58
+ ### Core Query Methods
59
+
60
+ #### Basic Queries
61
+
62
+ - `match(field, value, options?)` - Full-text search
63
+ - `multiMatch(fields, query, options?)` - Search multiple fields
64
+ - `matchPhrase(field, query)` - Exact phrase matching
65
+ - `term(field, value)` - Exact term matching
66
+ - `terms(field, values)` - Multiple exact values
67
+ - `range(field, conditions)` - Range queries (gte, lte, gt, lt)
68
+ - `exists(field)` - Field existence check
69
+ - `wildcard(field, pattern)` - Wildcard pattern matching
70
+ - `prefix(field, value)` - Prefix matching
71
+ - `fuzzy(field, value, options?)` - Typo tolerance
72
+ - `ids(values)` - Match by document IDs
73
+ - `matchAll()` - Match all documents
74
+
75
+ #### Geo Queries
76
+
77
+ - `geoDistance(field, center, options)` - Distance-based search
78
+ - `geoBoundingBox(field, options)` - Bounding box search
79
+ - `geoPolygon(field, options)` - Polygon search
80
+
81
+ #### Vector Search (KNN)
82
+
83
+ - `knn(field, queryVector, options)` - K-nearest neighbors semantic search
84
+
85
+ #### Advanced Queries
86
+
87
+ - `nested(path, fn, options?)` - Nested object queries
88
+ - `regexp(field, pattern, options?)` - Regular expression matching
89
+ - `constantScore(fn, options?)` - Constant scoring for filters
90
+ - `script(options)` - Script-based filtering
91
+ - `scriptScore(query, script, options?)` - Custom scoring with scripts
92
+ - `percolate(options)` - Match documents against stored queries
93
+
94
+ #### Suggestions & Autocomplete
95
+
96
+ - `suggest(fn)` - Add query suggestions (term, phrase, completion)
97
+ - `term(name, text, options)` - Term-level spell checking
98
+ - `phrase(name, text, options)` - Phrase-level corrections
99
+ - `completion(name, prefix, options)` - Fast autocomplete
100
+
101
+ ### Boolean Logic
102
+
103
+ ```typescript
104
+ query<Product>()
105
+ .bool()
106
+ .must(q => q.match('name', 'laptop')) // AND
107
+ .filter(q => q.range('price', { gte: 500 }))
108
+ .should(q => q.term('featured', true)) // OR
109
+ .mustNot(q => q.term('discontinued', true)) // NOT
110
+ .minimumShouldMatch(1)
111
+ .build();
112
+ ```
113
+
114
+ ### Conditional Building
115
+
116
+ Build queries dynamically based on runtime values:
117
+
118
+ ```typescript
119
+ const searchTerm = getUserInput();
120
+ const minPrice = getMinPrice();
121
+
122
+ query<Product>()
123
+ .bool()
124
+ .must(q =>
125
+ q.when(searchTerm, q => q.match('name', searchTerm)) || q.matchAll()
126
+ )
127
+ .filter(q =>
128
+ q.when(minPrice, q => q.range('price', { gte: minPrice })) || q.matchAll()
129
+ )
130
+ .build();
131
+ ```
132
+
133
+ ### Query Parameters
134
+
135
+ ```typescript
136
+ query<Product>()
137
+ .match('name', 'laptop')
138
+ .from(0) // Pagination offset
139
+ .size(20) // Results per page
140
+ .sort('price', 'asc') // Sort by field
141
+ ._source(['name', 'price']) // Which fields to return
142
+ .timeout('5s') // Query timeout
143
+ .trackScores(true) // Enable scoring in filter context
144
+ .explain(true) // Return scoring explanation
145
+ .minScore(10) // Minimum relevance score
146
+ .highlight(['name', 'description'], {
147
+ fragment_size: 150,
148
+ pre_tags: ['<mark>'],
149
+ post_tags: ['</mark>']
150
+ })
151
+ .build();
152
+ ```
153
+
154
+ ### Aggregations
155
+
156
+ Aggregations can be combined with queries or used standalone:
157
+
158
+ ```typescript
159
+ import { query, aggregations } from 'elastiq-ts';
160
+
161
+ type Product = {
162
+ category: string;
163
+ price: number;
164
+ created_at: string;
165
+ };
166
+
167
+ // Combined query + aggregations
168
+ const result = query<Product>()
169
+ .match('category', 'electronics')
170
+ .aggs(agg =>
171
+ agg
172
+ .terms('by_category', 'category', { size: 10 })
173
+ .avg('avg_price', 'price')
174
+ )
175
+ .size(20)
176
+ .build();
177
+
178
+ // Standalone aggregations (no query) - use query(false)
179
+ const aggsOnly = query<Product>(false)
180
+ .aggs(agg =>
181
+ agg
182
+ .terms('by_category', 'category')
183
+ .subAgg(sub =>
184
+ sub.avg('avg_price', 'price').max('max_price', 'price')
185
+ )
186
+ )
187
+ .size(0) // Common pattern: size=0 when only wanting agg results
188
+ .build();
189
+
190
+ // Standalone aggregation builder (for manual composition)
191
+ const standaloneAgg = aggregations<Product>()
192
+ .dateHistogram('sales_timeline', 'created_at', { interval: 'day' })
193
+ .subAgg(sub =>
194
+ sub.sum('daily_revenue', 'price')
195
+ .cardinality('unique_categories', 'category')
196
+ )
197
+ .build();
198
+ ```
199
+
200
+ ### Vector Search & Semantic Search
201
+
202
+ **Requires Elasticsearch 8.0+**
203
+
204
+ KNN (k-nearest neighbors) queries enable semantic search using vector embeddings from machine learning models.
205
+
206
+ ```typescript
207
+ import { query } from 'elastiq-ts';
208
+
209
+ type Product = {
210
+ id: string;
211
+ name: string;
212
+ description: string;
213
+ price: number;
214
+ category: string;
215
+ embedding: number[]; // Vector field
216
+ };
217
+
218
+ // Basic semantic search
219
+ const searchEmbedding = [0.23, 0.45, 0.67, 0.12, 0.89]; // From your ML model
220
+
221
+ const result = query<Product>()
222
+ .knn('embedding', searchEmbedding, {
223
+ k: 10, // Return top 10 nearest neighbors
224
+ num_candidates: 100 // Consider 100 candidates per shard
225
+ })
226
+ .size(10)
227
+ .build();
228
+
229
+ // Semantic search with filters
230
+ const filtered = query<Product>()
231
+ .knn('embedding', searchEmbedding, {
232
+ k: 20,
233
+ num_candidates: 200,
234
+ filter: {
235
+ bool: {
236
+ must: [{ term: { category: 'electronics' } }],
237
+ filter: [{ range: { price: { gte: 100, lte: 1000 } } }]
238
+ }
239
+ },
240
+ boost: 1.2, // Boost relevance scores
241
+ similarity: 0.7 // Minimum similarity threshold
242
+ })
243
+ .size(20)
244
+ .build();
245
+
246
+ // Product recommendations ("more like this")
247
+ const recommendations = query<Product>()
248
+ .knn('embedding', currentProductEmbedding, {
249
+ k: 10,
250
+ num_candidates: 100,
251
+ filter: {
252
+ bool: {
253
+ must_not: [{ term: { id: 'current-product-id' } }],
254
+ must: [{ term: { category: 'electronics' } }]
255
+ }
256
+ }
257
+ })
258
+ .size(10)
259
+ ._source(['id', 'name', 'price'])
260
+ .build();
261
+
262
+ // Image similarity search
263
+ const imageEmbedding = new Array(512).fill(0); // 512-dim vector from ResNet
264
+
265
+ const imageSimilarity = query<Product>()
266
+ .knn('embedding', imageEmbedding, {
267
+ k: 50,
268
+ num_candidates: 500,
269
+ similarity: 0.8 // High similarity threshold
270
+ })
271
+ .size(50)
272
+ .build();
273
+
274
+ // Hybrid search with aggregations
275
+ const hybridSearch = query<Product>()
276
+ .knn('embedding', searchEmbedding, {
277
+ k: 100,
278
+ num_candidates: 1000,
279
+ filter: { term: { category: 'electronics' } }
280
+ })
281
+ .aggs(agg =>
282
+ agg
283
+ .terms('categories', 'category', { size: 10 })
284
+ .range('price_ranges', 'price', {
285
+ ranges: [
286
+ { to: 100 },
287
+ { from: 100, to: 500 },
288
+ { from: 500 }
289
+ ]
290
+ })
291
+ )
292
+ .size(20)
293
+ .build();
294
+ ```
295
+
296
+ **Common Vector Dimensions:**
297
+ - **384-768**: Sentence transformers (all-MiniLM, all-mpnet)
298
+ - **512**: Image embeddings (ResNet, ViT)
299
+ - **1536**: OpenAI text-embedding-ada-002
300
+ - **3072**: OpenAI text-embedding-3-large
301
+
302
+ **Dense Vector Field Mapping Example:**
303
+ ```typescript
304
+ import type { DenseVectorOptions } from 'elastiq-ts';
305
+
306
+ const mapping: DenseVectorOptions = {
307
+ dims: 384,
308
+ index: true,
309
+ similarity: 'cosine', // 'l2_norm', 'dot_product', or 'cosine'
310
+ index_options: {
311
+ type: 'hnsw',
312
+ m: 16,
313
+ ef_construction: 100
314
+ }
315
+ };
316
+ ```
317
+
318
+ ### Script Queries & Custom Scoring
319
+
320
+ **Note:** Scripts must be enabled in Elasticsearch configuration. Use with caution as they can impact performance.
321
+
322
+ ```typescript
323
+ import { query } from 'elastiq-ts';
324
+
325
+ type Product = {
326
+ id: string;
327
+ name: string;
328
+ price: number;
329
+ popularity: number;
330
+ quality_score: number;
331
+ };
332
+
333
+ // Script-based filtering
334
+ const filtered = query<Product>()
335
+ .bool()
336
+ .must((q) => q.match('name', 'laptop'))
337
+ .filter((q) =>
338
+ q.script({
339
+ source: "doc['price'].value > params.threshold",
340
+ params: { threshold: 500 }
341
+ })
342
+ )
343
+ .build();
344
+
345
+ // Custom scoring with script_score
346
+ const customScored = query<Product>()
347
+ .scriptScore(
348
+ (q) => q.match('name', 'smartphone'),
349
+ {
350
+ source: "_score * Math.log(2 + doc['popularity'].value)",
351
+ lang: 'painless'
352
+ }
353
+ )
354
+ .size(20)
355
+ .build();
356
+
357
+ // Weighted multi-factor scoring
358
+ const weightedScore = query<Product>()
359
+ .scriptScore(
360
+ (q) => q.multiMatch(['name', 'description'], 'premium', { type: 'best_fields' }),
361
+ {
362
+ source: `
363
+ double quality = doc['quality_score'].value;
364
+ double popularity = doc['popularity'].value;
365
+ return _score * (quality * 0.7 + popularity * 0.3);
366
+ `,
367
+ params: {}
368
+ },
369
+ { min_score: 5.0, boost: 1.2 }
370
+ )
371
+ .build();
372
+
373
+ // Personalized scoring
374
+ const personalizedScore = query<Product>()
375
+ .scriptScore(
376
+ (q) => q.matchAll(),
377
+ {
378
+ source: `
379
+ double price_score = 1.0 / (1.0 + doc['price'].value / 1000);
380
+ double quality = doc['quality_score'].value / 10.0;
381
+ return _score * (price_score * params.price_weight + quality * params.quality_weight);
382
+ `,
383
+ params: {
384
+ price_weight: 0.3,
385
+ quality_weight: 0.7
386
+ }
387
+ }
388
+ )
389
+ .build();
390
+ ```
391
+
392
+ **Script Languages:**
393
+ - **painless** (default): Elasticsearch's primary scripting language
394
+ - **expression**: Fast, limited expression language
395
+ - **mustache**: Template-based scripting
396
+
397
+ ### Percolate Queries
398
+
399
+ Percolate queries enable reverse search - match documents against stored queries. Perfect for alerting, content classification, and saved searches.
400
+
401
+ ```typescript
402
+ type AlertRule = {
403
+ query: any;
404
+ name: string;
405
+ severity: string;
406
+ };
407
+
408
+ // Match document against stored queries
409
+ const alerts = query<AlertRule>()
410
+ .percolate({
411
+ field: 'query',
412
+ document: {
413
+ level: 'ERROR',
414
+ message: 'Database connection failed',
415
+ timestamp: '2024-01-15T10:30:00Z'
416
+ }
417
+ })
418
+ .size(100)
419
+ .build();
420
+
421
+ // Classify multiple documents
422
+ const classifications = query<AlertRule>()
423
+ .percolate({
424
+ field: 'query',
425
+ documents: [
426
+ { title: 'AI Breakthrough', content: 'Machine learning advances' },
427
+ { title: 'Market Update', content: 'Stock prices surge' }
428
+ ]
429
+ })
430
+ ._source(['name', 'category'])
431
+ .build();
432
+
433
+ // Match against stored document
434
+ const savedSearch = query<AlertRule>()
435
+ .percolate({
436
+ field: 'query',
437
+ index: 'user_content',
438
+ id: 'doc-123',
439
+ routing: 'user-456'
440
+ })
441
+ .size(20)
442
+ .build();
443
+
444
+ // Security alert system
445
+ const securityAlerts = query<AlertRule>()
446
+ .percolate({
447
+ field: 'query',
448
+ document: {
449
+ event_type: 'unauthorized_access',
450
+ severity: 'high',
451
+ ip_address: '192.168.1.100',
452
+ timestamp: '2024-01-15T14:00:00Z'
453
+ },
454
+ name: 'security_event_check'
455
+ })
456
+ .sort('severity', 'desc')
457
+ .build();
458
+ ```
459
+
460
+ **Common Use Cases:**
461
+
462
+ - **Alerting:** Match events against alert rules
463
+ - **Content Classification:** Categorize documents in real-time
464
+ - **Saved Searches:** Notify users when new content matches their searches
465
+ - **Monitoring:** Trigger actions based on metric thresholds
466
+
467
+ ### Query Suggestions & Autocomplete
468
+
469
+ Elasticsearch Suggesters provide spell-checking, phrase correction, and autocomplete functionality. Perfect for search-as-you-type experiences and fixing user typos.
470
+
471
+ ```typescript
472
+ import { query, suggest } from 'elastiq-ts';
473
+
474
+ type Product = {
475
+ name: string;
476
+ description: string;
477
+ suggest_field: string; // Must be type: completion
478
+ };
479
+
480
+ // Term suggester - Fix typos in individual terms
481
+ const termSuggestions = suggest<Product>()
482
+ .term('name-suggestions', 'laptpo', {
483
+ field: 'name',
484
+ size: 5,
485
+ suggest_mode: 'popular', // 'missing' | 'popular' | 'always'
486
+ string_distance: 'levenshtein',
487
+ max_edits: 2
488
+ })
489
+ .build();
490
+
491
+ // Phrase suggester - Fix entire phrases
492
+ const phraseSuggestions = suggest<Product>()
493
+ .phrase('description-suggestions', 'powerfull laptop', {
494
+ field: 'description',
495
+ size: 3,
496
+ confidence: 2.0,
497
+ max_errors: 1,
498
+ pre_tag: '<em>',
499
+ post_tag: '</em>',
500
+ direct_generator: [
501
+ {
502
+ field: 'description',
503
+ suggest_mode: 'always',
504
+ max_edits: 2,
505
+ min_word_length: 3
506
+ }
507
+ ]
508
+ })
509
+ .build();
510
+
511
+ // Completion suggester - Fast autocomplete
512
+ const autocomplete = suggest<Product>()
513
+ .completion('autocomplete', 'lap', {
514
+ field: 'suggest_field',
515
+ size: 10,
516
+ skip_duplicates: true,
517
+ fuzzy: {
518
+ fuzziness: 'AUTO',
519
+ transpositions: true,
520
+ min_length: 3,
521
+ prefix_length: 1
522
+ }
523
+ })
524
+ .build();
525
+
526
+ // Combine with query - Search with autocomplete
527
+ const searchWithSuggestions = query<Product>()
528
+ .match('name', 'laptpo')
529
+ .suggest((s) =>
530
+ s.term('spelling-correction', 'laptpo', {
531
+ field: 'name',
532
+ size: 3,
533
+ suggest_mode: 'popular'
534
+ })
535
+ )
536
+ .size(20)
537
+ .build();
538
+
539
+ // Multiple suggesters
540
+ const multiSuggest = suggest<Product>()
541
+ .term('name-term', 'laptpo', { field: 'name', size: 3 })
542
+ .completion('name-complete', 'lap', { field: 'suggest_field', size: 5 })
543
+ .build();
544
+
545
+ // Search-as-you-type with context filtering
546
+ const contextual = query<Product>()
547
+ .bool()
548
+ .filter((q) => q.term('category', 'electronics'))
549
+ .suggest((s) =>
550
+ s.completion('product-autocomplete', 'lapt', {
551
+ field: 'suggest_field',
552
+ size: 10,
553
+ contexts: {
554
+ category: 'electronics'
555
+ },
556
+ fuzzy: {
557
+ fuzziness: 'AUTO'
558
+ }
559
+ })
560
+ )
561
+ .size(0) // Only want suggestions, not search results
562
+ .build();
563
+
564
+ // Phrase correction with highlighting
565
+ const correction = suggest<Product>()
566
+ .phrase('phrase-fix', 'powerfull gaming laptop', {
567
+ field: 'description',
568
+ size: 3,
569
+ confidence: 1.5,
570
+ pre_tag: '<strong>',
571
+ post_tag: '</strong>',
572
+ collate: {
573
+ query: {
574
+ source: { match: { description: '{{suggestion}}' } }
575
+ },
576
+ prune: true
577
+ }
578
+ })
579
+ .build();
580
+ ```
581
+
582
+ **Suggester Types:**
583
+
584
+ - **Term:** Suggests corrections for individual terms based on edit distance
585
+ - **Phrase:** Suggests corrections for entire phrases using n-gram language models
586
+ - **Completion:** Fast prefix-based autocomplete (requires `completion` field type)
587
+
588
+ **Common Use Cases:**
589
+
590
+ - **Autocomplete:** Search-as-you-type suggestions for product names, categories
591
+ - **Spell Check:** Fix typos in search queries ("laptpo" โ†’ "laptop")
592
+ - **Did You Mean:** Suggest alternative queries when searches return few results
593
+ - **Phrase Correction:** Fix grammatical errors in multi-word queries
594
+
595
+ **Completion Field Mapping:**
596
+
597
+ ```typescript
598
+ import { indexBuilder } from 'elastiq-ts';
599
+
600
+ const index = indexBuilder<Product>()
601
+ .mappings({
602
+ properties: {
603
+ suggest_field: {
604
+ type: 'completion',
605
+ analyzer: 'simple',
606
+ search_analyzer: 'simple'
607
+ }
608
+ }
609
+ })
610
+ .build();
611
+ ```
612
+
613
+ ### Multi-Search API
614
+
615
+ Batch multiple search requests in a single API call using the NDJSON format.
616
+
617
+ ```typescript
618
+ import { query, msearch } from 'elastiq-ts';
619
+
620
+ const laptopQuery = query<Product>()
621
+ .match('name', 'laptop')
622
+ .range('price', { gte: 500, lte: 2000 })
623
+ .build();
624
+
625
+ const phoneQuery = query<Product>()
626
+ .match('name', 'smartphone')
627
+ .range('price', { gte: 300, lte: 1000 })
628
+ .build();
629
+
630
+ // Build as NDJSON string for Elasticsearch API
631
+ const ndjson = msearch<Product>()
632
+ .addQuery(laptopQuery, { index: 'products', preference: '_local' })
633
+ .addQuery(phoneQuery, { index: 'products', preference: '_local' })
634
+ .build();
635
+
636
+ // Or build as array of objects
637
+ const array = msearch<Product>()
638
+ .addQuery(laptopQuery, { index: 'products' })
639
+ .addQuery(phoneQuery, { index: 'products' })
640
+ .buildArray();
641
+ ```
642
+
643
+ **NDJSON Format (for Elasticsearch `_msearch` endpoint):**
644
+
645
+ ```ndjson
646
+ {"index":"products","preference":"_local"}
647
+ {"query":{"bool":{"must":[{"match":{"name":"laptop"}},{"range":{"price":{"gte":500,"lte":2000}}}]}}}
648
+ {"index":"products","preference":"_local"}
649
+ {"query":{"bool":{"must":[{"match":{"name":"smartphone"}},{"range":{"price":{"gte":300,"lte":1000}}}]}}}
650
+
651
+ ```
652
+
653
+ **Header Options:**
654
+
655
+ - `index`: Target index/indices (string or array)
656
+ - `routing`: Routing value for sharding
657
+ - `preference`: Node preference (\_local, \_primary, etc.)
658
+ - `search_type`: Search type (dfs_query_then_fetch, etc.)
659
+
660
+ **Common Use Cases:**
661
+
662
+ - **Batch Search:** Execute multiple searches in one request
663
+ - **Cross-Index Search:** Query different indices simultaneously
664
+ - **Performance Optimization:** Reduce HTTP overhead for multiple queries
665
+ - **Dashboard Queries:** Load multiple widgets/charts in parallel
666
+
667
+ ### Bulk Operations
668
+
669
+ Batch create, index, update, and delete operations efficiently.
670
+
671
+ ```typescript
672
+ import { bulk } from 'elastiq-ts';
673
+
674
+ type Product = {
675
+ id: string;
676
+ name: string;
677
+ price: number;
678
+ category: string;
679
+ };
680
+
681
+ const bulkOp = bulk<Product>()
682
+ // Index (create or replace)
683
+ .index(
684
+ { id: '1', name: 'Laptop Pro', price: 1299, category: 'electronics' },
685
+ { _index: 'products', _id: '1' }
686
+ )
687
+ // Create (fail if exists)
688
+ .create(
689
+ { id: '2', name: 'Wireless Mouse', price: 29, category: 'accessories' },
690
+ { _index: 'products', _id: '2' }
691
+ )
692
+ // Update (partial document)
693
+ .update({
694
+ _index: 'products',
695
+ _id: '3',
696
+ doc: { price: 999 }
697
+ })
698
+ // Update with script
699
+ .update({
700
+ _index: 'products',
701
+ _id: '4',
702
+ script: {
703
+ source: 'ctx._source.price *= params.multiplier',
704
+ params: { multiplier: 0.9 }
705
+ }
706
+ })
707
+ // Update with upsert
708
+ .update({
709
+ _index: 'products',
710
+ _id: '5',
711
+ doc: { price: 499 },
712
+ upsert: { id: '5', name: 'New Product', price: 499, category: 'electronics' }
713
+ })
714
+ // Delete
715
+ .delete({ _index: 'products', _id: '6' })
716
+ .build();
717
+
718
+ // Send to Elasticsearch /_bulk endpoint
719
+ // POST /_bulk
720
+ // Content-Type: application/x-ndjson
721
+ // Body: bulkOp
722
+ ```
723
+
724
+ **NDJSON Format:**
725
+
726
+ ```ndjson
727
+ {"index":{"_index":"products","_id":"1"}}
728
+ {"id":"1","name":"Laptop Pro","price":1299,"category":"electronics"}
729
+ {"create":{"_index":"products","_id":"2"}}
730
+ {"id":"2","name":"Wireless Mouse","price":29,"category":"accessories"}
731
+ {"update":{"_index":"products","_id":"3"}}
732
+ {"doc":{"price":999}}
733
+ {"update":{"_index":"products","_id":"4"}}
734
+ {"script":{"source":"ctx._source.price *= params.multiplier","params":{"multiplier":0.9}}}
735
+ {"update":{"_index":"products","_id":"5"}}
736
+ {"doc":{"price":499},"upsert":{"id":"5","name":"New Product","price":499,"category":"electronics"}}
737
+ {"delete":{"_index":"products","_id":"6"}}
738
+
739
+ ```
740
+
741
+ **Operation Types:**
742
+
743
+ - `index`: Create or replace document
744
+ - `create`: Create new document (fails if exists)
745
+ - `update`: Partial update with doc, script, or upsert
746
+ - `delete`: Remove document
747
+
748
+ **Update Options:**
749
+
750
+ - `doc`: Partial document merge
751
+ - `script`: Script-based update (Painless)
752
+ - `upsert`: Document to insert if not exists
753
+ - `doc_as_upsert`: Use doc as upsert document
754
+ - `retry_on_conflict`: Retry count for version conflicts
755
+
756
+ **Common Use Cases:**
757
+
758
+ - **Data Import:** Batch insert large datasets
759
+ - **Sync Operations:** Keep indices in sync with external systems
760
+ - **Price Updates:** Update multiple products efficiently
761
+ - **Batch Deletes:** Remove outdated documents in bulk
762
+
763
+ ### Index Management
764
+
765
+ Configure index mappings, settings, and aliases declaratively.
766
+
767
+ ```typescript
768
+ import { indexBuilder } from 'elastiq-ts';
769
+
770
+ type Product = {
771
+ id: string;
772
+ name: string;
773
+ description: string;
774
+ price: number;
775
+ category: string;
776
+ tags: string[];
777
+ inStock: boolean;
778
+ embedding: number[];
779
+ };
780
+
781
+ const indexConfig = indexBuilder<Product>()
782
+ .mappings({
783
+ properties: {
784
+ id: { type: 'keyword' },
785
+ name: { type: 'text', analyzer: 'standard' },
786
+ description: { type: 'text', analyzer: 'english' },
787
+ price: { type: 'float' },
788
+ category: { type: 'keyword' },
789
+ tags: { type: 'keyword' },
790
+ inStock: { type: 'boolean' },
791
+ embedding: {
792
+ type: 'dense_vector',
793
+ dims: 384,
794
+ index: true,
795
+ similarity: 'cosine'
796
+ }
797
+ }
798
+ })
799
+ .settings({
800
+ number_of_shards: 2,
801
+ number_of_replicas: 1,
802
+ refresh_interval: '5s',
803
+ 'index.max_result_window': 10000
804
+ })
805
+ .alias('products_current')
806
+ .alias('products_search', { is_write_index: true })
807
+ .build();
808
+
809
+ // Use with Elasticsearch Create Index API
810
+ // PUT /products
811
+ // Content-Type: application/json
812
+ // Body: JSON.stringify(indexConfig)
813
+ ```
814
+
815
+ **Field Types (20+ supported):**
816
+
817
+ - **Text:** `text`, `keyword`, `constant_keyword`
818
+ - **Numeric:** `long`, `integer`, `short`, `byte`, `double`, `float`, `half_float`, `scaled_float`
819
+ - **Date:** `date`, `date_nanos`
820
+ - **Boolean:** `boolean`
821
+ - **Binary:** `binary`
822
+ - **Range:** `integer_range`, `float_range`, `long_range`, `double_range`, `date_range`
823
+ - **Objects:** `object`, `nested`, `flattened`
824
+ - **Spatial:** `geo_point`, `geo_shape`
825
+ - **Specialized:** `ip`, `completion`, `token_count`, `dense_vector`, `rank_feature`, `rank_features`
826
+
827
+ **Mapping Properties:**
828
+
829
+ - `type`: Field type
830
+ - `analyzer`: Text analyzer (standard, english, etc.)
831
+ - `index`: Enable/disable indexing
832
+ - `store`: Store field separately
833
+ - `fields`: Multi-field mappings
834
+ - `null_value`: Default for null values
835
+ - `copy_to`: Copy field to another field
836
+ - `eager_global_ordinals`: Optimize aggregations
837
+
838
+ **Settings Options:**
839
+
840
+ - `number_of_shards`: Shard count (set at creation)
841
+ - `number_of_replicas`: Replica count
842
+ - `refresh_interval`: Auto-refresh interval
843
+ - `max_result_window`: Maximum result window size
844
+ - `analysis`: Custom analyzers, tokenizers, filters
845
+
846
+ **Alias Options:**
847
+
848
+ - `is_write_index`: Designate write target for alias
849
+ - `routing`: Default routing value
850
+ - `filter`: Filter documents visible through alias
851
+
852
+ **Real-World Examples:**
853
+
854
+ **E-commerce Index:**
855
+
856
+ ```typescript
857
+ const ecommerceIndex = indexBuilder<Product>()
858
+ .mappings({
859
+ properties: {
860
+ sku: { type: 'keyword' },
861
+ name: { type: 'text', analyzer: 'standard', fields: { keyword: { type: 'keyword' } } },
862
+ description: { type: 'text', analyzer: 'english' },
863
+ price: { type: 'scaled_float', scaling_factor: 100 },
864
+ category: { type: 'keyword' },
865
+ brand: { type: 'keyword' },
866
+ tags: { type: 'keyword' },
867
+ rating: { type: 'half_float' },
868
+ reviewCount: { type: 'integer' },
869
+ inStock: { type: 'boolean' },
870
+ createdAt: { type: 'date' }
871
+ }
872
+ })
873
+ .settings({
874
+ number_of_shards: 3,
875
+ number_of_replicas: 2,
876
+ refresh_interval: '1s'
877
+ })
878
+ .alias('products')
879
+ .build();
880
+ ```
881
+
882
+ **Vector Search Index:**
883
+
884
+ ```typescript
885
+ const vectorIndex = indexBuilder<Article>()
886
+ .mappings({
887
+ properties: {
888
+ title: { type: 'text' },
889
+ content: { type: 'text' },
890
+ embedding: {
891
+ type: 'dense_vector',
892
+ dims: 768,
893
+ index: true,
894
+ similarity: 'cosine',
895
+ index_options: {
896
+ type: 'hnsw',
897
+ m: 16,
898
+ ef_construction: 100
899
+ }
900
+ }
901
+ }
902
+ })
903
+ .settings({
904
+ number_of_shards: 1,
905
+ number_of_replicas: 0
906
+ })
907
+ .build();
908
+ ```
909
+
910
+ **Common Use Cases:**
911
+
912
+ - **Index Creation:** Define schema before indexing data
913
+ - **Schema Migration:** Version indices with aliases
914
+ - **Multi-Tenancy:** Create per-tenant indices programmatically
915
+ - **Vector Search Setup:** Configure dense_vector fields with HNSW
916
+
917
+ ## Examples
918
+
919
+ More examples available in [src/__tests__/examples.test.ts](src/__tests__/examples.test.ts).
920
+
921
+ ### E-commerce Product Search
922
+
923
+ ```typescript
924
+ const searchTerm = 'gaming laptop';
925
+ const category = 'electronics';
926
+ const minPrice = 800;
927
+ const maxPrice = 2000;
928
+
929
+ const result = query<Product>()
930
+ .bool()
931
+ .must(q => q.match('name', searchTerm, { operator: 'and', boost: 2 }))
932
+ .should(q => q.fuzzy('description', searchTerm, { fuzziness: 'AUTO' }))
933
+ .filter(q => q.term('category', category))
934
+ .filter(q => q.range('price', { gte: minPrice, lte: maxPrice }))
935
+ .minimumShouldMatch(1)
936
+ .highlight(['name', 'description'])
937
+ .timeout('5s')
938
+ .from(0)
939
+ .size(20)
940
+ .sort('price', 'asc')
941
+ .build();
942
+ ```
943
+
944
+ Produces:
945
+
946
+ ```json
947
+ {
948
+ "query": {
949
+ "bool": {
950
+ "must": [
951
+ {
952
+ "match": {
953
+ "name": {
954
+ "query": "gaming laptop",
955
+ "operator": "and",
956
+ "boost": 2
957
+ }
958
+ }
959
+ }
960
+ ],
961
+ "should": [
962
+ {
963
+ "fuzzy": {
964
+ "description": {
965
+ "value": "gaming laptop",
966
+ "fuzziness": "AUTO"
967
+ }
968
+ }
969
+ }
970
+ ],
971
+ "filter": [
972
+ { "term": { "category": "electronics" } },
973
+ {
974
+ "range": {
975
+ "price": { "gte": 800, "lte": 2000 }
976
+ }
977
+ }
978
+ ],
979
+ "minimum_should_match": 1
980
+ }
981
+ },
982
+ "highlight": {
983
+ "fields": {
984
+ "name": {},
985
+ "description": {}
986
+ }
987
+ },
988
+ "timeout": "5s",
989
+ "from": 0,
990
+ "size": 20,
991
+ "sort": [{ "price": "asc" }]
992
+ }
993
+ ```
994
+
995
+ ### Content Search with Filtering
996
+
997
+ ```typescript
998
+ type Article = {
999
+ title: string;
1000
+ content: string;
1001
+ author: string;
1002
+ published_date: string;
1003
+ };
1004
+
1005
+ const result = query<Article>()
1006
+ .bool()
1007
+ .must(q => q.multiMatch(['title', 'content'], 'elasticsearch', { type: 'best_fields' }))
1008
+ .filter(q => q.range('published_date', { gte: '2024-01-01' }))
1009
+ .should(q => q.match('author', 'jane', { boost: 2 }))
1010
+ .minimumShouldMatch(1)
1011
+ .highlight(['title', 'content'])
1012
+ .timeout('10s')
1013
+ .trackTotalHits(10000)
1014
+ .from(0)
1015
+ .size(20)
1016
+ .build();
1017
+ ```
1018
+
1019
+ ### Dynamic Search with Conditional Filters
1020
+
1021
+ ```typescript
1022
+ const buildDynamicQuery = (filters: SearchFilters) => {
1023
+ return query<Product>()
1024
+ .bool()
1025
+ .must(q =>
1026
+ q.when(filters.searchTerm,
1027
+ q => q.match('name', filters.searchTerm, { boost: 2 })
1028
+ ) || q.matchAll()
1029
+ )
1030
+ .filter(q =>
1031
+ q.when(filters.minPrice && filters.maxPrice,
1032
+ q => q.range('price', { gte: filters.minPrice, lte: filters.maxPrice })
1033
+ ) || q.matchAll()
1034
+ )
1035
+ .filter(q =>
1036
+ q.when(filters.category,
1037
+ q => q.term('category', filters.category)
1038
+ ) || q.matchAll()
1039
+ )
1040
+ .from(filters.offset || 0)
1041
+ .size(filters.limit || 20)
1042
+ .build();
1043
+ };
1044
+ ```
1045
+
1046
+ ### Geospatial Search
1047
+
1048
+ ```typescript
1049
+ type Restaurant = {
1050
+ name: string;
1051
+ cuisine: string;
1052
+ location: { lat: number; lon: number };
1053
+ rating: number;
1054
+ };
1055
+
1056
+ const result = query<Restaurant>()
1057
+ .match('cuisine', 'italian')
1058
+ .geoDistance(
1059
+ 'location',
1060
+ { lat: 40.7128, lon: -74.006 }, // The Big ๐ŸŽ
1061
+ { distance: '5km' }
1062
+ )
1063
+ .from(0)
1064
+ .size(20)
1065
+ .build();
1066
+ ```
1067
+
1068
+ ## TypeScript Support
1069
+
1070
+ elastiq-ts provides excellent TypeScript support with:
1071
+
1072
+ - **Field Autocomplete**: Type-safe field names with IntelliSense
1073
+ - **Type Validation**: Compile-time checking for query structure
1074
+ - **Generic Parameters**: Full type inference across builder chains
1075
+
1076
+ ```typescript
1077
+ type User = {
1078
+ id: string;
1079
+ name: string;
1080
+ email: string;
1081
+ age: number;
1082
+ };
1083
+
1084
+ // โœ… Type-safe field names
1085
+ const q1 = query<User>().match('name', 'John').build();
1086
+
1087
+ // โŒ TypeScript error: 'unknown_field' is not a valid field
1088
+ const q2 = query<User>().match('unknown_field', 'value').build();
1089
+
1090
+ // โŒ TypeScript error: age is number, not string
1091
+ const q3 = query<User>().match('age', 'should be a number').build();
1092
+ ```
1093
+
1094
+ ## Testing
1095
+
1096
+ ```bash
1097
+ # Run tests
1098
+ npm test
1099
+
1100
+ # Watch mode
1101
+ npm test:watch
1102
+
1103
+ # Coverage report
1104
+ npm test:coverage
1105
+
1106
+ # Type check
1107
+ npm run type-check
1108
+ ```
1109
+
1110
+ All queries are tested against the Elasticsearch DSL specification with 147+ passing tests.
1111
+
1112
+ ## Version Support
1113
+
1114
+ - **Node.js**: 20/22
1115
+ - **Elasticsearch**: 9.2.4
1116
+
1117
+ ## Roadmap
1118
+
1119
+ ### Current Release โœ…
1120
+
1121
+ - [x] Core query types (match, term, range, bool, etc.)
1122
+ - [x] Fuzzy queries for typo tolerance
1123
+ - [x] Query parameters (from, size, sort, timeout, etc.)
1124
+ - [x] Conditional query building
1125
+ - [x] Highlight support
1126
+ - [x] Aggregations (bucket and metric)
1127
+ - [x] Geo queries (distance, bounding box, polygon)
1128
+ - [x] Advanced patterns (regexp, constant_score)
1129
+ - [x] Sub-aggregation support
1130
+ - [x] Query + aggregations integration
1131
+ - [x] KNN (k-nearest neighbors) queries for vector search
1132
+ - [x] Semantic search with vector embeddings
1133
+ - [x] Dense vector field support
1134
+ - [x] Script queries and custom scoring
1135
+ - [x] Percolate queries for reverse search
1136
+ - [x] Multi-search API (NDJSON batched queries)
1137
+ - [x] Bulk operations (create, index, update, delete)
1138
+ - [x] Index management (mappings, settings, aliases)
1139
+ - [x] Full test coverage (388+ tests)
1140
+
1141
+ ### Planned for Future Releases
1142
+
1143
+ - [ ] Query suggestions/completions (term, phrase, completion)
1144
+
1145
+ ## Contributing
1146
+
1147
+ We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
1148
+
1149
+ ## License
1150
+
1151
+ MIT ยฉ 2026 misterrodger
1152
+
1153
+ ## Support
1154
+
1155
+ - ๐Ÿ“– [Documentation](https://github.com/misterrodger/elastiq-ts#readme)
1156
+ - ๐Ÿ› [Report Issues](https://github.com/misterrodger/elastiq-ts/issues)
1157
+ - ๐Ÿ’ฌ [Discussions](https://github.com/misterrodger/elastiq-ts/discussions)