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.
- package/LICENSE +21 -0
- package/README.md +1157 -0
- package/dist/__tests__/elastic.helpers.d.ts +17 -0
- package/dist/__tests__/elastic.helpers.d.ts.map +1 -0
- package/dist/__tests__/elastic.helpers.js +46 -0
- package/dist/aggregation-builder.d.ts +4 -0
- package/dist/aggregation-builder.d.ts.map +1 -0
- package/dist/aggregation-builder.js +153 -0
- package/dist/aggregation-types.d.ts +234 -0
- package/dist/aggregation-types.d.ts.map +1 -0
- package/dist/aggregation-types.js +6 -0
- package/dist/bulk-types.d.ts +67 -0
- package/dist/bulk-types.d.ts.map +1 -0
- package/dist/bulk-types.js +6 -0
- package/dist/bulk.d.ts +22 -0
- package/dist/bulk.d.ts.map +1 -0
- package/dist/bulk.js +55 -0
- package/dist/index-management.d.ts +30 -0
- package/dist/index-management.d.ts.map +1 -0
- package/dist/index-management.js +48 -0
- package/dist/index-types.d.ts +87 -0
- package/dist/index-types.d.ts.map +1 -0
- package/dist/index-types.js +6 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/multi-search-types.d.ts +41 -0
- package/dist/multi-search-types.d.ts.map +1 -0
- package/dist/multi-search-types.js +6 -0
- package/dist/multi-search.d.ts +20 -0
- package/dist/multi-search.d.ts.map +1 -0
- package/dist/multi-search.js +42 -0
- package/dist/query-builder.d.ts +12 -0
- package/dist/query-builder.d.ts.map +1 -0
- package/dist/query-builder.js +396 -0
- package/dist/suggester-types.d.ts +130 -0
- package/dist/suggester-types.d.ts.map +1 -0
- package/dist/suggester-types.js +6 -0
- package/dist/suggester.d.ts +21 -0
- package/dist/suggester.d.ts.map +1 -0
- package/dist/suggester.js +55 -0
- package/dist/types.d.ts +378 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/vector-types.d.ts +42 -0
- package/dist/vector-types.d.ts.map +1 -0
- package/dist/vector-types.js +6 -0
- 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
|
+
[](https://www.npmjs.com/package/elastiq-ts)
|
|
6
|
+
[](https://github.com/misterrodger/elastiq-ts/actions)
|
|
7
|
+
[](https://github.com/misterrodger/elastiq-ts)
|
|
8
|
+
[](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)
|