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 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
+ ```
@@ -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
+ }