elasticlink 0.8.0-beta → 1.0.0-beta.2
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 +631 -819
- package/dist/index.cjs +1528 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1890 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +1889 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1474 -18
- package/dist/index.js.map +1 -0
- package/package.json +25 -19
- package/dist/aggregation.builder.d.ts +0 -6
- package/dist/aggregation.builder.d.ts.map +0 -1
- package/dist/aggregation.builder.js +0 -64
- package/dist/aggregation.types.d.ts +0 -173
- package/dist/aggregation.types.d.ts.map +0 -1
- package/dist/aggregation.types.js +0 -6
- package/dist/bulk.builder.d.ts +0 -28
- package/dist/bulk.builder.d.ts.map +0 -1
- package/dist/bulk.builder.js +0 -51
- package/dist/bulk.types.d.ts +0 -46
- package/dist/bulk.types.d.ts.map +0 -1
- package/dist/bulk.types.js +0 -6
- package/dist/field.helpers.d.ts +0 -167
- package/dist/field.helpers.d.ts.map +0 -1
- package/dist/field.helpers.js +0 -282
- package/dist/field.types.d.ts +0 -255
- package/dist/field.types.d.ts.map +0 -1
- package/dist/field.types.js +0 -6
- package/dist/index-management.builder.d.ts +0 -33
- package/dist/index-management.builder.d.ts.map +0 -1
- package/dist/index-management.builder.js +0 -64
- package/dist/index-management.types.d.ts +0 -126
- package/dist/index-management.types.d.ts.map +0 -1
- package/dist/index-management.types.js +0 -6
- package/dist/mapping.builder.d.ts +0 -46
- package/dist/mapping.builder.d.ts.map +0 -1
- package/dist/mapping.builder.js +0 -39
- package/dist/mapping.types.d.ts +0 -160
- package/dist/mapping.types.d.ts.map +0 -1
- package/dist/mapping.types.js +0 -6
- package/dist/multi-search.builder.d.ts +0 -22
- package/dist/multi-search.builder.d.ts.map +0 -1
- package/dist/multi-search.builder.js +0 -39
- package/dist/multi-search.types.d.ts +0 -36
- package/dist/multi-search.types.d.ts.map +0 -1
- package/dist/multi-search.types.js +0 -6
- package/dist/query.builder.d.ts +0 -4
- package/dist/query.builder.d.ts.map +0 -1
- package/dist/query.builder.js +0 -264
- package/dist/query.types.d.ts +0 -324
- package/dist/query.types.d.ts.map +0 -1
- package/dist/query.types.js +0 -7
- package/dist/settings.presets.d.ts +0 -98
- package/dist/settings.presets.d.ts.map +0 -1
- package/dist/settings.presets.js +0 -115
- package/dist/suggester.builder.d.ts +0 -23
- package/dist/suggester.builder.d.ts.map +0 -1
- package/dist/suggester.builder.js +0 -51
- package/dist/suggester.types.d.ts +0 -50
- package/dist/suggester.types.d.ts.map +0 -1
- package/dist/suggester.types.js +0 -6
- package/dist/vector.types.d.ts +0 -17
- package/dist/vector.types.d.ts.map +0 -1
- package/dist/vector.types.js +0 -6
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,513 @@ 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
|
|
60
|
+
Boolean queries with aggregations:
|
|
78
61
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
62
|
+
```typescript
|
|
63
|
+
const facetedSearch = queryBuilder(productMappings)
|
|
64
|
+
.bool()
|
|
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)
|
|
71
|
+
.build();
|
|
72
|
+
```
|
|
99
73
|
|
|
100
|
-
|
|
74
|
+
Type safety in action — wrong field types are caught at compile time:
|
|
101
75
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
76
|
+
```typescript
|
|
77
|
+
queryBuilder(productMappings).match('category', 'electronics');
|
|
78
|
+
// ^^^^^^^^^^
|
|
79
|
+
// TypeScript error: 'category' is a keyword field — use term(), not match()
|
|
105
80
|
|
|
106
|
-
|
|
81
|
+
queryBuilder(productMappings).term('category', 'electronics'); // ✅ Correct
|
|
82
|
+
```
|
|
107
83
|
|
|
108
|
-
|
|
84
|
+
## Examples
|
|
109
85
|
|
|
110
|
-
|
|
86
|
+
More examples available in [src/\_\_tests\_\_/examples.test.ts](src/__tests__/examples.test.ts).
|
|
111
87
|
|
|
112
|
-
|
|
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
|
|
88
|
+
### E-commerce Product Search
|
|
118
89
|
|
|
119
|
-
|
|
90
|
+
A complete search request: boolean query with must/filter/should, aggregations for facets and price ranges, highlights, `_source` filtering, and pagination.
|
|
120
91
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
92
|
+
```typescript
|
|
93
|
+
const ecommerceMappings = mappings({
|
|
94
|
+
name: text(),
|
|
95
|
+
description: text(),
|
|
96
|
+
category: keyword(),
|
|
97
|
+
price: float(),
|
|
98
|
+
tags: keyword(),
|
|
99
|
+
in_stock: boolean()
|
|
100
|
+
});
|
|
125
101
|
|
|
126
|
-
|
|
102
|
+
const searchTerm = 'gaming laptop';
|
|
103
|
+
const category = 'electronics';
|
|
104
|
+
const minPrice = 800;
|
|
105
|
+
const maxPrice = 2000;
|
|
127
106
|
|
|
128
|
-
|
|
129
|
-
query(productMappings)
|
|
107
|
+
const result = queryBuilder(ecommerceMappings)
|
|
130
108
|
.bool()
|
|
131
|
-
.must(q => q.match('name', '
|
|
132
|
-
.
|
|
133
|
-
.
|
|
134
|
-
.
|
|
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))
|
|
135
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
|
+
)
|
|
120
|
+
.highlight(['name', 'description'], {
|
|
121
|
+
fragment_size: 150,
|
|
122
|
+
pre_tags: ['<mark>'],
|
|
123
|
+
post_tags: ['</mark>']
|
|
124
|
+
})
|
|
125
|
+
._source(['name', 'price', 'category', 'tags'])
|
|
126
|
+
.timeout('5s')
|
|
127
|
+
.from(0)
|
|
128
|
+
.size(20)
|
|
129
|
+
.sort('_score', 'desc')
|
|
136
130
|
.build();
|
|
137
131
|
```
|
|
138
132
|
|
|
139
|
-
|
|
133
|
+
<details>
|
|
134
|
+
<summary><b>Produced DSL</b> (click to expand)</summary>
|
|
135
|
+
|
|
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
|
+
```
|
|
171
|
+
|
|
172
|
+
</details>
|
|
140
173
|
|
|
141
|
-
|
|
174
|
+
### Dynamic Search with Conditional Filters
|
|
142
175
|
|
|
143
|
-
|
|
176
|
+
Build queries dynamically based on runtime values. `.when(condition, fn)` — when the condition is falsy, the builder is returned unchanged.
|
|
144
177
|
|
|
145
178
|
```typescript
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
};
|
|
154
191
|
```
|
|
155
192
|
|
|
156
|
-
|
|
157
|
-
>
|
|
158
|
-
> **Note:** When the condition is false, any already-introduced `.bool()` wrapper is preserved. A `.bool().when(false, fn)` chain produces `{ query: { bool: {} } }` — not an error, just an empty bool clause.
|
|
193
|
+
### Aggregations — Portfolio Analytics
|
|
159
194
|
|
|
160
|
-
|
|
195
|
+
Terms + sub-aggregation + date histogram in one request.
|
|
161
196
|
|
|
162
197
|
```typescript
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
.
|
|
174
|
-
.
|
|
175
|
-
.
|
|
176
|
-
.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
198
|
+
const instrumentMappings = mappings({
|
|
199
|
+
name: text(),
|
|
200
|
+
asset_class: keyword(),
|
|
201
|
+
sector: keyword(),
|
|
202
|
+
price: float(),
|
|
203
|
+
yield_rate: float(),
|
|
204
|
+
listed_date: date()
|
|
205
|
+
});
|
|
206
|
+
|
|
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) =>
|
|
212
|
+
agg
|
|
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
|
|
218
|
+
})
|
|
219
|
+
.subAgg((sub) => sub.percentiles('yield_percentiles', 'yield_rate', { percents: [25, 50, 75, 95] }))
|
|
220
|
+
)
|
|
221
|
+
.size(0)
|
|
181
222
|
.build();
|
|
182
223
|
```
|
|
183
224
|
|
|
184
|
-
|
|
225
|
+
<details>
|
|
226
|
+
<summary><b>Produced DSL</b> (click to expand)</summary>
|
|
227
|
+
|
|
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
|
+
}
|
|
252
|
+
```
|
|
185
253
|
|
|
186
|
-
|
|
254
|
+
</details>
|
|
187
255
|
|
|
188
|
-
|
|
189
|
-
- **Metric**: `avg()`, `sum()`, `min()`, `max()`, `cardinality()`, `percentiles()`, `stats()`, `valueCount()`
|
|
190
|
-
- **Composition**: `subAgg()` for nested aggregations — attaches sub-aggregations to the **last** aggregation defined before the call; chain order matters
|
|
256
|
+
### Geospatial Search
|
|
191
257
|
|
|
192
258
|
```typescript
|
|
193
|
-
|
|
259
|
+
const restaurantMappings = mappings({
|
|
260
|
+
name: text(),
|
|
261
|
+
cuisine: keyword(),
|
|
262
|
+
location: geoPoint(),
|
|
263
|
+
rating: float()
|
|
264
|
+
});
|
|
194
265
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
.term('
|
|
198
|
-
.
|
|
199
|
-
|
|
200
|
-
.terms('by_category', 'category', { size: 10 })
|
|
201
|
-
.avg('avg_price', 'price')
|
|
202
|
-
)
|
|
266
|
+
const result = queryBuilder(restaurantMappings)
|
|
267
|
+
.bool()
|
|
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')
|
|
203
271
|
.size(20)
|
|
204
272
|
.build();
|
|
205
|
-
|
|
206
|
-
// Standalone aggregations (no query) — use query(mappings, false)
|
|
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();
|
|
217
|
-
|
|
218
|
-
// Standalone aggregation builder (for manual composition)
|
|
219
|
-
const standaloneAgg = aggregations(productMappings)
|
|
220
|
-
.avg('avg_price', 'price')
|
|
221
|
-
.terms('by_category', 'category', { size: 10 })
|
|
222
|
-
.build();
|
|
223
273
|
```
|
|
224
274
|
|
|
225
|
-
|
|
275
|
+
## TypeScript Support
|
|
276
|
+
|
|
277
|
+
elasticlink provides mapping-aware TypeScript safety:
|
|
226
278
|
|
|
227
|
-
|
|
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`
|
|
228
284
|
|
|
229
285
|
```typescript
|
|
230
|
-
import {
|
|
286
|
+
import { queryBuilder, mappings, text, keyword, integer, type Infer } from 'elasticlink';
|
|
231
287
|
|
|
232
|
-
const
|
|
288
|
+
const userMappings = mappings({
|
|
233
289
|
name: text(),
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
category: keyword(),
|
|
237
|
-
embedding: denseVector({ dims: 384 }),
|
|
290
|
+
email: keyword(),
|
|
291
|
+
age: integer()
|
|
238
292
|
});
|
|
239
293
|
|
|
240
|
-
|
|
241
|
-
|
|
294
|
+
type User = Infer<typeof userMappings>;
|
|
295
|
+
// => { name: string; email: string; age: number }
|
|
242
296
|
|
|
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();
|
|
297
|
+
// ✅ 'name' is a text field — match() accepts it
|
|
298
|
+
const q1 = queryBuilder(userMappings).match('name', 'John').build();
|
|
250
299
|
|
|
251
|
-
//
|
|
252
|
-
const
|
|
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();
|
|
300
|
+
// ❌ TypeScript error: 'email' is keyword, not text — use term() instead
|
|
301
|
+
const q2 = queryBuilder(userMappings).match('email', 'john@example.com').build();
|
|
267
302
|
|
|
268
|
-
//
|
|
269
|
-
const
|
|
270
|
-
.knn('embedding', searchEmbedding, {
|
|
271
|
-
k: 100,
|
|
272
|
-
num_candidates: 1000,
|
|
273
|
-
filter: { term: { category: 'electronics' } }
|
|
274
|
-
})
|
|
275
|
-
.aggs(agg =>
|
|
276
|
-
agg
|
|
277
|
-
.terms('categories', 'category', { size: 10 })
|
|
278
|
-
.range('price_ranges', 'price', {
|
|
279
|
-
ranges: [
|
|
280
|
-
{ to: 100 },
|
|
281
|
-
{ from: 100, to: 500 },
|
|
282
|
-
{ from: 500 }
|
|
283
|
-
]
|
|
284
|
-
})
|
|
285
|
-
)
|
|
286
|
-
.size(20)
|
|
287
|
-
.build();
|
|
303
|
+
// ✅ Correct: use term() for keyword fields
|
|
304
|
+
const q3 = queryBuilder(userMappings).term('email', 'john@example.com').build();
|
|
288
305
|
```
|
|
289
306
|
|
|
290
|
-
|
|
307
|
+
## Vanilla JavaScript
|
|
291
308
|
|
|
292
|
-
|
|
293
|
-
- **512**: Image embeddings (ResNet, ViT)
|
|
294
|
-
- **1536**: OpenAI text-embedding-ada-002
|
|
295
|
-
- **3072**: OpenAI text-embedding-3-large
|
|
309
|
+
elasticlink works in vanilla JavaScript with full IntelliSense — no TypeScript required. The library ships both ESM and CommonJS builds, so `import` and `require()` both work.
|
|
296
310
|
|
|
297
|
-
|
|
311
|
+
### Setup
|
|
298
312
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
ef_construction: 100
|
|
313
|
+
For project-wide type checking, add a `jsconfig.json` to your project root:
|
|
314
|
+
|
|
315
|
+
```json
|
|
316
|
+
{
|
|
317
|
+
"compilerOptions": {
|
|
318
|
+
"checkJs": true,
|
|
319
|
+
"strict": true,
|
|
320
|
+
"module": "commonjs",
|
|
321
|
+
"moduleResolution": "node10",
|
|
322
|
+
"target": "ES2022"
|
|
310
323
|
}
|
|
311
|
-
}
|
|
324
|
+
}
|
|
312
325
|
```
|
|
313
326
|
|
|
314
|
-
|
|
327
|
+
Or enable per-file with `// @ts-check` at the top of any `.js` file.
|
|
315
328
|
|
|
316
|
-
|
|
329
|
+
### Usage
|
|
317
330
|
|
|
318
|
-
|
|
319
|
-
import { query, mappings, text, keyword, float, long } from 'elasticlink';
|
|
331
|
+
Query builder methods provide field-name autocomplete and type constraints in VS Code even without any configuration:
|
|
320
332
|
|
|
321
|
-
|
|
333
|
+
```js
|
|
334
|
+
const { mappings, text, keyword, float, queryBuilder } = require('elasticlink');
|
|
335
|
+
|
|
336
|
+
const productSchema = mappings({
|
|
322
337
|
name: text(),
|
|
323
338
|
price: float(),
|
|
324
|
-
|
|
325
|
-
quality_score: float(),
|
|
339
|
+
category: keyword()
|
|
326
340
|
});
|
|
327
341
|
|
|
328
|
-
//
|
|
329
|
-
const
|
|
330
|
-
.
|
|
331
|
-
.
|
|
332
|
-
.
|
|
333
|
-
q.script({
|
|
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
|
-
)
|
|
349
|
-
.size(20)
|
|
342
|
+
// Field names autocomplete, and are constrained by type:
|
|
343
|
+
const query = queryBuilder(productSchema)
|
|
344
|
+
.match('name', 'laptop') // 'name' is text — allowed in match()
|
|
345
|
+
.range('price', { gte: 500 }) // 'price' is numeric — allowed in range()
|
|
346
|
+
.term('category', 'electronics') // 'category' is keyword — allowed in term()
|
|
350
347
|
.build();
|
|
351
348
|
```
|
|
352
349
|
|
|
353
|
-
|
|
350
|
+
### Deriving Document Types
|
|
354
351
|
|
|
355
|
-
|
|
356
|
-
- **expression**: Fast, limited expression language
|
|
357
|
-
- **mustache**: Template-based scripting
|
|
352
|
+
In TypeScript you'd use `type Product = Infer<typeof schema>`. In JavaScript, use the `inferType()` helper:
|
|
358
353
|
|
|
359
|
-
|
|
354
|
+
> ⚠️ **Type-only helper.** `inferType()` always returns `undefined`. It exists so you can write `typeof _Product` in a JSDoc `@type` annotation. Never use the returned value at runtime — accessing a property on it (e.g. `_Product.name`) will throw `TypeError`.
|
|
360
355
|
|
|
361
|
-
|
|
356
|
+
```js
|
|
357
|
+
const { mappings, text, keyword, float, inferType } = require('elasticlink');
|
|
362
358
|
|
|
363
|
-
|
|
364
|
-
import { query, mappings, keyword, percolator } from 'elasticlink';
|
|
359
|
+
const schema = mappings({ name: text(), price: float(), category: keyword() });
|
|
365
360
|
|
|
366
|
-
const
|
|
367
|
-
query: percolator(),
|
|
368
|
-
name: keyword(),
|
|
369
|
-
severity: keyword(),
|
|
370
|
-
});
|
|
361
|
+
const _Product = inferType(schema); // undefined at runtime; only the type matters
|
|
371
362
|
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
|
|
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();
|
|
363
|
+
/** @type {typeof _Product} */
|
|
364
|
+
const doc = { name: 'Laptop', price: 999, category: 'electronics' };
|
|
365
|
+
// ^ full autocomplete on all properties
|
|
384
366
|
```
|
|
385
367
|
|
|
386
|
-
|
|
368
|
+
Alternatively, use the JSDoc import syntax directly:
|
|
369
|
+
|
|
370
|
+
```js
|
|
371
|
+
/** @type {import('elasticlink').Infer<typeof schema>} */
|
|
372
|
+
const doc = { name: 'Laptop', price: 999, category: 'electronics' };
|
|
373
|
+
```
|
|
387
374
|
|
|
388
|
-
-
|
|
389
|
-
- **Content Classification:** Categorize documents in real-time
|
|
390
|
-
- **Saved Searches:** Notify users when new content matches their searches
|
|
391
|
-
- **Monitoring:** Trigger actions based on metric thresholds
|
|
375
|
+
See [`examples/vanilla-js/`](examples/vanilla-js/) for a complete working example.
|
|
392
376
|
|
|
393
|
-
|
|
377
|
+
## Settings Presets
|
|
394
378
|
|
|
395
|
-
|
|
379
|
+
Ready-made index settings for common lifecycle stages. Use with `.settings()` on `indexBuilder()` or pass directly to the ES `_settings` API.
|
|
396
380
|
|
|
397
381
|
```typescript
|
|
398
|
-
import {
|
|
382
|
+
import { indexBuilder, productionSearchSettings, indexSortSettings, fastIngestSettings } from 'elasticlink';
|
|
399
383
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
description: text(),
|
|
403
|
-
suggest_field: completion(), // Must be type: completion
|
|
404
|
-
});
|
|
384
|
+
// Create index with production settings
|
|
385
|
+
const indexConfig = indexBuilder().mappings(myMappings).settings(productionSearchSettings()).build();
|
|
405
386
|
|
|
406
|
-
//
|
|
407
|
-
const
|
|
408
|
-
.
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
string_distance: 'levenshtein',
|
|
413
|
-
max_edits: 2
|
|
414
|
-
})
|
|
415
|
-
.build();
|
|
416
|
-
|
|
417
|
-
// Completion suggester - Fast autocomplete
|
|
418
|
-
const autocomplete = suggest(searchableMappings)
|
|
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
|
-
}
|
|
387
|
+
// Index-time sort for compression and early termination
|
|
388
|
+
const sortedConfig = indexBuilder()
|
|
389
|
+
.mappings(myMappings)
|
|
390
|
+
.settings({
|
|
391
|
+
...productionSearchSettings(),
|
|
392
|
+
index: indexSortSettings({ timestamp: 'desc', status: 'asc' })
|
|
429
393
|
})
|
|
430
394
|
.build();
|
|
431
395
|
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
.match('name', 'laptpo')
|
|
435
|
-
.suggest((s) =>
|
|
436
|
-
s.term('spelling-correction', 'laptpo', {
|
|
437
|
-
field: 'name',
|
|
438
|
-
size: 3,
|
|
439
|
-
suggest_mode: 'popular'
|
|
440
|
-
})
|
|
441
|
-
)
|
|
442
|
-
.size(20)
|
|
443
|
-
.build();
|
|
444
|
-
```
|
|
396
|
+
// Before bulk ingest — disables refresh, removes replicas, async translog
|
|
397
|
+
await client.indices.putSettings({ index: 'my-index', body: fastIngestSettings() });
|
|
445
398
|
|
|
446
|
-
|
|
399
|
+
// Perform bulk ingest...
|
|
447
400
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
401
|
+
// Restore production settings afterward
|
|
402
|
+
await client.indices.putSettings({ index: 'my-index', body: productionSearchSettings() });
|
|
403
|
+
await client.indices.refresh({ index: 'my-index' });
|
|
404
|
+
```
|
|
451
405
|
|
|
452
|
-
|
|
406
|
+
| Preset | Purpose |
|
|
407
|
+
| -------------------------------------- | --------------------------------------------------------------------------- |
|
|
408
|
+
| `productionSearchSettings(overrides?)` | Balanced production defaults — 1 replica, 5s refresh |
|
|
409
|
+
| `indexSortSettings(fields)` | Configure index-time sort order for disk compression and early termination |
|
|
410
|
+
| `fastIngestSettings(overrides?)` | Maximum indexing throughput — async translog, no replicas, refresh disabled |
|
|
453
411
|
|
|
454
|
-
|
|
412
|
+
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.
|
|
455
413
|
|
|
456
|
-
|
|
457
|
-
import { query, msearch } from 'elasticlink';
|
|
414
|
+
## API Overview
|
|
458
415
|
|
|
459
|
-
|
|
460
|
-
.match('name', 'laptop')
|
|
461
|
-
.range('price', { gte: 500, lte: 2000 })
|
|
462
|
-
.build();
|
|
416
|
+
### Query Builder
|
|
463
417
|
|
|
464
|
-
|
|
465
|
-
.match('name', 'smartphone')
|
|
466
|
-
.range('price', { gte: 300, lte: 1000 })
|
|
467
|
-
.build();
|
|
418
|
+
`queryBuilder(schema, includeQuery?)` creates a fluent, immutable query builder. Every chain method returns a new builder instance.
|
|
468
419
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
.
|
|
472
|
-
.
|
|
420
|
+
```typescript
|
|
421
|
+
queryBuilder(productMappings)
|
|
422
|
+
.bool()
|
|
423
|
+
.must((q) => q.match('name', 'laptop'))
|
|
424
|
+
.filter((q) => q.range('price', { gte: 500 }))
|
|
425
|
+
.should((q) => q.term('category', 'featured'))
|
|
426
|
+
.mustNot((q) => q.term('category', 'discontinued'))
|
|
427
|
+
.minimumShouldMatch(1)
|
|
473
428
|
.build();
|
|
474
|
-
|
|
475
|
-
// Or build as array of objects
|
|
476
|
-
const array = msearch(productMappings)
|
|
477
|
-
.addQuery(laptopQuery, { index: 'products' })
|
|
478
|
-
.addQuery(phoneQuery, { index: 'products' })
|
|
479
|
-
.buildArray();
|
|
480
429
|
```
|
|
481
430
|
|
|
482
|
-
|
|
431
|
+
#### Query Methods
|
|
483
432
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
433
|
+
| Category | Methods | Description |
|
|
434
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
435
|
+
| **Full-text** | `match`, `multiMatch`, `matchPhrase`, `matchPhrasePrefix`, `matchBoolPrefix`, `combinedFields` | Analyzed text search — field must be a text field |
|
|
436
|
+
| **Term-level** | `term`, `terms`, `prefix`, `wildcard`, `exists`, `ids`, `matchAll`, `matchNone` | Exact value matching — field must be keyword, numeric, date, boolean, or ip |
|
|
437
|
+
| **Range** | `range` | Range queries (`gte`, `lte`, `gt`, `lt`) on numeric, date, keyword, or ip fields |
|
|
438
|
+
| **Fuzzy & Pattern** | `fuzzy`, `regexp`, `queryString`, `simpleQueryString`, `moreLikeThis` | Typo tolerance, regex, Lucene syntax, and similarity search |
|
|
439
|
+
| **Boolean** | `bool` → `must`, `mustNot`, `should`, `filter`, `minimumShouldMatch` | Combine clauses with AND/OR/NOT/filter logic |
|
|
440
|
+
| **Conditional** | `when(condition, fn)` | Dynamic query building — skips the branch when condition is falsy |
|
|
441
|
+
| **Geo** | `geoDistance`, `geoBoundingBox`, `geoPolygon`, `geoShape` | Spatial queries — field must be geo_point or geo_shape |
|
|
442
|
+
| **Vector & Relevance** | `knn`, `sparseVector`, `rankFeature`, `distanceFeature` | Semantic/vector search, rank features, and decay functions |
|
|
443
|
+
| **Nested & Structure** | `nested`, `constantScore`, `hasChild`, `hasParent`, `parentId` | Query nested objects, parent/child joins, or wrap in constant-score filter |
|
|
444
|
+
| **Sorting & Pagination** | `from`, `size`, `sort`, `searchAfter`, `collapse` | Result ordering, pagination, and field collapsing |
|
|
445
|
+
| **Source & Fields** | `_source`, `sourceIncludes`, `sourceExcludes`, `fields`, `docValueFields`, `storedFields` | Control which fields are returned |
|
|
446
|
+
| **Highlighting & Scoring** | `highlight`, `rescore`, `indicesBoost`, `minScore`, `trackScores`, `explain` | Result highlighting, rescoring, and relevance tuning |
|
|
447
|
+
| **Positional** | `intervals`, `spanTerm`, `spanNear`, `spanOr`, `spanNot`, `spanFirst`, `spanContaining`, `spanWithin`, `spanMultiTerm`, `spanFieldMasking` | Term proximity and ordering queries |
|
|
448
|
+
| **Advanced** | `postFilter`, `scriptFields`, `runtimeMappings`, `script`, `scriptScore`, `functionScore`, `percolate` | Post-query filtering, computed fields, and custom scoring |
|
|
449
|
+
| **Configuration** | `timeout`, `preference`, `pit`, `terminateAfter`, `version`, `seqNoPrimaryTerm`, `trackTotalHits` | Search execution options |
|
|
450
|
+
| **Inline builders** | `aggs(fn)`, `suggest(fn)` | Attach aggregations or suggesters to the query |
|
|
451
|
+
| **Output** | `build()` | Returns the final Elasticsearch DSL object |
|
|
489
452
|
|
|
490
|
-
|
|
453
|
+
All query methods accept options from their corresponding `@elastic/elasticsearch` type. See [`query.types.ts`](src/query.types.ts) for complete signatures.
|
|
491
454
|
|
|
492
|
-
|
|
455
|
+
#### Conditional Building
|
|
493
456
|
|
|
494
|
-
- `
|
|
495
|
-
- `routing`: Routing value for sharding
|
|
496
|
-
- `preference`: Node preference (\_local, \_primary, etc.)
|
|
497
|
-
- `search_type`: Search type (dfs_query_then_fetch, etc.)
|
|
457
|
+
`.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.
|
|
498
458
|
|
|
499
|
-
|
|
459
|
+
```typescript
|
|
460
|
+
const searchTerm: string | undefined = param.searchTerm;
|
|
461
|
+
const minPrice: number | undefined = param.minPrice;
|
|
500
462
|
|
|
501
|
-
|
|
463
|
+
queryBuilder(productMappings)
|
|
464
|
+
.bool()
|
|
465
|
+
.when(searchTerm, (q) => q.must((q2) => q2.match('name', searchTerm!)))
|
|
466
|
+
.when(minPrice, (q) => q.filter((q2) => q2.range('price', { gte: minPrice! })))
|
|
467
|
+
.build();
|
|
468
|
+
```
|
|
502
469
|
|
|
503
|
-
|
|
504
|
-
import { bulk, mappings, keyword, text, float } from 'elasticlink';
|
|
470
|
+
> **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 (`!`).
|
|
505
471
|
|
|
506
|
-
|
|
507
|
-
id: keyword(),
|
|
508
|
-
name: text(),
|
|
509
|
-
price: float(),
|
|
510
|
-
category: keyword(),
|
|
511
|
-
});
|
|
472
|
+
#### Query Parameters
|
|
512
473
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
.
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
)
|
|
519
|
-
//
|
|
520
|
-
.
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
)
|
|
524
|
-
//
|
|
525
|
-
.
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
474
|
+
```typescript
|
|
475
|
+
queryBuilder(productMappings)
|
|
476
|
+
.match('name', 'laptop')
|
|
477
|
+
.from(0) // Pagination offset
|
|
478
|
+
.size(20) // Results per page
|
|
479
|
+
.sort('price', 'asc') // Sort by field
|
|
480
|
+
._source(['name', 'price']) // Which fields to return
|
|
481
|
+
.timeout('5s') // Query timeout
|
|
482
|
+
.trackScores(true) // Enable scoring in filter context
|
|
483
|
+
.trackTotalHits(true) // Track total hit count (or pass a number threshold)
|
|
484
|
+
.explain(true) // Return scoring explanation
|
|
485
|
+
.minScore(10) // Minimum relevance score
|
|
486
|
+
.version(true) // Include document version in results
|
|
487
|
+
.seqNoPrimaryTerm(true) // Include seq_no and primary_term for optimistic concurrency
|
|
488
|
+
.highlight(['name', 'description'], {
|
|
489
|
+
fragment_size: 150,
|
|
490
|
+
pre_tags: ['<mark>'],
|
|
491
|
+
post_tags: ['</mark>']
|
|
529
492
|
})
|
|
530
|
-
// Delete
|
|
531
|
-
.delete({ _index: 'products', _id: '4' })
|
|
532
493
|
.build();
|
|
533
|
-
|
|
534
|
-
// POST /_bulk with Content-Type: application/x-ndjson
|
|
535
494
|
```
|
|
536
495
|
|
|
537
|
-
|
|
496
|
+
### Aggregations
|
|
538
497
|
|
|
539
|
-
|
|
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"}}
|
|
498
|
+
Aggregations can be combined with queries via `.aggs()` or used standalone with the `aggregations()` builder.
|
|
547
499
|
|
|
500
|
+
```typescript
|
|
501
|
+
import { queryBuilder, aggregations } from 'elasticlink';
|
|
502
|
+
|
|
503
|
+
// Inline aggregations (most common)
|
|
504
|
+
const result = queryBuilder(productMappings)
|
|
505
|
+
.term('category', 'electronics')
|
|
506
|
+
.aggs((agg) =>
|
|
507
|
+
agg
|
|
508
|
+
.terms('by_category', 'category', { size: 10 })
|
|
509
|
+
.subAgg((sub) => sub.avg('avg_price', 'price').max('max_price', 'price'))
|
|
510
|
+
)
|
|
511
|
+
.size(20)
|
|
512
|
+
.build();
|
|
513
|
+
|
|
514
|
+
// Aggregations-only (no query) — use queryBuilder(mappings, false)
|
|
515
|
+
const aggsOnly = queryBuilder(productMappings, false)
|
|
516
|
+
.aggs((agg) => agg.terms('by_category', 'category'))
|
|
517
|
+
.size(0)
|
|
518
|
+
.build();
|
|
519
|
+
|
|
520
|
+
// Standalone aggregation builder (for manual composition)
|
|
521
|
+
const standaloneAgg = aggregations(productMappings)
|
|
522
|
+
.avg('avg_price', 'price')
|
|
523
|
+
.terms('by_category', 'category', { size: 10 })
|
|
524
|
+
.build();
|
|
548
525
|
```
|
|
549
526
|
|
|
550
|
-
|
|
527
|
+
#### Aggregation Methods
|
|
528
|
+
|
|
529
|
+
| Category | Methods | Description |
|
|
530
|
+
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
531
|
+
| **Bucket** | `terms`, `dateHistogram`, `histogram`, `range`, `dateRange`, `filters`, `significantTerms`, `rareTerms`, `multiTerms`, `autoDateHistogram`, `composite`, `filter`, `missing`, `geoDistance`, `geohashGrid`, `geotileGrid` | Group documents into buckets |
|
|
532
|
+
| **Metric** | `avg`, `sum`, `min`, `max`, `cardinality`, `percentiles`, `stats`, `valueCount`, `extendedStats`, `topHits`, `topMetrics`, `weightedAvg`, `geoBounds`, `geoCentroid` | Compute metrics over documents |
|
|
533
|
+
| **Pipeline** | `derivative`, `cumulativeSum`, `bucketScript`, `bucketSelector` | Compute over other aggregation results |
|
|
534
|
+
| **Structure** | `subAgg`, `nested`, `reverseNested`, `global` | Nest aggregations, navigate nested fields, or escape to global scope |
|
|
551
535
|
|
|
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
|
|
536
|
+
`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
537
|
|
|
558
538
|
### Index Management
|
|
559
539
|
|
|
560
|
-
Configure index mappings, settings, and aliases declaratively
|
|
540
|
+
Configure index mappings, settings, and aliases declaratively with `indexBuilder()`.
|
|
561
541
|
|
|
562
542
|
```typescript
|
|
563
543
|
import { indexBuilder, mappings, keyword, integer, float, date, text } from 'elasticlink';
|
|
@@ -567,7 +547,7 @@ const matterMappings = mappings({
|
|
|
567
547
|
practice_area: keyword(),
|
|
568
548
|
billing_rate: integer(),
|
|
569
549
|
risk_score: float(),
|
|
570
|
-
opened_at: date()
|
|
550
|
+
opened_at: date()
|
|
571
551
|
});
|
|
572
552
|
|
|
573
553
|
const indexConfig = indexBuilder()
|
|
@@ -582,11 +562,11 @@ const indexConfig = indexBuilder()
|
|
|
582
562
|
.build();
|
|
583
563
|
|
|
584
564
|
// PUT /matters-v1
|
|
585
|
-
|
|
586
|
-
// Body: JSON.stringify(indexConfig)
|
|
565
|
+
await client.indices.create({ index: 'matters-v1', ...indexConfig });
|
|
587
566
|
```
|
|
588
567
|
|
|
589
|
-
|
|
568
|
+
<details>
|
|
569
|
+
<summary><b>Produced DSL</b> (click to expand)</summary>
|
|
590
570
|
|
|
591
571
|
```json
|
|
592
572
|
{
|
|
@@ -611,9 +591,21 @@ Produces:
|
|
|
611
591
|
}
|
|
612
592
|
```
|
|
613
593
|
|
|
594
|
+
</details>
|
|
595
|
+
|
|
596
|
+
#### IndexBuilder Methods
|
|
597
|
+
|
|
598
|
+
| Method | Description |
|
|
599
|
+
| ------------------------------------ | ------------------------------------------------------------------------------ |
|
|
600
|
+
| `mappings(schemaOrFields, options?)` | Set index mappings from a `MappingsSchema` or raw field definitions |
|
|
601
|
+
| `settings(settings)` | Set index settings (`IndicesIndexSettings` from `@elastic/elasticsearch`) |
|
|
602
|
+
| `analysis(config)` | Configure custom analyzers, tokenizers, filters, char filters, and normalizers |
|
|
603
|
+
| `alias(name, options?)` | Add an index alias with optional filter, routing, and write index settings |
|
|
604
|
+
| `build()` | Returns the final `CreateIndexOptions` object |
|
|
605
|
+
|
|
614
606
|
#### Object and Nested Fields
|
|
615
607
|
|
|
616
|
-
Use `object()` for structured sub-documents queried with dot-notation
|
|
608
|
+
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
609
|
|
|
618
610
|
```typescript
|
|
619
611
|
import { mappings, text, keyword, float, integer, boolean, object, nested, type Infer } from 'elasticlink';
|
|
@@ -621,111 +613,85 @@ import { mappings, text, keyword, float, integer, boolean, object, nested, type
|
|
|
621
613
|
const productMappings = mappings({
|
|
622
614
|
name: text(),
|
|
623
615
|
in_stock: boolean(),
|
|
624
|
-
|
|
625
|
-
// object() — single structured value, queried with dot-notation (no wrapper needed)
|
|
626
616
|
address: object({
|
|
617
|
+
// Single structured value — queried with dot-notation
|
|
627
618
|
street: text(),
|
|
628
619
|
city: keyword(),
|
|
629
|
-
country: keyword()
|
|
620
|
+
country: keyword()
|
|
630
621
|
}),
|
|
631
|
-
|
|
632
|
-
// nested() — array of objects; cross-field queries require the .nested() wrapper
|
|
633
622
|
variants: nested({
|
|
623
|
+
// Array of objects — cross-field accuracy preserved
|
|
634
624
|
sku: keyword(),
|
|
635
625
|
color: keyword(),
|
|
636
626
|
price: float(),
|
|
637
|
-
stock: integer()
|
|
638
|
-
})
|
|
627
|
+
stock: integer()
|
|
628
|
+
})
|
|
639
629
|
});
|
|
640
630
|
|
|
641
|
-
// Infer<> produces the correct nested TypeScript types:
|
|
642
631
|
type Product = Infer<typeof productMappings>;
|
|
643
|
-
// {
|
|
644
|
-
// name: string;
|
|
645
|
-
// in_stock: boolean;
|
|
632
|
+
// { name: string; in_stock: boolean;
|
|
646
633
|
// address: { street: string; city: string; country: string };
|
|
647
|
-
// variants: Array<{ sku: string; color: string; price: number; stock: number }
|
|
648
|
-
// }
|
|
634
|
+
// variants: Array<{ sku: string; color: string; price: number; stock: number }> }
|
|
649
635
|
|
|
650
636
|
// object sub-fields — query with dot-notation directly
|
|
651
|
-
|
|
637
|
+
queryBuilder(productMappings)
|
|
652
638
|
.bool()
|
|
653
|
-
.filter(q => q.term('address.country', 'US'))
|
|
654
|
-
.filter(q => q.match('address.street', 'Main'))
|
|
639
|
+
.filter((q) => q.term('address.country', 'US'))
|
|
640
|
+
.filter((q) => q.match('address.street', 'Main'))
|
|
655
641
|
.build();
|
|
656
642
|
|
|
657
|
-
// nested sub-fields — must use .nested() wrapper;
|
|
658
|
-
|
|
659
|
-
.nested('variants', q => q.term('color', 'black'))
|
|
643
|
+
// nested sub-fields — must use .nested() wrapper; field names are relative
|
|
644
|
+
queryBuilder(productMappings)
|
|
645
|
+
.nested('variants', (q) => q.term('color', 'black'))
|
|
660
646
|
.build();
|
|
661
647
|
|
|
662
|
-
|
|
663
|
-
.nested('variants', q => q.range('price', { lte: 150 }), { score_mode: 'min' })
|
|
648
|
+
queryBuilder(productMappings)
|
|
649
|
+
.nested('variants', (q) => q.range('price', { lte: 150 }), { score_mode: 'min' })
|
|
664
650
|
.build();
|
|
665
651
|
```
|
|
666
652
|
|
|
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
|
|
653
|
+
> **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.
|
|
668
654
|
|
|
669
|
-
|
|
655
|
+
#### Field Helpers
|
|
670
656
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
```
|
|
693
|
-
|
|
694
|
-
> **Why the difference?**
|
|
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.
|
|
696
|
-
|
|
697
|
-
**Field Helpers** (shorthand for common field types):
|
|
657
|
+
| Category | Helpers |
|
|
658
|
+
| ---------- | ------------------------------------------------------------------------------------------------- |
|
|
659
|
+
| Text | `text`, `keyword`, `constantKeyword`, `matchOnlyText`, `searchAsYouType`, `wildcardField` |
|
|
660
|
+
| Numeric | `long`, `integer`, `short`, `byte`, `double`, `float`, `halfFloat`, `scaledFloat`, `unsignedLong` |
|
|
661
|
+
| Date | `date`, `dateNanos` |
|
|
662
|
+
| Boolean | `boolean` |
|
|
663
|
+
| Binary | `binary` |
|
|
664
|
+
| IP | `ip` |
|
|
665
|
+
| Range | `integerRange`, `floatRange`, `longRange`, `doubleRange`, `dateRange`, `ipRange` |
|
|
666
|
+
| Objects | `object`, `nested`, `flattened` |
|
|
667
|
+
| Spatial | `geoPoint`, `geoShape` |
|
|
668
|
+
| Vector | `denseVector`, `quantizedDenseVector`, `sparseVector` |
|
|
669
|
+
| Rank | `rankFeature`, `rankFeatures` |
|
|
670
|
+
| Semantic | `semanticText` |
|
|
671
|
+
| Completion | `completion` |
|
|
672
|
+
| Special | `percolator`, `alias`, `tokenCount`, `murmur3Hash`, `join` |
|
|
698
673
|
|
|
699
674
|
```typescript
|
|
700
|
-
import { text, keyword, integer, float, double, date, boolean, denseVector, scaledFloat, halfFloat } from 'elasticlink';
|
|
701
|
-
|
|
702
675
|
// Shorthand — pass options or use defaults
|
|
703
|
-
keyword()
|
|
704
|
-
integer()
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
676
|
+
keyword(); // { type: 'keyword' }
|
|
677
|
+
integer(); // { type: 'integer' }
|
|
678
|
+
text({ analyzer: 'english' }); // { type: 'text', analyzer: 'english' }
|
|
679
|
+
denseVector({ dims: 384, index: true, similarity: 'cosine' });
|
|
680
|
+
|
|
681
|
+
// Multi-fields carry type info — dot-notation paths work in queries
|
|
682
|
+
const m = mappings({
|
|
683
|
+
name: text({ fields: { raw: keyword() } }),
|
|
684
|
+
price: float({ fields: { string: keyword() } }),
|
|
685
|
+
});
|
|
686
|
+
const qb = queryBuilder(m);
|
|
687
|
+
qb.term('name.raw', 'exact match'); // ✓ name.raw is a KeywordFields<M> path
|
|
688
|
+
qb.match('name', 'full text'); // ✓ name is a TextFields<M> path
|
|
709
689
|
```
|
|
710
690
|
|
|
711
|
-
|
|
691
|
+
See [`field.types.ts`](src/field.types.ts) for all field helper option types.
|
|
712
692
|
|
|
713
|
-
|
|
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
|
|
727
|
-
|
|
728
|
-
All field helpers accept an optional options object. Common options across types:
|
|
693
|
+
<details>
|
|
694
|
+
<summary><b>Mapping Properties Reference</b> (click to expand)</summary>
|
|
729
695
|
|
|
730
696
|
| Option | Types | Description |
|
|
731
697
|
| ------------------------------ | ---------------------- | ------------------------------------------------------------------------------------ |
|
|
@@ -742,7 +708,7 @@ All field helpers accept an optional options object. Common options across types
|
|
|
742
708
|
| `dims` | `denseVector` | Number of dimensions (required for KNN indexing) |
|
|
743
709
|
| `similarity` | `denseVector` | Similarity function: `'cosine'`, `'dot_product'`, `'l2_norm'`, `'max_inner_product'` |
|
|
744
710
|
| `element_type` | `denseVector` | Element type: `'float'` (default), `'byte'`, `'bit'` |
|
|
745
|
-
| `fields` | text, keyword
|
|
711
|
+
| `fields` | text, keyword, numeric, date | Multi-fields — type-safe dot-notation paths (e.g. `name.raw`) in query constraints |
|
|
746
712
|
| `properties` | object, nested | Sub-field mappings |
|
|
747
713
|
| `enabled` | `object` | Disable indexing of object fields |
|
|
748
714
|
| `path` | `alias` | Path to the target field |
|
|
@@ -751,423 +717,269 @@ All field helpers accept an optional options object. Common options across types
|
|
|
751
717
|
| `preserve_position_increments` | `completion` | Preserve position increments |
|
|
752
718
|
| `orientation` | `geoShape` | Default orientation for polygons |
|
|
753
719
|
|
|
754
|
-
|
|
720
|
+
</details>
|
|
721
|
+
|
|
722
|
+
<details>
|
|
723
|
+
<summary><b>Index Settings Reference</b> (click to expand)</summary>
|
|
755
724
|
|
|
756
725
|
The `.settings()` method accepts the full `IndicesIndexSettings` type from `@elastic/elasticsearch`. Common options:
|
|
757
726
|
|
|
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'`)
|
|
727
|
+
| Setting | Type | Description |
|
|
728
|
+
| -------------------- | -------- | ------------------------------------------------- |
|
|
729
|
+
| `number_of_shards` | `number` | Primary shard count (set at creation, immutable) |
|
|
730
|
+
| `number_of_replicas` | `number` | Replica count (can be changed after creation) |
|
|
731
|
+
| `refresh_interval` | `string` | How often to refresh (`'1s'`, `'-1s'` to disable) |
|
|
732
|
+
| `max_result_window` | `number` | Max `from + size` (default: 10000) |
|
|
733
|
+
| `analysis` | `object` | Custom analyzers, tokenizers, filters |
|
|
734
|
+
| `codec` | `string` | Compression codec (`'best_compression'`) |
|
|
766
735
|
|
|
767
|
-
|
|
736
|
+
</details>
|
|
737
|
+
|
|
738
|
+
<details>
|
|
739
|
+
<summary><b>Alias Options Reference</b> (click to expand)</summary>
|
|
768
740
|
|
|
769
741
|
The `.alias()` method accepts an optional `IndicesAlias` object:
|
|
770
742
|
|
|
771
|
-
| Option
|
|
772
|
-
|
|
|
773
|
-
| `filter`
|
|
774
|
-
| `is_write_index`
|
|
775
|
-
| `routing`
|
|
776
|
-
| `index_routing`
|
|
777
|
-
| `search_routing`
|
|
778
|
-
| `is_hidden`
|
|
743
|
+
| Option | Type | Description |
|
|
744
|
+
| ---------------- | --------- | ------------------------------------------------------------ |
|
|
745
|
+
| `filter` | `object` | Query filter — only matching documents visible through alias |
|
|
746
|
+
| `is_write_index` | `boolean` | Designate this index as the write target for the alias |
|
|
747
|
+
| `routing` | `string` | Custom routing value for alias operations |
|
|
748
|
+
| `index_routing` | `string` | Routing value for index operations only |
|
|
749
|
+
| `search_routing` | `string` | Routing value for search operations only |
|
|
750
|
+
| `is_hidden` | `boolean` | Hide alias from wildcard expressions |
|
|
779
751
|
|
|
780
|
-
|
|
752
|
+
</details>
|
|
781
753
|
|
|
782
|
-
|
|
754
|
+
### Suggesters & Autocomplete
|
|
755
|
+
|
|
756
|
+
`suggest(schema)` creates a standalone suggester builder. Suggesters can also be attached inline via `queryBuilder().suggest(fn)`.
|
|
783
757
|
|
|
784
758
|
```typescript
|
|
785
|
-
import {
|
|
759
|
+
import { queryBuilder, suggest, mappings, text, keyword, completion } from 'elasticlink';
|
|
786
760
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
761
|
+
const searchableMappings = mappings({
|
|
762
|
+
name: text(),
|
|
763
|
+
description: text(),
|
|
764
|
+
suggest_field: completion()
|
|
765
|
+
});
|
|
792
766
|
|
|
793
|
-
//
|
|
794
|
-
const
|
|
795
|
-
.
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
767
|
+
// Standalone suggest
|
|
768
|
+
const autocomplete = suggest(searchableMappings)
|
|
769
|
+
.completion('autocomplete', 'lap', {
|
|
770
|
+
field: 'suggest_field',
|
|
771
|
+
size: 10,
|
|
772
|
+
skip_duplicates: true,
|
|
773
|
+
fuzzy: { fuzziness: 'AUTO', min_length: 3, prefix_length: 1 }
|
|
799
774
|
})
|
|
800
775
|
.build();
|
|
801
776
|
|
|
802
|
-
//
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
777
|
+
// Inline with query — search + spell-check in one request
|
|
778
|
+
const searchWithSuggestions = queryBuilder(searchableMappings)
|
|
779
|
+
.match('name', 'laptpo')
|
|
780
|
+
.suggest((s) =>
|
|
781
|
+
s
|
|
782
|
+
.completion('autocomplete', 'lap', { field: 'suggest_field', size: 5 })
|
|
783
|
+
.term('spelling', 'laptpo', { field: 'name', size: 3, suggest_mode: 'popular' })
|
|
784
|
+
)
|
|
785
|
+
.size(20)
|
|
786
|
+
.build();
|
|
810
787
|
```
|
|
811
788
|
|
|
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>`.
|
|
819
|
-
|
|
820
|
-
## Examples
|
|
789
|
+
| Method | Description |
|
|
790
|
+
| ----------------------------------- | ------------------------------------------------------------------- |
|
|
791
|
+
| `term(name, text, options)` | Suggest corrections for individual terms based on edit distance |
|
|
792
|
+
| `phrase(name, text, options)` | Suggest corrections for entire phrases using n-gram language models |
|
|
793
|
+
| `completion(name, prefix, options)` | Fast prefix-based autocomplete (requires `completion` field type) |
|
|
794
|
+
| `build()` | Returns `{ suggest: ... }` object |
|
|
821
795
|
|
|
822
|
-
|
|
796
|
+
See [`suggester.types.ts`](src/suggester.types.ts) for complete option types (`TermSuggesterOptions`, `PhraseSuggesterOptions`, `CompletionSuggesterOptions`).
|
|
823
797
|
|
|
824
|
-
###
|
|
798
|
+
### Multi-Search
|
|
825
799
|
|
|
826
|
-
|
|
800
|
+
`msearch(schema)` batches multiple search requests into a single API call using NDJSON format.
|
|
827
801
|
|
|
828
802
|
```typescript
|
|
829
|
-
|
|
830
|
-
name: text(),
|
|
831
|
-
description: text(),
|
|
832
|
-
category: keyword(),
|
|
833
|
-
price: float(),
|
|
834
|
-
tags: keyword(),
|
|
835
|
-
in_stock: boolean(),
|
|
836
|
-
});
|
|
803
|
+
import { queryBuilder, msearch } from 'elasticlink';
|
|
837
804
|
|
|
838
|
-
const
|
|
839
|
-
const category = 'electronics';
|
|
840
|
-
const minPrice = 800;
|
|
841
|
-
const maxPrice = 2000;
|
|
805
|
+
const laptopQuery = queryBuilder(productMappings).match('name', 'laptop').range('price', { gte: 500, lte: 2000 }).build();
|
|
842
806
|
|
|
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')
|
|
807
|
+
const phoneQuery = queryBuilder(productMappings).match('name', 'smartphone').range('price', { gte: 300, lte: 1000 }).build();
|
|
808
|
+
|
|
809
|
+
// NDJSON string for Elasticsearch _msearch endpoint
|
|
810
|
+
const ndjson = msearch(productMappings)
|
|
811
|
+
.addQuery(laptopQuery, { index: 'products', preference: '_local' })
|
|
812
|
+
.addQuery(phoneQuery, { index: 'products', preference: '_local' })
|
|
872
813
|
.build();
|
|
814
|
+
|
|
815
|
+
// Or as an array of objects
|
|
816
|
+
const array = msearch(productMappings)
|
|
817
|
+
.addQuery(laptopQuery, { index: 'products' })
|
|
818
|
+
.addQuery(phoneQuery, { index: 'products' })
|
|
819
|
+
.buildArray();
|
|
873
820
|
```
|
|
874
821
|
|
|
875
|
-
|
|
822
|
+
| Method | Description |
|
|
823
|
+
| ------------------------------ | -------------------------------------- |
|
|
824
|
+
| `add(request)` | Add a raw request (header + body) |
|
|
825
|
+
| `addQuery(body, header?)` | Add a built query with optional header |
|
|
826
|
+
| `addQueryBuilder(qb, header?)` | Add a QueryBuilder instance directly |
|
|
827
|
+
| `withParams(params)` | Set shared request parameters |
|
|
828
|
+
| `build()` | Returns NDJSON string |
|
|
829
|
+
| `buildArray()` | Returns array of header/body pairs |
|
|
830
|
+
| `buildParams()` | Returns shared request parameters |
|
|
876
831
|
|
|
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
|
-
```
|
|
832
|
+
Header options: `index`, `routing`, `preference`, `search_type`. See [`multi-search.types.ts`](src/multi-search.types.ts) for full types.
|
|
912
833
|
|
|
913
|
-
###
|
|
834
|
+
### Bulk Operations
|
|
914
835
|
|
|
915
|
-
|
|
836
|
+
`bulk(schema)` batches create, index, update, and delete operations efficiently.
|
|
916
837
|
|
|
917
838
|
```typescript
|
|
918
|
-
|
|
839
|
+
import { bulk, mappings, keyword, text, float } from 'elasticlink';
|
|
840
|
+
|
|
841
|
+
const productMappings = mappings({
|
|
842
|
+
id: keyword(),
|
|
919
843
|
name: text(),
|
|
920
|
-
asset_class: keyword(),
|
|
921
|
-
sector: keyword(),
|
|
922
844
|
price: float(),
|
|
923
|
-
|
|
924
|
-
listed_date: date(),
|
|
845
|
+
category: keyword()
|
|
925
846
|
});
|
|
926
847
|
|
|
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)
|
|
848
|
+
const bulkOp = bulk(productMappings)
|
|
849
|
+
.index({ id: '1', name: 'Laptop Pro', price: 1299, category: 'electronics' }, { _index: 'products', _id: '1' })
|
|
850
|
+
.create({ id: '2', name: 'Wireless Mouse', price: 29, category: 'accessories' }, { _index: 'products', _id: '2' })
|
|
851
|
+
.update({ _index: 'products', _id: '3', doc: { price: 999 } })
|
|
852
|
+
.delete({ _index: 'products', _id: '4' })
|
|
942
853
|
.build();
|
|
943
|
-
```
|
|
944
|
-
|
|
945
|
-
Produces:
|
|
946
854
|
|
|
947
|
-
|
|
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
|
-
}
|
|
855
|
+
// POST /_bulk with Content-Type: application/x-ndjson
|
|
974
856
|
```
|
|
975
857
|
|
|
976
|
-
|
|
858
|
+
| Method | Description |
|
|
859
|
+
| -------------------- | ------------------------------------------------------------------------------ |
|
|
860
|
+
| `index(doc, meta?)` | Index a document (create or replace) |
|
|
861
|
+
| `create(doc, meta?)` | Create a document (fail if exists) |
|
|
862
|
+
| `update(meta)` | Update with `doc`, `script`, `upsert`, `doc_as_upsert`, or `retry_on_conflict` |
|
|
863
|
+
| `delete(meta)` | Delete a document |
|
|
864
|
+
| `build()` | Returns NDJSON string |
|
|
865
|
+
| `buildArray()` | Returns array of action/document pairs |
|
|
977
866
|
|
|
978
|
-
|
|
867
|
+
### Vector Search & Semantic Search
|
|
868
|
+
|
|
869
|
+
KNN (k-nearest neighbors) queries enable semantic search using vector embeddings.
|
|
979
870
|
|
|
980
871
|
```typescript
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
872
|
+
import { queryBuilder, mappings, text, keyword, float, denseVector } from 'elasticlink';
|
|
873
|
+
|
|
874
|
+
const productWithEmbeddingMappings = mappings({
|
|
875
|
+
name: text(),
|
|
876
|
+
description: text(),
|
|
877
|
+
price: float(),
|
|
878
|
+
category: keyword(),
|
|
879
|
+
embedding: denseVector({ dims: 384 })
|
|
985
880
|
});
|
|
986
881
|
|
|
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();
|
|
882
|
+
const searchEmbedding = [0.23, 0.45, 0.67, 0.12, 0.89]; // From your ML model
|
|
994
883
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
.
|
|
998
|
-
|
|
999
|
-
|
|
884
|
+
// Basic semantic search
|
|
885
|
+
const result = queryBuilder(productWithEmbeddingMappings)
|
|
886
|
+
.knn('embedding', searchEmbedding, {
|
|
887
|
+
k: 10,
|
|
888
|
+
num_candidates: 100
|
|
889
|
+
})
|
|
890
|
+
.size(10)
|
|
1000
891
|
.build();
|
|
1001
892
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
.
|
|
893
|
+
// Hybrid search — KNN + filters + aggregations
|
|
894
|
+
const hybridSearch = queryBuilder(productWithEmbeddingMappings)
|
|
895
|
+
.knn('embedding', searchEmbedding, {
|
|
896
|
+
k: 100,
|
|
897
|
+
num_candidates: 1000,
|
|
898
|
+
filter: { term: { category: 'electronics' } },
|
|
899
|
+
boost: 1.2,
|
|
900
|
+
similarity: 0.7
|
|
901
|
+
})
|
|
902
|
+
.aggs((agg) => agg.terms('categories', 'category', { size: 10 }))
|
|
903
|
+
.size(20)
|
|
1005
904
|
.build();
|
|
1006
905
|
```
|
|
1007
906
|
|
|
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}
|
|
907
|
+
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
908
|
|
|
1016
|
-
|
|
909
|
+
### Script Queries & Custom Scoring
|
|
1017
910
|
|
|
1018
|
-
|
|
911
|
+
Script-based filtering and custom scoring for advanced relevance tuning.
|
|
1019
912
|
|
|
1020
913
|
```typescript
|
|
1021
|
-
|
|
914
|
+
import { queryBuilder, mappings, text, float, long } from 'elasticlink';
|
|
915
|
+
|
|
916
|
+
const scoredProductMappings = mappings({
|
|
1022
917
|
name: text(),
|
|
1023
|
-
|
|
1024
|
-
|
|
918
|
+
price: float(),
|
|
919
|
+
popularity: long()
|
|
1025
920
|
});
|
|
1026
921
|
|
|
1027
|
-
//
|
|
1028
|
-
const
|
|
1029
|
-
.
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
922
|
+
// Script-based filtering
|
|
923
|
+
const filtered = queryBuilder(scoredProductMappings)
|
|
924
|
+
.bool()
|
|
925
|
+
.must((q) => q.match('name', 'laptop'))
|
|
926
|
+
.filter((q) =>
|
|
927
|
+
q.script({
|
|
928
|
+
source: "doc['price'].value > params.threshold",
|
|
929
|
+
params: { threshold: 500 }
|
|
930
|
+
})
|
|
1033
931
|
)
|
|
1034
|
-
.size(0)
|
|
1035
932
|
.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
933
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
location: geoPoint(),
|
|
1080
|
-
rating: float(),
|
|
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)
|
|
934
|
+
// Custom scoring with script_score
|
|
935
|
+
const customScored = queryBuilder(scoredProductMappings)
|
|
936
|
+
.scriptScore((q) => q.match('name', 'smartphone'), {
|
|
937
|
+
source: "_score * Math.log(2 + doc['popularity'].value)",
|
|
938
|
+
lang: 'painless'
|
|
939
|
+
})
|
|
1091
940
|
.size(20)
|
|
1092
941
|
.build();
|
|
1093
942
|
```
|
|
1094
943
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
elasticlink provides mapping-aware TypeScript safety:
|
|
944
|
+
### Percolate Queries
|
|
1098
945
|
|
|
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
|
|
946
|
+
Percolate queries enable reverse search — match documents against stored queries.
|
|
1103
947
|
|
|
1104
948
|
```typescript
|
|
1105
|
-
import {
|
|
949
|
+
import { queryBuilder, mappings, keyword, percolator } from 'elasticlink';
|
|
1106
950
|
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
951
|
+
const alertRuleMappings = mappings({
|
|
952
|
+
query: percolator(),
|
|
953
|
+
name: keyword(),
|
|
954
|
+
severity: keyword()
|
|
1111
955
|
});
|
|
1112
956
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
957
|
+
const alerts = queryBuilder(alertRuleMappings)
|
|
958
|
+
.percolate({
|
|
959
|
+
field: 'query',
|
|
960
|
+
document: {
|
|
961
|
+
level: 'ERROR',
|
|
962
|
+
message: 'Database connection failed',
|
|
963
|
+
timestamp: '2024-01-15T10:30:00Z'
|
|
964
|
+
}
|
|
965
|
+
})
|
|
966
|
+
.size(100)
|
|
967
|
+
.build();
|
|
1124
968
|
```
|
|
1125
969
|
|
|
1126
|
-
|
|
970
|
+
**Common use cases:** alerting (match events against rules), content classification, saved search notifications, and metric threshold monitoring.
|
|
1127
971
|
|
|
1128
|
-
|
|
1129
|
-
# Run tests
|
|
1130
|
-
npm test
|
|
1131
|
-
|
|
1132
|
-
# Watch mode
|
|
1133
|
-
npm test:watch
|
|
1134
|
-
|
|
1135
|
-
# Coverage report
|
|
1136
|
-
npm test:coverage
|
|
972
|
+
## Compatibility
|
|
1137
973
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
974
|
+
| elasticlink | Node.js | Elasticsearch |
|
|
975
|
+
| ------------ | ---------- | ------------- |
|
|
976
|
+
| 1.0.0-beta.2 | 20, 22, 24 | 9.x (≥9.0.0) |
|
|
1141
977
|
|
|
1142
|
-
|
|
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
|
|
978
|
+
Tested against the versions listed. Peer dependency is `@elastic/elasticsearch >=9.0.0`.
|
|
1167
979
|
|
|
1168
980
|
## Development
|
|
1169
981
|
|
|
1170
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and code style.
|
|
982
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, design principles, and code style.
|
|
1171
983
|
|
|
1172
984
|
## License
|
|
1173
985
|
|