endee 1.7.1 → 1.8.0-dev.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 +556 -381
- package/dist/collection.d.ts +88 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +755 -0
- package/dist/collection.js.map +1 -0
- package/dist/constants.d.ts +53 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +53 -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 +21 -0
- package/dist/reranker.d.ts.map +1 -0
- package/dist/reranker.js +68 -0
- package/dist/reranker.js.map +1 -0
- package/dist/types/index.d.ts +133 -82
- 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,720 @@
|
|
|
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 `limit` and `ef_search`](#per-field-limit-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
|
+
- [Filter tuning](#filter-tuning)
|
|
32
|
+
7. [Object operations](#object-operations)
|
|
33
|
+
8. [Collection maintenance](#collection-maintenance)
|
|
34
|
+
9. [Backups](#backups)
|
|
35
|
+
10. [Database administration (root token)](#database-administration-root-token)
|
|
36
|
+
11. [Reference](#reference)
|
|
37
|
+
12. [Error handling](#error-handling)
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Install
|
|
21
42
|
|
|
22
43
|
```bash
|
|
23
44
|
npm install endee
|
|
24
45
|
```
|
|
25
46
|
|
|
26
|
-
|
|
47
|
+
Requires Node.js ≥ 18. Depends on `axios` and `@msgpack/msgpack`.
|
|
27
48
|
|
|
28
|
-
|
|
49
|
+
---
|
|
29
50
|
|
|
30
|
-
|
|
51
|
+
## Quick start
|
|
31
52
|
|
|
32
53
|
```typescript
|
|
33
|
-
import { Endee
|
|
54
|
+
import { Endee } from 'endee';
|
|
55
|
+
|
|
56
|
+
// A "db token" (db_name:secret) scopes you to one database.
|
|
57
|
+
const client = new Endee('my_db:xxxxxxxx');
|
|
58
|
+
client.setBaseUrl('http://localhost:8080/api/v2'); // include /api/v2
|
|
59
|
+
|
|
60
|
+
// 1. Create a collection with one of each field type.
|
|
61
|
+
await client.createCollection({
|
|
62
|
+
name: 'products',
|
|
63
|
+
fields: [
|
|
64
|
+
{
|
|
65
|
+
name: 'embedding',
|
|
66
|
+
type: 'vector',
|
|
67
|
+
params: { dimension: 8, space_type: 'cosine', precision: 'int8' },
|
|
68
|
+
},
|
|
69
|
+
{ name: 'keywords', type: 'sparse', sparse_model: 'default' },
|
|
70
|
+
{
|
|
71
|
+
name: 'colbert',
|
|
72
|
+
type: 'multi_vector',
|
|
73
|
+
params: { dimension: 8, space_type: 'cosine', precision: 'int8', pooling: 'mean' },
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
});
|
|
34
77
|
|
|
35
|
-
|
|
36
|
-
const client = new Endee();
|
|
37
|
-
```
|
|
78
|
+
const collection = await client.getCollection('products');
|
|
38
79
|
|
|
39
|
-
|
|
80
|
+
// 2. Insert an object carrying ALL THREE field types at once.
|
|
81
|
+
await collection.upsert([
|
|
82
|
+
{
|
|
83
|
+
id: 'p1',
|
|
84
|
+
meta: { name: 'Wireless Headphones', price: 99 },
|
|
85
|
+
filter: { category: 'electronics' },
|
|
86
|
+
fields: {
|
|
87
|
+
embedding: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8],
|
|
88
|
+
keywords: { indices: [3, 17, 42], values: [0.9, 0.5, 0.2] },
|
|
89
|
+
colbert: [Array(8).fill(0.1), Array(8).fill(0.2)],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
]);
|
|
40
93
|
|
|
41
|
-
|
|
42
|
-
const
|
|
94
|
+
// 3. Search the dense field. `results` is keyed by field name.
|
|
95
|
+
const res = await collection.search({
|
|
96
|
+
fields: { embedding: { query: Array(8).fill(0.2), 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
|
-
|
|
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**.
|
|
51
109
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
### Create a Dense Index
|
|
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).
|
|
70
126
|
|
|
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`) |
|
|
127
|
+
---
|
|
79
128
|
|
|
80
|
-
|
|
129
|
+
## Create a collection
|
|
81
130
|
|
|
82
|
-
|
|
131
|
+
A collection is a set of **named, typed fields**. Each field is a plain object
|
|
132
|
+
sent to the server as-is.
|
|
133
|
+
|
|
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
|
+
---
|
|
179
|
+
|
|
180
|
+
## Insert objects (combining vector types)
|
|
99
181
|
|
|
100
|
-
|
|
101
|
-
const index = await client.getIndex('my_vectors');
|
|
182
|
+
`collection.upsert(objects)` takes an array of objects. Each object is:
|
|
102
183
|
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
const index = await client.getIndex('my_index');
|
|
205
|
+
### One object, multiple field types
|
|
113
206
|
|
|
114
|
-
|
|
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:
|
|
210
|
+
|
|
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
|
-
});
|
|
255
|
+
`collection.search({ fields, 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).
|
|
150
259
|
|
|
151
|
-
|
|
152
|
-
console.log(`ID: ${item.id}, Similarity: ${item.similarity}`);
|
|
153
|
-
}
|
|
154
|
-
```
|
|
260
|
+
### The unified field shape
|
|
155
261
|
|
|
156
|
-
**
|
|
262
|
+
Every field is queried with the **same object form** — `{ query, limit?, ef_search? }`.
|
|
263
|
+
There is no bare-value shorthand; `query` is required. The `query` value matches
|
|
264
|
+
the field's type:
|
|
157
265
|
|
|
158
|
-
|
|
|
159
|
-
|
|
160
|
-
| `vector` |
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
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. |
|
|
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` | `[[...], [...]]` |
|
|
169
271
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
Use the `filter` parameter to restrict results. All filters are combined with **logical AND**.
|
|
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 `limit` and `ef_search`
|
|
196
282
|
|
|
197
|
-
|
|
283
|
+
`limit` is the **max number of hits to return for that field** (defaults to
|
|
284
|
+
`10`). It's set per field, so each field draws independently. `ef_search` tunes
|
|
285
|
+
that field's HNSW recall/latency.
|
|
198
286
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
287
|
+
```typescript
|
|
288
|
+
// embedding returns up to 35 hits, keywords up to 15 — each independent
|
|
289
|
+
await collection.search({
|
|
290
|
+
fields: {
|
|
291
|
+
embedding: { query: Array(8).fill(0.2), limit: 35, ef_search: 256 },
|
|
292
|
+
keywords: { query: { indices: [3, 17], values: [0.8, 0.4] }, limit: 15 },
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
```
|
|
202
296
|
|
|
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 |
|
|
297
|
+
### Single-field search
|
|
208
298
|
|
|
209
|
-
|
|
299
|
+
A single field is just the multi-field shape with one entry. `results` is keyed
|
|
300
|
+
by that field name:
|
|
210
301
|
|
|
211
302
|
```typescript
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
303
|
+
const res = await collection.search({
|
|
304
|
+
fields: { embedding: { query: Array(8).fill(0.2), limit: 3 } },
|
|
305
|
+
});
|
|
306
|
+
res.results.embedding;
|
|
307
|
+
// [
|
|
308
|
+
// { id: "p1", similarity: 0.97, meta: {...}, filter: {...} },
|
|
309
|
+
// { id: "p2", similarity: 0.88, meta: {...}, filter: {...} },
|
|
310
|
+
// ]
|
|
311
|
+
|
|
312
|
+
// sparse
|
|
313
|
+
await collection.search({
|
|
314
|
+
fields: { keywords: { query: { indices: [3, 17], values: [0.8, 0.4] }, limit: 3 } },
|
|
218
315
|
});
|
|
219
|
-
```
|
|
220
316
|
|
|
221
|
-
|
|
317
|
+
// multi-vector
|
|
318
|
+
await collection.search({
|
|
319
|
+
fields: { colbert: { query: [Array(8).fill(0.1), Array(8).fill(0.2)], limit: 3 } },
|
|
320
|
+
});
|
|
321
|
+
```
|
|
222
322
|
|
|
223
|
-
|
|
323
|
+
### Multi-field search
|
|
224
324
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
325
|
+
Query several fields and you get **each field's own ranked list, unfused** —
|
|
326
|
+
`results` is an object keyed by field name. Merge or display them however you
|
|
327
|
+
like, or hand them to [`rerank()`](#fusing-results-with-rerank).
|
|
228
328
|
|
|
229
329
|
```typescript
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
filterBoostPercentage: 30,
|
|
330
|
+
const res = await collection.search({
|
|
331
|
+
fields: {
|
|
332
|
+
embedding: { query: [0.2, 0.2, 0.3, 0.3], limit: 2 },
|
|
333
|
+
keywords: { query: { indices: [3, 17], values: [0.8, 0.4] }, limit: 2 },
|
|
334
|
+
},
|
|
236
335
|
});
|
|
237
336
|
```
|
|
238
337
|
|
|
239
|
-
|
|
338
|
+
```json
|
|
339
|
+
{
|
|
340
|
+
"results": {
|
|
341
|
+
"embedding": [
|
|
342
|
+
{ "id": "p1", "meta": { "name": "Wireless Headphones" },
|
|
343
|
+
"filter": { "category": "electronics" }, "similarity": 0.4938 },
|
|
344
|
+
{ "id": "p2", "meta": { "name": "Running Shoes" },
|
|
345
|
+
"filter": { "category": "footwear" }, "similarity": 0.4505 }
|
|
346
|
+
],
|
|
347
|
+
"keywords": [
|
|
348
|
+
{ "id": "p1", "meta": { "name": "Wireless Headphones" },
|
|
349
|
+
"filter": { "category": "electronics" }, "similarity": 0.9200 },
|
|
350
|
+
{ "id": "p2", "meta": { "name": "Running Shoes" },
|
|
351
|
+
"filter": { "category": "footwear" }, "similarity": 0.5591 }
|
|
352
|
+
]
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
```
|
|
240
356
|
|
|
241
357
|
```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
|
-
});
|
|
358
|
+
res.results.embedding; // this field's ranked hits
|
|
359
|
+
res.results.keywords; // that field's ranked hits
|
|
249
360
|
```
|
|
250
361
|
|
|
251
|
-
>
|
|
362
|
+
> `similarity` is in **each field's own scale** (cosine for dense ≈ 0.49, sparse
|
|
363
|
+
> dot for keywords ≈ 0.92) — scores are **not comparable across fields**. That's
|
|
364
|
+
> exactly why fusion is a separate, opt-in step.
|
|
252
365
|
|
|
253
|
-
|
|
366
|
+
### Fusing results with `rerank()`
|
|
254
367
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
368
|
+
`rerank(searchResults, { name, limit, fieldWeights, rrfK })` fuses the per-field
|
|
369
|
+
lists from `search()` into **one ranked list** using Reciprocal Rank Fusion. It's
|
|
370
|
+
a standalone function — run `search()`, then pass its result to `rerank()`:
|
|
258
371
|
|
|
259
372
|
```typescript
|
|
260
|
-
|
|
373
|
+
import { rerank } from 'endee';
|
|
261
374
|
|
|
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' },
|
|
375
|
+
const res = await collection.search({
|
|
376
|
+
fields: {
|
|
377
|
+
embedding: { query: [0.2, 0.2, 0.3, 0.3], limit: 50 },
|
|
378
|
+
keywords: { query: { indices: [3, 17], values: [0.8, 0.4] }, limit: 50 },
|
|
379
|
+
colbert: { query: [Array(8).fill(0.1), Array(8).fill(0.2)], limit: 50 },
|
|
276
380
|
},
|
|
277
|
-
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const fused = rerank(res, {
|
|
384
|
+
name: 'rrf',
|
|
385
|
+
limit: 10,
|
|
386
|
+
// optional: weight each field's contribution (must sum to 1.0; equal by default)
|
|
387
|
+
fieldWeights: { embedding: 0.5, keywords: 0.3, colbert: 0.2 },
|
|
388
|
+
// optional: RRF rank constant k (default 60)
|
|
389
|
+
rrfK: 60,
|
|
390
|
+
});
|
|
278
391
|
```
|
|
279
392
|
|
|
280
|
-
|
|
393
|
+
```json
|
|
394
|
+
{
|
|
395
|
+
"results": [
|
|
396
|
+
{ "id": "p1", "similarity": 0.0164,
|
|
397
|
+
"meta": { "name": "Wireless Headphones" }, "filter": { "category": "electronics" } },
|
|
398
|
+
{ "id": "p3", "similarity": 0.0160,
|
|
399
|
+
"meta": { "name": "Smart Watch" }, "filter": { "category": "electronics" } },
|
|
400
|
+
{ "id": "p2", "similarity": 0.0160,
|
|
401
|
+
"meta": { "name": "Running Shoes" }, "filter": { "category": "footwear" } }
|
|
402
|
+
]
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
`name` selects the algorithm (only `"rrf"` is supported today). `rrfK` is the RRF
|
|
407
|
+
rank constant (default `60`); a larger value flattens the contribution of top
|
|
408
|
+
ranks, a smaller value sharpens it. Each fused hit keeps the `meta`/`filter` from
|
|
409
|
+
its per-field hit; `similarity` is replaced with the RRF score (sum of
|
|
410
|
+
`weight / (rrfK + rank)` across the fields it appeared in).
|
|
281
411
|
|
|
282
|
-
|
|
283
|
-
|-------|----------|-------------|
|
|
284
|
-
| `id` | Yes | Unique identifier |
|
|
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
|
+
**Summary of return shapes**
|
|
290
413
|
|
|
291
|
-
|
|
414
|
+
| Call | `results` shape |
|
|
415
|
+
|------|-----------------|
|
|
416
|
+
| `search(...)` (1 or N fields) | `Record<field_name, SearchHit[]>` |
|
|
417
|
+
| `rerank(searchResults, { name: 'rrf', ... })` | `SearchHit[]` (fused) |
|
|
292
418
|
|
|
293
|
-
###
|
|
419
|
+
### Filtered search
|
|
294
420
|
|
|
295
|
-
|
|
421
|
+
`filter` applies to any search. It's an array of conditions:
|
|
296
422
|
|
|
297
423
|
```typescript
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
sparseValues: [0.7, 0.5, 0.4], // Sparse query weights
|
|
302
|
-
topK: 5,
|
|
424
|
+
await collection.search({
|
|
425
|
+
fields: { embedding: { query: Array(8).fill(0.2), limit: 5 } },
|
|
426
|
+
filter: [{ category: { $eq: 'electronics' } }],
|
|
303
427
|
});
|
|
304
|
-
|
|
305
|
-
for (const item of results) {
|
|
306
|
-
console.log(`ID: ${item.id}, Similarity: ${item.similarity}`);
|
|
307
|
-
}
|
|
308
428
|
```
|
|
309
429
|
|
|
310
|
-
|
|
430
|
+
Operators: `$eq`, `$in`, `$range`, `$gt`, `$gte`, `$lt`, `$lte` (see [Reference](#reference)).
|
|
311
431
|
|
|
312
|
-
|
|
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. |
|
|
432
|
+
### Filter tuning
|
|
323
433
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
- **Hybrid**: Provide all three for combined results
|
|
328
|
-
|
|
329
|
-
## Updating Filters
|
|
330
|
-
|
|
331
|
-
Use `index.updateFilters()` to update the filter fields of existing vectors without re-upserting them.
|
|
434
|
+
Two optional params tune the speed/recall trade-off of **filtered** queries.
|
|
435
|
+
They're sent to the server under `filter_params` and ignored on unfiltered
|
|
436
|
+
searches.
|
|
332
437
|
|
|
333
438
|
```typescript
|
|
334
|
-
await
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
439
|
+
await collection.search({
|
|
440
|
+
fields: { embedding: { query: Array(8).fill(0.2), limit: 10 } },
|
|
441
|
+
filter: [{ category: { $eq: 'rare' } }],
|
|
442
|
+
prefilterCardinalityThreshold: 5000, // default 10,000; range 1,000–1,000,000
|
|
443
|
+
filterBoostPercentage: 25, // default 0; max 100
|
|
444
|
+
});
|
|
338
445
|
```
|
|
339
446
|
|
|
340
|
-
**`
|
|
447
|
+
**`prefilterCardinalityThreshold`** — the cardinality below which the search
|
|
448
|
+
switches from HNSW filtered search to brute-force prefiltering. When very few
|
|
449
|
+
vectors match the filter, scanning the matched subset directly is faster and
|
|
450
|
+
more accurate than HNSW graph traversal. **Raising** it makes prefiltering kick
|
|
451
|
+
in more often (favors the exhaustive scan); **lowering** it favors HNSW graph
|
|
452
|
+
search (favors speed on large datasets).
|
|
341
453
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
454
|
+
**`filterBoostPercentage`** — in HNSW filtered search, candidates that fail the
|
|
455
|
+
filter are discarded, which can leave fewer than `limit` results. This expands
|
|
456
|
+
the internal candidate pool before filtering to compensate. `0` = no boost,
|
|
457
|
+
`100` = doubles the pool.
|
|
346
458
|
|
|
347
|
-
|
|
459
|
+
---
|
|
348
460
|
|
|
349
|
-
##
|
|
461
|
+
## Object operations
|
|
350
462
|
|
|
351
|
-
### Delete by ID
|
|
352
|
-
Delete vector with a specifc vector id.
|
|
353
|
-
```typescript
|
|
354
|
-
await index.deleteVector('vec1');
|
|
355
|
-
```
|
|
356
|
-
### Delete by Filter
|
|
357
|
-
Delete all vectors matching specific filters.
|
|
358
463
|
```typescript
|
|
359
|
-
|
|
464
|
+
// Fetch full stored objects by id (meta, filter, and the stored vectors).
|
|
465
|
+
const objs = await collection.getObjects(['p1', 'p2']);
|
|
466
|
+
// [
|
|
467
|
+
// { id: "p1", meta: {...}, filter: {...},
|
|
468
|
+
// vectors: { embedding: [...] }, // dense fields (original, pre-normalization)
|
|
469
|
+
// sparses: { keywords: { indices: [...], values: [...] } },
|
|
470
|
+
// multi_vectors: { colbert: [[...], [...]] } },
|
|
471
|
+
// ...
|
|
472
|
+
// ]
|
|
473
|
+
|
|
474
|
+
// Delete a single object by id.
|
|
475
|
+
await collection.deleteObject('p1'); // { deleted: "p1" }
|
|
476
|
+
|
|
477
|
+
// Delete every object matching a filter.
|
|
478
|
+
await collection.deleteByFilter([{ category: { $eq: 'footwear' } }]); // { deleted: <count> }
|
|
479
|
+
|
|
480
|
+
// Update only the filter tags on existing objects (no re-upsert of vectors).
|
|
481
|
+
await collection.updateFilters([{ id: 'p2', filter: { category: 'sale' } }]); // { updated: <count> }
|
|
360
482
|
```
|
|
361
483
|
|
|
362
|
-
|
|
363
|
-
Delete an entire Index.
|
|
364
|
-
```typescript
|
|
365
|
-
await client.deleteIndex('my_index');
|
|
366
|
-
```
|
|
484
|
+
---
|
|
367
485
|
|
|
368
|
-
|
|
486
|
+
## Collection maintenance
|
|
369
487
|
|
|
370
|
-
|
|
488
|
+
```typescript
|
|
489
|
+
await collection.describe(); // { name, fields, created_at, layout_version }
|
|
371
490
|
|
|
372
|
-
|
|
491
|
+
// Rebuild a field's HNSW graph with new M / ef_con (async).
|
|
492
|
+
await collection.rebuild({ field: 'embedding', m: 24, efCon: 200 }); // { status: "in_progress", ... }
|
|
493
|
+
await collection.rebuildStatus(); // { percent_complete, ... }
|
|
373
494
|
|
|
374
|
-
|
|
375
|
-
|
|
495
|
+
// Defragment storage in place.
|
|
496
|
+
await collection.shrink(); // { status: "ok", reclaimed_bytes: <int> }
|
|
376
497
|
```
|
|
377
498
|
|
|
378
|
-
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## Backups
|
|
502
|
+
|
|
503
|
+
Backups are per-database. Creating one is per-collection and **asynchronous**.
|
|
379
504
|
|
|
380
505
|
```typescript
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
// { name, spaceType, dimension, sparseDimension, isHybrid, count, precision, M }
|
|
384
|
-
```
|
|
506
|
+
// Start a backup of a collection (async).
|
|
507
|
+
await collection.createBackup('nightly'); // { backup_name: "nightly", status: "in_progress" }
|
|
385
508
|
|
|
386
|
-
|
|
509
|
+
// Poll for completion.
|
|
510
|
+
while ((await client.activeBackup()).active) {
|
|
511
|
+
// ...wait, then re-check
|
|
512
|
+
}
|
|
387
513
|
|
|
388
|
-
|
|
514
|
+
await client.listBackups(); // { nightly: { ...metadata }, ... }
|
|
515
|
+
await client.backupInfo('nightly'); // metadata for one backup
|
|
389
516
|
|
|
390
|
-
|
|
391
|
-
|
|
517
|
+
// Restore a backup into a new collection.
|
|
518
|
+
await client.restoreBackup('nightly', 'products_restored');
|
|
519
|
+
|
|
520
|
+
// Move backups around.
|
|
521
|
+
await client.downloadBackup('nightly', '/tmp/nightly.tar'); // writes a .tar, returns the path
|
|
522
|
+
await client.uploadBackup('/tmp/nightly.tar'); // uploads a .tar into this db
|
|
392
523
|
|
|
393
|
-
|
|
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
|
|
524
|
+
await client.deleteBackup('nightly');
|
|
398
525
|
```
|
|
399
526
|
|
|
400
|
-
**
|
|
527
|
+
> `downloadBackup` / `uploadBackup` use Node's filesystem (`fs`). With the **root
|
|
528
|
+
> token**, pass a third `dbName` arg to `downloadBackup` to target a specific
|
|
529
|
+
> database's backup.
|
|
401
530
|
|
|
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
|
|
531
|
+
---
|
|
407
532
|
|
|
408
|
-
##
|
|
533
|
+
## Database administration (root token)
|
|
534
|
+
|
|
535
|
+
Database and token administration requires the server's **root token** (its
|
|
536
|
+
`NDD_ROOT_TOKEN`). Create an admin client with that token:
|
|
409
537
|
|
|
410
538
|
```typescript
|
|
411
|
-
|
|
539
|
+
const admin = new Endee('roottoken');
|
|
540
|
+
admin.setBaseUrl('http://localhost:8080/api/v2');
|
|
541
|
+
```
|
|
412
542
|
|
|
413
|
-
|
|
414
|
-
|
|
543
|
+
> The root token administers databases/tokens; it **cannot run data ops directly**.
|
|
544
|
+
> A db token (which it mints) does the collection/object/search work.
|
|
415
545
|
|
|
416
|
-
|
|
417
|
-
await client.createIndex({
|
|
418
|
-
name: 'documents',
|
|
419
|
-
dimension: 384,
|
|
420
|
-
spaceType: 'cosine',
|
|
421
|
-
precision: Precision.INT16,
|
|
422
|
-
});
|
|
546
|
+
### Databases
|
|
423
547
|
|
|
424
|
-
|
|
425
|
-
|
|
548
|
+
```typescript
|
|
549
|
+
// Create a database → returns the new db_token.
|
|
550
|
+
const res = await admin.createDatabase('my_db', 'scale'); // db_type: starter|pro|scale|enterprise
|
|
551
|
+
const dbToken = res.db_token;
|
|
426
552
|
|
|
427
|
-
//
|
|
428
|
-
await
|
|
429
|
-
{
|
|
430
|
-
id: 'doc1',
|
|
431
|
-
vector: [0.1, 0.2 /* ... 384 dimensions */],
|
|
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
|
-
]);
|
|
553
|
+
await admin.listDatabases(); // [{ db_name, db_type, is_active, created_at }, ...]
|
|
554
|
+
await admin.getDatabase('my_db'); // single db info
|
|
442
555
|
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
556
|
+
await admin.setDatabaseType('my_db', 'pro'); // change tier
|
|
557
|
+
await admin.deactivateDatabase('my_db'); // block its tokens (data is kept)
|
|
558
|
+
await admin.activateDatabase('my_db'); // re-enable
|
|
559
|
+
await admin.deleteDatabase('my_db'); // delete db + ALL its data
|
|
560
|
+
```
|
|
448
561
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
562
|
+
### Collections across databases (admin view)
|
|
563
|
+
|
|
564
|
+
```typescript
|
|
565
|
+
await admin.listDbCollections('my_db'); // ["products", ...] for one db
|
|
566
|
+
await admin.listAllCollections(); // [{ db_name: "my_db", collections: [...] }, ...]
|
|
567
|
+
await admin.deleteDbCollection('my_db', 'products');
|
|
452
568
|
```
|
|
453
569
|
|
|
454
|
-
|
|
570
|
+
### Tokens for any database
|
|
455
571
|
|
|
456
|
-
|
|
572
|
+
```typescript
|
|
573
|
+
// token_type: "rw" (read-write, default) or "r" (read-only).
|
|
574
|
+
const t = await admin.createToken('my_db', 'ci-reader', 'r'); // { db_token, ... }
|
|
575
|
+
await admin.listTokens('my_db'); // [{ name, token_type, created_at }, ...] (no secrets)
|
|
576
|
+
await admin.deleteToken('my_db', 'ci-reader');
|
|
577
|
+
```
|
|
457
578
|
|
|
458
|
-
|
|
459
|
-
|----------|----------|-------|
|
|
460
|
-
| `createIndex` | `createCollection` | Same signature |
|
|
461
|
-
| `listIndexes` | `listCollections` | Same signature |
|
|
462
|
-
| `deleteIndex` | `deleteCollection` | Same signature |
|
|
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 |
|
|
579
|
+
### Self-service tokens (your own database)
|
|
467
580
|
|
|
468
|
-
**
|
|
581
|
+
Using a **db token**, you can manage your own database's tokens:
|
|
469
582
|
|
|
470
583
|
```typescript
|
|
471
|
-
|
|
584
|
+
await client.createMyToken('reader', 'r'); // { db_token, ... }
|
|
585
|
+
await client.listMyTokens();
|
|
586
|
+
await client.deleteMyToken('reader');
|
|
587
|
+
```
|
|
472
588
|
|
|
473
|
-
|
|
589
|
+
### Server info
|
|
474
590
|
|
|
475
|
-
|
|
476
|
-
await client.
|
|
591
|
+
```typescript
|
|
592
|
+
await client.health(); // { status, timestamp }
|
|
593
|
+
await client.stats(); // { version, uptime, total_requests }
|
|
594
|
+
```
|
|
477
595
|
|
|
478
|
-
|
|
479
|
-
const collection = await client.getCollection('products');
|
|
596
|
+
---
|
|
480
597
|
|
|
481
|
-
|
|
482
|
-
await collection.upsert([
|
|
483
|
-
{ id: 'p1', vector: [0.1, 0.2 /* ... */], meta: { title: 'Shoes' } },
|
|
484
|
-
]);
|
|
598
|
+
## Reference
|
|
485
599
|
|
|
486
|
-
|
|
487
|
-
const obj = await collection.getObject('p1');
|
|
600
|
+
**Distance / space types** (`space_type`)
|
|
488
601
|
|
|
489
|
-
|
|
490
|
-
|
|
602
|
+
| Value | Meaning |
|
|
603
|
+
|-------|---------|
|
|
604
|
+
| `cosine` | Cosine similarity (vectors L2-normalized client-side). Default. |
|
|
605
|
+
| `l2` | Euclidean distance. |
|
|
606
|
+
| `ip` | Inner product. |
|
|
491
607
|
|
|
492
|
-
|
|
493
|
-
await client.deleteCollection('products');
|
|
494
|
-
```
|
|
608
|
+
**Precisions** (`precision`) — memory/accuracy trade-off
|
|
495
609
|
|
|
496
|
-
|
|
610
|
+
`float32`, `float16`, `int16` (default), `int8`, `int8e`, `binary`. The
|
|
611
|
+
`Precision` enum is exported for convenience:
|
|
497
612
|
|
|
498
|
-
|
|
613
|
+
```typescript
|
|
614
|
+
import { Precision } from 'endee';
|
|
615
|
+
Precision.INT8; // "int8"
|
|
616
|
+
```
|
|
499
617
|
|
|
500
|
-
|
|
618
|
+
**Token types**: `rw` (read-write), `r` (read-only).
|
|
501
619
|
|
|
502
|
-
|
|
503
|
-
|--------|-------|-------------|
|
|
504
|
-
| `createIndex(options)` | `createCollection(options)` | Create a new index / collection |
|
|
505
|
-
| `listIndexes()` | `listCollections()` | List all indexes / collections |
|
|
506
|
-
| `deleteIndex(name)` | `deleteCollection(name)` | Delete an index / collection |
|
|
507
|
-
| `getIndex(name)` | `getCollection(name)` | Get a handle to an index / collection |
|
|
508
|
-
| `setBaseUrl(url)` | — | Set a custom base URL |
|
|
620
|
+
**Filter operators**
|
|
509
621
|
|
|
510
|
-
|
|
622
|
+
| Operator | Example | Meaning |
|
|
623
|
+
|----------|---------|---------|
|
|
624
|
+
| `$eq` | `{ category: { $eq: 'news' } }` | equals |
|
|
625
|
+
| `$in` | `{ tag: { $in: ['a', 'b'] } }` | in set |
|
|
626
|
+
| `$range` | `{ price: { $range: [10, 100] } }` | inclusive range |
|
|
627
|
+
| `$gt` / `$gte` | `{ price: { $gt: 50 } }` | greater than / or equal |
|
|
628
|
+
| `$lt` / `$lte` | `{ price: { $lte: 100 } }` | less than / or equal |
|
|
511
629
|
|
|
512
|
-
|
|
513
|
-
|--------|-------|-------------|
|
|
514
|
-
| `upsert(vectors)` | — | Insert or update vectors / objects |
|
|
515
|
-
| `query(options)` | — | Search for similar vectors / objects |
|
|
516
|
-
| `updateFilters(updates)` | — | Update filter fields on existing vectors by ID |
|
|
517
|
-
| `deleteVector(id)` | `deleteObject(id)` | Delete a vector / object by ID |
|
|
518
|
-
| `deleteWithFilter(filter)` | — | Delete vectors / objects by filter |
|
|
519
|
-
| `getVector(id)` | `getObject(id)` | Get a vector / object by ID |
|
|
520
|
-
| `describe()` | — | Get index / collection info |
|
|
630
|
+
**Client-side validation / limits**
|
|
521
631
|
|
|
522
|
-
|
|
632
|
+
| Limit | Value |
|
|
633
|
+
|-------|-------|
|
|
634
|
+
| Objects per `upsert` | ≤ 10,000 |
|
|
635
|
+
| `limit` (top-k) | 1 … 4096 |
|
|
636
|
+
| `efSearch` | 1 … 1024 |
|
|
637
|
+
| Filter key | ≤ 128 bytes |
|
|
638
|
+
| Filter string value | ≤ 1024 bytes |
|
|
639
|
+
| Vector dimension | must match the field's configured dimension |
|
|
523
640
|
|
|
524
|
-
|
|
641
|
+
**Exported TypeScript types**
|
|
525
642
|
|
|
526
643
|
```typescript
|
|
527
644
|
import type {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
645
|
+
FieldDefinition,
|
|
646
|
+
FieldType,
|
|
647
|
+
ObjectInput,
|
|
648
|
+
SearchOptions,
|
|
649
|
+
SearchHit,
|
|
650
|
+
SearchResponse,
|
|
651
|
+
RerankOptions,
|
|
652
|
+
RerankResponse,
|
|
653
|
+
FullObject,
|
|
654
|
+
CollectionMetadata,
|
|
655
|
+
UpdateFilterEntry,
|
|
656
|
+
RebuildOptions,
|
|
657
|
+
DatabaseInfo,
|
|
658
|
+
TokenInfo,
|
|
659
|
+
DbType,
|
|
660
|
+
TokenType,
|
|
533
661
|
SpaceType,
|
|
534
662
|
Precision,
|
|
535
|
-
UpdateFilterParams,
|
|
536
663
|
} from 'endee';
|
|
537
664
|
```
|
|
538
665
|
|
|
539
|
-
|
|
666
|
+
---
|
|
540
667
|
|
|
541
|
-
|
|
668
|
+
## Error handling
|
|
542
669
|
|
|
543
|
-
|
|
670
|
+
Non-2xx responses throw typed exceptions:
|
|
544
671
|
|
|
545
|
-
|
|
672
|
+
```typescript
|
|
673
|
+
import {
|
|
674
|
+
EndeeException, // base class for all client errors
|
|
675
|
+
APIException, // generic 4xx (e.g. bad request)
|
|
676
|
+
AuthenticationException, // 401
|
|
677
|
+
ForbiddenException, // 403 (e.g. read-only token on a write)
|
|
678
|
+
NotFoundException, // 404
|
|
679
|
+
ConflictException, // 409 (e.g. duplicate collection)
|
|
680
|
+
ServerException, // 5xx
|
|
681
|
+
} from 'endee';
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
await collection.upsert(objects);
|
|
685
|
+
} catch (e) {
|
|
686
|
+
if (e instanceof NotFoundException) {
|
|
687
|
+
console.error('not found:', e.message);
|
|
688
|
+
} else if (e instanceof EndeeException) {
|
|
689
|
+
// catches all of the above
|
|
690
|
+
console.error('request failed:', e.message);
|
|
691
|
+
} else {
|
|
692
|
+
throw e;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
All exception classes are importable from `endee`: `EndeeException` (base),
|
|
698
|
+
`APIException`, `AuthenticationException`, `ForbiddenException`,
|
|
699
|
+
`NotFoundException`, `ConflictException`, `ServerException`,
|
|
700
|
+
`SubscriptionException`.
|
|
701
|
+
|
|
702
|
+
Client-side input mistakes (bad dimensions, mismatched sparse lengths, oversized
|
|
703
|
+
batch, sum-of-weights ≠ 1.0, etc.) throw a plain `Error` **before** any network
|
|
704
|
+
call.
|
|
705
|
+
|
|
706
|
+
---
|
|
707
|
+
|
|
708
|
+
## End-to-end example
|
|
709
|
+
|
|
710
|
+
See `tests/integration_v2.ts` — a full walkthrough (create → multi-field upsert →
|
|
711
|
+
every search mode → object ops → maintenance → cleanup). Run it against a local
|
|
712
|
+
server with a db token:
|
|
713
|
+
|
|
714
|
+
```bash
|
|
715
|
+
npx tsx tests/integration_v2.ts --token my_db:xxxx --url http://localhost:8080/api/v2
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
## License
|
|
719
|
+
|
|
720
|
+
MIT
|