@zukhruf/embeddings 0.0.0-stage → 0.3.47

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ezzabuzaid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,77 @@
1
- # Temporary Holding Version
1
+ # @zukhruf/embeddings
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A program that compares texts by meaning needs a vector for each text, and the model call that makes the vector is slow. This package keeps the vectors of documents in a SQLite database. An upsert calls the model only for new or changed text. A search gives the nearest documents of a document, by exact cosine similarity, from the documents published near it. This kind of package is also known as a vector store.
4
+
5
+ The words in these documents have one meaning each. See the glossary in [CONTEXT.md](./CONTEXT.md).
6
+
7
+ ## Use it
8
+
9
+ This project is an experiment. It uses `node:sqlite` and an embedding model of the [AI SDK](https://ai-sdk.dev). The package does not start a model or a server. Give it a model that is ready.
10
+
11
+ ```sh
12
+ npm install @zukhruf/embeddings ai
13
+ ```
14
+
15
+ ```ts
16
+ import { DatabaseSync } from 'node:sqlite';
17
+
18
+ import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
19
+
20
+ import { Collection } from '@zukhruf/embeddings';
21
+
22
+ const local = createOpenAICompatible({
23
+ name: 'llama.cpp',
24
+ baseURL: 'http://127.0.0.1:8080/v1',
25
+ });
26
+ using database = new DatabaseSync('embeddings.sqlite');
27
+ const collection = new Collection({
28
+ database,
29
+ model: local.embeddingModel('embeddinggemma'),
30
+ });
31
+
32
+ await collection.upsert([
33
+ {
34
+ id: 'a',
35
+ text: 'Storm closes the port',
36
+ publishedAt: new Date('2026-10-20'),
37
+ },
38
+ {
39
+ id: 'b',
40
+ text: 'Port shut as storm hits',
41
+ publishedAt: new Date('2026-10-21'),
42
+ },
43
+ { id: 'c', text: 'Port opens again', publishedAt: new Date('2026-11-30') },
44
+ ]);
45
+
46
+ collection.nearest('a', { k: 10, withinDays: 7 }); // [{ id: 'b', similarity: … }]
47
+ collection.similarity('a', 'c');
48
+ ```
49
+
50
+ ## API
51
+
52
+ - `new Collection({ database, model })`. The caller opens and closes `database`. The collection makes its table `embedding_document` in it. `model` is an AI SDK embedding model object.
53
+ - `upsert(documents)` stores each document. It calls the model, through `embedMany`, only for a document whose text or model changed. It stores the new publish date of each document.
54
+ - `nearest(id, { k, withinDays })` gives at most `k` neighbours of the document, with the highest similarity first. A neighbour has a publish date at most `withinDays` days before or after the publish date of the document. Equal similarities sort by id.
55
+ - `neighbourGraph({ from, to, k, withinDays })` gives an iterator of `[id, neighbours]` entries, one for each document published from `from` to `to`, in the order of the publish dates and then of the ids. The neighbours of each entry are equal to the result of `nearest(id, { k, withinDays })`. A document outside the range has no entry, but it can be a neighbour of a document in the range. The call reads and decodes each vector one time ([ADR 0004](./docs/adr/0004-a-neighbour-graph-decodes-each-vector-once.md)). Use it when you need the neighbours of many documents.
56
+ - `similarity(a, b)` gives the cosine similarity of two stored documents.
57
+
58
+ ## Errors
59
+
60
+ - `UnknownDocumentError`: `nearest` or `similarity` got an id that the collection does not store.
61
+ - `DimensionMismatchError`: the model gave a vector with a dimension that is not the dimension of the stored vectors. The upsert stores none of its documents. To change to a model with another dimension, upsert all documents in one call.
62
+
63
+ ## Limits
64
+
65
+ - One database holds one collection.
66
+ - The collection stores the vector of the text, not the text. The caller chooses the text, for example a title and the first sentences.
67
+ - A document has one vector. A long text gives one mean vector, and its details become a small part of it.
68
+ - The search reads each document in the window ([ADR 0001](./docs/adr/0001-search-compares-each-document-in-the-window.md)). A window of many thousand documents is slow. A neighbour graph decodes each vector once, but it still calculates a similarity for each pair of documents in a window.
69
+ - After a model change, upsert all documents again ([ADR 0002](./docs/adr/0002-the-hash-includes-the-model.md)).
70
+
71
+ ## Decisions
72
+
73
+ - [ADR 0001: A search compares each document in the window, with no index](./docs/adr/0001-search-compares-each-document-in-the-window.md)
74
+ - [ADR 0002: The hash includes the model](./docs/adr/0002-the-hash-includes-the-model.md)
75
+ - [ADR 0003: All vectors of a collection have one dimension](./docs/adr/0003-all-vectors-of-a-collection-have-one-dimension.md)
76
+ - [ADR 0004: A neighbour graph decodes each vector once](./docs/adr/0004-a-neighbour-graph-decodes-each-vector-once.md)
77
+ - [Code copied from @deepagents/retrieval](./docs/copied-from-deepagents-retrieval.md)
@@ -0,0 +1,32 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import { type EmbeddingModel } from 'ai';
3
+ export type Document = {
4
+ readonly id: string;
5
+ readonly text: string;
6
+ readonly publishedAt: Date;
7
+ };
8
+ export type Neighbour = {
9
+ readonly id: string;
10
+ readonly similarity: number;
11
+ };
12
+ export type CollectionOptions = {
13
+ readonly database: DatabaseSync;
14
+ readonly model: Exclude<EmbeddingModel, string>;
15
+ };
16
+ export declare class Collection {
17
+ #private;
18
+ constructor(options: CollectionOptions);
19
+ upsert(documents: readonly Document[]): Promise<void>;
20
+ nearest(id: string, options: {
21
+ readonly k: number;
22
+ readonly withinDays: number;
23
+ }): Neighbour[];
24
+ neighbourGraph(options: {
25
+ readonly from: Date;
26
+ readonly to: Date;
27
+ readonly k: number;
28
+ readonly withinDays: number;
29
+ }): IteratorObject<[string, Neighbour[]]>;
30
+ similarity(a: string, b: string): number;
31
+ }
32
+ //# sourceMappingURL=collection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAE,KAAK,cAAc,EAAa,MAAM,IAAI,CAAC;AAWpD,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAE9B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAGhC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;CACjD,CAAC;AAKF,qBAAa,UAAU;;gBAIT,OAAO,EAAE,iBAAiB;IAQhC,MAAM,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAsD3D,OAAO,CACL,EAAE,EAAE,MAAM,EACV,OAAO,EAAE;QAAE,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAC3D,SAAS,EAAE;IAoBd,cAAc,CAAC,OAAO,EAAE;QACtB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;QACpB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;QAClB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;KAC7B,GAAG,cAAc,CAAC,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IAgCzC,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM;CAqDzC"}
@@ -0,0 +1,159 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { embedMany } from 'ai';
3
+ import { transaction } from '@zukhruf/sqlite-transaction';
4
+ import { DimensionMismatchError } from "./dimension-mismatch-error.js";
5
+ import { collectionSchema } from "./schema.js";
6
+ import { UnknownDocumentError } from "./unknown-document-error.js";
7
+ import { UnitVector } from "./vector.js";
8
+ const DAY_MS = 86_400_000;
9
+ // The embeddings of documents, one vector for each document, in a SQLite
10
+ // database. A search compares the document with each document published
11
+ // inside the window, with no index (docs/adr/0001).
12
+ export class Collection {
13
+ #database;
14
+ #model;
15
+ constructor(options) {
16
+ this.#database = options.database;
17
+ this.#model = options.model;
18
+ collectionSchema.apply(this.#database);
19
+ }
20
+ // Embeds the documents whose text or model changed since their last upsert,
21
+ // and stores the publish date of each document.
22
+ async upsert(documents) {
23
+ const stored = this.#database.prepare('SELECT "hash" FROM "embedding_document" WHERE "id" = ?');
24
+ const hashed = documents.map((document) => ({
25
+ document,
26
+ hash: this.#hash(document.text),
27
+ }));
28
+ const { changed = [], unchanged = [] } = Object.groupBy(hashed, ({ document, hash }) => stored.get(document.id)?.hash === hash ? 'unchanged' : 'changed');
29
+ const vectors = await this.#embed(changed.map(({ document }) => document.text));
30
+ transaction(this.#database, () => {
31
+ const write = this.#database.prepare(`INSERT INTO "embedding_document" ("id", "hash", "published_at", "vector") VALUES (?, ?, ?, ?)
32
+ ON CONFLICT ("id") DO UPDATE SET "hash" = excluded."hash", "published_at" = excluded."published_at", "vector" = excluded."vector"`);
33
+ for (const [index, { document, hash }] of changed.entries()) {
34
+ const vector = vectors[index];
35
+ if (vector === undefined)
36
+ throw new Error(`The model gave ${vectors.length} embeddings for ${changed.length} texts.`);
37
+ write.run(document.id, hash, document.publishedAt.getTime(), vector.toBlob());
38
+ }
39
+ const move = this.#database.prepare('UPDATE "embedding_document" SET "published_at" = ? WHERE "id" = ?');
40
+ for (const { document } of unchanged)
41
+ move.run(document.publishedAt.getTime(), document.id);
42
+ this.#checkDimension(changed.map(({ document }) => document.id), vectors);
43
+ }, { mode: 'immediate' });
44
+ }
45
+ // The k documents most similar to the document, by cosine similarity, from
46
+ // those published at most withinDays before or after it. The document
47
+ // itself is not one of them. Equal similarities sort by id.
48
+ nearest(id, options) {
49
+ const { vector, publishedAt } = this.#read(id);
50
+ const window = options.withinDays * DAY_MS;
51
+ const candidates = this.#database
52
+ .prepare('SELECT "id", "vector" FROM "embedding_document" WHERE "published_at" BETWEEN ? AND ? AND "id" != ?')
53
+ .all(publishedAt - window, publishedAt + window, id)
54
+ .map((row) => ({
55
+ id: String(row.id),
56
+ vector: UnitVector.fromBlob(blob(row.vector)),
57
+ }));
58
+ return ranked(vector, candidates, options.k);
59
+ }
60
+ // The k nearest neighbours of each document published from `from` to `to`.
61
+ // Each entry is what nearest gives for the document with the same k and
62
+ // withinDays. The call reads and decodes each vector once
63
+ // (docs/adr/0004). The entries come in the order of the publish dates, then
64
+ // of the ids.
65
+ neighbourGraph(options) {
66
+ const from = options.from.getTime();
67
+ const to = options.to.getTime();
68
+ const window = options.withinDays * DAY_MS;
69
+ const documents = this.#database
70
+ .prepare('SELECT "id", "published_at", "vector" FROM "embedding_document" WHERE "published_at" BETWEEN ? AND ? ORDER BY "published_at", "id"')
71
+ .all(from - window, to + window)
72
+ .map((row) => ({
73
+ id: String(row.id),
74
+ publishedAt: Number(row.published_at),
75
+ vector: UnitVector.fromBlob(blob(row.vector)),
76
+ }));
77
+ return documents
78
+ .values()
79
+ .filter(({ publishedAt }) => publishedAt >= from && publishedAt <= to)
80
+ .map((document) => [
81
+ document.id,
82
+ ranked(document.vector, documents.filter((other) => other.id !== document.id &&
83
+ Math.abs(other.publishedAt - document.publishedAt) <= window), options.k),
84
+ ]);
85
+ }
86
+ // The cosine similarity of two stored documents.
87
+ similarity(a, b) {
88
+ return this.#read(a).vector.cosine(this.#read(b).vector);
89
+ }
90
+ // No texts make no call to the model.
91
+ async #embed(texts) {
92
+ if (texts.length === 0)
93
+ return [];
94
+ const { embeddings } = await embedMany({
95
+ model: this.#model,
96
+ values: texts,
97
+ });
98
+ return embeddings.map((embedding) => UnitVector.normalize(embedding));
99
+ }
100
+ // The model is part of the hash, so a new model embeds the text again.
101
+ #hash(text) {
102
+ return createHash('sha256')
103
+ .update(JSON.stringify([this.#model.provider, this.#model.modelId, text]))
104
+ .digest('hex');
105
+ }
106
+ #read(id) {
107
+ const row = this.#database
108
+ .prepare('SELECT "vector", "published_at" FROM "embedding_document" WHERE "id" = ?')
109
+ .get(id);
110
+ if (row === undefined)
111
+ throw new UnknownDocumentError(id);
112
+ return {
113
+ vector: UnitVector.fromBlob(blob(row.vector)),
114
+ publishedAt: Number(row.published_at),
115
+ };
116
+ }
117
+ // Before the upsert, each stored vector has one dimension. The new vectors
118
+ // must have the dimension of a vector that the upsert did not replace, and
119
+ // of each other.
120
+ #checkDimension(replaced, vectors) {
121
+ const [first] = vectors;
122
+ if (first === undefined)
123
+ return;
124
+ const kept = this.#database
125
+ .prepare('SELECT length("vector") AS "bytes" FROM "embedding_document" WHERE "id" NOT IN (SELECT "value" FROM json_each(?)) LIMIT 1')
126
+ .get(JSON.stringify(replaced));
127
+ const expected = kept === undefined
128
+ ? first.dimension
129
+ : Number(kept.bytes) / Float32Array.BYTES_PER_ELEMENT;
130
+ for (const vector of vectors)
131
+ if (vector.dimension !== expected)
132
+ throw new DimensionMismatchError(expected, vector.dimension);
133
+ }
134
+ }
135
+ // The k candidates most similar to the vector. Equal similarities sort by id.
136
+ function ranked(vector, candidates, k) {
137
+ return candidates
138
+ .map((candidate) => ({
139
+ id: candidate.id,
140
+ similarity: vector.cosine(candidate.vector),
141
+ }))
142
+ .toSorted((left, right) => right.similarity - left.similarity || compareIds(left.id, right.id))
143
+ .slice(0, k);
144
+ }
145
+ // Compares by code unit, not by locale, so the order is the same on every
146
+ // machine.
147
+ function compareIds(left, right) {
148
+ if (left < right)
149
+ return -1;
150
+ if (left > right)
151
+ return 1;
152
+ return 0;
153
+ }
154
+ function blob(value) {
155
+ if (!(value instanceof Uint8Array))
156
+ throw new TypeError('A stored vector must be a BLOB.');
157
+ return value;
158
+ }
159
+ //# sourceMappingURL=collection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection.js","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export declare class DimensionMismatchError extends Error {
2
+ readonly expected: number;
3
+ readonly actual: number;
4
+ constructor(expected: number, actual: number);
5
+ }
6
+ //# sourceMappingURL=dimension-mismatch-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dimension-mismatch-error.d.ts","sourceRoot":"","sources":["../src/dimension-mismatch-error.ts"],"names":[],"mappings":"AAEA,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAQ7C"}
@@ -0,0 +1,13 @@
1
+ // The model gave a vector whose dimension is not the dimension of the vectors
2
+ // that the collection stores.
3
+ export class DimensionMismatchError extends Error {
4
+ expected;
5
+ actual;
6
+ constructor(expected, actual) {
7
+ super(`The collection stores vectors of dimension ${expected}, and the model gave a vector of dimension ${actual}. Upsert every document again with the new model, in one call.`);
8
+ this.name = 'DimensionMismatchError';
9
+ this.expected = expected;
10
+ this.actual = actual;
11
+ }
12
+ }
13
+ //# sourceMappingURL=dimension-mismatch-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dimension-mismatch-error.js","sourceRoot":"","sources":["../src/dimension-mismatch-error.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,8BAA8B;AAC9B,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IACtC,QAAQ,CAAS;IACjB,MAAM,CAAS;IAExB,YAAY,QAAgB,EAAE,MAAc;QAC1C,KAAK,CACH,8CAA8C,QAAQ,8CAA8C,MAAM,gEAAgE,CAC3K,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACrC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF"}
@@ -0,0 +1,4 @@
1
+ export { Collection, type CollectionOptions, type Document, type Neighbour, } from './collection.ts';
2
+ export { DimensionMismatchError } from './dimension-mismatch-error.ts';
3
+ export { UnknownDocumentError } from './unknown-document-error.ts';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,QAAQ,EACb,KAAK,SAAS,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { Collection, } from "./collection.js";
2
+ export { DimensionMismatchError } from "./dimension-mismatch-error.js";
3
+ export { UnknownDocumentError } from "./unknown-document-error.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,GAIX,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { Declaration } from '@zukhruf/sqlite-drift';
2
+ export declare const collectionSchema: Declaration;
3
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAKpD,eAAO,MAAM,gBAAgB,aAc5B,CAAC"}
package/dist/schema.js ADDED
@@ -0,0 +1,17 @@
1
+ import { Declaration } from '@zukhruf/sqlite-drift';
2
+ // The documents of a collection. Every row derives from a caller's document
3
+ // and the model, so a stale table is replaced with its rows: the next upsert
4
+ // of the caller embeds the documents again (docs/adr/0003).
5
+ export const collectionSchema = new Declaration([
6
+ `CREATE TABLE embedding_document (
7
+ id TEXT PRIMARY KEY,
8
+ -- sha256 of the model's provider and id and the text (docs/adr/0002).
9
+ hash TEXT NOT NULL,
10
+ -- Milliseconds since the Unix epoch.
11
+ published_at INTEGER NOT NULL,
12
+ -- A unit vector of float32 values, in the byte order of the platform.
13
+ vector BLOB NOT NULL
14
+ ) STRICT`,
15
+ `CREATE INDEX embedding_document_published_at ON embedding_document (published_at)`,
16
+ ], { rows: 'replace' });
17
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEpD,4EAA4E;AAC5E,6EAA6E;AAC7E,4DAA4D;AAC5D,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,WAAW,CAC7C;IACE;;;;;;;;SAQK;IACL,mFAAmF;CACpF,EACD,EAAE,IAAI,EAAE,SAAS,EAAE,CACpB,CAAC"}
@@ -0,0 +1,5 @@
1
+ export declare class UnknownDocumentError extends Error {
2
+ readonly id: string;
3
+ constructor(id: string);
4
+ }
5
+ //# sourceMappingURL=unknown-document-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unknown-document-error.d.ts","sourceRoot":"","sources":["../src/unknown-document-error.ts"],"names":[],"mappings":"AACA,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;gBAER,EAAE,EAAE,MAAM;CAKvB"}
@@ -0,0 +1,10 @@
1
+ // The collection stores no document with this id.
2
+ export class UnknownDocumentError extends Error {
3
+ id;
4
+ constructor(id) {
5
+ super(`The collection stores no document with the id ${id}.`);
6
+ this.name = 'UnknownDocumentError';
7
+ this.id = id;
8
+ }
9
+ }
10
+ //# sourceMappingURL=unknown-document-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unknown-document-error.js","sourceRoot":"","sources":["../src/unknown-document-error.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IACpC,EAAE,CAAS;IAEpB,YAAY,EAAU;QACpB,KAAK,CAAC,iDAAiD,EAAE,GAAG,CAAC,CAAC;QAC9D,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;IACf,CAAC;CACF"}
@@ -0,0 +1,10 @@
1
+ export declare class UnitVector {
2
+ #private;
3
+ private constructor();
4
+ static normalize(values: readonly number[]): UnitVector;
5
+ static fromBlob(blob: Uint8Array): UnitVector;
6
+ get dimension(): number;
7
+ toBlob(): Uint8Array;
8
+ cosine(other: UnitVector): number;
9
+ }
10
+ //# sourceMappingURL=vector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vector.d.ts","sourceRoot":"","sources":["../src/vector.ts"],"names":[],"mappings":"AAEA,qBAAa,UAAU;;IAGrB,OAAO;IAKP,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU;IAavD,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU;IAI7C,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,MAAM,IAAI,UAAU;IAWpB,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM;CAQlC"}
package/dist/vector.js ADDED
@@ -0,0 +1,38 @@
1
+ // A unit vector of float32 values. A dot product of two unit vectors is their
2
+ // cosine similarity.
3
+ export class UnitVector {
4
+ #values;
5
+ constructor(values) {
6
+ this.#values = values;
7
+ }
8
+ // Divides each value by the length of the vector.
9
+ static normalize(values) {
10
+ const length = Math.sqrt(values.reduce((sum, value) => sum + value * value, 0));
11
+ if (length === 0 || !Number.isFinite(length))
12
+ throw new RangeError(`An embedding must have a finite length that is not zero. Its length is ${length}.`);
13
+ return new UnitVector(Float32Array.from(values, (value) => value / length));
14
+ }
15
+ // The bytes that SQLite gives are copied, because a Float32Array needs an
16
+ // offset that is a multiple of 4.
17
+ static fromBlob(blob) {
18
+ return new UnitVector(new Float32Array(Uint8Array.from(blob).buffer));
19
+ }
20
+ get dimension() {
21
+ return this.#values.length;
22
+ }
23
+ toBlob() {
24
+ return new Uint8Array(this.#values.buffer, this.#values.byteOffset, this.#values.byteLength);
25
+ }
26
+ // An indexed loop: an entries() iterator with destructuring made the
27
+ // nearest search of each document in a window of 1,001 documents about two
28
+ // times slower.
29
+ cosine(other) {
30
+ const values = this.#values;
31
+ const others = other.#values;
32
+ let sum = 0;
33
+ for (let index = 0; index < values.length; index++)
34
+ sum += (values[index] ?? 0) * (others[index] ?? 0);
35
+ return sum;
36
+ }
37
+ }
38
+ //# sourceMappingURL=vector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vector.js","sourceRoot":"","sources":["../src/vector.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,qBAAqB;AACrB,MAAM,OAAO,UAAU;IACZ,OAAO,CAAe;IAE/B,YAAoB,MAAoB;QACtC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;IACxB,CAAC;IAED,kDAAkD;IAClD,MAAM,CAAC,SAAS,CAAC,MAAyB;QACxC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CACtB,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,GAAG,KAAK,EAAE,CAAC,CAAC,CACtD,CAAC;QACF,IAAI,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC1C,MAAM,IAAI,UAAU,CAClB,0EAA0E,MAAM,GAAG,CACpF,CAAC;QACJ,OAAO,IAAI,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC;IAC9E,CAAC;IAED,0EAA0E;IAC1E,kCAAkC;IAClC,MAAM,CAAC,QAAQ,CAAC,IAAgB;QAC9B,OAAO,IAAI,UAAU,CAAC,IAAI,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;IAC7B,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,UAAU,CACnB,IAAI,CAAC,OAAO,CAAC,MAAM,EACnB,IAAI,CAAC,OAAO,CAAC,UAAU,EACvB,IAAI,CAAC,OAAO,CAAC,UAAU,CACxB,CAAC;IACJ,CAAC;IAED,qEAAqE;IACrE,2EAA2E;IAC3E,gBAAgB;IAChB,MAAM,CAAC,KAAiB;QACtB,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC;QAC7B,IAAI,GAAG,GAAG,CAAC,CAAC;QACZ,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE;YAChD,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QACrD,OAAO,GAAG,CAAC;IACb,CAAC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
2
  "name": "@zukhruf/embeddings",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.47",
4
+ "description": "A collection of document embeddings in SQLite. It embeds only new or changed text through an AI SDK embedding model, and finds the nearest documents of a document by exact cosine similarity inside a publish-date window.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/JanuaryLabs/zukhruf",
9
+ "directory": "packages/embeddings"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/JanuaryLabs/zukhruf/issues"
13
+ },
14
+ "homepage": "https://github.com/JanuaryLabs/zukhruf/tree/main/packages/embeddings#readme",
15
+ "keywords": [
16
+ "embeddings",
17
+ "vector-search",
18
+ "nearest-neighbour",
19
+ "cosine-similarity",
20
+ "sqlite",
21
+ "node-sqlite",
22
+ "ai-sdk"
23
+ ],
24
+ "type": "module",
25
+ "exports": {
26
+ "./package.json": "./package.json",
27
+ ".": {
28
+ "@zukhruf/source": "./src/index.ts",
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js",
31
+ "default": "./dist/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "!dist/spec/**",
37
+ "!**/*.tsbuildinfo",
38
+ "!**/*.test.*"
39
+ ],
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "@zukhruf/sqlite-drift": "^0.3.47",
45
+ "@zukhruf/sqlite-transaction": "^0.3.47",
46
+ "ai": "^7.0.154"
47
+ },
48
+ "devDependencies": {
49
+ "@zukhruf/testing": "^0.3.47"
50
+ }
51
+ }