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