@statewalker/indexer-sqlite 0.0.0-stage → 0.1.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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +239 -2
  3. package/dist/block-ids.d.ts +20 -0
  4. package/dist/block-ids.d.ts.map +1 -0
  5. package/dist/block-ids.js +47 -0
  6. package/dist/block-ids.js.map +1 -0
  7. package/dist/create-sqlite-indexer.d.ts +18 -0
  8. package/dist/create-sqlite-indexer.d.ts.map +1 -0
  9. package/dist/create-sqlite-indexer.js +152 -0
  10. package/dist/create-sqlite-indexer.js.map +1 -0
  11. package/dist/enumeration.d.ts +18 -0
  12. package/dist/enumeration.d.ts.map +1 -0
  13. package/dist/enumeration.js +42 -0
  14. package/dist/enumeration.js.map +1 -0
  15. package/dist/fts-provider.d.ts +6 -0
  16. package/dist/fts-provider.d.ts.map +1 -0
  17. package/dist/fts-provider.js +143 -0
  18. package/dist/fts-provider.js.map +1 -0
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.js +8 -0
  21. package/dist/ngram-provider.d.ts +12 -0
  22. package/dist/ngram-provider.d.ts.map +1 -0
  23. package/dist/ngram-provider.js +142 -0
  24. package/dist/ngram-provider.js.map +1 -0
  25. package/dist/path-sql.d.ts +15 -0
  26. package/dist/path-sql.d.ts.map +1 -0
  27. package/dist/path-sql.js +23 -0
  28. package/dist/path-sql.js.map +1 -0
  29. package/dist/types.d.ts +24 -0
  30. package/dist/types.d.ts.map +1 -0
  31. package/dist/vector-provider.d.ts +12 -0
  32. package/dist/vector-provider.d.ts.map +1 -0
  33. package/dist/vector-provider.js +114 -0
  34. package/dist/vector-provider.js.map +1 -0
  35. package/package.json +61 -3
  36. package/src/block-ids.ts +75 -0
  37. package/src/create-sqlite-indexer.ts +218 -0
  38. package/src/enumeration.ts +54 -0
  39. package/src/fts-provider.ts +170 -0
  40. package/src/index.ts +9 -0
  41. package/src/ngram-provider.ts +153 -0
  42. package/src/path-sql.ts +17 -0
  43. package/src/types.ts +26 -0
  44. 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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"}
@@ -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"}
@@ -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"}
@@ -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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"}