elasticlink 0.2.2-beta → 0.3.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/README.md +162 -163
- package/dist/aggregation.builder.d.ts +4 -2
- package/dist/aggregation.builder.d.ts.map +1 -1
- package/dist/aggregation.builder.js +1 -1
- package/dist/aggregation.types.d.ts +15 -14
- package/dist/aggregation.types.d.ts.map +1 -1
- package/dist/bulk.builder.d.ts +115 -1
- package/dist/bulk.builder.d.ts.map +1 -1
- package/dist/bulk.builder.js +1 -1
- package/dist/field.helpers.d.ts +39 -44
- package/dist/field.helpers.d.ts.map +1 -1
- package/dist/field.helpers.js +15 -19
- package/dist/field.types.d.ts +35 -5
- package/dist/field.types.d.ts.map +1 -1
- package/dist/field.types.js +3 -3
- package/dist/index-management.builder.d.ts +5 -12
- package/dist/index-management.builder.d.ts.map +1 -1
- package/dist/index-management.builder.js +10 -32
- package/dist/index-management.types.d.ts +10 -64
- package/dist/index-management.types.d.ts.map +1 -1
- package/dist/index-management.types.js +0 -1
- package/dist/index.d.ts +16 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -8
- package/dist/mapping.builder.d.ts +24 -0
- package/dist/mapping.builder.d.ts.map +1 -0
- package/dist/mapping.builder.js +23 -0
- package/dist/mapping.types.d.ts +82 -0
- package/dist/mapping.types.d.ts.map +1 -0
- package/dist/mapping.types.js +6 -0
- package/dist/multi-search.builder.d.ts +5 -3
- package/dist/multi-search.builder.d.ts.map +1 -1
- package/dist/multi-search.builder.js +2 -2
- package/dist/multi-search.types.d.ts +7 -6
- package/dist/multi-search.types.d.ts.map +1 -1
- package/dist/query.builder.d.ts +3 -7
- package/dist/query.builder.d.ts.map +1 -1
- package/dist/query.builder.js +29 -91
- package/dist/query.types.d.ts +85 -235
- package/dist/query.types.d.ts.map +1 -1
- package/dist/query.types.js +2 -1
- package/dist/suggester.builder.d.ts +5 -3
- package/dist/suggester.builder.d.ts.map +1 -1
- package/dist/suggester.builder.js +2 -2
- package/dist/suggester.types.d.ts +5 -4
- package/dist/suggester.types.d.ts.map +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ elasticlink simplifies building Elasticsearch queries and index management in Ty
|
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
16
|
-
- **
|
|
16
|
+
- **Mapping-Aware Type Safety**: Define ES field types once via `mappings()`, get compile-time constraints — `match()` only accepts text fields, `term()` only keyword fields, etc.
|
|
17
17
|
- **Fluent API**: Chainable query builder with intuitive method names
|
|
18
18
|
- **Zero Runtime Overhead**: Compiles directly to Elasticsearch DSL objects
|
|
19
19
|
- **Well-Tested**: Comprehensive unit and integration test suite against live Elasticsearch
|
|
@@ -22,12 +22,13 @@ elasticlink simplifies building Elasticsearch queries and index management in Ty
|
|
|
22
22
|
|
|
23
23
|
## Compatibility
|
|
24
24
|
|
|
25
|
-
| elasticlink | Node.js
|
|
26
|
-
|
|
27
|
-
| 0.
|
|
28
|
-
| 0.
|
|
25
|
+
| elasticlink | Node.js | Elasticsearch |
|
|
26
|
+
|-------------|-------------|---------------|
|
|
27
|
+
| 0.3.0-beta | 20, 22, 24 | 9.x (≥9.0.0) |
|
|
28
|
+
| 0.2.0-beta | 20, 22 | 9.x (≥9.0.0) |
|
|
29
|
+
| 0.1.0-beta | 20, 22 | 9.x (≥9.0.0) |
|
|
29
30
|
|
|
30
|
-
Tested against the versions listed. Peer dependency is `@elastic/elasticsearch >=9.0.0`.
|
|
31
|
+
Tested against the versions listed. Peer dependency is `@elastic/elasticsearch >=9.0.0`.
|
|
31
32
|
|
|
32
33
|
## Installation
|
|
33
34
|
|
|
@@ -42,18 +43,21 @@ Requires Node.js 20+ and `@elastic/elasticsearch` 9.x as a peer dependency.
|
|
|
42
43
|
## Quick Start
|
|
43
44
|
|
|
44
45
|
```typescript
|
|
45
|
-
import { query } from 'elasticlink';
|
|
46
|
+
import { query, mappings, text, keyword, float, type Infer } from 'elasticlink';
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
name:
|
|
50
|
-
price:
|
|
51
|
-
category:
|
|
52
|
-
};
|
|
48
|
+
// Define field types once — this is the single source of truth
|
|
49
|
+
const productMappings = mappings({
|
|
50
|
+
name: text(),
|
|
51
|
+
price: float(),
|
|
52
|
+
category: keyword(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Derive a TS type from mappings (optional, for use elsewhere)
|
|
56
|
+
type Product = Infer<typeof productMappings>;
|
|
53
57
|
|
|
54
|
-
// Build a type-safe query
|
|
55
|
-
const q = query
|
|
56
|
-
.match('name', 'laptop'
|
|
58
|
+
// Build a type-safe query — field constraints are enforced at compile time
|
|
59
|
+
const q = query(productMappings)
|
|
60
|
+
.match('name', 'laptop') // ✅ 'name' is a text field
|
|
57
61
|
.range('price', { gte: 500, lte: 2000 })
|
|
58
62
|
.from(0)
|
|
59
63
|
.size(20)
|
|
@@ -112,12 +116,12 @@ const response = await client.search({ index: 'products', ...q });
|
|
|
112
116
|
### Boolean Logic
|
|
113
117
|
|
|
114
118
|
```typescript
|
|
115
|
-
query
|
|
119
|
+
query(productMappings)
|
|
116
120
|
.bool()
|
|
117
121
|
.must(q => q.match('name', 'laptop')) // AND
|
|
118
122
|
.filter(q => q.range('price', { gte: 500 }))
|
|
119
|
-
.should(q => q.term('
|
|
120
|
-
.mustNot(q => q.term('
|
|
123
|
+
.should(q => q.term('category', 'featured')) // OR
|
|
124
|
+
.mustNot(q => q.term('category', 'discontinued')) // NOT
|
|
121
125
|
.minimumShouldMatch(1)
|
|
122
126
|
.build();
|
|
123
127
|
```
|
|
@@ -130,7 +134,7 @@ Build queries dynamically based on runtime values:
|
|
|
130
134
|
const searchTerm = getUserInput();
|
|
131
135
|
const minPrice = getMinPrice();
|
|
132
136
|
|
|
133
|
-
query
|
|
137
|
+
query(productMappings)
|
|
134
138
|
.bool()
|
|
135
139
|
.must(q =>
|
|
136
140
|
q.when(searchTerm, q => q.match('name', searchTerm)) || q.matchAll()
|
|
@@ -144,7 +148,7 @@ query<Product>()
|
|
|
144
148
|
### Query Parameters
|
|
145
149
|
|
|
146
150
|
```typescript
|
|
147
|
-
query
|
|
151
|
+
query(productMappings)
|
|
148
152
|
.match('name', 'laptop')
|
|
149
153
|
.from(0) // Pagination offset
|
|
150
154
|
.size(20) // Results per page
|
|
@@ -176,9 +180,9 @@ Aggregations can be combined with queries or used standalone:
|
|
|
176
180
|
```typescript
|
|
177
181
|
import { query, aggregations } from 'elasticlink';
|
|
178
182
|
|
|
179
|
-
// Combined query + aggregations
|
|
180
|
-
const result = query
|
|
181
|
-
.
|
|
183
|
+
// Combined query + aggregations (inline aggs auto-thread mappings)
|
|
184
|
+
const result = query(productMappings)
|
|
185
|
+
.term('category', 'electronics')
|
|
182
186
|
.aggs(agg =>
|
|
183
187
|
agg
|
|
184
188
|
.terms('by_category', 'category', { size: 10 })
|
|
@@ -187,8 +191,8 @@ const result = query<Product>()
|
|
|
187
191
|
.size(20)
|
|
188
192
|
.build();
|
|
189
193
|
|
|
190
|
-
// Standalone aggregations (no query)
|
|
191
|
-
const aggsOnly = query
|
|
194
|
+
// Standalone aggregations (no query) — use query(mappings, false)
|
|
195
|
+
const aggsOnly = query(productMappings, false)
|
|
192
196
|
.aggs(agg =>
|
|
193
197
|
agg
|
|
194
198
|
.terms('by_category', 'category')
|
|
@@ -200,12 +204,9 @@ const aggsOnly = query<Product>(false)
|
|
|
200
204
|
.build();
|
|
201
205
|
|
|
202
206
|
// Standalone aggregation builder (for manual composition)
|
|
203
|
-
const standaloneAgg = aggregations
|
|
204
|
-
.
|
|
205
|
-
.
|
|
206
|
-
sub.sum('daily_revenue', 'price')
|
|
207
|
-
.cardinality('unique_categories', 'category')
|
|
208
|
-
)
|
|
207
|
+
const standaloneAgg = aggregations(productMappings)
|
|
208
|
+
.avg('avg_price', 'price')
|
|
209
|
+
.terms('by_category', 'category', { size: 10 })
|
|
209
210
|
.build();
|
|
210
211
|
```
|
|
211
212
|
|
|
@@ -216,21 +217,20 @@ const standaloneAgg = aggregations<Product>()
|
|
|
216
217
|
KNN (k-nearest neighbors) queries enable semantic search using vector embeddings from machine learning models.
|
|
217
218
|
|
|
218
219
|
```typescript
|
|
219
|
-
import { query } from 'elasticlink';
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
};
|
|
220
|
+
import { query, mappings, text, keyword, float, denseVector } from 'elasticlink';
|
|
221
|
+
|
|
222
|
+
const productWithEmbeddingMappings = mappings({
|
|
223
|
+
name: text(),
|
|
224
|
+
description: text(),
|
|
225
|
+
price: float(),
|
|
226
|
+
category: keyword(),
|
|
227
|
+
embedding: denseVector({ dims: 384 }),
|
|
228
|
+
});
|
|
229
229
|
|
|
230
230
|
// Basic semantic search
|
|
231
231
|
const searchEmbedding = [0.23, 0.45, 0.67, 0.12, 0.89]; // From your ML model
|
|
232
232
|
|
|
233
|
-
const result = query
|
|
233
|
+
const result = query(productWithEmbeddingMappings)
|
|
234
234
|
.knn('embedding', searchEmbedding, {
|
|
235
235
|
k: 10, // Return top 10 nearest neighbors
|
|
236
236
|
num_candidates: 100 // Consider 100 candidates per shard
|
|
@@ -239,7 +239,7 @@ const result = query<Product>()
|
|
|
239
239
|
.build();
|
|
240
240
|
|
|
241
241
|
// Semantic search with filters
|
|
242
|
-
const filtered = query
|
|
242
|
+
const filtered = query(productWithEmbeddingMappings)
|
|
243
243
|
.knn('embedding', searchEmbedding, {
|
|
244
244
|
k: 20,
|
|
245
245
|
num_candidates: 200,
|
|
@@ -256,7 +256,7 @@ const filtered = query<Product>()
|
|
|
256
256
|
.build();
|
|
257
257
|
|
|
258
258
|
// Hybrid search with aggregations
|
|
259
|
-
const hybridSearch = query
|
|
259
|
+
const hybridSearch = query(productWithEmbeddingMappings)
|
|
260
260
|
.knn('embedding', searchEmbedding, {
|
|
261
261
|
k: 100,
|
|
262
262
|
num_candidates: 1000,
|
|
@@ -304,18 +304,17 @@ const mapping: DenseVectorOptions = {
|
|
|
304
304
|
**Note:** Scripts must be enabled in Elasticsearch configuration. Use with caution as they can impact performance.
|
|
305
305
|
|
|
306
306
|
```typescript
|
|
307
|
-
import { query } from 'elasticlink';
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
};
|
|
307
|
+
import { query, mappings, text, keyword, float, long } from 'elasticlink';
|
|
308
|
+
|
|
309
|
+
const scoredProductMappings = mappings({
|
|
310
|
+
name: text(),
|
|
311
|
+
price: float(),
|
|
312
|
+
popularity: long(),
|
|
313
|
+
quality_score: float(),
|
|
314
|
+
});
|
|
316
315
|
|
|
317
316
|
// Script-based filtering
|
|
318
|
-
const filtered = query
|
|
317
|
+
const filtered = query(scoredProductMappings)
|
|
319
318
|
.bool()
|
|
320
319
|
.must((q) => q.match('name', 'laptop'))
|
|
321
320
|
.filter((q) =>
|
|
@@ -327,7 +326,7 @@ const filtered = query<Product>()
|
|
|
327
326
|
.build();
|
|
328
327
|
|
|
329
328
|
// Custom scoring with script_score
|
|
330
|
-
const customScored = query
|
|
329
|
+
const customScored = query(scoredProductMappings)
|
|
331
330
|
.scriptScore(
|
|
332
331
|
(q) => q.match('name', 'smartphone'),
|
|
333
332
|
{
|
|
@@ -349,14 +348,14 @@ const customScored = query<Product>()
|
|
|
349
348
|
Percolate queries enable reverse search - match documents against stored queries. Perfect for alerting, content classification, and saved searches.
|
|
350
349
|
|
|
351
350
|
```typescript
|
|
352
|
-
|
|
353
|
-
query:
|
|
354
|
-
name:
|
|
355
|
-
severity:
|
|
356
|
-
};
|
|
351
|
+
const alertRuleMappings = mappings({
|
|
352
|
+
query: percolator(),
|
|
353
|
+
name: keyword(),
|
|
354
|
+
severity: keyword(),
|
|
355
|
+
});
|
|
357
356
|
|
|
358
357
|
// Match document against stored queries
|
|
359
|
-
const alerts = query
|
|
358
|
+
const alerts = query(alertRuleMappings)
|
|
360
359
|
.percolate({
|
|
361
360
|
field: 'query',
|
|
362
361
|
document: {
|
|
@@ -381,16 +380,16 @@ const alerts = query<AlertRule>()
|
|
|
381
380
|
Elasticsearch Suggesters provide spell-checking, phrase correction, and autocomplete functionality. Perfect for search-as-you-type experiences and fixing user typos.
|
|
382
381
|
|
|
383
382
|
```typescript
|
|
384
|
-
import { query, suggest } from 'elasticlink';
|
|
383
|
+
import { query, suggest, mappings, text, keyword, completion } from 'elasticlink';
|
|
385
384
|
|
|
386
|
-
|
|
387
|
-
name:
|
|
388
|
-
description:
|
|
389
|
-
suggest_field:
|
|
390
|
-
};
|
|
385
|
+
const searchableMappings = mappings({
|
|
386
|
+
name: text(),
|
|
387
|
+
description: text(),
|
|
388
|
+
suggest_field: completion(), // Must be type: completion
|
|
389
|
+
});
|
|
391
390
|
|
|
392
391
|
// Term suggester - Fix typos in individual terms
|
|
393
|
-
const termSuggestions = suggest
|
|
392
|
+
const termSuggestions = suggest(searchableMappings)
|
|
394
393
|
.term('name-suggestions', 'laptpo', {
|
|
395
394
|
field: 'name',
|
|
396
395
|
size: 5,
|
|
@@ -401,7 +400,7 @@ const termSuggestions = suggest<Product>()
|
|
|
401
400
|
.build();
|
|
402
401
|
|
|
403
402
|
// Completion suggester - Fast autocomplete
|
|
404
|
-
const autocomplete = suggest
|
|
403
|
+
const autocomplete = suggest(searchableMappings)
|
|
405
404
|
.completion('autocomplete', 'lap', {
|
|
406
405
|
field: 'suggest_field',
|
|
407
406
|
size: 10,
|
|
@@ -416,7 +415,7 @@ const autocomplete = suggest<Product>()
|
|
|
416
415
|
.build();
|
|
417
416
|
|
|
418
417
|
// Combine with query - Search with autocomplete
|
|
419
|
-
const searchWithSuggestions = query
|
|
418
|
+
const searchWithSuggestions = query(searchableMappings)
|
|
420
419
|
.match('name', 'laptpo')
|
|
421
420
|
.suggest((s) =>
|
|
422
421
|
s.term('spelling-correction', 'laptpo', {
|
|
@@ -442,24 +441,24 @@ Batch multiple search requests in a single API call using the NDJSON format.
|
|
|
442
441
|
```typescript
|
|
443
442
|
import { query, msearch } from 'elasticlink';
|
|
444
443
|
|
|
445
|
-
const laptopQuery = query
|
|
444
|
+
const laptopQuery = query(productMappings)
|
|
446
445
|
.match('name', 'laptop')
|
|
447
446
|
.range('price', { gte: 500, lte: 2000 })
|
|
448
447
|
.build();
|
|
449
448
|
|
|
450
|
-
const phoneQuery = query
|
|
449
|
+
const phoneQuery = query(productMappings)
|
|
451
450
|
.match('name', 'smartphone')
|
|
452
451
|
.range('price', { gte: 300, lte: 1000 })
|
|
453
452
|
.build();
|
|
454
453
|
|
|
455
454
|
// Build as NDJSON string for Elasticsearch API
|
|
456
|
-
const ndjson = msearch
|
|
455
|
+
const ndjson = msearch(productMappings)
|
|
457
456
|
.addQuery(laptopQuery, { index: 'products', preference: '_local' })
|
|
458
457
|
.addQuery(phoneQuery, { index: 'products', preference: '_local' })
|
|
459
458
|
.build();
|
|
460
459
|
|
|
461
460
|
// Or build as array of objects
|
|
462
|
-
const array = msearch
|
|
461
|
+
const array = msearch(productMappings)
|
|
463
462
|
.addQuery(laptopQuery, { index: 'products' })
|
|
464
463
|
.addQuery(phoneQuery, { index: 'products' })
|
|
465
464
|
.buildArray();
|
|
@@ -487,16 +486,16 @@ const array = msearch<Product>()
|
|
|
487
486
|
Batch create, index, update, and delete operations efficiently.
|
|
488
487
|
|
|
489
488
|
```typescript
|
|
490
|
-
import { bulk } from 'elasticlink';
|
|
489
|
+
import { bulk, mappings, keyword, text, float } from 'elasticlink';
|
|
491
490
|
|
|
492
|
-
|
|
493
|
-
id:
|
|
494
|
-
name:
|
|
495
|
-
price:
|
|
496
|
-
category:
|
|
497
|
-
};
|
|
491
|
+
const productMappings = mappings({
|
|
492
|
+
id: keyword(),
|
|
493
|
+
name: text(),
|
|
494
|
+
price: float(),
|
|
495
|
+
category: keyword(),
|
|
496
|
+
});
|
|
498
497
|
|
|
499
|
-
const bulkOp = bulk
|
|
498
|
+
const bulkOp = bulk(productMappings)
|
|
500
499
|
// Index (create or replace)
|
|
501
500
|
.index(
|
|
502
501
|
{ id: '1', name: 'Laptop Pro', price: 1299, category: 'electronics' },
|
|
@@ -546,24 +545,18 @@ const bulkOp = bulk<Product>()
|
|
|
546
545
|
Configure index mappings, settings, and aliases declaratively.
|
|
547
546
|
|
|
548
547
|
```typescript
|
|
549
|
-
import { indexBuilder, keyword, integer, float, date, text } from 'elasticlink';
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
title:
|
|
553
|
-
practice_area:
|
|
554
|
-
billing_rate:
|
|
555
|
-
risk_score:
|
|
556
|
-
opened_at:
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
const indexConfig = indexBuilder
|
|
560
|
-
.mappings(
|
|
561
|
-
title: text({ analyzer: 'english' }),
|
|
562
|
-
practice_area: keyword(),
|
|
563
|
-
billing_rate: integer(),
|
|
564
|
-
risk_score: float(),
|
|
565
|
-
opened_at: date()
|
|
566
|
-
})
|
|
548
|
+
import { indexBuilder, mappings, keyword, integer, float, date, text } from 'elasticlink';
|
|
549
|
+
|
|
550
|
+
const matterMappings = mappings({
|
|
551
|
+
title: text({ analyzer: 'english' }),
|
|
552
|
+
practice_area: keyword(),
|
|
553
|
+
billing_rate: integer(),
|
|
554
|
+
risk_score: float(),
|
|
555
|
+
opened_at: date(),
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const indexConfig = indexBuilder()
|
|
559
|
+
.mappings(matterMappings)
|
|
567
560
|
.settings({
|
|
568
561
|
number_of_shards: 2,
|
|
569
562
|
number_of_replicas: 1,
|
|
@@ -694,21 +687,21 @@ More examples available in [src/\_\_tests\_\_/examples.test.ts](src/__tests__/ex
|
|
|
694
687
|
A complete search request: boolean query with must/filter/should, aggregations for facets and price ranges, highlights, `_source` filtering, and pagination.
|
|
695
688
|
|
|
696
689
|
```typescript
|
|
697
|
-
|
|
698
|
-
name:
|
|
699
|
-
description:
|
|
700
|
-
category:
|
|
701
|
-
price:
|
|
702
|
-
tags:
|
|
703
|
-
in_stock: boolean
|
|
704
|
-
};
|
|
690
|
+
const ecommerceMappings = mappings({
|
|
691
|
+
name: text(),
|
|
692
|
+
description: text(),
|
|
693
|
+
category: keyword(),
|
|
694
|
+
price: float(),
|
|
695
|
+
tags: keyword(),
|
|
696
|
+
in_stock: boolean(),
|
|
697
|
+
});
|
|
705
698
|
|
|
706
699
|
const searchTerm = 'gaming laptop';
|
|
707
700
|
const category = 'electronics';
|
|
708
701
|
const minPrice = 800;
|
|
709
702
|
const maxPrice = 2000;
|
|
710
703
|
|
|
711
|
-
const result = query
|
|
704
|
+
const result = query(ecommerceMappings)
|
|
712
705
|
.bool()
|
|
713
706
|
.must(q => q.match('name', searchTerm, { operator: 'and', boost: 2 }))
|
|
714
707
|
.should(q => q.fuzzy('description', searchTerm, { fuzziness: 'AUTO' }))
|
|
@@ -783,16 +776,16 @@ Produces:
|
|
|
783
776
|
Terms + sub-aggregation + date histogram in one request.
|
|
784
777
|
|
|
785
778
|
```typescript
|
|
786
|
-
|
|
787
|
-
name:
|
|
788
|
-
asset_class:
|
|
789
|
-
sector:
|
|
790
|
-
price:
|
|
791
|
-
yield_rate:
|
|
792
|
-
listed_date:
|
|
793
|
-
};
|
|
794
|
-
|
|
795
|
-
const result = query
|
|
779
|
+
const instrumentMappings = mappings({
|
|
780
|
+
name: text(),
|
|
781
|
+
asset_class: keyword(),
|
|
782
|
+
sector: keyword(),
|
|
783
|
+
price: float(),
|
|
784
|
+
yield_rate: float(),
|
|
785
|
+
listed_date: date(),
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
const result = query(instrumentMappings)
|
|
796
789
|
.bool()
|
|
797
790
|
.filter(q => q.term('asset_class', 'fixed-income'))
|
|
798
791
|
.filter(q => q.range('yield_rate', { gte: 3.0 }))
|
|
@@ -846,13 +839,13 @@ Produces:
|
|
|
846
839
|
Batch multiple independent searches into a single HTTP request.
|
|
847
840
|
|
|
848
841
|
```typescript
|
|
849
|
-
|
|
850
|
-
address:
|
|
851
|
-
property_class:
|
|
852
|
-
list_price:
|
|
853
|
-
};
|
|
842
|
+
const listingMappings = mappings({
|
|
843
|
+
address: text(),
|
|
844
|
+
property_class: keyword(),
|
|
845
|
+
list_price: long(),
|
|
846
|
+
});
|
|
854
847
|
|
|
855
|
-
const condoSearch = query
|
|
848
|
+
const condoSearch = query(listingMappings)
|
|
856
849
|
.bool()
|
|
857
850
|
.filter(q => q.term('property_class', 'condo'))
|
|
858
851
|
.filter(q => q.range('list_price', { lte: 2_000_000 }))
|
|
@@ -860,14 +853,14 @@ const condoSearch = query<Listing>()
|
|
|
860
853
|
.size(0)
|
|
861
854
|
.build();
|
|
862
855
|
|
|
863
|
-
const townhouseSearch = query
|
|
856
|
+
const townhouseSearch = query(listingMappings)
|
|
864
857
|
.bool()
|
|
865
858
|
.filter(q => q.term('property_class', 'townhouse'))
|
|
866
859
|
.aggs(agg => agg.avg('avg_price', 'list_price').min('min_price', 'list_price'))
|
|
867
860
|
.size(0)
|
|
868
861
|
.build();
|
|
869
862
|
|
|
870
|
-
const ndjson = msearch
|
|
863
|
+
const ndjson = msearch(listingMappings)
|
|
871
864
|
.addQuery(condoSearch, { index: 'listings' })
|
|
872
865
|
.addQuery(townhouseSearch, { index: 'listings' })
|
|
873
866
|
.build();
|
|
@@ -886,14 +879,14 @@ Produces:
|
|
|
886
879
|
### Suggesters — Autocomplete & Spell Check
|
|
887
880
|
|
|
888
881
|
```typescript
|
|
889
|
-
|
|
890
|
-
name:
|
|
891
|
-
practice_area:
|
|
892
|
-
name_suggest:
|
|
893
|
-
};
|
|
882
|
+
const attorneyMappings = mappings({
|
|
883
|
+
name: text(),
|
|
884
|
+
practice_area: keyword(),
|
|
885
|
+
name_suggest: completion(),
|
|
886
|
+
});
|
|
894
887
|
|
|
895
888
|
// Standalone suggest request
|
|
896
|
-
const suggestions = query
|
|
889
|
+
const suggestions = query(attorneyMappings)
|
|
897
890
|
.suggest(s =>
|
|
898
891
|
s
|
|
899
892
|
.completion('autocomplete', 'kap', { field: 'name_suggest', size: 5 })
|
|
@@ -925,7 +918,7 @@ Produces:
|
|
|
925
918
|
|
|
926
919
|
```typescript
|
|
927
920
|
const buildDynamicQuery = (filters: SearchFilters) => {
|
|
928
|
-
return query
|
|
921
|
+
return query(productMappings)
|
|
929
922
|
.bool()
|
|
930
923
|
.must(q =>
|
|
931
924
|
q.when(filters.searchTerm,
|
|
@@ -951,15 +944,15 @@ const buildDynamicQuery = (filters: SearchFilters) => {
|
|
|
951
944
|
### Geospatial Search
|
|
952
945
|
|
|
953
946
|
```typescript
|
|
954
|
-
|
|
955
|
-
name:
|
|
956
|
-
cuisine:
|
|
957
|
-
location:
|
|
958
|
-
rating:
|
|
959
|
-
};
|
|
960
|
-
|
|
961
|
-
const result = query
|
|
962
|
-
.
|
|
947
|
+
const restaurantMappings = mappings({
|
|
948
|
+
name: text(),
|
|
949
|
+
cuisine: keyword(),
|
|
950
|
+
location: geoPoint(),
|
|
951
|
+
rating: float(),
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
const result = query(restaurantMappings)
|
|
955
|
+
.term('cuisine', 'italian')
|
|
963
956
|
.geoDistance(
|
|
964
957
|
'location',
|
|
965
958
|
{ lat: 40.7128, lon: -74.006 },
|
|
@@ -972,25 +965,32 @@ const result = query<Restaurant>()
|
|
|
972
965
|
|
|
973
966
|
## TypeScript Support
|
|
974
967
|
|
|
975
|
-
elasticlink provides
|
|
968
|
+
elasticlink provides mapping-aware TypeScript safety:
|
|
976
969
|
|
|
977
|
-
- **Field
|
|
978
|
-
- **
|
|
979
|
-
-
|
|
970
|
+
- **Field-Type Constraints**: `match()` only accepts text fields, `term()` only keyword fields — enforced at compile time
|
|
971
|
+
- **Field Autocomplete**: IntelliSense knows your field names and their types
|
|
972
|
+
- **`Infer<S>`**: Derive TS document types from your mappings schema
|
|
980
973
|
|
|
981
974
|
```typescript
|
|
982
|
-
type
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
975
|
+
import { query, mappings, text, keyword, integer, type Infer } from 'elasticlink';
|
|
976
|
+
|
|
977
|
+
const userMappings = mappings({
|
|
978
|
+
name: text(),
|
|
979
|
+
email: keyword(),
|
|
980
|
+
age: integer(),
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
type User = Infer<typeof userMappings>;
|
|
984
|
+
// => { name: string; email: string; age: number }
|
|
985
|
+
|
|
986
|
+
// ✅ 'name' is a text field — match() accepts it
|
|
987
|
+
const q1 = query(userMappings).match('name', 'John').build();
|
|
988
988
|
|
|
989
|
-
//
|
|
990
|
-
const
|
|
989
|
+
// ❌ TypeScript error: 'email' is keyword, not text — use term() instead
|
|
990
|
+
const q2 = query(userMappings).match('email', 'john@example.com').build();
|
|
991
991
|
|
|
992
|
-
//
|
|
993
|
-
const
|
|
992
|
+
// ✅ Correct: use term() for keyword fields
|
|
993
|
+
const q3 = query(userMappings).term('email', 'john@example.com').build();
|
|
994
994
|
```
|
|
995
995
|
|
|
996
996
|
## Testing
|
|
@@ -1035,9 +1035,9 @@ npm run type-check
|
|
|
1035
1035
|
- [x] Integration test suite against live Elasticsearch 9.x
|
|
1036
1036
|
- [x] Types derived from official `@elastic/elasticsearch` for accuracy and completeness
|
|
1037
1037
|
|
|
1038
|
-
##
|
|
1038
|
+
## Development
|
|
1039
1039
|
|
|
1040
|
-
|
|
1040
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and code style.
|
|
1041
1041
|
|
|
1042
1042
|
## License
|
|
1043
1043
|
|
|
@@ -1045,6 +1045,5 @@ MIT © 2026 misterrodger
|
|
|
1045
1045
|
|
|
1046
1046
|
## Support
|
|
1047
1047
|
|
|
1048
|
-
-
|
|
1049
|
-
-
|
|
1050
|
-
- 💬 [Discussions](https://github.com/misterrodger/elasticlink/discussions)
|
|
1048
|
+
- [Documentation](https://github.com/misterrodger/elasticlink#readme)
|
|
1049
|
+
- [Report Issues](https://github.com/misterrodger/elasticlink/issues)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { FieldTypeString } from './index-management.types.js';
|
|
2
|
+
import type { MappingsSchema } from './mapping.types.js';
|
|
1
3
|
import { AggregationBuilder, AggregationState } from './aggregation.types.js';
|
|
2
|
-
export declare const createAggregationBuilder: <
|
|
3
|
-
export declare const aggregations: <
|
|
4
|
+
export declare const createAggregationBuilder: <M extends Record<string, FieldTypeString>>(state?: AggregationState) => AggregationBuilder<M>;
|
|
5
|
+
export declare const aggregations: <M extends Record<string, FieldTypeString>>(_schema: MappingsSchema<M>) => AggregationBuilder<M>;
|
|
4
6
|
//# sourceMappingURL=aggregation.builder.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aggregation.builder.d.ts","sourceRoot":"","sources":["../src/aggregation.builder.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAajB,MAAM,wBAAwB,CAAC;AAEhC,eAAO,MAAM,wBAAwB,
|
|
1
|
+
{"version":3,"file":"aggregation.builder.d.ts","sourceRoot":"","sources":["../src/aggregation.builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAajB,MAAM,wBAAwB,CAAC;AAEhC,eAAO,MAAM,wBAAwB,GACnC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EAEzC,QAAO,gBAAqB,KAC3B,kBAAkB,CAAC,CAAC,CAiNrB,CAAC;AAGH,eAAO,MAAM,YAAY,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACpE,SAAS,cAAc,CAAC,CAAC,CAAC,0BACM,CAAC"}
|
|
@@ -145,4 +145,4 @@ export const createAggregationBuilder = (state = {}) => ({
|
|
|
145
145
|
build: () => state
|
|
146
146
|
});
|
|
147
147
|
// Helper function to create aggregations without needing to import the builder
|
|
148
|
-
export const aggregations = () => createAggregationBuilder();
|
|
148
|
+
export const aggregations = (_schema) => createAggregationBuilder();
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
|
|
5
5
|
*/
|
|
6
6
|
import type { AggregationsTermsAggregation, AggregationsDateHistogramAggregation, AggregationsRangeAggregation, AggregationsHistogramAggregation, AggregationsAverageAggregation, AggregationsSumAggregation, AggregationsMinAggregation, AggregationsMaxAggregation, AggregationsCardinalityAggregation, AggregationsPercentilesAggregation, AggregationsStatsAggregation, AggregationsValueCountAggregation, AggregationsCalendarInterval } from '@elastic/elasticsearch/lib/api/types';
|
|
7
|
+
import type { FieldTypeString } from './index-management.types.js';
|
|
7
8
|
/**
|
|
8
9
|
* Options for terms aggregation (excludes 'field' which is handled by the builder)
|
|
9
10
|
* @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html
|
|
@@ -78,33 +79,33 @@ export type AggregationState = {
|
|
|
78
79
|
/**
|
|
79
80
|
* Aggregation builder interface
|
|
80
81
|
*/
|
|
81
|
-
export type AggregationBuilder<
|
|
82
|
+
export type AggregationBuilder<M extends Record<string, FieldTypeString>> = {
|
|
82
83
|
/** Terms aggregation - group by field values */
|
|
83
|
-
terms: <K extends keyof
|
|
84
|
+
terms: <K extends string & keyof M>(name: string, field: K, options?: TermsAggOptions) => AggregationBuilder<M>;
|
|
84
85
|
/** Date histogram aggregation - group by time intervals */
|
|
85
|
-
dateHistogram: <K extends keyof
|
|
86
|
+
dateHistogram: <K extends string & keyof M>(name: string, field: K, options: DateHistogramAggOptions) => AggregationBuilder<M>;
|
|
86
87
|
/** Range aggregation - group by numeric/date ranges */
|
|
87
|
-
range: <K extends keyof
|
|
88
|
+
range: <K extends string & keyof M>(name: string, field: K, options: RangeAggOptions) => AggregationBuilder<M>;
|
|
88
89
|
/** Histogram aggregation - group by numeric intervals */
|
|
89
|
-
histogram: <K extends keyof
|
|
90
|
+
histogram: <K extends string & keyof M>(name: string, field: K, options: HistogramAggOptions) => AggregationBuilder<M>;
|
|
90
91
|
/** Average aggregation */
|
|
91
|
-
avg: <K extends keyof
|
|
92
|
+
avg: <K extends string & keyof M>(name: string, field: K, options?: AvgAggOptions) => AggregationBuilder<M>;
|
|
92
93
|
/** Sum aggregation */
|
|
93
|
-
sum: <K extends keyof
|
|
94
|
+
sum: <K extends string & keyof M>(name: string, field: K, options?: SumAggOptions) => AggregationBuilder<M>;
|
|
94
95
|
/** Minimum value aggregation */
|
|
95
|
-
min: <K extends keyof
|
|
96
|
+
min: <K extends string & keyof M>(name: string, field: K, options?: MinAggOptions) => AggregationBuilder<M>;
|
|
96
97
|
/** Maximum value aggregation */
|
|
97
|
-
max: <K extends keyof
|
|
98
|
+
max: <K extends string & keyof M>(name: string, field: K, options?: MaxAggOptions) => AggregationBuilder<M>;
|
|
98
99
|
/** Cardinality aggregation - count unique values */
|
|
99
|
-
cardinality: <K extends keyof
|
|
100
|
+
cardinality: <K extends string & keyof M>(name: string, field: K, options?: CardinalityAggOptions) => AggregationBuilder<M>;
|
|
100
101
|
/** Percentiles aggregation */
|
|
101
|
-
percentiles: <K extends keyof
|
|
102
|
+
percentiles: <K extends string & keyof M>(name: string, field: K, options?: PercentilesAggOptions) => AggregationBuilder<M>;
|
|
102
103
|
/** Statistics aggregation (count, min, max, avg, sum) */
|
|
103
|
-
stats: <K extends keyof
|
|
104
|
+
stats: <K extends string & keyof M>(name: string, field: K, options?: StatsAggOptions) => AggregationBuilder<M>;
|
|
104
105
|
/** Value count aggregation */
|
|
105
|
-
valueCount: <K extends keyof
|
|
106
|
+
valueCount: <K extends string & keyof M>(name: string, field: K, options?: ValueCountAggOptions) => AggregationBuilder<M>;
|
|
106
107
|
/** Add sub-aggregation to parent bucket aggregation */
|
|
107
|
-
subAgg: (fn: (agg: AggregationBuilder<
|
|
108
|
+
subAgg: (fn: (agg: AggregationBuilder<M>) => AggregationBuilder<M>) => AggregationBuilder<M>;
|
|
108
109
|
/** Build aggregation DSL */
|
|
109
110
|
build: () => AggregationState;
|
|
110
111
|
};
|