elasticlink 0.7.0-beta → 1.0.0-beta.1
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 +571 -828
- package/dist/aggregation.builder.d.ts.map +1 -1
- package/dist/aggregation.builder.js +33 -17
- package/dist/aggregation.types.d.ts +90 -5
- package/dist/aggregation.types.d.ts.map +1 -1
- package/dist/bulk.builder.d.ts +2 -1
- package/dist/bulk.builder.d.ts.map +1 -1
- package/dist/bulk.builder.js +6 -6
- package/dist/bulk.types.d.ts +4 -3
- package/dist/bulk.types.d.ts.map +1 -1
- package/dist/field.helpers.d.ts +92 -18
- package/dist/field.helpers.d.ts.map +1 -1
- package/dist/field.helpers.js +108 -57
- package/dist/field.types.d.ts +108 -15
- package/dist/field.types.d.ts.map +1 -1
- package/dist/index-management.builder.d.ts +8 -3
- package/dist/index-management.builder.d.ts.map +1 -1
- package/dist/index-management.builder.js +6 -1
- package/dist/index-management.types.d.ts +40 -30
- package/dist/index-management.types.d.ts.map +1 -1
- package/dist/index.d.ts +10 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/mapping.builder.d.ts +9 -2
- package/dist/mapping.builder.d.ts.map +1 -1
- package/dist/mapping.types.d.ts +46 -3
- package/dist/mapping.types.d.ts.map +1 -1
- package/dist/multi-search.builder.d.ts +5 -4
- package/dist/multi-search.builder.d.ts.map +1 -1
- package/dist/multi-search.builder.js +15 -6
- package/dist/multi-search.types.d.ts +17 -3
- package/dist/multi-search.types.d.ts.map +1 -1
- package/dist/query.builder.d.ts.map +1 -1
- package/dist/query.builder.js +257 -192
- package/dist/query.types.d.ts +161 -20
- package/dist/query.types.d.ts.map +1 -1
- package/dist/settings.presets.d.ts +29 -10
- package/dist/settings.presets.d.ts.map +1 -1
- package/dist/settings.presets.js +27 -5
- package/dist/suggester.types.d.ts +54 -14
- package/dist/suggester.types.d.ts.map +1 -1
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# elasticlink
|
|
2
2
|
|
|
3
|
-
> **Beta**: elasticlink is in active beta. APIs may change before the first stable release.
|
|
4
|
-
|
|
5
3
|
[](https://www.npmjs.com/package/elasticlink)
|
|
6
4
|
[](https://github.com/misterrodger/elasticlink/actions)
|
|
7
5
|
[](https://codecov.io/gh/misterrodger/elasticlink)
|
|
@@ -9,30 +7,16 @@
|
|
|
9
7
|
|
|
10
8
|
> **Type-safe, fluent API for Elasticsearch queries and index management**
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
Define your Elasticsearch mappings once, then build queries with full IntelliSense and compile-time safety. `match()` only accepts text fields, `term()` only keyword fields, etc. Compiles to standard Elasticsearch DSL with no runtime overhead.
|
|
13
11
|
|
|
14
12
|
## Features
|
|
15
13
|
|
|
16
|
-
- **Mapping-Aware Type Safety**:
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
|
|
23
|
-
## Compatibility
|
|
24
|
-
|
|
25
|
-
| elasticlink | Node.js | Elasticsearch |
|
|
26
|
-
|-------------|-------------|---------------|
|
|
27
|
-
| 0.7.0-beta | 20, 22, 24 | 9.x (≥9.0.0) |
|
|
28
|
-
| 0.6.0-beta | 20, 22, 24 | 9.x (≥9.0.0) |
|
|
29
|
-
| 0.5.0-beta | 20, 22, 24 | 9.x (≥9.0.0) |
|
|
30
|
-
| 0.4.0-beta | 20, 22, 24 | 9.x (≥9.0.0) |
|
|
31
|
-
| 0.3.0-beta | 20, 22, 24 | 9.x (≥9.0.0) |
|
|
32
|
-
| 0.2.0-beta | 20, 22 | 9.x (≥9.0.0) |
|
|
33
|
-
| 0.1.0-beta | 20, 22 | 9.x (≥9.0.0) |
|
|
34
|
-
|
|
35
|
-
Tested against the versions listed. Peer dependency is `@elastic/elasticsearch >=9.0.0`.
|
|
14
|
+
- **Mapping-Aware Type Safety**: define your ES mappings once with `mappings()`, and every query method constrains its field arguments to the correct type.
|
|
15
|
+
- **Types from the source**: autocomplete for field names, query options, and aggregation methods. Structural types are hand-rolled, leaf option types are derived from `@elastic/elasticsearch` types, so they stay current.
|
|
16
|
+
- **Fluent Chainable API**: readable query building with boolean logic, conditional branches (using `.when()`), aggregations, and pagination in a single chain.
|
|
17
|
+
- **No Runtime Overhead or Lock-In**: builds plain Elasticsearch DSL objects. Pass them to the official `@elastic/elasticsearch` client or any HTTP client.
|
|
18
|
+
- **Production-Ready Settings Presets**: helpers for production search, fast ingest, and index sort presets for common lifecycle stages, with full type support.
|
|
19
|
+
- **40+ Field Helpers**: `text()`, `keyword()`, `denseVector()`, `nested()`, and more with options for IDE tooltips.
|
|
36
20
|
|
|
37
21
|
## Installation
|
|
38
22
|
|
|
@@ -47,516 +31,443 @@ Requires Node.js 20+ and `@elastic/elasticsearch` 9.x as a peer dependency.
|
|
|
47
31
|
## Quick Start
|
|
48
32
|
|
|
49
33
|
```typescript
|
|
50
|
-
import {
|
|
34
|
+
import { queryBuilder, mappings, text, keyword, float, type Infer } from 'elasticlink';
|
|
51
35
|
|
|
52
36
|
// Define field types once — this is the single source of truth
|
|
53
37
|
const productMappings = mappings({
|
|
54
38
|
name: text(),
|
|
39
|
+
description: text(),
|
|
55
40
|
price: float(),
|
|
56
|
-
category: keyword()
|
|
41
|
+
category: keyword()
|
|
57
42
|
});
|
|
58
43
|
|
|
59
|
-
// Derive a TS type from mappings (optional, for use elsewhere)
|
|
44
|
+
// Derive a TS type from mappings (optional, for use elsewhere in your application)
|
|
60
45
|
type Product = Infer<typeof productMappings>;
|
|
61
46
|
|
|
62
47
|
// Build a type-safe query — field constraints are enforced at compile time
|
|
63
|
-
const
|
|
64
|
-
.match('name', 'laptop')
|
|
48
|
+
const elasticQuery = queryBuilder(productMappings)
|
|
49
|
+
.match('name', 'laptop') // ✅ 'name' is a text field
|
|
65
50
|
.range('price', { gte: 500, lte: 2000 })
|
|
51
|
+
.sort('price', 'asc')
|
|
66
52
|
.from(0)
|
|
67
53
|
.size(20)
|
|
68
54
|
.build();
|
|
69
55
|
|
|
70
56
|
// Send to Elasticsearch
|
|
71
|
-
const response = await client.search({ index: 'products', ...
|
|
57
|
+
const response = await client.search({ index: 'products', ...elasticQuery });
|
|
72
58
|
```
|
|
73
59
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
### Core Query Methods
|
|
77
|
-
|
|
78
|
-
#### Basic Queries
|
|
79
|
-
|
|
80
|
-
- `match(field, value, options?)` - Full-text search
|
|
81
|
-
- `multiMatch(fields, query, options?)` - Search multiple fields
|
|
82
|
-
- `matchPhrase(field, query)` - Exact phrase matching
|
|
83
|
-
- `matchPhrasePrefix(field, query, options?)` - Prefix phrase matching
|
|
84
|
-
- `matchBoolPrefix(field, value, options?)` - Analyze and build a bool query from query terms, with the last term as a prefix (search-as-you-type)
|
|
85
|
-
- `combinedFields(fields, query, options?)` - Search across multiple fields treating them as one combined field
|
|
86
|
-
- `queryString(query, options?)` - Lucene query string syntax
|
|
87
|
-
- `simpleQueryString(query, options?)` - Simplified query string syntax with limited operators
|
|
88
|
-
- `moreLikeThis(fields, like, options?)` - Find documents similar to a given document or text
|
|
89
|
-
- `term(field, value)` - Exact term matching
|
|
90
|
-
- `terms(field, values)` - Multiple exact values
|
|
91
|
-
- `range(field, conditions)` - Range queries (gte, lte, gt, lt)
|
|
92
|
-
- `exists(field)` - Field existence check
|
|
93
|
-
- `wildcard(field, pattern)` - Wildcard pattern matching
|
|
94
|
-
- `prefix(field, value)` - Prefix matching
|
|
95
|
-
- `fuzzy(field, value, options?)` - Typo tolerance
|
|
96
|
-
- `ids(values)` - Match by document IDs
|
|
97
|
-
- `matchAll()` - Match all documents
|
|
98
|
-
|
|
99
|
-
#### Geo Queries
|
|
100
|
-
|
|
101
|
-
- `geoDistance(field, center, options)` - Distance-based search
|
|
102
|
-
- `geoBoundingBox(field, options)` - Bounding box search
|
|
103
|
-
- `geoPolygon(field, options)` - Polygon search
|
|
104
|
-
|
|
105
|
-
#### Vector Search (KNN)
|
|
106
|
-
|
|
107
|
-
- `knn(field, queryVector, options)` - K-nearest neighbors semantic search
|
|
108
|
-
|
|
109
|
-
#### Advanced Queries
|
|
110
|
-
|
|
111
|
-
- `nested(path, fn, options?)` - Nested object queries
|
|
112
|
-
- `regexp(field, pattern, options?)` - Regular expression matching
|
|
113
|
-
- `constantScore(fn, options?)` - Constant scoring for filters
|
|
114
|
-
- `script(options)` - Script-based filtering
|
|
115
|
-
- `scriptScore(query, script, options?)` - Custom scoring with scripts
|
|
116
|
-
- `percolate(options)` - Match documents against stored queries
|
|
117
|
-
|
|
118
|
-
#### Suggestions & Autocomplete
|
|
119
|
-
|
|
120
|
-
- `suggest(fn)` - Add query suggestions (term, phrase, completion)
|
|
121
|
-
- `term(name, text, options)` - Term-level spell checking
|
|
122
|
-
- `phrase(name, text, options)` - Phrase-level corrections
|
|
123
|
-
- `completion(name, prefix, options)` - Fast autocomplete
|
|
124
|
-
|
|
125
|
-
### Boolean Logic
|
|
60
|
+
Boolean queries with aggregations:
|
|
126
61
|
|
|
127
62
|
```typescript
|
|
128
|
-
|
|
63
|
+
const facetedSearch = queryBuilder(productMappings)
|
|
129
64
|
.bool()
|
|
130
|
-
.must(q => q.match('name', 'laptop'))
|
|
131
|
-
.filter(q => q.
|
|
132
|
-
.
|
|
133
|
-
.
|
|
134
|
-
.
|
|
65
|
+
.must((q) => q.match('name', 'gaming laptop', { operator: 'and' }))
|
|
66
|
+
.filter((q) => q.term('category', 'electronics'))
|
|
67
|
+
.filter((q) => q.range('price', { gte: 800, lte: 2000 }))
|
|
68
|
+
.aggs((agg) => agg.terms('by_category', 'category', { size: 10 }).avg('avg_price', 'price'))
|
|
69
|
+
.highlight(['name', 'description'])
|
|
70
|
+
.size(20)
|
|
135
71
|
.build();
|
|
136
72
|
```
|
|
137
73
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
Build queries dynamically based on runtime values. `.when(condition, fn)` — the chain is never broken. When the condition is falsy, the builder is returned unchanged, so no extra clauses or arrays are added. If you already called `.bool()`, that empty wrapper is preserved.
|
|
141
|
-
|
|
142
|
-
`condition` resolves as: functions are called, booleans are used as-is, and any other value uses a nullish check (`!= null`) — so numeric `0` and empty string `''` are treated as truthy.
|
|
74
|
+
Type safety in action — wrong field types are caught at compile time:
|
|
143
75
|
|
|
144
76
|
```typescript
|
|
145
|
-
|
|
146
|
-
|
|
77
|
+
queryBuilder(productMappings).match('category', 'electronics');
|
|
78
|
+
// ^^^^^^^^^^
|
|
79
|
+
// TypeScript error: 'category' is a keyword field — use term(), not match()
|
|
147
80
|
|
|
148
|
-
|
|
149
|
-
.bool()
|
|
150
|
-
.when(searchTerm, q => q.must(q2 => q2.match('name', searchTerm!)))
|
|
151
|
-
.when(minPrice, q => q.filter(q2 => q2.range('price', { gte: minPrice! })))
|
|
152
|
-
.build();
|
|
81
|
+
queryBuilder(productMappings).term('category', 'electronics'); // ✅ Correct
|
|
153
82
|
```
|
|
154
83
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
84
|
+
## Examples
|
|
85
|
+
|
|
86
|
+
More examples available in [src/\_\_tests\_\_/examples.test.ts](src/__tests__/examples.test.ts).
|
|
87
|
+
|
|
88
|
+
### E-commerce Product Search
|
|
158
89
|
|
|
159
|
-
|
|
90
|
+
A complete search request: boolean query with must/filter/should, aggregations for facets and price ranges, highlights, `_source` filtering, and pagination.
|
|
160
91
|
|
|
161
92
|
```typescript
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
93
|
+
const ecommerceMappings = mappings({
|
|
94
|
+
name: text(),
|
|
95
|
+
description: text(),
|
|
96
|
+
category: keyword(),
|
|
97
|
+
price: float(),
|
|
98
|
+
tags: keyword(),
|
|
99
|
+
in_stock: boolean()
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const searchTerm = 'gaming laptop';
|
|
103
|
+
const category = 'electronics';
|
|
104
|
+
const minPrice = 800;
|
|
105
|
+
const maxPrice = 2000;
|
|
106
|
+
|
|
107
|
+
const result = queryBuilder(ecommerceMappings)
|
|
108
|
+
.bool()
|
|
109
|
+
.must((q) => q.match('name', searchTerm, { operator: 'and', boost: 2 }))
|
|
110
|
+
.should((q) => q.fuzzy('description', searchTerm, { fuzziness: 'AUTO' }))
|
|
111
|
+
.filter((q) => q.term('category', category))
|
|
112
|
+
.filter((q) => q.range('price', { gte: minPrice, lte: maxPrice }))
|
|
113
|
+
.filter((q) => q.term('in_stock', true))
|
|
114
|
+
.minimumShouldMatch(1)
|
|
115
|
+
.aggs((agg) =>
|
|
116
|
+
agg.terms('by_category', 'category', { size: 10 }).range('price_ranges', 'price', {
|
|
117
|
+
ranges: [{ to: 800 }, { from: 800, to: 1500 }, { from: 1500 }]
|
|
118
|
+
})
|
|
119
|
+
)
|
|
175
120
|
.highlight(['name', 'description'], {
|
|
176
|
-
fragment_size: 150,
|
|
177
|
-
pre_tags: ['<mark>'],
|
|
121
|
+
fragment_size: 150,
|
|
122
|
+
pre_tags: ['<mark>'],
|
|
178
123
|
post_tags: ['</mark>']
|
|
179
124
|
})
|
|
125
|
+
._source(['name', 'price', 'category', 'tags'])
|
|
126
|
+
.timeout('5s')
|
|
127
|
+
.from(0)
|
|
128
|
+
.size(20)
|
|
129
|
+
.sort('_score', 'desc')
|
|
180
130
|
.build();
|
|
181
131
|
```
|
|
182
132
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
Aggregations can be combined with queries or used standalone:
|
|
133
|
+
<details>
|
|
134
|
+
<summary><b>Produced DSL</b> (click to expand)</summary>
|
|
186
135
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"query": {
|
|
139
|
+
"bool": {
|
|
140
|
+
"must": [{ "match": { "name": { "query": "gaming laptop", "operator": "and", "boost": 2 } } }],
|
|
141
|
+
"should": [{ "fuzzy": { "description": { "value": "gaming laptop", "fuzziness": "AUTO" } } }],
|
|
142
|
+
"filter": [
|
|
143
|
+
{ "term": { "category": "electronics" } },
|
|
144
|
+
{ "range": { "price": { "gte": 800, "lte": 2000 } } },
|
|
145
|
+
{ "term": { "in_stock": true } }
|
|
146
|
+
],
|
|
147
|
+
"minimum_should_match": 1
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"aggs": {
|
|
151
|
+
"by_category": { "terms": { "field": "category", "size": 10 } },
|
|
152
|
+
"price_ranges": {
|
|
153
|
+
"range": {
|
|
154
|
+
"field": "price",
|
|
155
|
+
"ranges": [{ "to": 800 }, { "from": 800, "to": 1500 }, { "from": 1500 }]
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"highlight": {
|
|
160
|
+
"fields": { "name": { "fragment_size": 150 }, "description": { "fragment_size": 150 } },
|
|
161
|
+
"pre_tags": ["<mark>"],
|
|
162
|
+
"post_tags": ["</mark>"]
|
|
163
|
+
},
|
|
164
|
+
"_source": ["name", "price", "category", "tags"],
|
|
165
|
+
"timeout": "5s",
|
|
166
|
+
"from": 0,
|
|
167
|
+
"size": 20,
|
|
168
|
+
"sort": [{ "_score": "desc" }]
|
|
169
|
+
}
|
|
170
|
+
```
|
|
190
171
|
|
|
191
|
-
|
|
192
|
-
import { query, aggregations } from 'elasticlink';
|
|
172
|
+
</details>
|
|
193
173
|
|
|
194
|
-
|
|
195
|
-
const result = query(productMappings)
|
|
196
|
-
.term('category', 'electronics')
|
|
197
|
-
.aggs(agg =>
|
|
198
|
-
agg
|
|
199
|
-
.terms('by_category', 'category', { size: 10 })
|
|
200
|
-
.avg('avg_price', 'price')
|
|
201
|
-
)
|
|
202
|
-
.size(20)
|
|
203
|
-
.build();
|
|
174
|
+
### Dynamic Search with Conditional Filters
|
|
204
175
|
|
|
205
|
-
|
|
206
|
-
const aggsOnly = query(productMappings, false)
|
|
207
|
-
.aggs(agg =>
|
|
208
|
-
agg
|
|
209
|
-
.terms('by_category', 'category')
|
|
210
|
-
.subAgg(sub =>
|
|
211
|
-
sub.avg('avg_price', 'price').max('max_price', 'price')
|
|
212
|
-
)
|
|
213
|
-
)
|
|
214
|
-
.size(0) // Common pattern: size=0 when only wanting agg results
|
|
215
|
-
.build();
|
|
176
|
+
Build queries dynamically based on runtime values. `.when(condition, fn)` — when the condition is falsy, the builder is returned unchanged.
|
|
216
177
|
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
178
|
+
```typescript
|
|
179
|
+
const buildDynamicQuery = (filters: SearchFilters) => {
|
|
180
|
+
return queryBuilder(productMappings)
|
|
181
|
+
.bool()
|
|
182
|
+
.when(filters.searchTerm, (q) => q.must((q2) => q2.match('name', filters.searchTerm!, { boost: 2 })))
|
|
183
|
+
.when(filters.category, (q) => q.filter((q2) => q2.term('category', filters.category!)))
|
|
184
|
+
.when(filters.minPrice != null && filters.maxPrice != null, (q) =>
|
|
185
|
+
q.filter((q2) => q2.range('price', { gte: filters.minPrice!, lte: filters.maxPrice! }))
|
|
186
|
+
)
|
|
187
|
+
.from(filters.offset || 0)
|
|
188
|
+
.size(filters.limit || 20)
|
|
189
|
+
.build();
|
|
190
|
+
};
|
|
222
191
|
```
|
|
223
192
|
|
|
224
|
-
###
|
|
193
|
+
### Aggregations — Portfolio Analytics
|
|
225
194
|
|
|
226
|
-
|
|
195
|
+
Terms + sub-aggregation + date histogram in one request.
|
|
227
196
|
|
|
228
197
|
```typescript
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const productWithEmbeddingMappings = mappings({
|
|
198
|
+
const instrumentMappings = mappings({
|
|
232
199
|
name: text(),
|
|
233
|
-
|
|
200
|
+
asset_class: keyword(),
|
|
201
|
+
sector: keyword(),
|
|
234
202
|
price: float(),
|
|
235
|
-
|
|
236
|
-
|
|
203
|
+
yield_rate: float(),
|
|
204
|
+
listed_date: date()
|
|
237
205
|
});
|
|
238
206
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
.
|
|
244
|
-
k: 10, // Return top 10 nearest neighbors
|
|
245
|
-
num_candidates: 100 // Consider 100 candidates per shard
|
|
246
|
-
})
|
|
247
|
-
.size(10)
|
|
248
|
-
.build();
|
|
249
|
-
|
|
250
|
-
// Semantic search with filters
|
|
251
|
-
const filtered = query(productWithEmbeddingMappings)
|
|
252
|
-
.knn('embedding', searchEmbedding, {
|
|
253
|
-
k: 20,
|
|
254
|
-
num_candidates: 200,
|
|
255
|
-
filter: {
|
|
256
|
-
bool: {
|
|
257
|
-
must: [{ term: { category: 'electronics' } }],
|
|
258
|
-
filter: [{ range: { price: { gte: 100, lte: 1000 } } }]
|
|
259
|
-
}
|
|
260
|
-
},
|
|
261
|
-
boost: 1.2, // Boost relevance scores
|
|
262
|
-
similarity: 0.7 // Minimum similarity threshold
|
|
263
|
-
})
|
|
264
|
-
.size(20)
|
|
265
|
-
.build();
|
|
266
|
-
|
|
267
|
-
// Hybrid search with aggregations
|
|
268
|
-
const hybridSearch = query(productWithEmbeddingMappings)
|
|
269
|
-
.knn('embedding', searchEmbedding, {
|
|
270
|
-
k: 100,
|
|
271
|
-
num_candidates: 1000,
|
|
272
|
-
filter: { term: { category: 'electronics' } }
|
|
273
|
-
})
|
|
274
|
-
.aggs(agg =>
|
|
207
|
+
const result = queryBuilder(instrumentMappings)
|
|
208
|
+
.bool()
|
|
209
|
+
.filter((q) => q.term('asset_class', 'fixed-income'))
|
|
210
|
+
.filter((q) => q.range('yield_rate', { gte: 3.0 }))
|
|
211
|
+
.aggs((agg) =>
|
|
275
212
|
agg
|
|
276
|
-
.terms('
|
|
277
|
-
.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
{ from: 500 }
|
|
282
|
-
]
|
|
213
|
+
.terms('by_sector', 'sector', { size: 10 })
|
|
214
|
+
.subAgg((sub) => sub.avg('avg_yield', 'yield_rate').max('max_price', 'price'))
|
|
215
|
+
.dateHistogram('listings_over_time', 'listed_date', {
|
|
216
|
+
interval: 'quarter',
|
|
217
|
+
min_doc_count: 1
|
|
283
218
|
})
|
|
219
|
+
.subAgg((sub) => sub.percentiles('yield_percentiles', 'yield_rate', { percents: [25, 50, 75, 95] }))
|
|
284
220
|
)
|
|
285
|
-
.size(
|
|
221
|
+
.size(0)
|
|
286
222
|
.build();
|
|
287
223
|
```
|
|
288
224
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
- **384-768**: Sentence transformers (all-MiniLM, all-mpnet)
|
|
292
|
-
- **512**: Image embeddings (ResNet, ViT)
|
|
293
|
-
- **1536**: OpenAI text-embedding-ada-002
|
|
294
|
-
- **3072**: OpenAI text-embedding-3-large
|
|
295
|
-
|
|
296
|
-
**Dense Vector Field Mapping Example:**
|
|
225
|
+
<details>
|
|
226
|
+
<summary><b>Produced DSL</b> (click to expand)</summary>
|
|
297
227
|
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"query": {
|
|
231
|
+
"bool": {
|
|
232
|
+
"filter": [{ "term": { "asset_class": "fixed-income" } }, { "range": { "yield_rate": { "gte": 3.0 } } }]
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
"aggs": {
|
|
236
|
+
"by_sector": {
|
|
237
|
+
"terms": { "field": "sector", "size": 10 },
|
|
238
|
+
"aggs": {
|
|
239
|
+
"avg_yield": { "avg": { "field": "yield_rate" } },
|
|
240
|
+
"max_price": { "max": { "field": "price" } }
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
"listings_over_time": {
|
|
244
|
+
"date_histogram": { "field": "listed_date", "interval": "quarter", "min_doc_count": 1 },
|
|
245
|
+
"aggs": {
|
|
246
|
+
"yield_percentiles": { "percentiles": { "field": "yield_rate", "percents": [25, 50, 75, 95] } }
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
"size": 0
|
|
251
|
+
}
|
|
311
252
|
```
|
|
312
253
|
|
|
313
|
-
|
|
254
|
+
</details>
|
|
314
255
|
|
|
315
|
-
|
|
256
|
+
### Geospatial Search
|
|
316
257
|
|
|
317
258
|
```typescript
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const scoredProductMappings = mappings({
|
|
259
|
+
const restaurantMappings = mappings({
|
|
321
260
|
name: text(),
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
261
|
+
cuisine: keyword(),
|
|
262
|
+
location: geoPoint(),
|
|
263
|
+
rating: float()
|
|
325
264
|
});
|
|
326
265
|
|
|
327
|
-
|
|
328
|
-
const filtered = query(scoredProductMappings)
|
|
266
|
+
const result = queryBuilder(restaurantMappings)
|
|
329
267
|
.bool()
|
|
330
|
-
.
|
|
331
|
-
.filter((q) =>
|
|
332
|
-
|
|
333
|
-
source: "doc['price'].value > params.threshold",
|
|
334
|
-
params: { threshold: 500 }
|
|
335
|
-
})
|
|
336
|
-
)
|
|
337
|
-
.build();
|
|
338
|
-
|
|
339
|
-
// Custom scoring with script_score
|
|
340
|
-
const customScored = query(scoredProductMappings)
|
|
341
|
-
.scriptScore(
|
|
342
|
-
(q) => q.match('name', 'smartphone'),
|
|
343
|
-
{
|
|
344
|
-
source: "_score * Math.log(2 + doc['popularity'].value)",
|
|
345
|
-
lang: 'painless'
|
|
346
|
-
}
|
|
347
|
-
)
|
|
268
|
+
.filter((q) => q.term('cuisine', 'italian'))
|
|
269
|
+
.filter((q) => q.geoDistance('location', { lat: 40.7128, lon: -74.006 }, { distance: '5km' }))
|
|
270
|
+
.sort('rating', 'desc')
|
|
348
271
|
.size(20)
|
|
349
272
|
.build();
|
|
350
273
|
```
|
|
351
274
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
- **painless** (default): Elasticsearch's primary scripting language
|
|
355
|
-
- **expression**: Fast, limited expression language
|
|
356
|
-
- **mustache**: Template-based scripting
|
|
275
|
+
## TypeScript Support
|
|
357
276
|
|
|
358
|
-
|
|
277
|
+
elasticlink provides mapping-aware TypeScript safety:
|
|
359
278
|
|
|
360
|
-
|
|
279
|
+
- **Field-Type Constraints**: enforced at compile time across all methods — `match()` only accepts text fields, `term()` only keyword/numeric fields, `sort()` only sortable fields (keyword, numeric, date, boolean, ip), `collapse()` only keyword/numeric fields, `highlight()` only text/keyword fields
|
|
280
|
+
- **Field Autocomplete**: IntelliSense knows your field names and their types
|
|
281
|
+
- **`Infer<S>`**: Derive TS document types from your mappings schema for use elsewhere in your application
|
|
282
|
+
- **Exported Field Group Types**: `TextFields<M>`, `KeywordFields<M>`, `NumericFields<M>`, `DateFields<M>`, `BooleanFields<M>`, `GeoPointFields<M>`, `GeoShapeFields<M>`, `VectorFields<M>`, `DenseVectorFields<M>`, `SparseVectorFields<M>`, `RankFeatureFields<M>`, `CompletionFields<M>`, `IpFields<M>`, `FieldsOfType<M, T>`, and others are exported for use in your own typed utilities
|
|
283
|
+
- **Exported Builder Types**: `QueryBuilder<M>`, `ClauseBuilder<M>`, `RootAggregationBuilder<M>`, `NestedAggregationBuilder<M>`, `NestedEntryBuilder<M>`, `MSearchBuilder<M>`, `BulkBuilder<T>`, `SuggesterBuilder<M>`, `IndexBuilder<M>`, `AnalysisConfig`, `IndexSortFieldSpec`, `MSearchHeader`, `MSearchRequestParams`
|
|
361
284
|
|
|
362
285
|
```typescript
|
|
363
|
-
import {
|
|
286
|
+
import { queryBuilder, mappings, text, keyword, integer, type Infer } from 'elasticlink';
|
|
364
287
|
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
288
|
+
const userMappings = mappings({
|
|
289
|
+
name: text(),
|
|
290
|
+
email: keyword(),
|
|
291
|
+
age: integer()
|
|
369
292
|
});
|
|
370
293
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
.percolate({
|
|
374
|
-
field: 'query',
|
|
375
|
-
document: {
|
|
376
|
-
level: 'ERROR',
|
|
377
|
-
message: 'Database connection failed',
|
|
378
|
-
timestamp: '2024-01-15T10:30:00Z'
|
|
379
|
-
}
|
|
380
|
-
})
|
|
381
|
-
.size(100)
|
|
382
|
-
.build();
|
|
383
|
-
```
|
|
294
|
+
type User = Infer<typeof userMappings>;
|
|
295
|
+
// => { name: string; email: string; age: number }
|
|
384
296
|
|
|
385
|
-
|
|
297
|
+
// ✅ 'name' is a text field — match() accepts it
|
|
298
|
+
const q1 = queryBuilder(userMappings).match('name', 'John').build();
|
|
299
|
+
|
|
300
|
+
// ❌ TypeScript error: 'email' is keyword, not text — use term() instead
|
|
301
|
+
const q2 = queryBuilder(userMappings).match('email', 'john@example.com').build();
|
|
386
302
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
- **Monitoring:** Trigger actions based on metric thresholds
|
|
303
|
+
// ✅ Correct: use term() for keyword fields
|
|
304
|
+
const q3 = queryBuilder(userMappings).term('email', 'john@example.com').build();
|
|
305
|
+
```
|
|
391
306
|
|
|
392
|
-
|
|
307
|
+
## Settings Presets
|
|
393
308
|
|
|
394
|
-
|
|
309
|
+
Ready-made index settings for common lifecycle stages. Use with `.settings()` on `indexBuilder()` or pass directly to the ES `_settings` API.
|
|
395
310
|
|
|
396
311
|
```typescript
|
|
397
|
-
import {
|
|
312
|
+
import { indexBuilder, productionSearchSettings, indexSortSettings, fastIngestSettings } from 'elasticlink';
|
|
398
313
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
description: text(),
|
|
402
|
-
suggest_field: completion(), // Must be type: completion
|
|
403
|
-
});
|
|
314
|
+
// Create index with production settings
|
|
315
|
+
const indexConfig = indexBuilder().mappings(myMappings).settings(productionSearchSettings()).build();
|
|
404
316
|
|
|
405
|
-
//
|
|
406
|
-
const
|
|
407
|
-
.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
string_distance: 'levenshtein',
|
|
412
|
-
max_edits: 2
|
|
317
|
+
// Index-time sort for compression and early termination
|
|
318
|
+
const sortedConfig = indexBuilder()
|
|
319
|
+
.mappings(myMappings)
|
|
320
|
+
.settings({
|
|
321
|
+
...productionSearchSettings(),
|
|
322
|
+
index: indexSortSettings({ timestamp: 'desc', status: 'asc' })
|
|
413
323
|
})
|
|
414
324
|
.build();
|
|
415
325
|
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
.completion('autocomplete', 'lap', {
|
|
419
|
-
field: 'suggest_field',
|
|
420
|
-
size: 10,
|
|
421
|
-
skip_duplicates: true,
|
|
422
|
-
fuzzy: {
|
|
423
|
-
fuzziness: 'AUTO',
|
|
424
|
-
transpositions: true,
|
|
425
|
-
min_length: 3,
|
|
426
|
-
prefix_length: 1
|
|
427
|
-
}
|
|
428
|
-
})
|
|
429
|
-
.build();
|
|
326
|
+
// Before bulk ingest — disables refresh, removes replicas, async translog
|
|
327
|
+
await client.indices.putSettings({ index: 'my-index', body: fastIngestSettings() });
|
|
430
328
|
|
|
431
|
-
//
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
field: 'name',
|
|
437
|
-
size: 3,
|
|
438
|
-
suggest_mode: 'popular'
|
|
439
|
-
})
|
|
440
|
-
)
|
|
441
|
-
.size(20)
|
|
442
|
-
.build();
|
|
329
|
+
// Perform bulk ingest...
|
|
330
|
+
|
|
331
|
+
// Restore production settings afterward
|
|
332
|
+
await client.indices.putSettings({ index: 'my-index', body: productionSearchSettings() });
|
|
333
|
+
await client.indices.refresh({ index: 'my-index' });
|
|
443
334
|
```
|
|
444
335
|
|
|
445
|
-
|
|
336
|
+
| Preset | Purpose |
|
|
337
|
+
| -------------------------------------- | --------------------------------------------------------------------------- |
|
|
338
|
+
| `productionSearchSettings(overrides?)` | Balanced production defaults — 1 replica, 5s refresh |
|
|
339
|
+
| `indexSortSettings(fields)` | Configure index-time sort order for disk compression and early termination |
|
|
340
|
+
| `fastIngestSettings(overrides?)` | Maximum indexing throughput — async translog, no replicas, refresh disabled |
|
|
446
341
|
|
|
447
|
-
-
|
|
448
|
-
- **Phrase:** Suggests corrections for entire phrases using n-gram language models
|
|
449
|
-
- **Completion:** Fast prefix-based autocomplete (requires `completion` field type)
|
|
342
|
+
All presets accept an optional `overrides` argument typed as `Partial<IndicesIndexSettings>`. `fastIngestSettings` deep-merges the `translog` key so individual translog overrides don't clobber the other defaults.
|
|
450
343
|
|
|
451
|
-
|
|
344
|
+
## API Overview
|
|
452
345
|
|
|
453
|
-
|
|
346
|
+
### Query Builder
|
|
454
347
|
|
|
455
|
-
|
|
456
|
-
import { query, msearch } from 'elasticlink';
|
|
348
|
+
`queryBuilder(schema, includeQuery?)` creates a fluent, immutable query builder. Every chain method returns a new builder instance.
|
|
457
349
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
.
|
|
350
|
+
```typescript
|
|
351
|
+
queryBuilder(productMappings)
|
|
352
|
+
.bool()
|
|
353
|
+
.must((q) => q.match('name', 'laptop'))
|
|
354
|
+
.filter((q) => q.range('price', { gte: 500 }))
|
|
355
|
+
.should((q) => q.term('category', 'featured'))
|
|
356
|
+
.mustNot((q) => q.term('category', 'discontinued'))
|
|
357
|
+
.minimumShouldMatch(1)
|
|
461
358
|
.build();
|
|
359
|
+
```
|
|
462
360
|
|
|
463
|
-
|
|
464
|
-
.match('name', 'smartphone')
|
|
465
|
-
.range('price', { gte: 300, lte: 1000 })
|
|
466
|
-
.build();
|
|
361
|
+
#### Query Methods
|
|
467
362
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
363
|
+
| Category | Methods | Description |
|
|
364
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
365
|
+
| **Full-text** | `match`, `multiMatch`, `matchPhrase`, `matchPhrasePrefix`, `matchBoolPrefix`, `combinedFields` | Analyzed text search — field must be a text field |
|
|
366
|
+
| **Term-level** | `term`, `terms`, `prefix`, `wildcard`, `exists`, `ids`, `matchAll`, `matchNone` | Exact value matching — field must be keyword, numeric, date, boolean, or ip |
|
|
367
|
+
| **Range** | `range` | Range queries (`gte`, `lte`, `gt`, `lt`) on numeric, date, keyword, or ip fields |
|
|
368
|
+
| **Fuzzy & Pattern** | `fuzzy`, `regexp`, `queryString`, `simpleQueryString`, `moreLikeThis` | Typo tolerance, regex, Lucene syntax, and similarity search |
|
|
369
|
+
| **Boolean** | `bool` → `must`, `mustNot`, `should`, `filter`, `minimumShouldMatch` | Combine clauses with AND/OR/NOT/filter logic |
|
|
370
|
+
| **Conditional** | `when(condition, fn)` | Dynamic query building — skips the branch when condition is falsy |
|
|
371
|
+
| **Geo** | `geoDistance`, `geoBoundingBox`, `geoPolygon`, `geoShape` | Spatial queries — field must be geo_point or geo_shape |
|
|
372
|
+
| **Vector & Relevance** | `knn`, `sparseVector`, `rankFeature`, `distanceFeature` | Semantic/vector search, rank features, and decay functions |
|
|
373
|
+
| **Nested & Structure** | `nested`, `constantScore`, `hasChild`, `hasParent`, `parentId` | Query nested objects, parent/child joins, or wrap in constant-score filter |
|
|
374
|
+
| **Sorting & Pagination** | `from`, `size`, `sort`, `searchAfter`, `collapse` | Result ordering, pagination, and field collapsing |
|
|
375
|
+
| **Source & Fields** | `_source`, `sourceIncludes`, `sourceExcludes`, `fields`, `docValueFields`, `storedFields` | Control which fields are returned |
|
|
376
|
+
| **Highlighting & Scoring** | `highlight`, `rescore`, `indicesBoost`, `minScore`, `trackScores`, `explain` | Result highlighting, rescoring, and relevance tuning |
|
|
377
|
+
| **Positional** | `intervals`, `spanTerm`, `spanNear`, `spanOr`, `spanNot`, `spanFirst`, `spanContaining`, `spanWithin`, `spanMultiTerm`, `spanFieldMasking` | Term proximity and ordering queries |
|
|
378
|
+
| **Advanced** | `postFilter`, `scriptFields`, `runtimeMappings`, `script`, `scriptScore`, `functionScore`, `percolate` | Post-query filtering, computed fields, and custom scoring |
|
|
379
|
+
| **Configuration** | `timeout`, `preference`, `pit`, `terminateAfter`, `version`, `seqNoPrimaryTerm`, `trackTotalHits` | Search execution options |
|
|
380
|
+
| **Inline builders** | `aggs(fn)`, `suggest(fn)` | Attach aggregations or suggesters to the query |
|
|
381
|
+
| **Output** | `build()` | Returns the final Elasticsearch DSL object |
|
|
473
382
|
|
|
474
|
-
|
|
475
|
-
const array = msearch(productMappings)
|
|
476
|
-
.addQuery(laptopQuery, { index: 'products' })
|
|
477
|
-
.addQuery(phoneQuery, { index: 'products' })
|
|
478
|
-
.buildArray();
|
|
479
|
-
```
|
|
383
|
+
All query methods accept options from their corresponding `@elastic/elasticsearch` type. See [`query.types.ts`](src/query.types.ts) for complete signatures.
|
|
480
384
|
|
|
481
|
-
|
|
385
|
+
#### Conditional Building
|
|
482
386
|
|
|
483
|
-
|
|
484
|
-
{"index":"products","preference":"_local"}
|
|
485
|
-
{"query":{"bool":{"must":[{"match":{"name":"laptop"}},{"range":{"price":{"gte":500,"lte":2000}}}]}}}
|
|
486
|
-
{"index":"products","preference":"_local"}
|
|
487
|
-
{"query":{"bool":{"must":[{"match":{"name":"smartphone"}},{"range":{"price":{"gte":300,"lte":1000}}}]}}}
|
|
387
|
+
`.when(condition, fn)` — the chain is never broken. When the condition is falsy, the builder is returned unchanged. `condition` resolves as: functions are called, booleans are used as-is, and any other value uses a nullish check (`!= null`) — so numeric `0` and empty string `''` are treated as truthy.
|
|
488
388
|
|
|
389
|
+
```typescript
|
|
390
|
+
const searchTerm: string | undefined = param.searchTerm;
|
|
391
|
+
const minPrice: number | undefined = param.minPrice;
|
|
392
|
+
|
|
393
|
+
queryBuilder(productMappings)
|
|
394
|
+
.bool()
|
|
395
|
+
.when(searchTerm, (q) => q.must((q2) => q2.match('name', searchTerm!)))
|
|
396
|
+
.when(minPrice, (q) => q.filter((q2) => q2.range('price', { gte: minPrice! })))
|
|
397
|
+
.build();
|
|
489
398
|
```
|
|
490
399
|
|
|
491
|
-
**
|
|
400
|
+
> **Note:** TypeScript cannot narrow closure variables inside callbacks. Even though `.when(searchTerm, fn)` guarantees the value is defined inside `fn`, you still need a non-null assertion (`!`).
|
|
492
401
|
|
|
493
|
-
|
|
494
|
-
- `routing`: Routing value for sharding
|
|
495
|
-
- `preference`: Node preference (\_local, \_primary, etc.)
|
|
496
|
-
- `search_type`: Search type (dfs_query_then_fetch, etc.)
|
|
402
|
+
#### Query Parameters
|
|
497
403
|
|
|
498
|
-
|
|
404
|
+
```typescript
|
|
405
|
+
queryBuilder(productMappings)
|
|
406
|
+
.match('name', 'laptop')
|
|
407
|
+
.from(0) // Pagination offset
|
|
408
|
+
.size(20) // Results per page
|
|
409
|
+
.sort('price', 'asc') // Sort by field
|
|
410
|
+
._source(['name', 'price']) // Which fields to return
|
|
411
|
+
.timeout('5s') // Query timeout
|
|
412
|
+
.trackScores(true) // Enable scoring in filter context
|
|
413
|
+
.trackTotalHits(true) // Track total hit count (or pass a number threshold)
|
|
414
|
+
.explain(true) // Return scoring explanation
|
|
415
|
+
.minScore(10) // Minimum relevance score
|
|
416
|
+
.version(true) // Include document version in results
|
|
417
|
+
.seqNoPrimaryTerm(true) // Include seq_no and primary_term for optimistic concurrency
|
|
418
|
+
.highlight(['name', 'description'], {
|
|
419
|
+
fragment_size: 150,
|
|
420
|
+
pre_tags: ['<mark>'],
|
|
421
|
+
post_tags: ['</mark>']
|
|
422
|
+
})
|
|
423
|
+
.build();
|
|
424
|
+
```
|
|
499
425
|
|
|
500
|
-
|
|
426
|
+
### Aggregations
|
|
501
427
|
|
|
502
|
-
|
|
503
|
-
import { bulk, mappings, keyword, text, float } from 'elasticlink';
|
|
428
|
+
Aggregations can be combined with queries via `.aggs()` or used standalone with the `aggregations()` builder.
|
|
504
429
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
name: text(),
|
|
508
|
-
price: float(),
|
|
509
|
-
category: keyword(),
|
|
510
|
-
});
|
|
430
|
+
```typescript
|
|
431
|
+
import { queryBuilder, aggregations } from 'elasticlink';
|
|
511
432
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
.
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
.create(
|
|
520
|
-
{ id: '2', name: 'Wireless Mouse', price: 29, category: 'accessories' },
|
|
521
|
-
{ _index: 'products', _id: '2' }
|
|
433
|
+
// Inline aggregations (most common)
|
|
434
|
+
const result = queryBuilder(productMappings)
|
|
435
|
+
.term('category', 'electronics')
|
|
436
|
+
.aggs((agg) =>
|
|
437
|
+
agg
|
|
438
|
+
.terms('by_category', 'category', { size: 10 })
|
|
439
|
+
.subAgg((sub) => sub.avg('avg_price', 'price').max('max_price', 'price'))
|
|
522
440
|
)
|
|
523
|
-
|
|
524
|
-
.update({
|
|
525
|
-
_index: 'products',
|
|
526
|
-
_id: '3',
|
|
527
|
-
doc: { price: 999 }
|
|
528
|
-
})
|
|
529
|
-
// Delete
|
|
530
|
-
.delete({ _index: 'products', _id: '4' })
|
|
441
|
+
.size(20)
|
|
531
442
|
.build();
|
|
532
443
|
|
|
533
|
-
//
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
```ndjson
|
|
539
|
-
{"index":{"_index":"products","_id":"1"}}
|
|
540
|
-
{"id":"1","name":"Laptop Pro","price":1299,"category":"electronics"}
|
|
541
|
-
{"create":{"_index":"products","_id":"2"}}
|
|
542
|
-
{"id":"2","name":"Wireless Mouse","price":29,"category":"accessories"}
|
|
543
|
-
{"update":{"_index":"products","_id":"3"}}
|
|
544
|
-
{"doc":{"price":999}}
|
|
545
|
-
{"delete":{"_index":"products","_id":"4"}}
|
|
444
|
+
// Aggregations-only (no query) — use queryBuilder(mappings, false)
|
|
445
|
+
const aggsOnly = queryBuilder(productMappings, false)
|
|
446
|
+
.aggs((agg) => agg.terms('by_category', 'category'))
|
|
447
|
+
.size(0)
|
|
448
|
+
.build();
|
|
546
449
|
|
|
450
|
+
// Standalone aggregation builder (for manual composition)
|
|
451
|
+
const standaloneAgg = aggregations(productMappings)
|
|
452
|
+
.avg('avg_price', 'price')
|
|
453
|
+
.terms('by_category', 'category', { size: 10 })
|
|
454
|
+
.build();
|
|
547
455
|
```
|
|
548
456
|
|
|
549
|
-
|
|
457
|
+
#### Aggregation Methods
|
|
458
|
+
|
|
459
|
+
| Category | Methods | Description |
|
|
460
|
+
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
461
|
+
| **Bucket** | `terms`, `dateHistogram`, `histogram`, `range`, `dateRange`, `filters`, `significantTerms`, `rareTerms`, `multiTerms`, `autoDateHistogram`, `composite`, `filter`, `missing`, `geoDistance`, `geohashGrid`, `geotileGrid` | Group documents into buckets |
|
|
462
|
+
| **Metric** | `avg`, `sum`, `min`, `max`, `cardinality`, `percentiles`, `stats`, `valueCount`, `extendedStats`, `topHits`, `topMetrics`, `weightedAvg`, `geoBounds`, `geoCentroid` | Compute metrics over documents |
|
|
463
|
+
| **Pipeline** | `derivative`, `cumulativeSum`, `bucketScript`, `bucketSelector` | Compute over other aggregation results |
|
|
464
|
+
| **Structure** | `subAgg`, `nested`, `reverseNested`, `global` | Nest aggregations, navigate nested fields, or escape to global scope |
|
|
550
465
|
|
|
551
|
-
- `
|
|
552
|
-
- `script`: Script-based update (Painless)
|
|
553
|
-
- `upsert`: Document to insert if not exists
|
|
554
|
-
- `doc_as_upsert`: Use doc as upsert document
|
|
555
|
-
- `retry_on_conflict`: Retry count for version conflicts
|
|
466
|
+
`subAgg()` attaches sub-aggregations to the **last** aggregation defined before the call — chain order matters. See [`aggregation.types.ts`](src/aggregation.types.ts) for complete option types.
|
|
556
467
|
|
|
557
468
|
### Index Management
|
|
558
469
|
|
|
559
|
-
Configure index mappings, settings, and aliases declaratively
|
|
470
|
+
Configure index mappings, settings, and aliases declaratively with `indexBuilder()`.
|
|
560
471
|
|
|
561
472
|
```typescript
|
|
562
473
|
import { indexBuilder, mappings, keyword, integer, float, date, text } from 'elasticlink';
|
|
@@ -566,7 +477,7 @@ const matterMappings = mappings({
|
|
|
566
477
|
practice_area: keyword(),
|
|
567
478
|
billing_rate: integer(),
|
|
568
479
|
risk_score: float(),
|
|
569
|
-
opened_at: date()
|
|
480
|
+
opened_at: date()
|
|
570
481
|
});
|
|
571
482
|
|
|
572
483
|
const indexConfig = indexBuilder()
|
|
@@ -581,11 +492,11 @@ const indexConfig = indexBuilder()
|
|
|
581
492
|
.build();
|
|
582
493
|
|
|
583
494
|
// PUT /matters-v1
|
|
584
|
-
|
|
585
|
-
// Body: JSON.stringify(indexConfig)
|
|
495
|
+
await client.indices.create({ index: 'matters-v1', ...indexConfig });
|
|
586
496
|
```
|
|
587
497
|
|
|
588
|
-
|
|
498
|
+
<details>
|
|
499
|
+
<summary><b>Produced DSL</b> (click to expand)</summary>
|
|
589
500
|
|
|
590
501
|
```json
|
|
591
502
|
{
|
|
@@ -610,9 +521,21 @@ Produces:
|
|
|
610
521
|
}
|
|
611
522
|
```
|
|
612
523
|
|
|
524
|
+
</details>
|
|
525
|
+
|
|
526
|
+
#### IndexBuilder Methods
|
|
527
|
+
|
|
528
|
+
| Method | Description |
|
|
529
|
+
| ------------------------------------ | ------------------------------------------------------------------------------ |
|
|
530
|
+
| `mappings(schemaOrFields, options?)` | Set index mappings from a `MappingsSchema` or raw field definitions |
|
|
531
|
+
| `settings(settings)` | Set index settings (`IndicesIndexSettings` from `@elastic/elasticsearch`) |
|
|
532
|
+
| `analysis(config)` | Configure custom analyzers, tokenizers, filters, char filters, and normalizers |
|
|
533
|
+
| `alias(name, options?)` | Add an index alias with optional filter, routing, and write index settings |
|
|
534
|
+
| `build()` | Returns the final `CreateIndexOptions` object |
|
|
535
|
+
|
|
613
536
|
#### Object and Nested Fields
|
|
614
537
|
|
|
615
|
-
Use `object()` for structured sub-documents queried with dot-notation
|
|
538
|
+
Use `object()` for structured sub-documents queried with dot-notation. Use `nested()` for arrays of objects where cross-field queries within the same element must be accurate.
|
|
616
539
|
|
|
617
540
|
```typescript
|
|
618
541
|
import { mappings, text, keyword, float, integer, boolean, object, nested, type Infer } from 'elasticlink';
|
|
@@ -620,111 +543,85 @@ import { mappings, text, keyword, float, integer, boolean, object, nested, type
|
|
|
620
543
|
const productMappings = mappings({
|
|
621
544
|
name: text(),
|
|
622
545
|
in_stock: boolean(),
|
|
623
|
-
|
|
624
|
-
// object() — single structured value, queried with dot-notation (no wrapper needed)
|
|
625
546
|
address: object({
|
|
547
|
+
// Single structured value — queried with dot-notation
|
|
626
548
|
street: text(),
|
|
627
549
|
city: keyword(),
|
|
628
|
-
country: keyword()
|
|
550
|
+
country: keyword()
|
|
629
551
|
}),
|
|
630
|
-
|
|
631
|
-
// nested() — array of objects; cross-field queries require the .nested() wrapper
|
|
632
552
|
variants: nested({
|
|
553
|
+
// Array of objects — cross-field accuracy preserved
|
|
633
554
|
sku: keyword(),
|
|
634
555
|
color: keyword(),
|
|
635
556
|
price: float(),
|
|
636
|
-
stock: integer()
|
|
637
|
-
})
|
|
557
|
+
stock: integer()
|
|
558
|
+
})
|
|
638
559
|
});
|
|
639
560
|
|
|
640
|
-
// Infer<> produces the correct nested TypeScript types:
|
|
641
561
|
type Product = Infer<typeof productMappings>;
|
|
642
|
-
// {
|
|
643
|
-
// name: string;
|
|
644
|
-
// in_stock: boolean;
|
|
562
|
+
// { name: string; in_stock: boolean;
|
|
645
563
|
// address: { street: string; city: string; country: string };
|
|
646
|
-
// variants: Array<{ sku: string; color: string; price: number; stock: number }
|
|
647
|
-
// }
|
|
564
|
+
// variants: Array<{ sku: string; color: string; price: number; stock: number }> }
|
|
648
565
|
|
|
649
566
|
// object sub-fields — query with dot-notation directly
|
|
650
|
-
|
|
567
|
+
queryBuilder(productMappings)
|
|
651
568
|
.bool()
|
|
652
|
-
.filter(q => q.term('address.country', 'US'))
|
|
653
|
-
.filter(q => q.match('address.street', 'Main'))
|
|
569
|
+
.filter((q) => q.term('address.country', 'US'))
|
|
570
|
+
.filter((q) => q.match('address.street', 'Main'))
|
|
654
571
|
.build();
|
|
655
572
|
|
|
656
|
-
// nested sub-fields — must use .nested() wrapper;
|
|
657
|
-
|
|
658
|
-
.nested('variants', q => q.term('color', 'black'))
|
|
573
|
+
// nested sub-fields — must use .nested() wrapper; field names are relative
|
|
574
|
+
queryBuilder(productMappings)
|
|
575
|
+
.nested('variants', (q) => q.term('color', 'black'))
|
|
659
576
|
.build();
|
|
660
577
|
|
|
661
|
-
|
|
662
|
-
.nested('variants', q => q.range('price', { lte: 150 }), { score_mode: 'min' })
|
|
578
|
+
queryBuilder(productMappings)
|
|
579
|
+
.nested('variants', (q) => q.range('price', { lte: 150 }), { score_mode: 'min' })
|
|
663
580
|
.build();
|
|
664
581
|
```
|
|
665
582
|
|
|
666
|
-
> **Inner field names are relative.** Inside a `.nested()` callback, you write field names relative to the nested path (e.g. `'color'`, not `'variants.color'`). The library automatically qualifies them
|
|
667
|
-
|
|
668
|
-
Object and nested fields can be composed to any depth. A 2-level deep mapping works the same way:
|
|
669
|
-
|
|
670
|
-
```typescript
|
|
671
|
-
const orderMappings = mappings({
|
|
672
|
-
title: text(),
|
|
673
|
-
shipments: nested({
|
|
674
|
-
tracking: keyword(),
|
|
675
|
-
address: object({ // object() inside nested()
|
|
676
|
-
city: keyword(),
|
|
677
|
-
country: keyword(),
|
|
678
|
-
}),
|
|
679
|
-
}),
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
// Root-level fields are queried directly
|
|
683
|
-
query(orderMappings)
|
|
684
|
-
.term('title', 'urgent')
|
|
685
|
-
.build();
|
|
686
|
-
|
|
687
|
-
// Nested sub-fields use .nested(); dot-notation inside is relative to the nested path
|
|
688
|
-
query(orderMappings)
|
|
689
|
-
.nested('shipments', q => q.term('address.city', 'London')) // ✅ 'address.city' relative to 'shipments'
|
|
690
|
-
.build();
|
|
691
|
-
```
|
|
583
|
+
> **Inner field names are relative.** Inside a `.nested()` callback, you write field names relative to the nested path (e.g. `'color'`, not `'variants.color'`). The library automatically qualifies them in the generated DSL.
|
|
692
584
|
|
|
693
|
-
|
|
694
|
-
> `object` fields are stored inline in the parent document — Elasticsearch flattens their sub-fields and you query them directly. `nested` fields are stored as separate hidden documents to preserve the relationship between sub-fields within each array element. Without the `.nested()` wrapper, a query like `color=black AND price<50` could incorrectly match a product where different variants have those values.
|
|
585
|
+
#### Field Helpers
|
|
695
586
|
|
|
696
|
-
|
|
587
|
+
| Category | Helpers |
|
|
588
|
+
| ---------- | ------------------------------------------------------------------------------------------------- |
|
|
589
|
+
| Text | `text`, `keyword`, `constantKeyword`, `matchOnlyText`, `searchAsYouType`, `wildcardField` |
|
|
590
|
+
| Numeric | `long`, `integer`, `short`, `byte`, `double`, `float`, `halfFloat`, `scaledFloat`, `unsignedLong` |
|
|
591
|
+
| Date | `date`, `dateNanos` |
|
|
592
|
+
| Boolean | `boolean` |
|
|
593
|
+
| Binary | `binary` |
|
|
594
|
+
| IP | `ip` |
|
|
595
|
+
| Range | `integerRange`, `floatRange`, `longRange`, `doubleRange`, `dateRange`, `ipRange` |
|
|
596
|
+
| Objects | `object`, `nested`, `flattened` |
|
|
597
|
+
| Spatial | `geoPoint`, `geoShape` |
|
|
598
|
+
| Vector | `denseVector`, `quantizedDenseVector`, `sparseVector` |
|
|
599
|
+
| Rank | `rankFeature`, `rankFeatures` |
|
|
600
|
+
| Semantic | `semanticText` |
|
|
601
|
+
| Completion | `completion` |
|
|
602
|
+
| Special | `percolator`, `alias`, `tokenCount`, `murmur3Hash`, `join` |
|
|
697
603
|
|
|
698
604
|
```typescript
|
|
699
|
-
import { text, keyword, integer, float, double, date, boolean, denseVector, scaledFloat, halfFloat } from 'elasticlink';
|
|
700
|
-
|
|
701
605
|
// Shorthand — pass options or use defaults
|
|
702
|
-
keyword()
|
|
703
|
-
integer()
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
606
|
+
keyword(); // { type: 'keyword' }
|
|
607
|
+
integer(); // { type: 'integer' }
|
|
608
|
+
text({ analyzer: 'english' }); // { type: 'text', analyzer: 'english' }
|
|
609
|
+
denseVector({ dims: 384, index: true, similarity: 'cosine' });
|
|
610
|
+
|
|
611
|
+
// Multi-fields carry type info — dot-notation paths work in queries
|
|
612
|
+
const m = mappings({
|
|
613
|
+
name: text({ fields: { raw: keyword() } }),
|
|
614
|
+
price: float({ fields: { string: keyword() } }),
|
|
615
|
+
});
|
|
616
|
+
const qb = queryBuilder(m);
|
|
617
|
+
qb.term('name.raw', 'exact match'); // ✓ name.raw is a KeywordFields<M> path
|
|
618
|
+
qb.match('name', 'full text'); // ✓ name is a TextFields<M> path
|
|
708
619
|
```
|
|
709
620
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
| Category | Helpers |
|
|
713
|
-
| ----------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
714
|
-
| Text | `text`, `keyword`, `constantKeyword`, `matchOnlyText`, `searchAsYouType`, `wildcardField` |
|
|
715
|
-
| Numeric | `long`, `integer`, `short`, `byte`, `double`, `float`, `halfFloat`, `scaledFloat` |
|
|
716
|
-
| Date | `date` |
|
|
717
|
-
| Boolean | `boolean` |
|
|
718
|
-
| Range | `integerRange`, `floatRange`, `longRange`, `doubleRange`, `dateRange` |
|
|
719
|
-
| Objects | `object`, `nested`, `flattened` |
|
|
720
|
-
| Spatial | `geoPoint`, `geoShape` |
|
|
721
|
-
| Vector | `denseVector`, `quantizedDenseVector` |
|
|
722
|
-
| Specialized | `ip`, `binary`, `completion`, `percolator` |
|
|
723
|
-
| Alias | `alias` |
|
|
724
|
-
|
|
725
|
-
#### Mapping Properties
|
|
621
|
+
See [`field.types.ts`](src/field.types.ts) for all field helper option types.
|
|
726
622
|
|
|
727
|
-
|
|
623
|
+
<details>
|
|
624
|
+
<summary><b>Mapping Properties Reference</b> (click to expand)</summary>
|
|
728
625
|
|
|
729
626
|
| Option | Types | Description |
|
|
730
627
|
| ------------------------------ | ---------------------- | ------------------------------------------------------------------------------------ |
|
|
@@ -741,7 +638,7 @@ All field helpers accept an optional options object. Common options across types
|
|
|
741
638
|
| `dims` | `denseVector` | Number of dimensions (required for KNN indexing) |
|
|
742
639
|
| `similarity` | `denseVector` | Similarity function: `'cosine'`, `'dot_product'`, `'l2_norm'`, `'max_inner_product'` |
|
|
743
640
|
| `element_type` | `denseVector` | Element type: `'float'` (default), `'byte'`, `'bit'` |
|
|
744
|
-
| `fields` | text, keyword
|
|
641
|
+
| `fields` | text, keyword, numeric, date | Multi-fields — type-safe dot-notation paths (e.g. `name.raw`) in query constraints |
|
|
745
642
|
| `properties` | object, nested | Sub-field mappings |
|
|
746
643
|
| `enabled` | `object` | Disable indexing of object fields |
|
|
747
644
|
| `path` | `alias` | Path to the target field |
|
|
@@ -750,423 +647,269 @@ All field helpers accept an optional options object. Common options across types
|
|
|
750
647
|
| `preserve_position_increments` | `completion` | Preserve position increments |
|
|
751
648
|
| `orientation` | `geoShape` | Default orientation for polygons |
|
|
752
649
|
|
|
753
|
-
|
|
650
|
+
</details>
|
|
651
|
+
|
|
652
|
+
<details>
|
|
653
|
+
<summary><b>Index Settings Reference</b> (click to expand)</summary>
|
|
754
654
|
|
|
755
655
|
The `.settings()` method accepts the full `IndicesIndexSettings` type from `@elastic/elasticsearch`. Common options:
|
|
756
656
|
|
|
757
|
-
| Setting | Type | Description
|
|
758
|
-
| -------------------- | -------- |
|
|
759
|
-
| `number_of_shards` | `number` | Primary shard count (set at creation, immutable)
|
|
760
|
-
| `number_of_replicas` | `number` | Replica count (can be changed after creation)
|
|
761
|
-
| `refresh_interval` | `string` | How often to refresh (`'1s'`, `'-1s'` to disable)
|
|
762
|
-
| `max_result_window` | `number` | Max `from + size` (default: 10000)
|
|
763
|
-
| `analysis` | `object` | Custom analyzers, tokenizers, filters
|
|
764
|
-
| `codec` | `string` | Compression codec (`'best_compression'`)
|
|
657
|
+
| Setting | Type | Description |
|
|
658
|
+
| -------------------- | -------- | ------------------------------------------------- |
|
|
659
|
+
| `number_of_shards` | `number` | Primary shard count (set at creation, immutable) |
|
|
660
|
+
| `number_of_replicas` | `number` | Replica count (can be changed after creation) |
|
|
661
|
+
| `refresh_interval` | `string` | How often to refresh (`'1s'`, `'-1s'` to disable) |
|
|
662
|
+
| `max_result_window` | `number` | Max `from + size` (default: 10000) |
|
|
663
|
+
| `analysis` | `object` | Custom analyzers, tokenizers, filters |
|
|
664
|
+
| `codec` | `string` | Compression codec (`'best_compression'`) |
|
|
665
|
+
|
|
666
|
+
</details>
|
|
765
667
|
|
|
766
|
-
|
|
668
|
+
<details>
|
|
669
|
+
<summary><b>Alias Options Reference</b> (click to expand)</summary>
|
|
767
670
|
|
|
768
671
|
The `.alias()` method accepts an optional `IndicesAlias` object:
|
|
769
672
|
|
|
770
|
-
| Option
|
|
771
|
-
|
|
|
772
|
-
| `filter`
|
|
773
|
-
| `is_write_index`
|
|
774
|
-
| `routing`
|
|
775
|
-
| `index_routing`
|
|
776
|
-
| `search_routing`
|
|
777
|
-
| `is_hidden`
|
|
673
|
+
| Option | Type | Description |
|
|
674
|
+
| ---------------- | --------- | ------------------------------------------------------------ |
|
|
675
|
+
| `filter` | `object` | Query filter — only matching documents visible through alias |
|
|
676
|
+
| `is_write_index` | `boolean` | Designate this index as the write target for the alias |
|
|
677
|
+
| `routing` | `string` | Custom routing value for alias operations |
|
|
678
|
+
| `index_routing` | `string` | Routing value for index operations only |
|
|
679
|
+
| `search_routing` | `string` | Routing value for search operations only |
|
|
680
|
+
| `is_hidden` | `boolean` | Hide alias from wildcard expressions |
|
|
778
681
|
|
|
779
|
-
|
|
682
|
+
</details>
|
|
780
683
|
|
|
781
|
-
|
|
684
|
+
### Suggesters & Autocomplete
|
|
685
|
+
|
|
686
|
+
`suggest(schema)` creates a standalone suggester builder. Suggesters can also be attached inline via `queryBuilder().suggest(fn)`.
|
|
782
687
|
|
|
783
688
|
```typescript
|
|
784
|
-
import {
|
|
689
|
+
import { queryBuilder, suggest, mappings, text, keyword, completion } from 'elasticlink';
|
|
785
690
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
691
|
+
const searchableMappings = mappings({
|
|
692
|
+
name: text(),
|
|
693
|
+
description: text(),
|
|
694
|
+
suggest_field: completion()
|
|
695
|
+
});
|
|
791
696
|
|
|
792
|
-
//
|
|
793
|
-
const
|
|
794
|
-
.
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
697
|
+
// Standalone suggest
|
|
698
|
+
const autocomplete = suggest(searchableMappings)
|
|
699
|
+
.completion('autocomplete', 'lap', {
|
|
700
|
+
field: 'suggest_field',
|
|
701
|
+
size: 10,
|
|
702
|
+
skip_duplicates: true,
|
|
703
|
+
fuzzy: { fuzziness: 'AUTO', min_length: 3, prefix_length: 1 }
|
|
798
704
|
})
|
|
799
705
|
.build();
|
|
800
706
|
|
|
801
|
-
//
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
707
|
+
// Inline with query — search + spell-check in one request
|
|
708
|
+
const searchWithSuggestions = queryBuilder(searchableMappings)
|
|
709
|
+
.match('name', 'laptpo')
|
|
710
|
+
.suggest((s) =>
|
|
711
|
+
s
|
|
712
|
+
.completion('autocomplete', 'lap', { field: 'suggest_field', size: 5 })
|
|
713
|
+
.term('spelling', 'laptpo', { field: 'name', size: 3, suggest_mode: 'popular' })
|
|
714
|
+
)
|
|
715
|
+
.size(20)
|
|
716
|
+
.build();
|
|
809
717
|
```
|
|
810
718
|
|
|
811
|
-
|
|
|
812
|
-
|
|
|
813
|
-
| `
|
|
814
|
-
| `
|
|
815
|
-
| `
|
|
816
|
-
|
|
817
|
-
`fastIngestSettings` deep-merges the `translog` key so individual translog overrides don't clobber the other defaults. All presets accept an optional `overrides` argument typed as `Partial<IndicesIndexSettings>`.
|
|
719
|
+
| Method | Description |
|
|
720
|
+
| ----------------------------------- | ------------------------------------------------------------------- |
|
|
721
|
+
| `term(name, text, options)` | Suggest corrections for individual terms based on edit distance |
|
|
722
|
+
| `phrase(name, text, options)` | Suggest corrections for entire phrases using n-gram language models |
|
|
723
|
+
| `completion(name, prefix, options)` | Fast prefix-based autocomplete (requires `completion` field type) |
|
|
724
|
+
| `build()` | Returns `{ suggest: ... }` object |
|
|
818
725
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
More examples available in [src/\_\_tests\_\_/examples.test.ts](src/__tests__/examples.test.ts).
|
|
726
|
+
See [`suggester.types.ts`](src/suggester.types.ts) for complete option types (`TermSuggesterOptions`, `PhraseSuggesterOptions`, `CompletionSuggesterOptions`).
|
|
822
727
|
|
|
823
|
-
###
|
|
728
|
+
### Multi-Search
|
|
824
729
|
|
|
825
|
-
|
|
730
|
+
`msearch(schema)` batches multiple search requests into a single API call using NDJSON format.
|
|
826
731
|
|
|
827
732
|
```typescript
|
|
828
|
-
|
|
829
|
-
name: text(),
|
|
830
|
-
description: text(),
|
|
831
|
-
category: keyword(),
|
|
832
|
-
price: float(),
|
|
833
|
-
tags: keyword(),
|
|
834
|
-
in_stock: boolean(),
|
|
835
|
-
});
|
|
733
|
+
import { queryBuilder, msearch } from 'elasticlink';
|
|
836
734
|
|
|
837
|
-
const
|
|
838
|
-
const category = 'electronics';
|
|
839
|
-
const minPrice = 800;
|
|
840
|
-
const maxPrice = 2000;
|
|
735
|
+
const laptopQuery = queryBuilder(productMappings).match('name', 'laptop').range('price', { gte: 500, lte: 2000 }).build();
|
|
841
736
|
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
.
|
|
847
|
-
.
|
|
848
|
-
.filter(q => q.term('in_stock', true))
|
|
849
|
-
.minimumShouldMatch(1)
|
|
850
|
-
.aggs(agg =>
|
|
851
|
-
agg
|
|
852
|
-
.terms('by_category', 'category', { size: 10 })
|
|
853
|
-
.range('price_ranges', 'price', {
|
|
854
|
-
ranges: [
|
|
855
|
-
{ to: 800 },
|
|
856
|
-
{ from: 800, to: 1500 },
|
|
857
|
-
{ from: 1500 }
|
|
858
|
-
]
|
|
859
|
-
})
|
|
860
|
-
)
|
|
861
|
-
.highlight(['name', 'description'], {
|
|
862
|
-
fragment_size: 150,
|
|
863
|
-
pre_tags: ['<mark>'],
|
|
864
|
-
post_tags: ['</mark>']
|
|
865
|
-
})
|
|
866
|
-
._source(['name', 'price', 'category', 'tags'])
|
|
867
|
-
.timeout('5s')
|
|
868
|
-
.from(0)
|
|
869
|
-
.size(20)
|
|
870
|
-
.sort('_score', 'desc')
|
|
737
|
+
const phoneQuery = queryBuilder(productMappings).match('name', 'smartphone').range('price', { gte: 300, lte: 1000 }).build();
|
|
738
|
+
|
|
739
|
+
// NDJSON string for Elasticsearch _msearch endpoint
|
|
740
|
+
const ndjson = msearch(productMappings)
|
|
741
|
+
.addQuery(laptopQuery, { index: 'products', preference: '_local' })
|
|
742
|
+
.addQuery(phoneQuery, { index: 'products', preference: '_local' })
|
|
871
743
|
.build();
|
|
744
|
+
|
|
745
|
+
// Or as an array of objects
|
|
746
|
+
const array = msearch(productMappings)
|
|
747
|
+
.addQuery(laptopQuery, { index: 'products' })
|
|
748
|
+
.addQuery(phoneQuery, { index: 'products' })
|
|
749
|
+
.buildArray();
|
|
872
750
|
```
|
|
873
751
|
|
|
874
|
-
|
|
752
|
+
| Method | Description |
|
|
753
|
+
| ------------------------------ | -------------------------------------- |
|
|
754
|
+
| `add(request)` | Add a raw request (header + body) |
|
|
755
|
+
| `addQuery(body, header?)` | Add a built query with optional header |
|
|
756
|
+
| `addQueryBuilder(qb, header?)` | Add a QueryBuilder instance directly |
|
|
757
|
+
| `withParams(params)` | Set shared request parameters |
|
|
758
|
+
| `build()` | Returns NDJSON string |
|
|
759
|
+
| `buildArray()` | Returns array of header/body pairs |
|
|
760
|
+
| `buildParams()` | Returns shared request parameters |
|
|
875
761
|
|
|
876
|
-
|
|
877
|
-
{
|
|
878
|
-
"query": {
|
|
879
|
-
"bool": {
|
|
880
|
-
"must": [{ "match": { "name": { "query": "gaming laptop", "operator": "and", "boost": 2 } } }],
|
|
881
|
-
"should": [{ "fuzzy": { "description": { "value": "gaming laptop", "fuzziness": "AUTO" } } }],
|
|
882
|
-
"filter": [
|
|
883
|
-
{ "term": { "category": "electronics" } },
|
|
884
|
-
{ "range": { "price": { "gte": 800, "lte": 2000 } } },
|
|
885
|
-
{ "term": { "in_stock": true } }
|
|
886
|
-
],
|
|
887
|
-
"minimum_should_match": 1
|
|
888
|
-
}
|
|
889
|
-
},
|
|
890
|
-
"aggs": {
|
|
891
|
-
"by_category": { "terms": { "field": "category", "size": 10 } },
|
|
892
|
-
"price_ranges": {
|
|
893
|
-
"range": {
|
|
894
|
-
"field": "price",
|
|
895
|
-
"ranges": [{ "to": 800 }, { "from": 800, "to": 1500 }, { "from": 1500 }]
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
},
|
|
899
|
-
"highlight": {
|
|
900
|
-
"fields": { "name": { "fragment_size": 150 }, "description": { "fragment_size": 150 } },
|
|
901
|
-
"pre_tags": ["<mark>"],
|
|
902
|
-
"post_tags": ["</mark>"]
|
|
903
|
-
},
|
|
904
|
-
"_source": ["name", "price", "category", "tags"],
|
|
905
|
-
"timeout": "5s",
|
|
906
|
-
"from": 0,
|
|
907
|
-
"size": 20,
|
|
908
|
-
"sort": [{ "_score": "desc" }]
|
|
909
|
-
}
|
|
910
|
-
```
|
|
762
|
+
Header options: `index`, `routing`, `preference`, `search_type`. See [`multi-search.types.ts`](src/multi-search.types.ts) for full types.
|
|
911
763
|
|
|
912
|
-
###
|
|
764
|
+
### Bulk Operations
|
|
913
765
|
|
|
914
|
-
|
|
766
|
+
`bulk(schema)` batches create, index, update, and delete operations efficiently.
|
|
915
767
|
|
|
916
768
|
```typescript
|
|
917
|
-
|
|
769
|
+
import { bulk, mappings, keyword, text, float } from 'elasticlink';
|
|
770
|
+
|
|
771
|
+
const productMappings = mappings({
|
|
772
|
+
id: keyword(),
|
|
918
773
|
name: text(),
|
|
919
|
-
asset_class: keyword(),
|
|
920
|
-
sector: keyword(),
|
|
921
774
|
price: float(),
|
|
922
|
-
|
|
923
|
-
listed_date: date(),
|
|
775
|
+
category: keyword()
|
|
924
776
|
});
|
|
925
777
|
|
|
926
|
-
const
|
|
927
|
-
.
|
|
928
|
-
.
|
|
929
|
-
.
|
|
930
|
-
.
|
|
931
|
-
agg
|
|
932
|
-
.terms('by_sector', 'sector', { size: 10 })
|
|
933
|
-
.subAgg(sub => sub.avg('avg_yield', 'yield_rate').max('max_price', 'price'))
|
|
934
|
-
.dateHistogram('listings_over_time', 'listed_date', {
|
|
935
|
-
interval: 'quarter',
|
|
936
|
-
min_doc_count: 1
|
|
937
|
-
})
|
|
938
|
-
.subAgg(sub => sub.percentiles('yield_percentiles', 'yield_rate', { percents: [25, 50, 75, 95] }))
|
|
939
|
-
)
|
|
940
|
-
.size(0)
|
|
778
|
+
const bulkOp = bulk(productMappings)
|
|
779
|
+
.index({ id: '1', name: 'Laptop Pro', price: 1299, category: 'electronics' }, { _index: 'products', _id: '1' })
|
|
780
|
+
.create({ id: '2', name: 'Wireless Mouse', price: 29, category: 'accessories' }, { _index: 'products', _id: '2' })
|
|
781
|
+
.update({ _index: 'products', _id: '3', doc: { price: 999 } })
|
|
782
|
+
.delete({ _index: 'products', _id: '4' })
|
|
941
783
|
.build();
|
|
942
|
-
```
|
|
943
784
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
```json
|
|
947
|
-
{
|
|
948
|
-
"query": {
|
|
949
|
-
"bool": {
|
|
950
|
-
"filter": [
|
|
951
|
-
{ "term": { "asset_class": "fixed-income" } },
|
|
952
|
-
{ "range": { "yield_rate": { "gte": 3.0 } } }
|
|
953
|
-
]
|
|
954
|
-
}
|
|
955
|
-
},
|
|
956
|
-
"aggs": {
|
|
957
|
-
"by_sector": {
|
|
958
|
-
"terms": { "field": "sector", "size": 10 },
|
|
959
|
-
"aggs": {
|
|
960
|
-
"avg_yield": { "avg": { "field": "yield_rate" } },
|
|
961
|
-
"max_price": { "max": { "field": "price" } }
|
|
962
|
-
}
|
|
963
|
-
},
|
|
964
|
-
"listings_over_time": {
|
|
965
|
-
"date_histogram": { "field": "listed_date", "interval": "quarter", "min_doc_count": 1 },
|
|
966
|
-
"aggs": {
|
|
967
|
-
"yield_percentiles": { "percentiles": { "field": "yield_rate", "percents": [25, 50, 75, 95] } }
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
},
|
|
971
|
-
"size": 0
|
|
972
|
-
}
|
|
785
|
+
// POST /_bulk with Content-Type: application/x-ndjson
|
|
973
786
|
```
|
|
974
787
|
|
|
975
|
-
|
|
788
|
+
| Method | Description |
|
|
789
|
+
| -------------------- | ------------------------------------------------------------------------------ |
|
|
790
|
+
| `index(doc, meta?)` | Index a document (create or replace) |
|
|
791
|
+
| `create(doc, meta?)` | Create a document (fail if exists) |
|
|
792
|
+
| `update(meta)` | Update with `doc`, `script`, `upsert`, `doc_as_upsert`, or `retry_on_conflict` |
|
|
793
|
+
| `delete(meta)` | Delete a document |
|
|
794
|
+
| `build()` | Returns NDJSON string |
|
|
795
|
+
| `buildArray()` | Returns array of action/document pairs |
|
|
796
|
+
|
|
797
|
+
### Vector Search & Semantic Search
|
|
976
798
|
|
|
977
|
-
|
|
799
|
+
KNN (k-nearest neighbors) queries enable semantic search using vector embeddings.
|
|
978
800
|
|
|
979
801
|
```typescript
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
802
|
+
import { queryBuilder, mappings, text, keyword, float, denseVector } from 'elasticlink';
|
|
803
|
+
|
|
804
|
+
const productWithEmbeddingMappings = mappings({
|
|
805
|
+
name: text(),
|
|
806
|
+
description: text(),
|
|
807
|
+
price: float(),
|
|
808
|
+
category: keyword(),
|
|
809
|
+
embedding: denseVector({ dims: 384 })
|
|
984
810
|
});
|
|
985
811
|
|
|
986
|
-
const
|
|
987
|
-
.bool()
|
|
988
|
-
.filter(q => q.term('property_class', 'condo'))
|
|
989
|
-
.filter(q => q.range('list_price', { lte: 2_000_000 }))
|
|
990
|
-
.aggs(agg => agg.avg('avg_price', 'list_price'))
|
|
991
|
-
.size(0)
|
|
992
|
-
.build();
|
|
812
|
+
const searchEmbedding = [0.23, 0.45, 0.67, 0.12, 0.89]; // From your ML model
|
|
993
813
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
.
|
|
997
|
-
|
|
998
|
-
|
|
814
|
+
// Basic semantic search
|
|
815
|
+
const result = queryBuilder(productWithEmbeddingMappings)
|
|
816
|
+
.knn('embedding', searchEmbedding, {
|
|
817
|
+
k: 10,
|
|
818
|
+
num_candidates: 100
|
|
819
|
+
})
|
|
820
|
+
.size(10)
|
|
999
821
|
.build();
|
|
1000
822
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
.
|
|
823
|
+
// Hybrid search — KNN + filters + aggregations
|
|
824
|
+
const hybridSearch = queryBuilder(productWithEmbeddingMappings)
|
|
825
|
+
.knn('embedding', searchEmbedding, {
|
|
826
|
+
k: 100,
|
|
827
|
+
num_candidates: 1000,
|
|
828
|
+
filter: { term: { category: 'electronics' } },
|
|
829
|
+
boost: 1.2,
|
|
830
|
+
similarity: 0.7
|
|
831
|
+
})
|
|
832
|
+
.aggs((agg) => agg.terms('categories', 'category', { size: 10 }))
|
|
833
|
+
.size(20)
|
|
1004
834
|
.build();
|
|
1005
835
|
```
|
|
1006
836
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
```ndjson
|
|
1010
|
-
{"index":"listings"}
|
|
1011
|
-
{"query":{"bool":{"filter":[{"term":{"property_class":"condo"}},{"range":{"list_price":{"lte":2000000}}}]}},"aggs":{"avg_price":{"avg":{"field":"list_price"}}},"size":0}
|
|
1012
|
-
{"index":"listings"}
|
|
1013
|
-
{"query":{"bool":{"filter":[{"term":{"property_class":"townhouse"}}]}},"aggs":{"avg_price":{"avg":{"field":"list_price"}},"min_price":{"min":{"field":"list_price"}}},"size":0}
|
|
837
|
+
Additional vector and relevance methods: `sparseVector(field, options)` for learned sparse retrieval, `rankFeature(field, options?)` for rank feature scoring, and `distanceFeature(field, options)` for recency/proximity decay functions.
|
|
1014
838
|
|
|
1015
|
-
|
|
839
|
+
### Script Queries & Custom Scoring
|
|
1016
840
|
|
|
1017
|
-
|
|
841
|
+
Script-based filtering and custom scoring for advanced relevance tuning.
|
|
1018
842
|
|
|
1019
843
|
```typescript
|
|
1020
|
-
|
|
844
|
+
import { queryBuilder, mappings, text, float, long } from 'elasticlink';
|
|
845
|
+
|
|
846
|
+
const scoredProductMappings = mappings({
|
|
1021
847
|
name: text(),
|
|
1022
|
-
|
|
1023
|
-
|
|
848
|
+
price: float(),
|
|
849
|
+
popularity: long()
|
|
1024
850
|
});
|
|
1025
851
|
|
|
1026
|
-
//
|
|
1027
|
-
const
|
|
1028
|
-
.
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
852
|
+
// Script-based filtering
|
|
853
|
+
const filtered = queryBuilder(scoredProductMappings)
|
|
854
|
+
.bool()
|
|
855
|
+
.must((q) => q.match('name', 'laptop'))
|
|
856
|
+
.filter((q) =>
|
|
857
|
+
q.script({
|
|
858
|
+
source: "doc['price'].value > params.threshold",
|
|
859
|
+
params: { threshold: 500 }
|
|
860
|
+
})
|
|
1032
861
|
)
|
|
1033
|
-
.size(0)
|
|
1034
862
|
.build();
|
|
1035
|
-
```
|
|
1036
|
-
|
|
1037
|
-
Produces:
|
|
1038
|
-
|
|
1039
|
-
```json
|
|
1040
|
-
{
|
|
1041
|
-
"suggest": {
|
|
1042
|
-
"autocomplete": {
|
|
1043
|
-
"prefix": "kap",
|
|
1044
|
-
"completion": { "field": "name_suggest", "size": 5 }
|
|
1045
|
-
},
|
|
1046
|
-
"spelling": {
|
|
1047
|
-
"text": "wiliams",
|
|
1048
|
-
"term": { "field": "name", "size": 3 }
|
|
1049
|
-
}
|
|
1050
|
-
},
|
|
1051
|
-
"size": 0
|
|
1052
|
-
}
|
|
1053
|
-
```
|
|
1054
|
-
|
|
1055
|
-
### Dynamic Search with Conditional Filters
|
|
1056
|
-
|
|
1057
|
-
```typescript
|
|
1058
|
-
const buildDynamicQuery = (filters: SearchFilters) => {
|
|
1059
|
-
return query(productMappings)
|
|
1060
|
-
.bool()
|
|
1061
|
-
.when(filters.searchTerm, q => q.must(q2 => q2.match('name', filters.searchTerm!, { boost: 2 })))
|
|
1062
|
-
.when(filters.category, q => q.filter(q2 => q2.term('category', filters.category!)))
|
|
1063
|
-
.when(filters.minPrice != null && filters.maxPrice != null,
|
|
1064
|
-
q => q.filter(q2 => q2.range('price', { gte: filters.minPrice!, lte: filters.maxPrice! }))
|
|
1065
|
-
)
|
|
1066
|
-
.from(filters.offset || 0)
|
|
1067
|
-
.size(filters.limit || 20)
|
|
1068
|
-
.build();
|
|
1069
|
-
};
|
|
1070
|
-
```
|
|
1071
|
-
|
|
1072
|
-
### Geospatial Search
|
|
1073
863
|
|
|
1074
|
-
|
|
1075
|
-
const
|
|
1076
|
-
name
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
const result = query(restaurantMappings)
|
|
1083
|
-
.term('cuisine', 'italian')
|
|
1084
|
-
.geoDistance(
|
|
1085
|
-
'location',
|
|
1086
|
-
{ lat: 40.7128, lon: -74.006 },
|
|
1087
|
-
{ distance: '5km' }
|
|
1088
|
-
)
|
|
1089
|
-
.from(0)
|
|
864
|
+
// Custom scoring with script_score
|
|
865
|
+
const customScored = queryBuilder(scoredProductMappings)
|
|
866
|
+
.scriptScore((q) => q.match('name', 'smartphone'), {
|
|
867
|
+
source: "_score * Math.log(2 + doc['popularity'].value)",
|
|
868
|
+
lang: 'painless'
|
|
869
|
+
})
|
|
1090
870
|
.size(20)
|
|
1091
871
|
.build();
|
|
1092
872
|
```
|
|
1093
873
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
elasticlink provides mapping-aware TypeScript safety:
|
|
874
|
+
### Percolate Queries
|
|
1097
875
|
|
|
1098
|
-
|
|
1099
|
-
- **Field Autocomplete**: IntelliSense knows your field names and their types
|
|
1100
|
-
- **`Infer<S>`**: Derive TS document types from your mappings schema
|
|
1101
|
-
- **Exported Field Group Types**: `SortableFields<M>`, `CollapsibleFields<M>`, `HighlightableFields<M>`, `TextFields<M>`, `KeywordFields<M>`, and others are exported for use in your own typed utilities
|
|
876
|
+
Percolate queries enable reverse search — match documents against stored queries.
|
|
1102
877
|
|
|
1103
878
|
```typescript
|
|
1104
|
-
import {
|
|
879
|
+
import { queryBuilder, mappings, keyword, percolator } from 'elasticlink';
|
|
1105
880
|
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
881
|
+
const alertRuleMappings = mappings({
|
|
882
|
+
query: percolator(),
|
|
883
|
+
name: keyword(),
|
|
884
|
+
severity: keyword()
|
|
1110
885
|
});
|
|
1111
886
|
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
887
|
+
const alerts = queryBuilder(alertRuleMappings)
|
|
888
|
+
.percolate({
|
|
889
|
+
field: 'query',
|
|
890
|
+
document: {
|
|
891
|
+
level: 'ERROR',
|
|
892
|
+
message: 'Database connection failed',
|
|
893
|
+
timestamp: '2024-01-15T10:30:00Z'
|
|
894
|
+
}
|
|
895
|
+
})
|
|
896
|
+
.size(100)
|
|
897
|
+
.build();
|
|
1123
898
|
```
|
|
1124
899
|
|
|
1125
|
-
|
|
900
|
+
**Common use cases:** alerting (match events against rules), content classification, saved search notifications, and metric threshold monitoring.
|
|
1126
901
|
|
|
1127
|
-
|
|
1128
|
-
# Run tests
|
|
1129
|
-
npm test
|
|
1130
|
-
|
|
1131
|
-
# Watch mode
|
|
1132
|
-
npm test:watch
|
|
902
|
+
## Compatibility
|
|
1133
903
|
|
|
1134
|
-
|
|
1135
|
-
|
|
904
|
+
| elasticlink | Node.js | Elasticsearch |
|
|
905
|
+
| ------------ | ---------- | ------------- |
|
|
906
|
+
| 1.0.0-beta.1 | 20, 22, 24 | 9.x (≥9.0.0) |
|
|
1136
907
|
|
|
1137
|
-
|
|
1138
|
-
npm run type-check
|
|
1139
|
-
```
|
|
1140
|
-
|
|
1141
|
-
## Roadmap
|
|
1142
|
-
|
|
1143
|
-
### Current Release ✅
|
|
1144
|
-
|
|
1145
|
-
- [x] Core query types (match, term, range, bool, etc.)
|
|
1146
|
-
- [x] Fuzzy queries for typo tolerance
|
|
1147
|
-
- [x] Query parameters (from, size, sort, timeout, etc.)
|
|
1148
|
-
- [x] Conditional query building
|
|
1149
|
-
- [x] Highlight support
|
|
1150
|
-
- [x] Aggregations (bucket and metric)
|
|
1151
|
-
- [x] Geo queries (distance, bounding box, polygon)
|
|
1152
|
-
- [x] Advanced patterns (regexp, constant_score)
|
|
1153
|
-
- [x] Sub-aggregation support
|
|
1154
|
-
- [x] Query + aggregations integration
|
|
1155
|
-
- [x] KNN (k-nearest neighbors) queries for vector search
|
|
1156
|
-
- [x] Semantic search with vector embeddings
|
|
1157
|
-
- [x] Dense vector field support
|
|
1158
|
-
- [x] Script queries and custom scoring
|
|
1159
|
-
- [x] Percolate queries for reverse search
|
|
1160
|
-
- [x] Multi-search API (NDJSON batched queries)
|
|
1161
|
-
- [x] Bulk operations (create, index, update, delete)
|
|
1162
|
-
- [x] Index management (mappings, settings, aliases)
|
|
1163
|
-
- [x] Query suggestions/completions (term, phrase, completion)
|
|
1164
|
-
- [x] Integration test suite against live Elasticsearch 9.x
|
|
1165
|
-
- [x] Types derived from official `@elastic/elasticsearch` for accuracy and completeness
|
|
908
|
+
Tested against the versions listed. Peer dependency is `@elastic/elasticsearch >=9.0.0`.
|
|
1166
909
|
|
|
1167
910
|
## Development
|
|
1168
911
|
|
|
1169
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and code style.
|
|
912
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, design principles, and code style.
|
|
1170
913
|
|
|
1171
914
|
## License
|
|
1172
915
|
|