endee 1.7.1 → 1.8.0-dev.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 +537 -384
- package/dist/collection.d.ts +83 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +724 -0
- package/dist/collection.js.map +1 -0
- package/dist/constants.d.ts +38 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +38 -0
- package/dist/constants.js.map +1 -0
- package/dist/endee.d.ts +93 -0
- package/dist/endee.d.ts.map +1 -0
- package/dist/endee.js +316 -0
- package/dist/endee.js.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/reranker.d.ts +32 -0
- package/dist/reranker.d.ts.map +1 -0
- package/dist/reranker.js +96 -0
- package/dist/reranker.js.map +1 -0
- package/dist/types/index.d.ts +126 -85
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/endee.class.d.ts +0 -18
- package/dist/endee.class.d.ts.map +0 -1
- package/dist/endee.class.js +0 -279
- package/dist/endee.class.js.map +0 -1
- package/dist/index.class.d.ts +0 -32
- package/dist/index.class.d.ts.map +0 -1
- package/dist/index.class.js +0 -391
- package/dist/index.class.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,545 +1,698 @@
|
|
|
1
|
-
# Endee
|
|
2
|
-
|
|
3
|
-
Endee is a
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
# Endee — TypeScript / JavaScript Client
|
|
2
|
+
|
|
3
|
+
Endee is a high-performance C++ vector database for AI search, RAG, and hybrid
|
|
4
|
+
retrieval. A **collection** holds objects, and each object can carry values for
|
|
5
|
+
several **named, typed fields** at once — dense vectors, sparse vectors, and
|
|
6
|
+
multi-vectors — so a single object can be searched many ways.
|
|
7
|
+
|
|
8
|
+
- **Multi-field objects** — one object, many vector fields (dense + sparse + multi-vector).
|
|
9
|
+
- **Hybrid search** — query any subset of fields; `search()` returns per-field results, which you can fuse with **`rerank()`** (RRF).
|
|
10
|
+
- **Client-side normalization** — cosine vectors are L2-normalized for you.
|
|
11
|
+
- **Full control plane** — collections, objects, filters, rebuild/shrink, backups, databases, and tokens.
|
|
12
|
+
- **TypeScript first** — full type definitions and IntelliSense for every call.
|
|
13
|
+
- **ES modules** — native ESM with proper tree-shaking.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Table of contents
|
|
18
|
+
|
|
19
|
+
1. [Install](#install)
|
|
20
|
+
2. [Quick start](#quick-start)
|
|
21
|
+
3. [Connecting & tokens](#connecting--tokens)
|
|
22
|
+
4. [Create a collection](#create-a-collection)
|
|
23
|
+
5. [Insert objects (combining vector types)](#insert-objects-combining-vector-types)
|
|
24
|
+
6. [Search](#search)
|
|
25
|
+
- [The unified field shape](#the-unified-field-shape)
|
|
26
|
+
- [Per-field `weight` and `ef_search`](#per-field-weight-and-ef_search)
|
|
27
|
+
- [Single field](#single-field-search)
|
|
28
|
+
- [Multi-field](#multi-field-search)
|
|
29
|
+
- [Fusing results with `rerank()`](#fusing-results-with-rerank)
|
|
30
|
+
- [Filters](#filtered-search)
|
|
31
|
+
7. [Object operations](#object-operations)
|
|
32
|
+
8. [Collection maintenance](#collection-maintenance)
|
|
33
|
+
9. [Backups](#backups)
|
|
34
|
+
10. [Database administration (root token)](#database-administration-root-token)
|
|
35
|
+
11. [Reference](#reference)
|
|
36
|
+
12. [Error handling](#error-handling)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Install
|
|
21
41
|
|
|
22
42
|
```bash
|
|
23
43
|
npm install endee
|
|
24
44
|
```
|
|
25
45
|
|
|
26
|
-
|
|
46
|
+
Requires Node.js ≥ 18. Depends on `axios` and `@msgpack/msgpack`.
|
|
27
47
|
|
|
28
|
-
|
|
48
|
+
---
|
|
29
49
|
|
|
30
|
-
|
|
50
|
+
## Quick start
|
|
31
51
|
|
|
32
52
|
```typescript
|
|
33
|
-
import { Endee
|
|
53
|
+
import { Endee } from 'endee';
|
|
54
|
+
|
|
55
|
+
// A "db token" (db_name:secret) scopes you to one database.
|
|
56
|
+
const client = new Endee('my_db:xxxxxxxx');
|
|
57
|
+
client.setBaseUrl('http://localhost:8080/api/v2'); // include /api/v2
|
|
58
|
+
|
|
59
|
+
// 1. Create a collection with one of each field type.
|
|
60
|
+
await client.createCollection({
|
|
61
|
+
name: 'products',
|
|
62
|
+
fields: [
|
|
63
|
+
{
|
|
64
|
+
name: 'embedding',
|
|
65
|
+
type: 'vector',
|
|
66
|
+
params: { dimension: 8, space_type: 'cosine', precision: 'int8' },
|
|
67
|
+
},
|
|
68
|
+
{ name: 'keywords', type: 'sparse', sparse_model: 'default' },
|
|
69
|
+
{
|
|
70
|
+
name: 'colbert',
|
|
71
|
+
type: 'multi_vector',
|
|
72
|
+
params: { dimension: 8, space_type: 'cosine', precision: 'int8', pooling: 'mean' },
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
});
|
|
34
76
|
|
|
35
|
-
|
|
36
|
-
const client = new Endee();
|
|
37
|
-
```
|
|
77
|
+
const collection = await client.getCollection('products');
|
|
38
78
|
|
|
39
|
-
|
|
79
|
+
// 2. Insert an object carrying ALL THREE field types at once.
|
|
80
|
+
await collection.upsert([
|
|
81
|
+
{
|
|
82
|
+
id: 'p1',
|
|
83
|
+
meta: { name: 'Wireless Headphones', price: 99 },
|
|
84
|
+
filter: { category: 'electronics' },
|
|
85
|
+
fields: {
|
|
86
|
+
embedding: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8],
|
|
87
|
+
keywords: { indices: [3, 17, 42], values: [0.9, 0.5, 0.2] },
|
|
88
|
+
colbert: [Array(8).fill(0.1), Array(8).fill(0.2)],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
]);
|
|
40
92
|
|
|
41
|
-
|
|
42
|
-
const
|
|
93
|
+
// 3. Search the dense field. `results` is keyed by field name.
|
|
94
|
+
const res = await collection.search({
|
|
95
|
+
fields: { embedding: { query: Array(8).fill(0.2) } },
|
|
96
|
+
limit: 5,
|
|
97
|
+
});
|
|
98
|
+
for (const h of res.results.embedding) {
|
|
99
|
+
console.log(h.id, h.similarity, h.meta);
|
|
100
|
+
}
|
|
43
101
|
```
|
|
44
102
|
|
|
45
|
-
|
|
103
|
+
---
|
|
46
104
|
|
|
47
|
-
|
|
105
|
+
## Connecting & tokens
|
|
48
106
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// Set custom base URL for non-default port
|
|
53
|
-
client.setBaseUrl('http://0.0.0.0:8081/api/v1');
|
|
54
|
-
```
|
|
107
|
+
**Authentication is always required.** The server has no anonymous mode — every
|
|
108
|
+
request must carry a token, or it is rejected with **401 Unauthorized**.
|
|
55
109
|
|
|
56
|
-
|
|
110
|
+
A **db token** has the form `db_name:secret` and scopes you to all collection /
|
|
111
|
+
object / search work inside one database. (Database administration and minting
|
|
112
|
+
tokens require the server's **root token** — those control-plane operations are
|
|
113
|
+
not yet exposed in the JS client; see [Reference](#reference).)
|
|
57
114
|
|
|
58
115
|
```typescript
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
await client.createIndex({
|
|
62
|
-
name: 'my_vectors',
|
|
63
|
-
dimension: 384,
|
|
64
|
-
spaceType: 'cosine',
|
|
65
|
-
precision: Precision.INT16,
|
|
66
|
-
});
|
|
116
|
+
const client = new Endee('my_db:xxxxxxxx');
|
|
117
|
+
client.setBaseUrl('http://localhost:8080/api/v2'); // must point at the /api/v2 root
|
|
67
118
|
```
|
|
68
119
|
|
|
69
|
-
|
|
120
|
+
- A token of the form `db_name:secret:region` is parsed automatically — the
|
|
121
|
+
client targets `https://<region>.endee.io/api/v2` and strips the region from
|
|
122
|
+
the token it sends.
|
|
123
|
+
- `setBaseUrl` must point at the `/api/v2` root of your server.
|
|
124
|
+
- A missing/invalid token → `AuthenticationException` (401); a read-only token
|
|
125
|
+
on a write → `ForbiddenException` (403).
|
|
126
|
+
|
|
127
|
+
---
|
|
70
128
|
|
|
71
|
-
|
|
72
|
-
|-----------|-------------|
|
|
73
|
-
| `name` | Unique name for your index |
|
|
74
|
-
| `dimension` | Vector dimensionality (must match your embedding model's output) |
|
|
75
|
-
| `spaceType` | Distance metric - `"cosine"`, `"l2"`, or `"ip"` (inner product) |
|
|
76
|
-
| `M` | Graph connectivity - higher values increase recall but use more memory (default: 16) |
|
|
77
|
-
| `efCon` | Construction-time parameter - higher values improve index quality (default: 128) |
|
|
78
|
-
| `precision` | Quantization precision (default: `Precision.INT16`) |
|
|
129
|
+
## Create a collection
|
|
79
130
|
|
|
80
|
-
|
|
131
|
+
A collection is a set of **named, typed fields**. Each field is a plain object
|
|
132
|
+
sent to the server as-is.
|
|
81
133
|
|
|
82
|
-
|
|
134
|
+
| Field type | Required keys | `params` |
|
|
135
|
+
|------------|---------------|----------|
|
|
136
|
+
| `vector` | `name`, `type` | `dimension`, `space_type`, `precision` (+ optional `M`, `ef_con`) |
|
|
137
|
+
| `sparse` | `name`, `type`, `sparse_model` | — (no `params`) |
|
|
138
|
+
| `multi_vector` | `name`, `type` | `dimension`, `space_type`, `precision`, **`pooling`** (`"mean"`/`"max"`) (+ optional `M`, `ef_con`) |
|
|
83
139
|
|
|
84
140
|
```typescript
|
|
85
|
-
await client.
|
|
86
|
-
name: '
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
141
|
+
await client.createCollection({
|
|
142
|
+
name: 'products',
|
|
143
|
+
fields: [
|
|
144
|
+
// Dense embedding (single vector per object)
|
|
145
|
+
{
|
|
146
|
+
name: 'embedding',
|
|
147
|
+
type: 'vector',
|
|
148
|
+
params: { dimension: 768, space_type: 'cosine', precision: 'int8' },
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
// Sparse / keyword field (BM25-style)
|
|
152
|
+
{ name: 'keywords', type: 'sparse', sparse_model: 'default' },
|
|
153
|
+
|
|
154
|
+
// Multi-vector field (many vectors per object)
|
|
155
|
+
{
|
|
156
|
+
name: 'colbert',
|
|
157
|
+
type: 'multi_vector',
|
|
158
|
+
params: { dimension: 768, space_type: 'cosine', precision: 'int8', pooling: 'mean' },
|
|
159
|
+
},
|
|
160
|
+
],
|
|
91
161
|
});
|
|
92
162
|
```
|
|
93
163
|
|
|
94
|
-
|
|
164
|
+
- `space_type`: `cosine` (default), `l2`, `ip`.
|
|
165
|
+
- `precision`: `float32`, `float16`, `int16` (default), `int8`, `int8e`, `binary`.
|
|
166
|
+
- `M` / `ef_con`: HNSW build params (optional; sensible defaults applied).
|
|
167
|
+
- **Cosine vectors are L2-normalized by the client** before sending.
|
|
168
|
+
|
|
169
|
+
Other client-level collection calls:
|
|
95
170
|
|
|
96
171
|
```typescript
|
|
97
|
-
//
|
|
98
|
-
const
|
|
172
|
+
await client.listCollections(); // [{ name: "products", ... }, ...]
|
|
173
|
+
const collection = await client.getCollection('products');
|
|
174
|
+
await collection.describe(); // { name, fields, created_at, layout_version }
|
|
175
|
+
await client.deleteCollection('products');
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
99
179
|
|
|
100
|
-
|
|
101
|
-
const index = await client.getIndex('my_vectors');
|
|
180
|
+
## Insert objects (combining vector types)
|
|
102
181
|
|
|
103
|
-
|
|
104
|
-
|
|
182
|
+
`collection.upsert(objects)` takes an array of objects. Each object is:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
{
|
|
186
|
+
id: '<unique string id>',
|
|
187
|
+
meta?: { /* ... */ }, // arbitrary JSON, returned in search results (optional)
|
|
188
|
+
filter?: { /* ... */ }, // tag object used by filtered search (optional)
|
|
189
|
+
fields?: { // field_name -> value, ONE entry per field you set
|
|
190
|
+
'<dense_field>': [/* float, ... */], // dense → array
|
|
191
|
+
'<sparse_field>': { indices: [/* ... */], values: [/* ... */] }, // sparse → object
|
|
192
|
+
'<multi_field>': [[/* float, ... */], [/* float, ... */]], // multi → array of arrays
|
|
193
|
+
},
|
|
194
|
+
}
|
|
105
195
|
```
|
|
106
196
|
|
|
107
|
-
|
|
197
|
+
Per-field value formats:
|
|
108
198
|
|
|
109
|
-
|
|
199
|
+
| Field type | Value format |
|
|
200
|
+
|------------|--------------|
|
|
201
|
+
| `vector` | `[0.1, 0.2, ...]` — a flat array of floats |
|
|
202
|
+
| `sparse` | `{ indices: [3, 17], values: [0.9, 0.5] }` (also accepts `sparse_indices`/`sparse_values`) |
|
|
203
|
+
| `multi_vector` | `[[...], [...], ...]` — an array of equal-length float arrays |
|
|
110
204
|
|
|
111
|
-
|
|
112
|
-
|
|
205
|
+
### One object, multiple field types
|
|
206
|
+
|
|
207
|
+
A single object can populate **any combination** of the collection's fields in
|
|
208
|
+
one call. They're stored together under the same id and can be searched
|
|
209
|
+
independently or together:
|
|
113
210
|
|
|
114
|
-
|
|
211
|
+
```typescript
|
|
212
|
+
await collection.upsert([
|
|
115
213
|
{
|
|
116
|
-
id: '
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
214
|
+
id: 'p1',
|
|
215
|
+
meta: { name: 'Wireless Headphones', price: 99 },
|
|
216
|
+
filter: { category: 'electronics' },
|
|
217
|
+
fields: {
|
|
218
|
+
embedding: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8], // dense
|
|
219
|
+
keywords: { indices: [3, 17, 42], values: [0.9, 0.5, 0.2] }, // sparse
|
|
220
|
+
colbert: [
|
|
221
|
+
// multi-vector
|
|
222
|
+
[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8],
|
|
223
|
+
[0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9],
|
|
224
|
+
],
|
|
225
|
+
},
|
|
120
226
|
},
|
|
121
227
|
{
|
|
122
|
-
id: '
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
228
|
+
id: 'p2',
|
|
229
|
+
meta: { name: 'Running Shoes' },
|
|
230
|
+
filter: { category: 'footwear' },
|
|
231
|
+
fields: {
|
|
232
|
+
// An object may set only SOME fields — here just dense + sparse.
|
|
233
|
+
embedding: [0.5, 0.4, 0.3, 0.2, 0.1, 0.05, 0.02, 0.01],
|
|
234
|
+
keywords: { indices: [5, 17, 90], values: [0.7, 0.6, 0.1] },
|
|
235
|
+
},
|
|
126
236
|
},
|
|
127
237
|
]);
|
|
238
|
+
// → { upserted: 2 }
|
|
128
239
|
```
|
|
129
240
|
|
|
130
|
-
|
|
241
|
+
Notes:
|
|
131
242
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
243
|
+
- You don't have to fill every field on every object — set whatever subset you have.
|
|
244
|
+
- `upsert` is insert-or-replace by `id`.
|
|
245
|
+
- Cosine `vector`/`multi_vector` values are normalized client-side before sending;
|
|
246
|
+
the original norm is stored so `getObjects` returns the original (pre-normalization) vectors.
|
|
247
|
+
- Validation is done client-side (fails fast): batch size ≤ 10,000, no duplicate
|
|
248
|
+
ids in a batch, dense/multi dims must match the field, sparse `indices`/`values`
|
|
249
|
+
lengths must match, filter keys ≤ 128 bytes / string values ≤ 1024 bytes.
|
|
138
250
|
|
|
139
|
-
|
|
251
|
+
---
|
|
140
252
|
|
|
141
|
-
|
|
253
|
+
## Search
|
|
142
254
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
ef: 128,
|
|
148
|
-
includeVectors: true,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
for (const item of results) {
|
|
152
|
-
console.log(`ID: ${item.id}, Similarity: ${item.similarity}`);
|
|
153
|
-
}
|
|
154
|
-
```
|
|
255
|
+
`collection.search({ fields, limit, filter })` queries one or more fields **in a
|
|
256
|
+
single request** and **always returns one ranked list per field**, keyed by field
|
|
257
|
+
name. To collapse those lists into a single ranked list, fuse them with
|
|
258
|
+
[`rerank()`](#fusing-results-with-rerank).
|
|
155
259
|
|
|
156
|
-
|
|
260
|
+
### The unified field shape
|
|
157
261
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
| `topK` | Number of results to return (default: 10, max: 512) |
|
|
162
|
-
| `ef` | Search quality parameter (default: 128, max: 1024) |
|
|
163
|
-
| `includeVectors` | Include vector data in results (default: false) |
|
|
164
|
-
| `filter` | Optional filter criteria (array of filter objects) |
|
|
165
|
-
| `sparseIndices` | Sparse vector indices for hybrid search (default: null) |
|
|
166
|
-
| `sparseValues` | Sparse vector values for hybrid search (default: null) |
|
|
167
|
-
| `prefilterCardinalityThreshold` | Controls when search switches from HNSW filtered search to brute-force prefiltering on the matched subset (default: 10,000, range: 1,000–1,000,000). See [Filter Tuning](#filter-tuning) for details. |
|
|
168
|
-
| `filterBoostPercentage` | Expands the internal HNSW candidate pool by this percentage when a filter is active, compensating for filtered-out results (default: 0, range: 0–100). See [Filter Tuning](#filter-tuning) for details. |
|
|
262
|
+
Every field is queried with the **same object form** — `{ query, weight?, ef_search? }`.
|
|
263
|
+
There is no bare-value shorthand; `query` is required. The `query` value matches
|
|
264
|
+
the field's type:
|
|
169
265
|
|
|
170
|
-
|
|
266
|
+
| Field type | `query` shape |
|
|
267
|
+
|------------|-------------|
|
|
268
|
+
| `vector` | `[0.2, 0.2, ...]` |
|
|
269
|
+
| `sparse` | `{ indices: [3, 17], values: [0.8, 0.4] }` |
|
|
270
|
+
| `multi_vector` | `[[...], [...]]` |
|
|
171
271
|
|
|
172
|
-
|
|
272
|
+
Each result **hit** is:
|
|
173
273
|
|
|
174
274
|
```typescript
|
|
175
|
-
|
|
176
|
-
vector: [0.15, 0.25 /* ... */],
|
|
177
|
-
topK: 5,
|
|
178
|
-
filter: [
|
|
179
|
-
{ category: { $eq: 'tech' } },
|
|
180
|
-
{ score: { $range: [80, 100] } },
|
|
181
|
-
],
|
|
182
|
-
});
|
|
275
|
+
{ id: 'p1', similarity: 0.94, meta: { /* ... */ }, filter: { /* ... */ } }
|
|
183
276
|
```
|
|
184
277
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
| Operator | Description | Example |
|
|
188
|
-
|----------|-------------|---------|
|
|
189
|
-
| `$eq` | Exact match | `{ status: { $eq: 'published' } }` |
|
|
190
|
-
| `$in` | Match any in list | `{ tags: { $in: ['ai', 'ml'] } }` |
|
|
191
|
-
| `$range` | Numeric range (inclusive) | `{ score: { $range: [70, 95] } }` |
|
|
192
|
-
|
|
193
|
-
> **Note:** The `$range` operator supports values within **[0 - 999]**. Normalize larger values before upserting.
|
|
278
|
+
> Search results carry `meta`/`filter` but **not** the stored vectors. To get the
|
|
279
|
+
> vectors back, use [`getObjects`](#object-operations).
|
|
194
280
|
|
|
195
|
-
###
|
|
281
|
+
### Per-field `weight` and `ef_search`
|
|
196
282
|
|
|
197
|
-
|
|
283
|
+
`weight` is the **fraction of the request `limit` to draw from that field**: the
|
|
284
|
+
effective per-field limit is `ceil(weight * limit)`. Omit `weight` to use the full
|
|
285
|
+
`limit`. `ef_search` tunes that field's HNSW recall/latency.
|
|
198
286
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
287
|
+
```typescript
|
|
288
|
+
// limit 50 → embedding fetches ceil(0.7*50)=35, keywords fetches ceil(0.3*50)=15
|
|
289
|
+
await collection.search({
|
|
290
|
+
fields: {
|
|
291
|
+
embedding: { query: Array(8).fill(0.2), weight: 0.7, ef_search: 256 },
|
|
292
|
+
keywords: { query: { indices: [3, 17], values: [0.8, 0.4] }, weight: 0.3 },
|
|
293
|
+
},
|
|
294
|
+
limit: 50,
|
|
295
|
+
});
|
|
296
|
+
```
|
|
202
297
|
|
|
203
|
-
|
|
204
|
-
|-------|----------|
|
|
205
|
-
| `1,000` | Prefilter only for very selective filters — minimum value |
|
|
206
|
-
| `10,000` | Prefilter only when the filter matches ≤10,000 vectors **(default)** |
|
|
207
|
-
| `1,000,000` | Prefilter for almost all filtered searches — maximum value |
|
|
298
|
+
### Single-field search
|
|
208
299
|
|
|
209
|
-
|
|
300
|
+
A single field is just the multi-field shape with one entry. `results` is keyed
|
|
301
|
+
by that field name:
|
|
210
302
|
|
|
211
303
|
```typescript
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
304
|
+
const res = await collection.search({
|
|
305
|
+
fields: { embedding: { query: Array(8).fill(0.2) } },
|
|
306
|
+
limit: 3,
|
|
307
|
+
});
|
|
308
|
+
res.results.embedding;
|
|
309
|
+
// [
|
|
310
|
+
// { id: "p1", similarity: 0.97, meta: {...}, filter: {...} },
|
|
311
|
+
// { id: "p2", similarity: 0.88, meta: {...}, filter: {...} },
|
|
312
|
+
// ]
|
|
313
|
+
|
|
314
|
+
// sparse
|
|
315
|
+
await collection.search({
|
|
316
|
+
fields: { keywords: { query: { indices: [3, 17], values: [0.8, 0.4] } } },
|
|
317
|
+
limit: 3,
|
|
218
318
|
});
|
|
219
|
-
```
|
|
220
319
|
|
|
221
|
-
|
|
320
|
+
// multi-vector
|
|
321
|
+
await collection.search({
|
|
322
|
+
fields: { colbert: { query: [Array(8).fill(0.1), Array(8).fill(0.2)] } },
|
|
323
|
+
limit: 3,
|
|
324
|
+
});
|
|
325
|
+
```
|
|
222
326
|
|
|
223
|
-
|
|
327
|
+
### Multi-field search
|
|
224
328
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
329
|
+
Query several fields and you get **each field's own ranked list, unfused** —
|
|
330
|
+
`results` is an object keyed by field name. Merge or display them however you
|
|
331
|
+
like, or hand them to [`rerank()`](#fusing-results-with-rerank).
|
|
228
332
|
|
|
229
333
|
```typescript
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
334
|
+
const res = await collection.search({
|
|
335
|
+
fields: {
|
|
336
|
+
embedding: { query: [0.2, 0.2, 0.3, 0.3] },
|
|
337
|
+
keywords: { query: { indices: [3, 17], values: [0.8, 0.4] } },
|
|
338
|
+
},
|
|
339
|
+
limit: 2,
|
|
236
340
|
});
|
|
237
341
|
```
|
|
238
342
|
|
|
239
|
-
|
|
343
|
+
```json
|
|
344
|
+
{
|
|
345
|
+
"results": {
|
|
346
|
+
"embedding": [
|
|
347
|
+
{ "id": "p1", "meta": { "name": "Wireless Headphones" },
|
|
348
|
+
"filter": { "category": "electronics" }, "similarity": 0.4938 },
|
|
349
|
+
{ "id": "p2", "meta": { "name": "Running Shoes" },
|
|
350
|
+
"filter": { "category": "footwear" }, "similarity": 0.4505 }
|
|
351
|
+
],
|
|
352
|
+
"keywords": [
|
|
353
|
+
{ "id": "p1", "meta": { "name": "Wireless Headphones" },
|
|
354
|
+
"filter": { "category": "electronics" }, "similarity": 0.9200 },
|
|
355
|
+
{ "id": "p2", "meta": { "name": "Running Shoes" },
|
|
356
|
+
"filter": { "category": "footwear" }, "similarity": 0.5591 }
|
|
357
|
+
]
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
```
|
|
240
361
|
|
|
241
362
|
```typescript
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
topK: 10,
|
|
245
|
-
filter: [{ category: { $eq: 'rare' } }],
|
|
246
|
-
prefilterCardinalityThreshold: 5000, // switch to brute-force for small match sets
|
|
247
|
-
filterBoostPercentage: 25, // boost candidates for HNSW filtered search
|
|
248
|
-
});
|
|
363
|
+
res.results.embedding; // this field's ranked hits
|
|
364
|
+
res.results.keywords; // that field's ranked hits
|
|
249
365
|
```
|
|
250
366
|
|
|
251
|
-
>
|
|
252
|
-
|
|
253
|
-
|
|
367
|
+
> `similarity` is in **each field's own scale** (cosine for dense ≈ 0.49, sparse
|
|
368
|
+
> dot for keywords ≈ 0.92) — scores are **not comparable across fields**. That's
|
|
369
|
+
> exactly why fusion is a separate, opt-in step.
|
|
254
370
|
|
|
255
|
-
###
|
|
371
|
+
### Fusing results with `rerank()`
|
|
256
372
|
|
|
257
|
-
|
|
373
|
+
`rerank(searchResults, { name, limit, fieldWeights })` fuses the per-field lists
|
|
374
|
+
from `search()` into **one ranked list** using Reciprocal Rank Fusion. It's a
|
|
375
|
+
standalone function — run `search()`, then pass its result to `rerank()`:
|
|
258
376
|
|
|
259
377
|
```typescript
|
|
260
|
-
|
|
378
|
+
import { rerank } from 'endee';
|
|
261
379
|
|
|
262
|
-
await
|
|
263
|
-
{
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
sparseValues: [0.8, 0.5, 0.3], // Weights for each position
|
|
268
|
-
meta: { title: 'Document 1' },
|
|
269
|
-
},
|
|
270
|
-
{
|
|
271
|
-
id: 'doc2',
|
|
272
|
-
vector: [0.3, 0.4 /* ... */],
|
|
273
|
-
sparseIndices: [15, 100, 500],
|
|
274
|
-
sparseValues: [0.9, 0.4, 0.6],
|
|
275
|
-
meta: { title: 'Document 2' },
|
|
380
|
+
const res = await collection.search({
|
|
381
|
+
fields: {
|
|
382
|
+
embedding: { query: [0.2, 0.2, 0.3, 0.3] },
|
|
383
|
+
keywords: { query: { indices: [3, 17], values: [0.8, 0.4] } },
|
|
384
|
+
colbert: { query: [Array(8).fill(0.1), Array(8).fill(0.2)] },
|
|
276
385
|
},
|
|
277
|
-
|
|
386
|
+
limit: 50,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const fused = rerank(res, {
|
|
390
|
+
name: 'rrf',
|
|
391
|
+
limit: 10,
|
|
392
|
+
// optional: weight each field's contribution (must sum to 1.0; equal by default)
|
|
393
|
+
fieldWeights: { embedding: 0.5, keywords: 0.3, colbert: 0.2 },
|
|
394
|
+
// optional: RRF rank constant k (default 60)
|
|
395
|
+
rrfK: 60,
|
|
396
|
+
});
|
|
278
397
|
```
|
|
279
398
|
|
|
280
|
-
|
|
399
|
+
```json
|
|
400
|
+
{
|
|
401
|
+
"results": [
|
|
402
|
+
{ "id": "p1", "similarity": 0.0164,
|
|
403
|
+
"meta": { "name": "Wireless Headphones" }, "filter": { "category": "electronics" } },
|
|
404
|
+
{ "id": "p3", "similarity": 0.0160,
|
|
405
|
+
"meta": { "name": "Smart Watch" }, "filter": { "category": "electronics" } },
|
|
406
|
+
{ "id": "p2", "similarity": 0.0160,
|
|
407
|
+
"meta": { "name": "Running Shoes" }, "filter": { "category": "footwear" } }
|
|
408
|
+
]
|
|
409
|
+
}
|
|
410
|
+
```
|
|
281
411
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
| `vector` | Yes | Dense embedding vector |
|
|
286
|
-
| `sparseIndices` | Yes (hybrid) | Non-zero term positions in sparse vector |
|
|
287
|
-
| `sparseValues` | Yes (hybrid) | Weights for each sparse index |
|
|
288
|
-
| `meta` | No | Metadata dictionary |
|
|
289
|
-
| `filter` | No | Filter fields |
|
|
412
|
+
`name` selects the algorithm (only `"rrf"` is supported today). Each fused hit
|
|
413
|
+
keeps the `meta`/`filter` from its per-field hit; `similarity` is replaced with
|
|
414
|
+
the RRF score (sum of `weight / (rrfK + rank)` across the fields it appeared in).
|
|
290
415
|
|
|
291
|
-
|
|
416
|
+
**Summary of return shapes**
|
|
292
417
|
|
|
293
|
-
|
|
418
|
+
| Call | `results` shape |
|
|
419
|
+
|------|-----------------|
|
|
420
|
+
| `search(...)` (1 or N fields) | `Record<field_name, SearchHit[]>` |
|
|
421
|
+
| `rerank(searchResults, { name: 'rrf', ... })` | `SearchHit[]` (fused) |
|
|
294
422
|
|
|
295
|
-
|
|
423
|
+
### Filtered search
|
|
424
|
+
|
|
425
|
+
`filter` applies to any search. It's an array of conditions:
|
|
296
426
|
|
|
297
427
|
```typescript
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
topK: 5,
|
|
428
|
+
await collection.search({
|
|
429
|
+
fields: { embedding: { query: Array(8).fill(0.2) } },
|
|
430
|
+
limit: 5,
|
|
431
|
+
filter: [{ category: { $eq: 'electronics' } }],
|
|
303
432
|
});
|
|
304
|
-
|
|
305
|
-
for (const item of results) {
|
|
306
|
-
console.log(`ID: ${item.id}, Similarity: ${item.similarity}`);
|
|
307
|
-
}
|
|
308
433
|
```
|
|
309
434
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
| Parameter | Description |
|
|
313
|
-
|-----------|-------------|
|
|
314
|
-
| `vector` | Dense query vector |
|
|
315
|
-
| `sparseIndices` | Sparse query positions |
|
|
316
|
-
| `sparseValues` | Sparse query weights |
|
|
317
|
-
| `topK` | Number of results to return |
|
|
318
|
-
| `ef` | Search quality parameter (max 1024, default: 128) |
|
|
319
|
-
| `includeVectors` | Include vector data in results (default: false) |
|
|
320
|
-
| `filter` | Optional filter criteria |
|
|
321
|
-
| `prefilterCardinalityThreshold` | Controls when search switches from HNSW filtered search to brute-force prefiltering on the matched subset (default: 10,000, range: 1,000–1,000,000). See [Filter Tuning](#filter-tuning) for details. |
|
|
322
|
-
| `filterBoostPercentage` | Expands the internal HNSW candidate pool by this percentage when a filter is active, compensating for filtered-out results (default: 0, range: 0–100). See [Filter Tuning](#filter-tuning) for details. |
|
|
435
|
+
Operators: `$eq`, `$in`, `$range`, `$gt`, `$gte`, `$lt`, `$lte` (see [Reference](#reference)).
|
|
323
436
|
|
|
324
|
-
|
|
325
|
-
- **Dense only**: Provide only `vector`
|
|
326
|
-
- **Sparse only**: Provide only `sparseIndices` and `sparseValues`
|
|
327
|
-
- **Hybrid**: Provide all three for combined results
|
|
437
|
+
---
|
|
328
438
|
|
|
329
|
-
##
|
|
330
|
-
|
|
331
|
-
Use `index.updateFilters()` to update the filter fields of existing vectors without re-upserting them.
|
|
439
|
+
## Object operations
|
|
332
440
|
|
|
333
441
|
```typescript
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
442
|
+
// Fetch full stored objects by id (meta, filter, and the stored vectors).
|
|
443
|
+
const objs = await collection.getObjects(['p1', 'p2']);
|
|
444
|
+
// [
|
|
445
|
+
// { id: "p1", meta: {...}, filter: {...},
|
|
446
|
+
// vectors: { embedding: [...] }, // dense fields (original, pre-normalization)
|
|
447
|
+
// sparses: { keywords: { indices: [...], values: [...] } },
|
|
448
|
+
// multi_vectors: { colbert: [[...], [...]] } },
|
|
449
|
+
// ...
|
|
450
|
+
// ]
|
|
451
|
+
|
|
452
|
+
// Delete a single object by id.
|
|
453
|
+
await collection.deleteObject('p1'); // { deleted: "p1" }
|
|
454
|
+
|
|
455
|
+
// Delete every object matching a filter.
|
|
456
|
+
await collection.deleteByFilter([{ category: { $eq: 'footwear' } }]); // { deleted: <count> }
|
|
457
|
+
|
|
458
|
+
// Update only the filter tags on existing objects (no re-upsert of vectors).
|
|
459
|
+
await collection.updateFilters([{ id: 'p2', filter: { category: 'sale' } }]); // { updated: <count> }
|
|
338
460
|
```
|
|
339
461
|
|
|
340
|
-
|
|
462
|
+
---
|
|
341
463
|
|
|
342
|
-
|
|
343
|
-
|-------|----------|-------------|
|
|
344
|
-
| `id` | Yes | ID of the vector to update |
|
|
345
|
-
| `filter` | Yes | New filter object to replace the existing filter |
|
|
464
|
+
## Collection maintenance
|
|
346
465
|
|
|
347
|
-
|
|
466
|
+
```typescript
|
|
467
|
+
await collection.describe(); // { name, fields, created_at, layout_version }
|
|
348
468
|
|
|
349
|
-
|
|
469
|
+
// Rebuild a field's HNSW graph with new M / ef_con (async).
|
|
470
|
+
await collection.rebuild({ field: 'embedding', m: 24, efCon: 200 }); // { status: "in_progress", ... }
|
|
471
|
+
await collection.rebuildStatus(); // { percent_complete, ... }
|
|
350
472
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
```typescript
|
|
354
|
-
await index.deleteVector('vec1');
|
|
355
|
-
```
|
|
356
|
-
### Delete by Filter
|
|
357
|
-
Delete all vectors matching specific filters.
|
|
358
|
-
```typescript
|
|
359
|
-
await index.deleteWithFilter([{'category': {'$eq' : 'tech'}}]);
|
|
473
|
+
// Defragment storage in place.
|
|
474
|
+
await collection.shrink(); // { status: "ok", reclaimed_bytes: <int> }
|
|
360
475
|
```
|
|
361
476
|
|
|
362
|
-
|
|
363
|
-
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## Backups
|
|
480
|
+
|
|
481
|
+
Backups are per-database. Creating one is per-collection and **asynchronous**.
|
|
482
|
+
|
|
364
483
|
```typescript
|
|
365
|
-
|
|
366
|
-
|
|
484
|
+
// Start a backup of a collection (async).
|
|
485
|
+
await collection.createBackup('nightly'); // { backup_name: "nightly", status: "in_progress" }
|
|
367
486
|
|
|
368
|
-
|
|
487
|
+
// Poll for completion.
|
|
488
|
+
while ((await client.activeBackup()).active) {
|
|
489
|
+
// ...wait, then re-check
|
|
490
|
+
}
|
|
369
491
|
|
|
370
|
-
|
|
492
|
+
await client.listBackups(); // { nightly: { ...metadata }, ... }
|
|
493
|
+
await client.backupInfo('nightly'); // metadata for one backup
|
|
371
494
|
|
|
372
|
-
|
|
495
|
+
// Restore a backup into a new collection.
|
|
496
|
+
await client.restoreBackup('nightly', 'products_restored');
|
|
373
497
|
|
|
374
|
-
|
|
375
|
-
|
|
498
|
+
// Move backups around.
|
|
499
|
+
await client.downloadBackup('nightly', '/tmp/nightly.tar'); // writes a .tar, returns the path
|
|
500
|
+
await client.uploadBackup('/tmp/nightly.tar'); // uploads a .tar into this db
|
|
501
|
+
|
|
502
|
+
await client.deleteBackup('nightly');
|
|
376
503
|
```
|
|
377
504
|
|
|
378
|
-
|
|
505
|
+
> `downloadBackup` / `uploadBackup` use Node's filesystem (`fs`). With the **root
|
|
506
|
+
> token**, pass a third `dbName` arg to `downloadBackup` to target a specific
|
|
507
|
+
> database's backup.
|
|
379
508
|
|
|
380
|
-
|
|
381
|
-
const info = index.describe();
|
|
382
|
-
console.log(info);
|
|
383
|
-
// { name, spaceType, dimension, sparseDimension, isHybrid, count, precision, M }
|
|
384
|
-
```
|
|
509
|
+
---
|
|
385
510
|
|
|
386
|
-
##
|
|
511
|
+
## Database administration (root token)
|
|
387
512
|
|
|
388
|
-
|
|
513
|
+
Database and token administration requires the server's **root token** (its
|
|
514
|
+
`NDD_ROOT_TOKEN`). Create an admin client with that token:
|
|
389
515
|
|
|
390
516
|
```typescript
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
Precision.BINARY; // Binary quantization (1-bit) - smallest storage, fastest search
|
|
394
|
-
Precision.INT8; // 8-bit integer quantization - balanced performance
|
|
395
|
-
Precision.INT16; // 16-bit integer quantization (default) - higher precision
|
|
396
|
-
Precision.FLOAT16; // 16-bit floating point - good balance
|
|
397
|
-
Precision.FLOAT32; // 32-bit floating point - highest precision
|
|
517
|
+
const admin = new Endee('roottoken');
|
|
518
|
+
admin.setBaseUrl('http://localhost:8080/api/v2');
|
|
398
519
|
```
|
|
399
520
|
|
|
400
|
-
**
|
|
521
|
+
> The root token administers databases/tokens; it **cannot run data ops directly**.
|
|
522
|
+
> A db token (which it mints) does the collection/object/search work.
|
|
401
523
|
|
|
402
|
-
|
|
403
|
-
- `INT8` : Recommended for most use cases - good balance of accuracy and performance
|
|
404
|
-
- `INT16` (default) : When you need better accuracy than INT8 but less storage than FLOAT32
|
|
405
|
-
- `FLOAT16`: Good compromise between precision and storage for embeddings
|
|
406
|
-
- `FLOAT32`: When you need maximum precision and storage is not a concern
|
|
407
|
-
|
|
408
|
-
## Complete Example
|
|
524
|
+
### Databases
|
|
409
525
|
|
|
410
526
|
```typescript
|
|
411
|
-
|
|
527
|
+
// Create a database → returns the new db_token.
|
|
528
|
+
const res = await admin.createDatabase('my_db', 'scale'); // db_type: starter|pro|scale|enterprise
|
|
529
|
+
const dbToken = res.db_token;
|
|
412
530
|
|
|
413
|
-
//
|
|
414
|
-
|
|
531
|
+
await admin.listDatabases(); // [{ db_name, db_type, is_active, created_at }, ...]
|
|
532
|
+
await admin.getDatabase('my_db'); // single db info
|
|
415
533
|
|
|
416
|
-
|
|
417
|
-
await
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
precision: Precision.INT16,
|
|
422
|
-
});
|
|
534
|
+
await admin.setDatabaseType('my_db', 'pro'); // change tier
|
|
535
|
+
await admin.deactivateDatabase('my_db'); // block its tokens (data is kept)
|
|
536
|
+
await admin.activateDatabase('my_db'); // re-enable
|
|
537
|
+
await admin.deleteDatabase('my_db'); // delete db + ALL its data
|
|
538
|
+
```
|
|
423
539
|
|
|
424
|
-
|
|
425
|
-
const index = await client.getIndex('documents');
|
|
540
|
+
### Collections across databases (admin view)
|
|
426
541
|
|
|
427
|
-
|
|
428
|
-
await
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
meta: { title: 'First Document' },
|
|
433
|
-
filter: { category: 'tech' },
|
|
434
|
-
},
|
|
435
|
-
{
|
|
436
|
-
id: 'doc2',
|
|
437
|
-
vector: [0.3, 0.4 /* ... 384 dimensions */],
|
|
438
|
-
meta: { title: 'Second Document' },
|
|
439
|
-
filter: { category: 'science' },
|
|
440
|
-
},
|
|
441
|
-
]);
|
|
542
|
+
```typescript
|
|
543
|
+
await admin.listDbCollections('my_db'); // ["products", ...] for one db
|
|
544
|
+
await admin.listAllCollections(); // [{ db_name: "my_db", collections: [...] }, ...]
|
|
545
|
+
await admin.deleteDbCollection('my_db', 'products');
|
|
546
|
+
```
|
|
442
547
|
|
|
443
|
-
|
|
444
|
-
const results = await index.query({
|
|
445
|
-
vector: [0.15, 0.25 /* ... */],
|
|
446
|
-
topK: 5,
|
|
447
|
-
});
|
|
548
|
+
### Tokens for any database
|
|
448
549
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
550
|
+
```typescript
|
|
551
|
+
// token_type: "rw" (read-write, default) or "r" (read-only).
|
|
552
|
+
const t = await admin.createToken('my_db', 'ci-reader', 'r'); // { db_token, ... }
|
|
553
|
+
await admin.listTokens('my_db'); // [{ name, token_type, created_at }, ...] (no secrets)
|
|
554
|
+
await admin.deleteToken('my_db', 'ci-reader');
|
|
452
555
|
```
|
|
453
556
|
|
|
454
|
-
|
|
557
|
+
### Self-service tokens (your own database)
|
|
455
558
|
|
|
456
|
-
|
|
559
|
+
Using a **db token**, you can manage your own database's tokens:
|
|
457
560
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
| `getIndex` | `getCollection` | Returns same object |
|
|
464
|
-
| `getVector` | `getObject` | Method on the collection handle |
|
|
465
|
-
| `deleteVector` | `deleteObject` | Method on the collection handle |
|
|
466
|
-
| `Index` (export) | `Collection` (export) | Same class, two names |
|
|
561
|
+
```typescript
|
|
562
|
+
await client.createMyToken('reader', 'r'); // { db_token, ... }
|
|
563
|
+
await client.listMyTokens();
|
|
564
|
+
await client.deleteMyToken('reader');
|
|
565
|
+
```
|
|
467
566
|
|
|
468
|
-
|
|
567
|
+
### Server info
|
|
469
568
|
|
|
470
569
|
```typescript
|
|
471
|
-
|
|
570
|
+
await client.health(); // { status, timestamp }
|
|
571
|
+
await client.stats(); // { version, uptime, total_requests }
|
|
572
|
+
```
|
|
472
573
|
|
|
473
|
-
|
|
574
|
+
---
|
|
474
575
|
|
|
475
|
-
|
|
476
|
-
await client.createCollection({ name: 'products', dimension: 384, spaceType: 'cosine' });
|
|
576
|
+
## Reference
|
|
477
577
|
|
|
478
|
-
|
|
479
|
-
const collection = await client.getCollection('products');
|
|
578
|
+
**Distance / space types** (`space_type`)
|
|
480
579
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
580
|
+
| Value | Meaning |
|
|
581
|
+
|-------|---------|
|
|
582
|
+
| `cosine` | Cosine similarity (vectors L2-normalized client-side). Default. |
|
|
583
|
+
| `l2` | Euclidean distance. |
|
|
584
|
+
| `ip` | Inner product. |
|
|
485
585
|
|
|
486
|
-
|
|
487
|
-
const obj = await collection.getObject('p1');
|
|
586
|
+
**Precisions** (`precision`) — memory/accuracy trade-off
|
|
488
587
|
|
|
489
|
-
|
|
490
|
-
|
|
588
|
+
`float32`, `float16`, `int16` (default), `int8`, `int8e`, `binary`. The
|
|
589
|
+
`Precision` enum is exported for convenience:
|
|
491
590
|
|
|
492
|
-
|
|
493
|
-
|
|
591
|
+
```typescript
|
|
592
|
+
import { Precision } from 'endee';
|
|
593
|
+
Precision.INT8; // "int8"
|
|
494
594
|
```
|
|
495
595
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
## API Reference
|
|
596
|
+
**Token types**: `rw` (read-write), `r` (read-only).
|
|
499
597
|
|
|
500
|
-
|
|
598
|
+
**Filter operators**
|
|
501
599
|
|
|
502
|
-
|
|
|
503
|
-
|
|
504
|
-
| `
|
|
505
|
-
| `
|
|
506
|
-
| `
|
|
507
|
-
| `
|
|
508
|
-
| `
|
|
600
|
+
| Operator | Example | Meaning |
|
|
601
|
+
|----------|---------|---------|
|
|
602
|
+
| `$eq` | `{ category: { $eq: 'news' } }` | equals |
|
|
603
|
+
| `$in` | `{ tag: { $in: ['a', 'b'] } }` | in set |
|
|
604
|
+
| `$range` | `{ price: { $range: [10, 100] } }` | inclusive range |
|
|
605
|
+
| `$gt` / `$gte` | `{ price: { $gt: 50 } }` | greater than / or equal |
|
|
606
|
+
| `$lt` / `$lte` | `{ price: { $lte: 100 } }` | less than / or equal |
|
|
509
607
|
|
|
510
|
-
|
|
608
|
+
**Client-side validation / limits**
|
|
511
609
|
|
|
512
|
-
|
|
|
513
|
-
|
|
514
|
-
| `upsert
|
|
515
|
-
| `
|
|
516
|
-
| `
|
|
517
|
-
|
|
|
518
|
-
|
|
|
519
|
-
|
|
|
520
|
-
| `describe()` | — | Get index / collection info |
|
|
610
|
+
| Limit | Value |
|
|
611
|
+
|-------|-------|
|
|
612
|
+
| Objects per `upsert` | ≤ 10,000 |
|
|
613
|
+
| `limit` (top-k) | 1 … 4096 |
|
|
614
|
+
| `efSearch` | 1 … 1024 |
|
|
615
|
+
| Filter key | ≤ 128 bytes |
|
|
616
|
+
| Filter string value | ≤ 1024 bytes |
|
|
617
|
+
| Vector dimension | must match the field's configured dimension |
|
|
521
618
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
The package includes comprehensive TypeScript types:
|
|
619
|
+
**Exported TypeScript types**
|
|
525
620
|
|
|
526
621
|
```typescript
|
|
527
622
|
import type {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
623
|
+
FieldDefinition,
|
|
624
|
+
FieldType,
|
|
625
|
+
ObjectInput,
|
|
626
|
+
SearchOptions,
|
|
627
|
+
SearchHit,
|
|
628
|
+
SearchResponse,
|
|
629
|
+
RerankOptions,
|
|
630
|
+
RerankResponse,
|
|
631
|
+
FullObject,
|
|
632
|
+
CollectionMetadata,
|
|
633
|
+
UpdateFilterEntry,
|
|
634
|
+
RebuildOptions,
|
|
635
|
+
DatabaseInfo,
|
|
636
|
+
TokenInfo,
|
|
637
|
+
DbType,
|
|
638
|
+
TokenType,
|
|
533
639
|
SpaceType,
|
|
534
640
|
Precision,
|
|
535
|
-
UpdateFilterParams,
|
|
536
641
|
} from 'endee';
|
|
537
642
|
```
|
|
538
643
|
|
|
539
|
-
|
|
644
|
+
---
|
|
540
645
|
|
|
541
|
-
|
|
646
|
+
## Error handling
|
|
542
647
|
|
|
543
|
-
|
|
648
|
+
Non-2xx responses throw typed exceptions:
|
|
544
649
|
|
|
545
|
-
|
|
650
|
+
```typescript
|
|
651
|
+
import {
|
|
652
|
+
EndeeException, // base class for all client errors
|
|
653
|
+
APIException, // generic 4xx (e.g. bad request)
|
|
654
|
+
AuthenticationException, // 401
|
|
655
|
+
ForbiddenException, // 403 (e.g. read-only token on a write)
|
|
656
|
+
NotFoundException, // 404
|
|
657
|
+
ConflictException, // 409 (e.g. duplicate collection)
|
|
658
|
+
ServerException, // 5xx
|
|
659
|
+
} from 'endee';
|
|
660
|
+
|
|
661
|
+
try {
|
|
662
|
+
await collection.upsert(objects);
|
|
663
|
+
} catch (e) {
|
|
664
|
+
if (e instanceof NotFoundException) {
|
|
665
|
+
console.error('not found:', e.message);
|
|
666
|
+
} else if (e instanceof EndeeException) {
|
|
667
|
+
// catches all of the above
|
|
668
|
+
console.error('request failed:', e.message);
|
|
669
|
+
} else {
|
|
670
|
+
throw e;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
All exception classes are importable from `endee`: `EndeeException` (base),
|
|
676
|
+
`APIException`, `AuthenticationException`, `ForbiddenException`,
|
|
677
|
+
`NotFoundException`, `ConflictException`, `ServerException`,
|
|
678
|
+
`SubscriptionException`.
|
|
679
|
+
|
|
680
|
+
Client-side input mistakes (bad dimensions, mismatched sparse lengths, oversized
|
|
681
|
+
batch, sum-of-weights ≠ 1.0, etc.) throw a plain `Error` **before** any network
|
|
682
|
+
call.
|
|
683
|
+
|
|
684
|
+
---
|
|
685
|
+
|
|
686
|
+
## End-to-end example
|
|
687
|
+
|
|
688
|
+
See `tests/integration_v2.ts` — a full walkthrough (create → multi-field upsert →
|
|
689
|
+
every search mode → object ops → maintenance → cleanup). Run it against a local
|
|
690
|
+
server with a db token:
|
|
691
|
+
|
|
692
|
+
```bash
|
|
693
|
+
npx tsx tests/integration_v2.ts --token my_db:xxxx --url http://localhost:8080/api/v2
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
## License
|
|
697
|
+
|
|
698
|
+
MIT
|