quiver-client 0.22.0
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 +91 -0
- package/dist/client.d.ts +258 -0
- package/dist/client.js +431 -0
- package/dist/dcpe.d.ts +57 -0
- package/dist/dcpe.js +336 -0
- package/dist/encryption.d.ts +25 -0
- package/dist/encryption.js +154 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/vector.d.ts +26 -0
- package/dist/vector.js +170 -0
- package/package.json +87 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# quiver-client (TypeScript)
|
|
2
|
+
|
|
3
|
+
A small, dependency-free TypeScript/JavaScript client for
|
|
4
|
+
[Quiver](https://github.com/achref-soua/quiver) — a security-first, memory-frugal
|
|
5
|
+
vector database. It mirrors the server's REST contract and uses the global
|
|
6
|
+
`fetch`, so it runs on Node ≥ 20 and modern runtimes with no dependencies.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm add quiver-client
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { Client } from "quiver-client";
|
|
14
|
+
|
|
15
|
+
const q = new Client("http://127.0.0.1:6333", { apiKey: "…" });
|
|
16
|
+
|
|
17
|
+
// Create a memory-frugal, disk-resident collection (PQ codes in RAM,
|
|
18
|
+
// graph + vectors on encrypted SSD), or use "hnsw" (default) / "ivf".
|
|
19
|
+
await q.createCollection("items", 384, {
|
|
20
|
+
metric: "cosine",
|
|
21
|
+
index: "disk_vamana",
|
|
22
|
+
pqSubspaces: 48,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await q.upsert("items", [
|
|
26
|
+
{ id: "a", vector: embed("…"), payload: { tag: "x" } },
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const hits = await q.search("items", embed("query"), {
|
|
30
|
+
k: 5,
|
|
31
|
+
filter: { eq: { field: "tag", value: "x" } },
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Embeddings are produced by the caller — Quiver is model-agnostic.
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
| Method | Description |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `createCollection(name, dim, { metric?, index?, pqSubspaces? })` | Create a collection and pick its index |
|
|
42
|
+
| `listCollections()` / `getCollection(name)` / `deleteCollection(name)` | Collection CRUD |
|
|
43
|
+
| `upsert(collection, points)` / `deletePoints(collection, ids)` / `getPoint(collection, id)` | Points |
|
|
44
|
+
| `search(collection, vector, { k?, filter?, efSearch?, withPayload?, withVector? })` | Filtered k-NN |
|
|
45
|
+
| `upsertDocuments(collection, documents)` / `deleteDocuments(collection, ids)` | Multi-vector (ColBERT) documents |
|
|
46
|
+
| `searchMultiVector(collection, query, { k?, filter? })` | MaxSim late-interaction search |
|
|
47
|
+
| `healthz()` | Liveness probe |
|
|
48
|
+
|
|
49
|
+
Create a multi-vector collection with `createCollection(name, dim, { metric: "cosine", multivector: true })`, then index documents as token sets and rank them by MaxSim.
|
|
50
|
+
|
|
51
|
+
Errors from the server (or transport) reject with a `QuiverError` carrying the
|
|
52
|
+
HTTP `status`. A custom `fetch` can be injected via the constructor for testing
|
|
53
|
+
or a bespoke transport.
|
|
54
|
+
|
|
55
|
+
## Client-side payload encryption
|
|
56
|
+
|
|
57
|
+
Seal payload fields with a key Quiver never sees; the server stores and returns
|
|
58
|
+
ciphertext it cannot read (ADR-0012). The helper lives at the
|
|
59
|
+
`quiver-client/encryption` subpath, so the core client stays dependency-free —
|
|
60
|
+
install the optional peer dependency to use it:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pnpm add @stablelib/xchacha20poly1305
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { Client } from "quiver-client";
|
|
68
|
+
import { PayloadCipher } from "quiver-client/encryption";
|
|
69
|
+
|
|
70
|
+
const cipher = PayloadCipher.fromHex("…64 hex chars…"); // a dedicated key, never the at-rest one
|
|
71
|
+
const q = new Client("http://127.0.0.1:6333", { apiKey: "…" });
|
|
72
|
+
|
|
73
|
+
// Keep `tier` server-filterable; hide `ssn` from the server.
|
|
74
|
+
const payload = { tier: "gold", ...cipher.seal({ ssn: "078-05-1120" }) };
|
|
75
|
+
await q.upsert("people", [{ id: "p1", vector: embed("…"), payload }]);
|
|
76
|
+
|
|
77
|
+
const point = await q.getPoint("people", "p1");
|
|
78
|
+
const secret = cipher.open(point!.payload); // -> { ssn: "078-05-1120" }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The envelope (XChaCha20-Poly1305) matches the Rust reference and the Python SDK
|
|
82
|
+
byte-for-byte — see [client-side encryption](https://github.com/achref-soua/quiver/blob/main/docs/security/crypto.md#client-side-payload-encryption-adr-0012).
|
|
83
|
+
|
|
84
|
+
## Develop
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pnpm install
|
|
88
|
+
pnpm typecheck # tsc --noEmit
|
|
89
|
+
pnpm test # vitest
|
|
90
|
+
pnpm build # tsc -> dist/ (ESM + .d.ts)
|
|
91
|
+
```
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { VectorCipher } from "./vector.js";
|
|
2
|
+
/** An error from the Quiver server or the transport. `status` is the HTTP code
|
|
3
|
+
* when the failure came from the server, or `undefined` for a transport error. */
|
|
4
|
+
export declare class QuiverError extends Error {
|
|
5
|
+
readonly status?: number;
|
|
6
|
+
constructor(message: string, status?: number);
|
|
7
|
+
}
|
|
8
|
+
/** A point to upsert: a caller-supplied id, its vector, and an optional payload. */
|
|
9
|
+
export interface Point {
|
|
10
|
+
id: string;
|
|
11
|
+
vector: number[];
|
|
12
|
+
payload?: unknown;
|
|
13
|
+
}
|
|
14
|
+
/** A search hit (or a fetched point, with `score` 0). */
|
|
15
|
+
export interface Match {
|
|
16
|
+
id: string;
|
|
17
|
+
score: number;
|
|
18
|
+
payload?: unknown;
|
|
19
|
+
vector?: number[];
|
|
20
|
+
}
|
|
21
|
+
/** What a {@link QuiverClient.snapshot} captured (ADR-0050). */
|
|
22
|
+
export interface SnapshotInfo {
|
|
23
|
+
manifestVersion: number;
|
|
24
|
+
files: number;
|
|
25
|
+
bytes: number;
|
|
26
|
+
}
|
|
27
|
+
/** A multi-vector (late-interaction / ColBERT) document: an id, its set of token
|
|
28
|
+
* vectors, and an optional payload. */
|
|
29
|
+
export interface Document {
|
|
30
|
+
id: string;
|
|
31
|
+
vectors: number[][];
|
|
32
|
+
payload?: unknown;
|
|
33
|
+
}
|
|
34
|
+
/** A multi-vector document hit, ranked by MaxSim late interaction. */
|
|
35
|
+
export interface DocumentMatch {
|
|
36
|
+
id: string;
|
|
37
|
+
score: number;
|
|
38
|
+
payload?: unknown;
|
|
39
|
+
vectors?: number[][];
|
|
40
|
+
}
|
|
41
|
+
/** The index structure a collection is served by (ADR-0007, ADR-0034).
|
|
42
|
+
* `colbert` is the ColBERTv2/PLAID token-pool index for multivector collections. */
|
|
43
|
+
export type IndexKind = "hnsw" | "vamana" | "disk_vamana" | "ivf" | "colbert";
|
|
44
|
+
/** A distance metric. */
|
|
45
|
+
export type Metric = "l2" | "cosine" | "dot";
|
|
46
|
+
/** Client-side vector encryption mode — the server never holds the key (ADR-0031,
|
|
47
|
+
* ADR-0032): `none` (plaintext, the server ranks), `dcpe` (the server ranks
|
|
48
|
+
* ciphertexts but leaks distance ordering by design; not semantically secure), or
|
|
49
|
+
* `client_side` (semantically secure opaque AEAD; the server does not rank, so you
|
|
50
|
+
* fetch and rank locally). */
|
|
51
|
+
export type VectorEncryption = "none" | "dcpe" | "client_side";
|
|
52
|
+
/** The value type of a filterable payload field (ADR-0022). */
|
|
53
|
+
export type FieldType = "keyword" | "numeric";
|
|
54
|
+
/** A payload field declared filterable for hybrid (pre-filtered) search. */
|
|
55
|
+
export interface FilterableField {
|
|
56
|
+
path: string;
|
|
57
|
+
fieldType?: FieldType;
|
|
58
|
+
}
|
|
59
|
+
/** Metadata about a collection. */
|
|
60
|
+
export interface CollectionInfo {
|
|
61
|
+
name: string;
|
|
62
|
+
dim: number;
|
|
63
|
+
metric: string;
|
|
64
|
+
count: number;
|
|
65
|
+
index: IndexKind;
|
|
66
|
+
pqSubspaces?: number;
|
|
67
|
+
filterable: FilterableField[];
|
|
68
|
+
multivector: boolean;
|
|
69
|
+
vectorEncryption: VectorEncryption;
|
|
70
|
+
}
|
|
71
|
+
/** Options for constructing a {@link Client}. */
|
|
72
|
+
export interface ClientOptions {
|
|
73
|
+
apiKey?: string;
|
|
74
|
+
timeoutMs?: number;
|
|
75
|
+
/** Inject a `fetch` implementation (for tests or a custom transport). */
|
|
76
|
+
fetch?: typeof fetch;
|
|
77
|
+
}
|
|
78
|
+
/** Options for {@link Client.createCollection}. */
|
|
79
|
+
export interface CreateCollectionOptions {
|
|
80
|
+
metric?: Metric;
|
|
81
|
+
/** Index structure; `disk_vamana` is the memory-frugal disk path (l2/cosine);
|
|
82
|
+
* `colbert` is the ColBERTv2/PLAID token-pool index for multivector collections. */
|
|
83
|
+
index?: IndexKind;
|
|
84
|
+
/** Product-quantization subspaces for `disk_vamana` / `ivf` (must divide dim). */
|
|
85
|
+
pqSubspaces?: number;
|
|
86
|
+
/** Payload fields to index for hybrid (pre-filtered) search. */
|
|
87
|
+
filterable?: FilterableField[];
|
|
88
|
+
/** Create a multi-vector (late-interaction / ColBERT) collection. */
|
|
89
|
+
multivector?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Client-side vector encryption (the server never holds the key): `"dcpe"` is
|
|
92
|
+
* experimental property-preserving encryption (ADR-0031; the server ranks,
|
|
93
|
+
* requires `l2`, not semantically secure); `"client_side"` is semantically
|
|
94
|
+
* secure opaque AEAD (ADR-0032; the server does not rank — use
|
|
95
|
+
* {@link Client.fetch} / {@link Client.searchClientSide}). Defaults to `"none"`.
|
|
96
|
+
*/
|
|
97
|
+
vectorEncryption?: VectorEncryption;
|
|
98
|
+
}
|
|
99
|
+
/** Options for {@link Client.search}. */
|
|
100
|
+
export interface SearchOptions {
|
|
101
|
+
k?: number;
|
|
102
|
+
/** A Quiver payload filter expression (see the API docs). */
|
|
103
|
+
filter?: unknown;
|
|
104
|
+
efSearch?: number;
|
|
105
|
+
withPayload?: boolean;
|
|
106
|
+
withVector?: boolean;
|
|
107
|
+
}
|
|
108
|
+
/** A sparse query vector for hybrid search (ADR-0043): parallel dimension ids
|
|
109
|
+
* (`indices`) and weights (`values`). */
|
|
110
|
+
export interface SparseVector {
|
|
111
|
+
indices: number[];
|
|
112
|
+
values: number[];
|
|
113
|
+
}
|
|
114
|
+
/** Options for {@link Client.hybridSearch}. Provide `vector`, `sparse`, or both;
|
|
115
|
+
* at least one is required. */
|
|
116
|
+
export interface HybridSearchOptions {
|
|
117
|
+
/** Dense query vector (omit for pure-sparse/text search). */
|
|
118
|
+
vector?: number[];
|
|
119
|
+
/** Sparse query vector (omit for pure-dense/text search). */
|
|
120
|
+
sparse?: SparseVector;
|
|
121
|
+
/** Full-text query, tokenized server-side and scored by BM25 (ADR-0046). */
|
|
122
|
+
queryText?: string;
|
|
123
|
+
k?: number;
|
|
124
|
+
/** A Quiver payload filter expression (applied on both sides). */
|
|
125
|
+
filter?: unknown;
|
|
126
|
+
efSearch?: number;
|
|
127
|
+
/** RRF rank-bias constant (default 60). */
|
|
128
|
+
rrfK0?: number;
|
|
129
|
+
withPayload?: boolean;
|
|
130
|
+
withVector?: boolean;
|
|
131
|
+
}
|
|
132
|
+
/** A text point for {@link Client.upsertText} (ADR-0047): the server embeds
|
|
133
|
+
* `text` with the collection's configured provider and indexes it for BM25. */
|
|
134
|
+
export interface TextPoint {
|
|
135
|
+
id: string;
|
|
136
|
+
text: string;
|
|
137
|
+
payload?: unknown;
|
|
138
|
+
}
|
|
139
|
+
/** Options for {@link Client.searchText} (ADR-0047). */
|
|
140
|
+
export interface SearchTextOptions {
|
|
141
|
+
k?: number;
|
|
142
|
+
/** A Quiver payload filter expression. */
|
|
143
|
+
filter?: unknown;
|
|
144
|
+
efSearch?: number;
|
|
145
|
+
/** RRF rank-bias constant (default 60). */
|
|
146
|
+
rrfK0?: number;
|
|
147
|
+
withPayload?: boolean;
|
|
148
|
+
withVector?: boolean;
|
|
149
|
+
/** Opt-in: rerank the candidate pool with the collection's rerank provider. */
|
|
150
|
+
rerank?: boolean;
|
|
151
|
+
}
|
|
152
|
+
/** Options for {@link Client.fetch}. */
|
|
153
|
+
export interface FetchOptions {
|
|
154
|
+
/** A Quiver payload filter expression to narrow the set. */
|
|
155
|
+
filter?: unknown;
|
|
156
|
+
/** Maximum number of points to return (default 100). */
|
|
157
|
+
limit?: number;
|
|
158
|
+
withPayload?: boolean;
|
|
159
|
+
withVector?: boolean;
|
|
160
|
+
}
|
|
161
|
+
/** Options for {@link Client.searchClientSide}. */
|
|
162
|
+
export interface ClientSideSearchOptions {
|
|
163
|
+
k?: number;
|
|
164
|
+
/** A Quiver payload filter expression (applied server-side, on cleartext fields). */
|
|
165
|
+
filter?: unknown;
|
|
166
|
+
/** Metric to rank by, client-side (default `"l2"`). */
|
|
167
|
+
metric?: Metric;
|
|
168
|
+
/** How many candidates to fetch before ranking locally (default 10000). */
|
|
169
|
+
candidateLimit?: number;
|
|
170
|
+
}
|
|
171
|
+
/** A synchronous-feeling, promise-based Quiver REST client. */
|
|
172
|
+
export declare class Client {
|
|
173
|
+
#private;
|
|
174
|
+
constructor(baseUrl?: string, opts?: ClientOptions);
|
|
175
|
+
/** Create a collection. Rejects with {@link QuiverError} if the name is taken
|
|
176
|
+
* or the index/metric combination is unsupported. */
|
|
177
|
+
createCollection(name: string, dim: number, opts?: CreateCollectionOptions): Promise<CollectionInfo>;
|
|
178
|
+
/** List all collections. */
|
|
179
|
+
listCollections(): Promise<CollectionInfo[]>;
|
|
180
|
+
/** Fetch one collection's metadata. */
|
|
181
|
+
getCollection(name: string): Promise<CollectionInfo>;
|
|
182
|
+
/** Delete a collection; resolves to whether it existed. */
|
|
183
|
+
deleteCollection(name: string): Promise<boolean>;
|
|
184
|
+
/** Insert or replace points; resolves to the number upserted. */
|
|
185
|
+
upsert(collection: string, points: Point[]): Promise<number>;
|
|
186
|
+
/** Delete points by id; resolves to the number deleted. */
|
|
187
|
+
deletePoints(collection: string, ids: string[]): Promise<number>;
|
|
188
|
+
/** Fetch a point by id, or `null` if it does not exist. */
|
|
189
|
+
getPoint(collection: string, id: string): Promise<Match | null>;
|
|
190
|
+
/** Search for the `k` nearest points to `vector`, nearest first. */
|
|
191
|
+
search(collection: string, vector: number[], opts?: SearchOptions): Promise<Match[]>;
|
|
192
|
+
/** Hybrid search fused with Reciprocal Rank Fusion (ADR-0043/0046). Provide a
|
|
193
|
+
* dense `vector`, a `sparse` query vector, and/or a full-text `queryText` (scored
|
|
194
|
+
* by BM25) — at least one is required. The same payload `filter` applies to every
|
|
195
|
+
* side. */
|
|
196
|
+
hybridSearch(collection: string, opts?: HybridSearchOptions): Promise<Match[]>;
|
|
197
|
+
/** Embed each point's `text` server-side and upsert it (ADR-0047); the text is
|
|
198
|
+
* also indexed for BM25. Requires an `[embedding.<collection>]` provider on the
|
|
199
|
+
* server. Resolves to the number upserted. */
|
|
200
|
+
upsertText(collection: string, points: TextPoint[]): Promise<number>;
|
|
201
|
+
/** Embed `text` server-side and search dense ⊕ BM25, optionally reranking the
|
|
202
|
+
* candidate pool in one call (ADR-0047). Requires an `[embedding.<collection>]`
|
|
203
|
+
* provider (and a `[rerank.<collection>]` provider for `rerank: true`). */
|
|
204
|
+
searchText(collection: string, text: string, opts?: SearchTextOptions): Promise<Match[]>;
|
|
205
|
+
/** Fetch points without ranking; an optional payload `filter` narrows the set
|
|
206
|
+
* and `limit` bounds it. The retrieval path for `client_side`-encrypted
|
|
207
|
+
* collections (ADR-0032): the server returns the entitled set (each payload
|
|
208
|
+
* carries the sealed vector under `__quiver_vec__`) and you decrypt and rank
|
|
209
|
+
* locally (see {@link searchClientSide}). Also a general list-points call for
|
|
210
|
+
* any collection; returned matches carry `score` 0. */
|
|
211
|
+
fetch(collection: string, opts?: FetchOptions): Promise<Match[]>;
|
|
212
|
+
/** Upsert a large (sync or async) iterable of points in server-friendly
|
|
213
|
+
* batches; resolves to the total upserted. `batch` must stay within the
|
|
214
|
+
* server's `max_batch_size` (ADR-0040, default 1000). `onProgress` is called
|
|
215
|
+
* with the running total after each batch (may be sync or async). Mirrors the
|
|
216
|
+
* Python `upsert_iter`. */
|
|
217
|
+
upsertIter(collection: string, points: Iterable<Point> | AsyncIterable<Point>, opts?: {
|
|
218
|
+
batch?: number;
|
|
219
|
+
onProgress?: (total: number) => void | Promise<void>;
|
|
220
|
+
}): Promise<number>;
|
|
221
|
+
/** Yield points page by page, for export / re-embedding. The REST fetch is
|
|
222
|
+
* limit-bounded without a server cursor, so this returns up to `batch` points
|
|
223
|
+
* in one page — pass a narrowing `filter` for large collections (a server-side
|
|
224
|
+
* scroll cursor is a follow-up). Mirrors the Python async `scroll`. */
|
|
225
|
+
scroll(collection: string, opts?: {
|
|
226
|
+
filter?: unknown;
|
|
227
|
+
batch?: number;
|
|
228
|
+
withPayload?: boolean;
|
|
229
|
+
withVector?: boolean;
|
|
230
|
+
}): AsyncGenerator<Match>;
|
|
231
|
+
/** Delete every point matching `filter`; resolves to the number deleted.
|
|
232
|
+
* Fetches matching ids (paged by `batch`) and deletes them until none remain —
|
|
233
|
+
* useful for GDPR erasure and re-indexing. Mirrors the Python `delete_by_filter`. */
|
|
234
|
+
deleteByFilter(collection: string, filter: unknown, opts?: {
|
|
235
|
+
batch?: number;
|
|
236
|
+
}): Promise<number>;
|
|
237
|
+
/** Take a consistent online snapshot (backup) of the whole database into a
|
|
238
|
+
* server-local directory, which must not already exist (ADR-0050); admin-only.
|
|
239
|
+
* Resolves to the captured manifest version and the file/byte counts. */
|
|
240
|
+
snapshot(destination: string): Promise<SnapshotInfo>;
|
|
241
|
+
/** Nearest-neighbour search over a `client_side`-encrypted collection (ADR-0032),
|
|
242
|
+
* done entirely client-side: {@link fetch} the (optionally filtered) candidate
|
|
243
|
+
* set, decrypt each vector with `cipher` (a `VectorCipher` from
|
|
244
|
+
* `quiver-client/vector`), rank by metric, and return the top `k`. The server
|
|
245
|
+
* never ranks and never sees the key. This mode suits small/medium or
|
|
246
|
+
* pre-filtered collections; `candidateLimit` bounds how many points are fetched
|
|
247
|
+
* before ranking. Each result carries the decrypted `vector` and a `score` under
|
|
248
|
+
* the chosen metric. */
|
|
249
|
+
searchClientSide(collection: string, query: number[], cipher: VectorCipher, opts?: ClientSideSearchOptions): Promise<Match[]>;
|
|
250
|
+
/** Insert or replace multi-vector documents; resolves to the number upserted. */
|
|
251
|
+
upsertDocuments(collection: string, documents: Document[]): Promise<number>;
|
|
252
|
+
/** Delete multi-vector documents by id; resolves to the number deleted. */
|
|
253
|
+
deleteDocuments(collection: string, ids: string[]): Promise<number>;
|
|
254
|
+
/** Rank documents by MaxSim late interaction against the `query` token set. */
|
|
255
|
+
searchMultiVector(collection: string, query: number[][], opts?: SearchOptions): Promise<DocumentMatch[]>;
|
|
256
|
+
/** Whether the server's liveness probe succeeds. */
|
|
257
|
+
healthz(): Promise<boolean>;
|
|
258
|
+
}
|