@statewalker/indexer-sqlite 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +239 -2
- package/dist/block-ids.d.ts +20 -0
- package/dist/block-ids.d.ts.map +1 -0
- package/dist/block-ids.js +47 -0
- package/dist/block-ids.js.map +1 -0
- package/dist/create-sqlite-indexer.d.ts +18 -0
- package/dist/create-sqlite-indexer.d.ts.map +1 -0
- package/dist/create-sqlite-indexer.js +152 -0
- package/dist/create-sqlite-indexer.js.map +1 -0
- package/dist/enumeration.d.ts +18 -0
- package/dist/enumeration.d.ts.map +1 -0
- package/dist/enumeration.js +42 -0
- package/dist/enumeration.js.map +1 -0
- package/dist/fts-provider.d.ts +6 -0
- package/dist/fts-provider.d.ts.map +1 -0
- package/dist/fts-provider.js +143 -0
- package/dist/fts-provider.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/ngram-provider.d.ts +12 -0
- package/dist/ngram-provider.d.ts.map +1 -0
- package/dist/ngram-provider.js +142 -0
- package/dist/ngram-provider.js.map +1 -0
- package/dist/path-sql.d.ts +15 -0
- package/dist/path-sql.d.ts.map +1 -0
- package/dist/path-sql.js +23 -0
- package/dist/path-sql.js.map +1 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/vector-provider.d.ts +12 -0
- package/dist/vector-provider.d.ts.map +1 -0
- package/dist/vector-provider.js +114 -0
- package/dist/vector-provider.js.map +1 -0
- package/package.json +61 -3
- package/src/block-ids.ts +75 -0
- package/src/create-sqlite-indexer.ts +218 -0
- package/src/enumeration.ts +54 -0
- package/src/fts-provider.ts +170 -0
- package/src/index.ts +9 -0
- package/src/ngram-provider.ts +153 -0
- package/src/path-sql.ts +17 -0
- package/src/types.ts +26 -0
- package/src/vector-provider.ts +132 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { pathPrefixSql } from "./path-sql.js";
|
|
2
|
+
import { newEnumeration } from "./enumeration.js";
|
|
3
|
+
import { toAsyncIterable } from "@statewalker/indexer-core";
|
|
4
|
+
import { inTransaction } from "@statewalker/db-sqlite";
|
|
5
|
+
import { FULL_TEXT_TYPE } from "@statewalker/indexer-fulltext";
|
|
6
|
+
//#region src/fts-provider.ts
|
|
7
|
+
/** `en` gets the Porter stemmer; any other language (known or not) gets plain `unicode61`. */
|
|
8
|
+
function tokenizerFor(language) {
|
|
9
|
+
return language === "en" ? "porter unicode61 remove_diacritics 2" : "unicode61 remove_diacritics 2";
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* One FTS5 MATCH expression per query string: every word with a letter or a
|
|
13
|
+
* digit, double-quoted (so `AND`, `*`, `NEAR(` are plain words), OR-joined so
|
|
14
|
+
* partial matches are returned. `null` when nothing is searchable.
|
|
15
|
+
*/
|
|
16
|
+
function toMatchExpression(query) {
|
|
17
|
+
const words = query.split(/\s+/).filter((w) => /[\p{L}\p{N}]/u.test(w)).map((w) => `"${w.replaceAll("\"", "\"\"")}"`);
|
|
18
|
+
return words.length > 0 ? words.join(" OR ") : null;
|
|
19
|
+
}
|
|
20
|
+
function newSqliteFullTextProvider() {
|
|
21
|
+
return {
|
|
22
|
+
type: FULL_TEXT_TYPE,
|
|
23
|
+
async drop({ db, prefix }) {
|
|
24
|
+
await db.exec(`DROP TABLE IF EXISTS ${prefix}fts`);
|
|
25
|
+
},
|
|
26
|
+
async open({ db, prefix, blockIds }, config) {
|
|
27
|
+
const table = `${prefix}fts`;
|
|
28
|
+
const blocks = blockIds.table;
|
|
29
|
+
await db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS ${table} USING fts5(content, metadata UNINDEXED, tokenize='${tokenizerFor(config.language)}')`);
|
|
30
|
+
const list = newEnumeration(db, blockIds, `SELECT rowid AS id FROM ${table}`);
|
|
31
|
+
let closed = false;
|
|
32
|
+
const ensureOpen = () => {
|
|
33
|
+
if (closed) throw new Error("FullTextIndex is closed");
|
|
34
|
+
};
|
|
35
|
+
const collectSelectors = async (selectors) => {
|
|
36
|
+
const all = [];
|
|
37
|
+
for await (const s of toAsyncIterable(selectors)) all.push(s);
|
|
38
|
+
return all;
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
async getIndexInfo() {
|
|
42
|
+
return {
|
|
43
|
+
language: config.language,
|
|
44
|
+
metadata: config.metadata
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
async *search(params) {
|
|
48
|
+
ensureOpen();
|
|
49
|
+
const topK = params.topK ?? 100;
|
|
50
|
+
const scope = pathPrefixSql("b.path", params.paths ?? []);
|
|
51
|
+
const merged = /* @__PURE__ */ new Map();
|
|
52
|
+
for (const query of params.queries ?? []) {
|
|
53
|
+
const match = toMatchExpression(query);
|
|
54
|
+
if (!match) continue;
|
|
55
|
+
const rows = await db.query(`SELECT ${table}.rowid AS id, -bm25(${table}) AS score, snippet(${table}, 0, '', '', '…', 16) AS snippet FROM ${table} JOIN ${blocks} b ON b.id = ${table}.rowid WHERE ${table} MATCH ? AND ${scope.sql} ORDER BY score DESC LIMIT ?`, [
|
|
56
|
+
match,
|
|
57
|
+
...scope.params,
|
|
58
|
+
topK
|
|
59
|
+
]);
|
|
60
|
+
for (const r of rows) {
|
|
61
|
+
const id = Number(r.id);
|
|
62
|
+
const prev = merged.get(id);
|
|
63
|
+
if (!prev) merged.set(id, {
|
|
64
|
+
score: r.score,
|
|
65
|
+
best: r.score,
|
|
66
|
+
snippet: r.snippet
|
|
67
|
+
});
|
|
68
|
+
else {
|
|
69
|
+
prev.score += r.score;
|
|
70
|
+
if (r.score > prev.best) Object.assign(prev, {
|
|
71
|
+
best: r.score,
|
|
72
|
+
snippet: r.snippet
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const top = [...merged.entries()].sort((a, b) => b[1].score - a[1].score).slice(0, topK);
|
|
78
|
+
const refs = await blockIds.refs(db, top.map(([id]) => id));
|
|
79
|
+
for (const [id, hit] of top) {
|
|
80
|
+
const ref = refs.get(id);
|
|
81
|
+
if (ref) yield {
|
|
82
|
+
...ref,
|
|
83
|
+
score: hit.score,
|
|
84
|
+
snippet: hit.snippet
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
async addDocument(blocksIn) {
|
|
89
|
+
ensureOpen();
|
|
90
|
+
if (blocksIn.length === 0) return;
|
|
91
|
+
await inTransaction(db, async (tx) => {
|
|
92
|
+
const ids = await blockIds.ensure(tx, blocksIn);
|
|
93
|
+
for (const [i, id] of ids.entries()) {
|
|
94
|
+
const block = blocksIn[i];
|
|
95
|
+
await tx.query(`DELETE FROM ${table} WHERE rowid = ?`, [id]);
|
|
96
|
+
await tx.query(`INSERT INTO ${table} (rowid, content, metadata) VALUES (?, ?, ?)`, [
|
|
97
|
+
id,
|
|
98
|
+
block.content,
|
|
99
|
+
block.metadata ? JSON.stringify(block.metadata) : null
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
async addDocuments(batches) {
|
|
105
|
+
for await (const batch of batches) await this.addDocument(batch);
|
|
106
|
+
},
|
|
107
|
+
async deleteDocuments(selectors) {
|
|
108
|
+
ensureOpen();
|
|
109
|
+
const sels = await collectSelectors(selectors);
|
|
110
|
+
await inTransaction(db, async (tx) => {
|
|
111
|
+
const ids = await blockIds.select(tx, sels);
|
|
112
|
+
await tx.query(`DELETE FROM ${table} WHERE rowid IN (SELECT value FROM json_each(?))`, [JSON.stringify(ids)]);
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
getSize: (p) => list.size(p),
|
|
116
|
+
getDocumentPaths: (p) => list.paths(p),
|
|
117
|
+
getDocumentBlocksRefs: (p) => list.refs(p),
|
|
118
|
+
async *getDocumentsBlocks(p) {
|
|
119
|
+
const scope = pathPrefixSql("b.path", p ? [p] : []);
|
|
120
|
+
const rows = await db.query(`SELECT b.path, b.block_id, f.content, f.metadata FROM ${table} f JOIN ${blocks} b ON b.id = f.rowid WHERE ${scope.sql} ORDER BY b.id`, scope.params);
|
|
121
|
+
for (const r of rows) yield {
|
|
122
|
+
path: r.path,
|
|
123
|
+
blockId: r.block_id,
|
|
124
|
+
content: r.content,
|
|
125
|
+
...r.metadata ? { metadata: JSON.parse(r.metadata) } : {}
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
async close() {
|
|
129
|
+
closed = true;
|
|
130
|
+
},
|
|
131
|
+
async flush() {},
|
|
132
|
+
async deleteIndex() {
|
|
133
|
+
closed = true;
|
|
134
|
+
await db.exec(`DROP TABLE IF EXISTS ${table}`);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
export { newSqliteFullTextProvider, toMatchExpression };
|
|
142
|
+
|
|
143
|
+
//# sourceMappingURL=fts-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fts-provider.js","names":[],"sources":["../src/fts-provider.ts"],"sourcesContent":["import { inTransaction } from \"@statewalker/db-sqlite\";\nimport type { DocumentPath, Metadata, PathSelector } from \"@statewalker/indexer-api\";\nimport { toAsyncIterable } from \"@statewalker/indexer-core\";\nimport {\n FULL_TEXT_TYPE,\n type FullTextBlock,\n type FullTextConfig,\n type FullTextIndex,\n type FulltextQuery,\n type FulltextResult,\n} from \"@statewalker/indexer-fulltext\";\nimport { newEnumeration } from \"./enumeration.js\";\nimport { pathPrefixSql } from \"./path-sql.js\";\nimport type { SqliteModalityProvider } from \"./types.js\";\n\n/** `en` gets the Porter stemmer; any other language (known or not) gets plain `unicode61`. */\nfunction tokenizerFor(language: string): string {\n return language === \"en\"\n ? \"porter unicode61 remove_diacritics 2\"\n : \"unicode61 remove_diacritics 2\";\n}\n\n/**\n * One FTS5 MATCH expression per query string: every word with a letter or a\n * digit, double-quoted (so `AND`, `*`, `NEAR(` are plain words), OR-joined so\n * partial matches are returned. `null` when nothing is searchable.\n */\nexport function toMatchExpression(query: string): string | null {\n const words = query\n .split(/\\s+/)\n .filter((w) => /[\\p{L}\\p{N}]/u.test(w))\n .map((w) => `\"${w.replaceAll('\"', '\"\"')}\"`);\n return words.length > 0 ? words.join(\" OR \") : null;\n}\n\nexport function newSqliteFullTextProvider(): SqliteModalityProvider<FullTextConfig, FullTextIndex> {\n return {\n type: FULL_TEXT_TYPE,\n\n async drop({ db, prefix }) {\n await db.exec(`DROP TABLE IF EXISTS ${prefix}fts`);\n },\n\n async open({ db, prefix, blockIds }, config) {\n const table = `${prefix}fts`;\n const blocks = blockIds.table;\n await db.exec(\n `CREATE VIRTUAL TABLE IF NOT EXISTS ${table} USING fts5(content, metadata UNINDEXED, tokenize='${tokenizerFor(config.language)}')`,\n );\n const list = newEnumeration(db, blockIds, `SELECT rowid AS id FROM ${table}`);\n let closed = false;\n const ensureOpen = () => {\n if (closed) throw new Error(\"FullTextIndex is closed\");\n };\n const collectSelectors = async (selectors: PathSelector[] | AsyncIterable<PathSelector>) => {\n const all: PathSelector[] = [];\n for await (const s of toAsyncIterable(selectors)) all.push(s);\n return all;\n };\n\n const index: FullTextIndex = {\n async getIndexInfo() {\n return { language: config.language, metadata: config.metadata };\n },\n\n async *search(params: FulltextQuery): AsyncGenerator<FulltextResult> {\n ensureOpen();\n const topK = params.topK ?? 100;\n const scope = pathPrefixSql(\"b.path\", params.paths ?? []);\n // Several queries accumulate (SUM): a block matching more of them ranks higher.\n const merged = new Map<number, { score: number; best: number; snippet: string }>();\n for (const query of params.queries ?? []) {\n const match = toMatchExpression(query);\n if (!match) continue;\n const rows = await db.query<{ id: number; score: number; snippet: string }>(\n `SELECT ${table}.rowid AS id, -bm25(${table}) AS score, snippet(${table}, 0, '', '', '…', 16) AS snippet FROM ${table} JOIN ${blocks} b ON b.id = ${table}.rowid WHERE ${table} MATCH ? AND ${scope.sql} ORDER BY score DESC LIMIT ?`,\n [match, ...scope.params, topK],\n );\n for (const r of rows) {\n const id = Number(r.id);\n const prev = merged.get(id);\n if (!prev) merged.set(id, { score: r.score, best: r.score, snippet: r.snippet });\n else {\n prev.score += r.score;\n if (r.score > prev.best) Object.assign(prev, { best: r.score, snippet: r.snippet });\n }\n }\n }\n const top = [...merged.entries()].sort((a, b) => b[1].score - a[1].score).slice(0, topK);\n const refs = await blockIds.refs(\n db,\n top.map(([id]) => id),\n );\n for (const [id, hit] of top) {\n const ref = refs.get(id);\n if (ref) yield { ...ref, score: hit.score, snippet: hit.snippet };\n }\n },\n\n async addDocument(blocksIn: FullTextBlock[]) {\n ensureOpen();\n if (blocksIn.length === 0) return;\n await inTransaction(db, async (tx) => {\n const ids = await blockIds.ensure(tx, blocksIn);\n for (const [i, id] of ids.entries()) {\n const block = blocksIn[i] as FullTextBlock;\n // FTS5 has no UPSERT: delete then insert, atomically inside the transaction.\n await tx.query(`DELETE FROM ${table} WHERE rowid = ?`, [id]);\n await tx.query(`INSERT INTO ${table} (rowid, content, metadata) VALUES (?, ?, ?)`, [\n id,\n block.content,\n block.metadata ? JSON.stringify(block.metadata) : null,\n ]);\n }\n });\n },\n\n async addDocuments(batches) {\n for await (const batch of batches) await this.addDocument(batch);\n },\n\n async deleteDocuments(selectors) {\n ensureOpen();\n const sels = await collectSelectors(selectors);\n await inTransaction(db, async (tx) => {\n const ids = await blockIds.select(tx, sels);\n await tx.query(`DELETE FROM ${table} WHERE rowid IN (SELECT value FROM json_each(?))`, [\n JSON.stringify(ids),\n ]);\n });\n },\n\n getSize: (p?: DocumentPath) => list.size(p),\n getDocumentPaths: (p?: DocumentPath) => list.paths(p),\n getDocumentBlocksRefs: (p?: DocumentPath) => list.refs(p),\n\n async *getDocumentsBlocks(p?: DocumentPath): AsyncGenerator<FullTextBlock> {\n const scope = pathPrefixSql(\"b.path\", p ? [p] : []);\n const rows = await db.query<{\n path: string;\n block_id: string;\n content: string;\n metadata: string | null;\n }>(\n `SELECT b.path, b.block_id, f.content, f.metadata FROM ${table} f JOIN ${blocks} b ON b.id = f.rowid WHERE ${scope.sql} ORDER BY b.id`,\n scope.params,\n );\n for (const r of rows) {\n yield {\n path: r.path as DocumentPath,\n blockId: r.block_id,\n content: r.content,\n ...(r.metadata ? { metadata: JSON.parse(r.metadata) as Metadata } : {}),\n };\n }\n },\n\n async close() {\n closed = true;\n },\n async flush() {},\n async deleteIndex() {\n closed = true;\n await db.exec(`DROP TABLE IF EXISTS ${table}`);\n },\n };\n return index;\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BlockIds, openBlockIds } from "./block-ids.js";
|
|
2
|
+
import { SqliteModalityContext, SqliteModalityProvider } from "./types.js";
|
|
3
|
+
import { SqliteIndexerOptions, createSqliteIndexer } from "./create-sqlite-indexer.js";
|
|
4
|
+
import { BlockEntry, Enumeration, newEnumeration } from "./enumeration.js";
|
|
5
|
+
import { newSqliteFullTextProvider } from "./fts-provider.js";
|
|
6
|
+
import { newSqliteNgramProvider } from "./ngram-provider.js";
|
|
7
|
+
import { pathPrefixSql } from "./path-sql.js";
|
|
8
|
+
import { newSqliteVectorProvider } from "./vector-provider.js";
|
|
9
|
+
export { type BlockEntry, type BlockIds, type Enumeration, type SqliteIndexerOptions, type SqliteModalityContext, type SqliteModalityProvider, createSqliteIndexer, newEnumeration, newSqliteFullTextProvider, newSqliteNgramProvider, newSqliteVectorProvider, openBlockIds, pathPrefixSql };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { pathPrefixSql } from "./path-sql.js";
|
|
2
|
+
import { openBlockIds } from "./block-ids.js";
|
|
3
|
+
import { createSqliteIndexer } from "./create-sqlite-indexer.js";
|
|
4
|
+
import { newEnumeration } from "./enumeration.js";
|
|
5
|
+
import { newSqliteFullTextProvider } from "./fts-provider.js";
|
|
6
|
+
import { newSqliteNgramProvider } from "./ngram-provider.js";
|
|
7
|
+
import { newSqliteVectorProvider } from "./vector-provider.js";
|
|
8
|
+
export { createSqliteIndexer, newEnumeration, newSqliteFullTextProvider, newSqliteNgramProvider, newSqliteVectorProvider, openBlockIds, pathPrefixSql };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SqliteModalityProvider } from "./types.js";
|
|
2
|
+
import { NgramConfig, NgramIndex } from "@statewalker/indexer-ngram";
|
|
3
|
+
//#region src/ngram-provider.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Wraps the id-based `NgramStore` (own transactions; never call it inside
|
|
6
|
+
* `inTransaction`). Tables live under `{prefix}ng_`. Size and enumeration join
|
|
7
|
+
* the store's `idTable`. Score = match count; ties keep the store's
|
|
8
|
+
* ascending-id order.
|
|
9
|
+
*/
|
|
10
|
+
export declare function newSqliteNgramProvider(): SqliteModalityProvider<NgramConfig, NgramIndex>;
|
|
11
|
+
//#endregion
|
|
12
|
+
//# sourceMappingURL=ngram-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngram-provider.d.ts","names":[],"sources":["../src/ngram-provider.ts"],"mappings":";;;;;;;;;wBA6CgB,0BAA0B,uBAAuB,aAAa"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { newEnumeration } from "./enumeration.js";
|
|
2
|
+
import { toAsyncIterable } from "@statewalker/indexer-core";
|
|
3
|
+
import { NgramStore } from "@statewalker/db-sqlite-index/ngram";
|
|
4
|
+
import { NGRAM_TYPE } from "@statewalker/indexer-ngram";
|
|
5
|
+
//#region src/ngram-provider.ts
|
|
6
|
+
/**
|
|
7
|
+
* Blocks per `NgramStore.upsert`: each upsert is one transaction, and a
|
|
8
|
+
* Durable Object retries a write that runs past its ~15 s deadline.
|
|
9
|
+
*/
|
|
10
|
+
const UPSERT_BATCH = 500;
|
|
11
|
+
/** 1-based line of each offset, from the positions of `\n` in `text`. */
|
|
12
|
+
function withLines(text, matches) {
|
|
13
|
+
const breaks = [];
|
|
14
|
+
for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) breaks.push(i);
|
|
15
|
+
let line = 1;
|
|
16
|
+
let b = 0;
|
|
17
|
+
return [...matches].sort((x, y) => x.start - y.start).map(({ start, end }) => {
|
|
18
|
+
while (b < breaks.length && breaks[b] < start) {
|
|
19
|
+
b++;
|
|
20
|
+
line++;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
start,
|
|
24
|
+
end,
|
|
25
|
+
line
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Wraps the id-based `NgramStore` (own transactions; never call it inside
|
|
31
|
+
* `inTransaction`). Tables live under `{prefix}ng_`. Size and enumeration join
|
|
32
|
+
* the store's `idTable`. Score = match count; ties keep the store's
|
|
33
|
+
* ascending-id order.
|
|
34
|
+
*/
|
|
35
|
+
function newSqliteNgramProvider() {
|
|
36
|
+
return {
|
|
37
|
+
type: NGRAM_TYPE,
|
|
38
|
+
async drop({ db, prefix }) {
|
|
39
|
+
await new NgramStore({
|
|
40
|
+
db,
|
|
41
|
+
prefix: `${prefix}ng_`
|
|
42
|
+
}).drop();
|
|
43
|
+
},
|
|
44
|
+
async open({ db, prefix, blockIds }, config) {
|
|
45
|
+
const storeKind = config.store ?? "fts5";
|
|
46
|
+
const store = new NgramStore({
|
|
47
|
+
db,
|
|
48
|
+
prefix: `${prefix}ng_`,
|
|
49
|
+
store: storeKind
|
|
50
|
+
});
|
|
51
|
+
await store.init();
|
|
52
|
+
const list = newEnumeration(db, blockIds, `SELECT id FROM ${store.idTable}`);
|
|
53
|
+
let closed = false;
|
|
54
|
+
const ensureOpen = () => {
|
|
55
|
+
if (closed) throw new Error("NgramIndex is closed");
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
async getIndexInfo() {
|
|
59
|
+
return {
|
|
60
|
+
store: storeKind,
|
|
61
|
+
...config.metadata ? { metadata: config.metadata } : {}
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
async *search(params) {
|
|
65
|
+
ensureOpen();
|
|
66
|
+
const allowed = params.paths?.length ? new Set(await blockIds.select(db, params.paths.map((path) => ({ path })))) : void 0;
|
|
67
|
+
const result = await store.search(params.pattern, {
|
|
68
|
+
regex: params.regex ?? false,
|
|
69
|
+
caseSensitive: params.caseSensitive ?? true,
|
|
70
|
+
limit: params.topK ?? 100,
|
|
71
|
+
...allowed ? { filter: (id) => allowed.has(id) } : {}
|
|
72
|
+
});
|
|
73
|
+
const hits = result.hits.filter((h) => h.matches.length > 0);
|
|
74
|
+
const ids = hits.map((h) => h.id);
|
|
75
|
+
const [texts, refs] = await Promise.all([store.getTexts(ids), blockIds.refs(db, ids)]);
|
|
76
|
+
const ranked = hits.sort((x, y) => y.matches.length - x.matches.length);
|
|
77
|
+
for (const h of ranked) {
|
|
78
|
+
const ref = refs.get(h.id);
|
|
79
|
+
if (!ref) continue;
|
|
80
|
+
const count = h.matches.length;
|
|
81
|
+
yield {
|
|
82
|
+
...ref,
|
|
83
|
+
score: count,
|
|
84
|
+
count,
|
|
85
|
+
matches: withLines(texts.get(h.id) ?? "", h.matches)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return { truncated: result.truncated };
|
|
89
|
+
},
|
|
90
|
+
async addDocument(blocks) {
|
|
91
|
+
ensureOpen();
|
|
92
|
+
for (let i = 0; i < blocks.length; i += UPSERT_BATCH) {
|
|
93
|
+
const batch = blocks.slice(i, i + UPSERT_BATCH);
|
|
94
|
+
const ids = await blockIds.ensure(db, batch);
|
|
95
|
+
await store.upsert(ids.map((id, j) => ({
|
|
96
|
+
id,
|
|
97
|
+
text: batch[j].content
|
|
98
|
+
})));
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
async addDocuments(batches) {
|
|
102
|
+
for await (const batch of batches) await this.addDocument(batch);
|
|
103
|
+
},
|
|
104
|
+
async deleteDocuments(selectors) {
|
|
105
|
+
ensureOpen();
|
|
106
|
+
const sels = [];
|
|
107
|
+
for await (const s of toAsyncIterable(selectors)) sels.push(s);
|
|
108
|
+
const ids = await blockIds.select(db, sels);
|
|
109
|
+
if (ids.length === 0) return;
|
|
110
|
+
await store.delete(ids);
|
|
111
|
+
},
|
|
112
|
+
getSize: (p) => list.size(p),
|
|
113
|
+
getDocumentPaths: (p) => list.paths(p),
|
|
114
|
+
getDocumentBlocksRefs: (p) => list.refs(p),
|
|
115
|
+
async *getDocumentsBlocks(p) {
|
|
116
|
+
const entries = await list.entries(p);
|
|
117
|
+
const texts = await store.getTexts(entries.map((e) => e.id));
|
|
118
|
+
for (const e of entries) {
|
|
119
|
+
const content = texts.get(e.id);
|
|
120
|
+
if (content !== void 0) yield {
|
|
121
|
+
path: e.path,
|
|
122
|
+
blockId: e.blockId,
|
|
123
|
+
content
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
async close() {
|
|
128
|
+
closed = true;
|
|
129
|
+
},
|
|
130
|
+
async flush() {},
|
|
131
|
+
async deleteIndex() {
|
|
132
|
+
closed = true;
|
|
133
|
+
await store.drop();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
export { newSqliteNgramProvider };
|
|
141
|
+
|
|
142
|
+
//# sourceMappingURL=ngram-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngram-provider.js","names":[],"sources":["../src/ngram-provider.ts"],"sourcesContent":["import { NgramStore } from \"@statewalker/db-sqlite-index/ngram\";\nimport type { DocumentPath, PathSelector } from \"@statewalker/indexer-api\";\nimport { toAsyncIterable } from \"@statewalker/indexer-core\";\nimport {\n NGRAM_TYPE,\n type NgramBlock,\n type NgramConfig,\n type NgramIndex,\n type NgramMatch,\n type NgramQuery,\n type NgramResult,\n type NgramSearchSummary,\n} from \"@statewalker/indexer-ngram\";\nimport { newEnumeration } from \"./enumeration.js\";\nimport type { SqliteModalityProvider } from \"./types.js\";\n\n/**\n * Blocks per `NgramStore.upsert`: each upsert is one transaction, and a\n * Durable Object retries a write that runs past its ~15 s deadline.\n */\nconst UPSERT_BATCH = 500;\n\n/** 1-based line of each offset, from the positions of `\\n` in `text`. */\nfunction withLines(text: string, matches: { start: number; end: number }[]): NgramMatch[] {\n const breaks: number[] = [];\n for (let i = text.indexOf(\"\\n\"); i !== -1; i = text.indexOf(\"\\n\", i + 1)) breaks.push(i);\n let line = 1;\n let b = 0;\n return [...matches]\n .sort((x, y) => x.start - y.start)\n .map(({ start, end }) => {\n while (b < breaks.length && (breaks[b] as number) < start) {\n b++;\n line++;\n }\n return { start, end, line };\n });\n}\n\n/**\n * Wraps the id-based `NgramStore` (own transactions; never call it inside\n * `inTransaction`). Tables live under `{prefix}ng_`. Size and enumeration join\n * the store's `idTable`. Score = match count; ties keep the store's\n * ascending-id order.\n */\nexport function newSqliteNgramProvider(): SqliteModalityProvider<NgramConfig, NgramIndex> {\n return {\n type: NGRAM_TYPE,\n\n async drop({ db, prefix }) {\n await new NgramStore({ db, prefix: `${prefix}ng_` }).drop(); // drops either store's tables\n },\n\n async open({ db, prefix, blockIds }, config) {\n const storeKind = config.store ?? \"fts5\";\n const store = new NgramStore({ db, prefix: `${prefix}ng_`, store: storeKind });\n await store.init();\n const list = newEnumeration(db, blockIds, `SELECT id FROM ${store.idTable}`);\n let closed = false;\n const ensureOpen = () => {\n if (closed) throw new Error(\"NgramIndex is closed\");\n };\n\n const index: NgramIndex = {\n async getIndexInfo() {\n return { store: storeKind, ...(config.metadata ? { metadata: config.metadata } : {}) };\n },\n\n async *search(params: NgramQuery): AsyncGenerator<NgramResult, NgramSearchSummary> {\n ensureOpen();\n const allowed = params.paths?.length\n ? new Set(\n await blockIds.select(\n db,\n params.paths.map((path) => ({ path })),\n ),\n )\n : undefined;\n const result = await store.search(params.pattern, {\n regex: params.regex ?? false,\n caseSensitive: params.caseSensitive ?? true,\n limit: params.topK ?? 100,\n ...(allowed ? { filter: (id: number) => allowed.has(id) } : {}),\n });\n const hits = result.hits.filter((h) => h.matches.length > 0);\n const ids = hits.map((h) => h.id);\n const [texts, refs] = await Promise.all([store.getTexts(ids), blockIds.refs(db, ids)]);\n // Array sort is stable: equal counts keep the store's ascending-id order.\n const ranked = hits.sort((x, y) => y.matches.length - x.matches.length);\n for (const h of ranked) {\n const ref = refs.get(h.id);\n if (!ref) continue;\n const count = h.matches.length;\n yield {\n ...ref,\n score: count,\n count,\n matches: withLines(texts.get(h.id) ?? \"\", h.matches),\n };\n }\n return { truncated: result.truncated };\n },\n\n async addDocument(blocks: NgramBlock[]) {\n ensureOpen();\n for (let i = 0; i < blocks.length; i += UPSERT_BATCH) {\n const batch = blocks.slice(i, i + UPSERT_BATCH);\n const ids = await blockIds.ensure(db, batch);\n await store.upsert(\n ids.map((id, j) => ({ id, text: (batch[j] as NgramBlock).content })),\n );\n }\n },\n\n async addDocuments(batches) {\n for await (const batch of batches) await this.addDocument(batch);\n },\n\n async deleteDocuments(selectors) {\n ensureOpen();\n const sels: PathSelector[] = [];\n for await (const s of toAsyncIterable(selectors)) sels.push(s);\n const ids = await blockIds.select(db, sels);\n if (ids.length === 0) return;\n await store.delete(ids);\n },\n\n getSize: (p?: DocumentPath) => list.size(p),\n getDocumentPaths: (p?: DocumentPath) => list.paths(p),\n getDocumentBlocksRefs: (p?: DocumentPath) => list.refs(p),\n\n async *getDocumentsBlocks(p?: DocumentPath): AsyncGenerator<NgramBlock> {\n const entries = await list.entries(p);\n const texts = await store.getTexts(entries.map((e) => e.id));\n for (const e of entries) {\n const content = texts.get(e.id);\n if (content !== undefined) yield { path: e.path, blockId: e.blockId, content };\n }\n },\n\n async close() {\n closed = true;\n },\n async flush() {},\n async deleteIndex() {\n closed = true;\n await store.drop();\n },\n };\n return index;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/path-sql.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* SQL condition matching `column` under any of `prefixes`, on path-component
|
|
4
|
+
* boundaries (`/foo` matches `/foo` and `/foo/x`, never `/foobar`) and
|
|
5
|
+
* case-sensitively. Uses `substr` rather than `LIKE`: SQLite's `LIKE` ignores
|
|
6
|
+
* ASCII case, and `PRAGMA case_sensitive_like` is not authorised in Durable
|
|
7
|
+
* Objects. `column` must be table-qualified (`b.path`): `json_each` has a `path`
|
|
8
|
+
* column of its own. No prefixes → `1` (no constraint).
|
|
9
|
+
*/
|
|
10
|
+
export declare function pathPrefixSql(column: string, prefixes: readonly string[]): {
|
|
11
|
+
sql: string;
|
|
12
|
+
params: string[];
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
//# sourceMappingURL=path-sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-sql.d.ts","names":[],"sources":["../src/path-sql.ts"],"mappings":";;;;;;;;;wBAQgB,cACd,gBACA;EACG;EAAa"}
|
package/dist/path-sql.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/path-sql.ts
|
|
2
|
+
/**
|
|
3
|
+
* SQL condition matching `column` under any of `prefixes`, on path-component
|
|
4
|
+
* boundaries (`/foo` matches `/foo` and `/foo/x`, never `/foobar`) and
|
|
5
|
+
* case-sensitively. Uses `substr` rather than `LIKE`: SQLite's `LIKE` ignores
|
|
6
|
+
* ASCII case, and `PRAGMA case_sensitive_like` is not authorised in Durable
|
|
7
|
+
* Objects. `column` must be table-qualified (`b.path`): `json_each` has a `path`
|
|
8
|
+
* column of its own. No prefixes → `1` (no constraint).
|
|
9
|
+
*/
|
|
10
|
+
function pathPrefixSql(column, prefixes) {
|
|
11
|
+
if (prefixes.length === 0) return {
|
|
12
|
+
sql: "1",
|
|
13
|
+
params: []
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
sql: `EXISTS (SELECT 1 FROM json_each(?) p WHERE CASE WHEN substr(p.value, -1) = '/' THEN substr(${column}, 1, length(p.value)) = p.value ELSE ${column} = p.value OR substr(${column}, 1, length(p.value) + 1) = p.value || '/' END)`,
|
|
17
|
+
params: [JSON.stringify(prefixes)]
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { pathPrefixSql };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=path-sql.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-sql.js","names":[],"sources":["../src/path-sql.ts"],"sourcesContent":["/**\n * SQL condition matching `column` under any of `prefixes`, on path-component\n * boundaries (`/foo` matches `/foo` and `/foo/x`, never `/foobar`) and\n * case-sensitively. Uses `substr` rather than `LIKE`: SQLite's `LIKE` ignores\n * ASCII case, and `PRAGMA case_sensitive_like` is not authorised in Durable\n * Objects. `column` must be table-qualified (`b.path`): `json_each` has a `path`\n * column of its own. No prefixes → `1` (no constraint).\n */\nexport function pathPrefixSql(\n column: string,\n prefixes: readonly string[],\n): { sql: string; params: string[] } {\n if (prefixes.length === 0) return { sql: \"1\", params: [] };\n // One JSON parameter, however many prefixes: Durable Objects cap bound variables at 100.\n const sql = `EXISTS (SELECT 1 FROM json_each(?) p WHERE CASE WHEN substr(p.value, -1) = '/' THEN substr(${column}, 1, length(p.value)) = p.value ELSE ${column} = p.value OR substr(${column}, 1, length(p.value) + 1) = p.value || '/' END)`;\n return { sql, params: [JSON.stringify(prefixes)] };\n}\n"],"mappings":";;;;;;;;;AAQA,SAAgB,cACd,QACA,UACmC;CACnC,IAAI,SAAS,WAAW,GAAG,OAAO;EAAE,KAAK;EAAK,QAAQ,CAAC;CAAE;CAGzD,OAAO;EAAE,KAAA,8FADiG,OAAO,uCAAuC,OAAO,uBAAuB,OAAO;EAC/K,QAAQ,CAAC,KAAK,UAAU,QAAQ,CAAC;CAAE;AACnD"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { BlockIds } from "./block-ids.js";
|
|
2
|
+
import { Db } from "@statewalker/db-api";
|
|
3
|
+
import { ScoredHit, SearchIndex } from "@statewalker/indexer-api";
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
export interface SqliteModalityContext {
|
|
6
|
+
/** The database. Inside `inTransaction(db, (tx) => …)` use `tx` and `ctx.blockIds` methods with `tx`. */
|
|
7
|
+
db: Db;
|
|
8
|
+
/** Unique table-name prefix for this sub-index, e.g. `idx_docs__body_`. */
|
|
9
|
+
prefix: string;
|
|
10
|
+
/** The Index's shared block-id table. */
|
|
11
|
+
blockIds: BlockIds;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A modality backed by tables in one SQLite `Db`. Unlike `ModalityProvider`,
|
|
15
|
+
* opening is async (tables are created, existing data is adopted) and the
|
|
16
|
+
* provider can drop its tables without an open instance.
|
|
17
|
+
*/
|
|
18
|
+
export interface SqliteModalityProvider<C = unknown, I extends SearchIndex<unknown, unknown, ScoredHit> = SearchIndex<unknown, unknown, ScoredHit>> {
|
|
19
|
+
readonly type: string;
|
|
20
|
+
open(ctx: SqliteModalityContext, config: C): Promise<I>;
|
|
21
|
+
drop(ctx: SqliteModalityContext): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;iBAIiB;;EAEf,IAAI;;EAEJ;;EAEA,UAAU;;;;;;;iBAQK,uBACf,aACA,UAAU,8BAA8B,aAAa,8BAA8B;WAE1E;EACT,KAAK,KAAK,uBAAuB,QAAQ,IAAI,QAAQ;EACrD,KAAK,KAAK,wBAAwB"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SqliteModalityProvider } from "./types.js";
|
|
2
|
+
import { VectorConfig, VectorIndex } from "@statewalker/indexer-vector";
|
|
3
|
+
//#region src/vector-provider.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Wraps the id-based `VectorIndex` engine. The engine owns its tables under
|
|
6
|
+
* `{prefix}vec_` and its own transactions (never call it inside `inTransaction`);
|
|
7
|
+
* this provider adds only the `{path, blockId}` mapping. Size and enumeration
|
|
8
|
+
* join the engine's `idTable`, so they list exactly what the engine holds.
|
|
9
|
+
*/
|
|
10
|
+
export declare function newSqliteVectorProvider(): SqliteModalityProvider<VectorConfig, VectorIndex>;
|
|
11
|
+
//#endregion
|
|
12
|
+
//# sourceMappingURL=vector-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vector-provider.d.ts","names":[],"sources":["../src/vector-provider.ts"],"mappings":";;;;;;;;;wBAoBgB,2BAA2B,uBAAuB,cAAc"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { newEnumeration } from "./enumeration.js";
|
|
2
|
+
import { toAsyncIterable, validateDimensionality } from "@statewalker/indexer-core";
|
|
3
|
+
import { VectorIndex, dropVectorIndex } from "@statewalker/db-sqlite-index/vector";
|
|
4
|
+
import { VECTOR_TYPE } from "@statewalker/indexer-vector";
|
|
5
|
+
//#region src/vector-provider.ts
|
|
6
|
+
/**
|
|
7
|
+
* Wraps the id-based `VectorIndex` engine. The engine owns its tables under
|
|
8
|
+
* `{prefix}vec_` and its own transactions (never call it inside `inTransaction`);
|
|
9
|
+
* this provider adds only the `{path, blockId}` mapping. Size and enumeration
|
|
10
|
+
* join the engine's `idTable`, so they list exactly what the engine holds.
|
|
11
|
+
*/
|
|
12
|
+
function newSqliteVectorProvider() {
|
|
13
|
+
return {
|
|
14
|
+
type: VECTOR_TYPE,
|
|
15
|
+
async drop({ db, prefix }) {
|
|
16
|
+
await dropVectorIndex(db, `${prefix}vec_`);
|
|
17
|
+
},
|
|
18
|
+
async open({ db, prefix, blockIds }, config) {
|
|
19
|
+
const engine = new VectorIndex({
|
|
20
|
+
db,
|
|
21
|
+
prefix: `${prefix}vec_`,
|
|
22
|
+
dims: config.dimensionality,
|
|
23
|
+
metric: "cosine"
|
|
24
|
+
});
|
|
25
|
+
await engine.init();
|
|
26
|
+
const list = newEnumeration(db, blockIds, `SELECT id FROM ${engine.idTable}`);
|
|
27
|
+
let closed = false;
|
|
28
|
+
const ensureOpen = () => {
|
|
29
|
+
if (closed) throw new Error("VectorIndex is closed");
|
|
30
|
+
};
|
|
31
|
+
const info = {
|
|
32
|
+
dimensionality: config.dimensionality,
|
|
33
|
+
model: config.model,
|
|
34
|
+
metadata: config.metadata
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
async getIndexInfo() {
|
|
38
|
+
return { ...info };
|
|
39
|
+
},
|
|
40
|
+
async *search(params) {
|
|
41
|
+
ensureOpen();
|
|
42
|
+
const k = params.topK ?? 100;
|
|
43
|
+
const allowed = params.paths?.length ? new Set(await blockIds.select(db, params.paths.map((path) => ({ path })))) : void 0;
|
|
44
|
+
const filter = allowed ? (id) => allowed.has(id) : void 0;
|
|
45
|
+
const best = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const q of params.embeddings ?? []) for (const hit of await engine.search(q, {
|
|
47
|
+
k,
|
|
48
|
+
...filter ? { filter } : {}
|
|
49
|
+
})) {
|
|
50
|
+
const score = 1 - hit.distance;
|
|
51
|
+
if (score > (best.get(hit.id) ?? Number.NEGATIVE_INFINITY)) best.set(hit.id, score);
|
|
52
|
+
}
|
|
53
|
+
const top = [...best.entries()].sort((a, b) => b[1] - a[1]).slice(0, k);
|
|
54
|
+
const refs = await blockIds.refs(db, top.map(([id]) => id));
|
|
55
|
+
for (const [id, score] of top) {
|
|
56
|
+
const ref = refs.get(id);
|
|
57
|
+
if (ref) yield {
|
|
58
|
+
...ref,
|
|
59
|
+
score
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
async addDocument(blocks) {
|
|
64
|
+
ensureOpen();
|
|
65
|
+
if (blocks.length === 0) return;
|
|
66
|
+
for (const b of blocks) validateDimensionality(info, b.embedding);
|
|
67
|
+
const ids = await blockIds.ensure(db, blocks);
|
|
68
|
+
await engine.upsert(ids.map((id, i) => ({
|
|
69
|
+
id,
|
|
70
|
+
vector: blocks[i].embedding
|
|
71
|
+
})));
|
|
72
|
+
},
|
|
73
|
+
async addDocuments(batches) {
|
|
74
|
+
for await (const batch of batches) await this.addDocument(batch);
|
|
75
|
+
},
|
|
76
|
+
async deleteDocuments(selectors) {
|
|
77
|
+
ensureOpen();
|
|
78
|
+
const sels = [];
|
|
79
|
+
for await (const s of toAsyncIterable(selectors)) sels.push(s);
|
|
80
|
+
const ids = await blockIds.select(db, sels);
|
|
81
|
+
if (ids.length === 0) return;
|
|
82
|
+
await engine.delete(ids);
|
|
83
|
+
},
|
|
84
|
+
getSize: (p) => list.size(p),
|
|
85
|
+
getDocumentPaths: (p) => list.paths(p),
|
|
86
|
+
getDocumentBlocksRefs: (p) => list.refs(p),
|
|
87
|
+
async *getDocumentsBlocks(p) {
|
|
88
|
+
const entries = await list.entries(p);
|
|
89
|
+
const vectors = await engine.getVectors(entries.map((e) => e.id));
|
|
90
|
+
for (const e of entries) {
|
|
91
|
+
const embedding = vectors.get(e.id);
|
|
92
|
+
if (embedding) yield {
|
|
93
|
+
path: e.path,
|
|
94
|
+
blockId: e.blockId,
|
|
95
|
+
embedding
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
async close() {
|
|
100
|
+
closed = true;
|
|
101
|
+
},
|
|
102
|
+
async flush() {},
|
|
103
|
+
async deleteIndex() {
|
|
104
|
+
closed = true;
|
|
105
|
+
await dropVectorIndex(db, `${prefix}vec_`);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { newSqliteVectorProvider };
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=vector-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vector-provider.js","names":["VectorEngine"],"sources":["../src/vector-provider.ts"],"sourcesContent":["import { dropVectorIndex, VectorIndex as VectorEngine } from \"@statewalker/db-sqlite-index/vector\";\nimport type { DocumentPath, PathSelector } from \"@statewalker/indexer-api\";\nimport { toAsyncIterable, validateDimensionality } from \"@statewalker/indexer-core\";\nimport {\n VECTOR_TYPE,\n type VectorBlock,\n type VectorConfig,\n type VectorIndex,\n type VectorQuery,\n type VectorResult,\n} from \"@statewalker/indexer-vector\";\nimport { newEnumeration } from \"./enumeration.js\";\nimport type { SqliteModalityProvider } from \"./types.js\";\n\n/**\n * Wraps the id-based `VectorIndex` engine. The engine owns its tables under\n * `{prefix}vec_` and its own transactions (never call it inside `inTransaction`);\n * this provider adds only the `{path, blockId}` mapping. Size and enumeration\n * join the engine's `idTable`, so they list exactly what the engine holds.\n */\nexport function newSqliteVectorProvider(): SqliteModalityProvider<VectorConfig, VectorIndex> {\n return {\n type: VECTOR_TYPE,\n\n async drop({ db, prefix }) {\n await dropVectorIndex(db, `${prefix}vec_`);\n },\n\n async open({ db, prefix, blockIds }, config) {\n const engine = new VectorEngine({\n db,\n prefix: `${prefix}vec_`,\n dims: config.dimensionality,\n metric: \"cosine\",\n });\n await engine.init();\n const list = newEnumeration(db, blockIds, `SELECT id FROM ${engine.idTable}`);\n let closed = false;\n const ensureOpen = () => {\n if (closed) throw new Error(\"VectorIndex is closed\");\n };\n const info = {\n dimensionality: config.dimensionality,\n model: config.model,\n metadata: config.metadata,\n };\n\n const index: VectorIndex = {\n async getIndexInfo() {\n return { ...info };\n },\n\n async *search(params: VectorQuery): AsyncGenerator<VectorResult> {\n ensureOpen();\n const k = params.topK ?? 100;\n const allowed = params.paths?.length\n ? new Set(\n await blockIds.select(\n db,\n params.paths.map((path) => ({ path })),\n ),\n )\n : undefined;\n const filter = allowed ? (id: number) => allowed.has(id) : undefined;\n // Several embeddings merge by max score, like every other backend.\n const best = new Map<number, number>();\n for (const q of params.embeddings ?? []) {\n for (const hit of await engine.search(q, { k, ...(filter ? { filter } : {}) })) {\n const score = 1 - hit.distance;\n if (score > (best.get(hit.id) ?? Number.NEGATIVE_INFINITY)) best.set(hit.id, score);\n }\n }\n const top = [...best.entries()].sort((a, b) => b[1] - a[1]).slice(0, k);\n const refs = await blockIds.refs(\n db,\n top.map(([id]) => id),\n );\n for (const [id, score] of top) {\n const ref = refs.get(id);\n if (ref) yield { ...ref, score };\n }\n },\n\n async addDocument(blocks: VectorBlock[]) {\n ensureOpen();\n if (blocks.length === 0) return;\n for (const b of blocks) validateDimensionality(info, b.embedding);\n const ids = await blockIds.ensure(db, blocks);\n await engine.upsert(\n ids.map((id, i) => ({ id, vector: (blocks[i] as VectorBlock).embedding })),\n );\n },\n\n async addDocuments(batches) {\n for await (const batch of batches) await this.addDocument(batch);\n },\n\n async deleteDocuments(selectors) {\n ensureOpen();\n const sels: PathSelector[] = [];\n for await (const s of toAsyncIterable(selectors)) sels.push(s);\n const ids = await blockIds.select(db, sels);\n if (ids.length === 0) return;\n await engine.delete(ids);\n },\n\n getSize: (p?: DocumentPath) => list.size(p),\n getDocumentPaths: (p?: DocumentPath) => list.paths(p),\n getDocumentBlocksRefs: (p?: DocumentPath) => list.refs(p),\n\n async *getDocumentsBlocks(p?: DocumentPath): AsyncGenerator<VectorBlock> {\n const entries = await list.entries(p);\n const vectors = await engine.getVectors(entries.map((e) => e.id));\n for (const e of entries) {\n const embedding = vectors.get(e.id);\n if (embedding) yield { path: e.path, blockId: e.blockId, embedding };\n }\n },\n\n async close() {\n closed = true;\n },\n async flush() {},\n async deleteIndex() {\n closed = true;\n await dropVectorIndex(db, `${prefix}vec_`);\n },\n };\n return index;\n },\n };\n}\n"],"mappings":"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"}
|