@statewalker/indexer-sqlite 0.1.1 → 0.1.2
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/dist/create-sqlite-indexer.d.ts.map +1 -1
- package/dist/create-sqlite-indexer.js +54 -35
- package/dist/create-sqlite-indexer.js.map +1 -1
- package/dist/enumeration.d.ts +5 -2
- package/dist/enumeration.d.ts.map +1 -1
- package/dist/enumeration.js +8 -2
- package/dist/enumeration.js.map +1 -1
- package/dist/fts-provider.js +2 -1
- package/dist/fts-provider.js.map +1 -1
- package/dist/ngram-provider.js +2 -1
- package/dist/ngram-provider.js.map +1 -1
- package/dist/vector-provider.js +3 -1
- package/dist/vector-provider.js.map +1 -1
- package/package.json +6 -6
- package/src/create-sqlite-indexer.ts +48 -24
- package/src/enumeration.ts +13 -2
- package/src/fts-provider.ts +4 -1
- package/src/ngram-provider.ts +4 -1
- package/src/vector-provider.ts +5 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-sqlite-indexer.d.ts","names":[],"sources":["../src/create-sqlite-indexer.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"create-sqlite-indexer.d.ts","names":[],"sources":["../src/create-sqlite-indexer.ts"],"mappings":";;;;iBAciB;;EAEf,IAAI;;EAEJ,WAAW;;;;;;;wBAuBS,oBAAoB,SAAS,uBAAuB,QAAQ"}
|
|
@@ -38,7 +38,7 @@ async function createSqliteIndexer(options) {
|
|
|
38
38
|
await db.query(`DELETE FROM ${MANIFEST_TABLE} WHERE name = ?`, [name]);
|
|
39
39
|
manifest.delete(name);
|
|
40
40
|
}
|
|
41
|
-
async function openIndex(name, subIndexes, options) {
|
|
41
|
+
async function openIndex(name, subIndexes, options, metadata) {
|
|
42
42
|
const onMissing = options?.onMissingProvider ?? "throw";
|
|
43
43
|
const blockIds = await openBlockIds(db, `${indexPrefix(name)}blocks`);
|
|
44
44
|
const bindings = [];
|
|
@@ -68,25 +68,33 @@ async function createSqliteIndexer(options) {
|
|
|
68
68
|
});
|
|
69
69
|
effective[sub] = spec;
|
|
70
70
|
}
|
|
71
|
+
const index = createCompositeIndex({
|
|
72
|
+
name,
|
|
73
|
+
metadata,
|
|
74
|
+
bindings,
|
|
75
|
+
onClose: async () => {
|
|
76
|
+
if (indexes.get(name) === index) indexes.delete(name);
|
|
77
|
+
},
|
|
78
|
+
onDeleteIndex: async () => {
|
|
79
|
+
for (const [sub, spec] of Object.entries(manifest.get(name)?.subIndexes ?? {})) await providers.get(spec.type)?.drop(contextFor(name, sub, blockIds));
|
|
80
|
+
await db.exec(`DROP TABLE IF EXISTS ${blockIds.table}`);
|
|
81
|
+
await db.query(`DELETE FROM ${MANIFEST_TABLE} WHERE name = ?`, [name]);
|
|
82
|
+
manifest.delete(name);
|
|
83
|
+
indexes.delete(name);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
71
86
|
return {
|
|
72
|
-
index
|
|
73
|
-
name,
|
|
74
|
-
bindings,
|
|
75
|
-
onDeleteIndex: async () => {
|
|
76
|
-
for (const [sub, spec] of Object.entries(manifest.get(name)?.subIndexes ?? {})) await providers.get(spec.type)?.drop(contextFor(name, sub, blockIds));
|
|
77
|
-
await db.exec(`DROP TABLE IF EXISTS ${blockIds.table}`);
|
|
78
|
-
await db.query(`DELETE FROM ${MANIFEST_TABLE} WHERE name = ?`, [name]);
|
|
79
|
-
manifest.delete(name);
|
|
80
|
-
indexes.delete(name);
|
|
81
|
-
}
|
|
82
|
-
}),
|
|
87
|
+
index,
|
|
83
88
|
effective
|
|
84
89
|
};
|
|
85
90
|
}
|
|
86
91
|
return {
|
|
87
92
|
async getIndexNames() {
|
|
88
93
|
ensureOpen();
|
|
89
|
-
return [...manifest.
|
|
94
|
+
return [...manifest.entries()].map(([name, entry]) => entry.metadata !== void 0 ? {
|
|
95
|
+
name,
|
|
96
|
+
metadata: entry.metadata
|
|
97
|
+
} : { name });
|
|
90
98
|
},
|
|
91
99
|
createIndex(params) {
|
|
92
100
|
return runExclusive(async () => {
|
|
@@ -100,9 +108,11 @@ async function createSqliteIndexer(options) {
|
|
|
100
108
|
indexes.delete(name);
|
|
101
109
|
await dropIndexTables(name, existing?.subIndexes ?? {});
|
|
102
110
|
}
|
|
103
|
-
const { index, effective } = await openIndex(name, subIndexes);
|
|
104
|
-
await writeManifest(name, {
|
|
105
|
-
|
|
111
|
+
const { index, effective } = await openIndex(name, subIndexes, void 0, metadata);
|
|
112
|
+
await writeManifest(name, {
|
|
113
|
+
subIndexes: effective,
|
|
114
|
+
...metadata !== void 0 ? { metadata } : {}
|
|
115
|
+
});
|
|
106
116
|
indexes.set(name, index);
|
|
107
117
|
return index;
|
|
108
118
|
});
|
|
@@ -114,8 +124,11 @@ async function createSqliteIndexer(options) {
|
|
|
114
124
|
if (cached) return cached;
|
|
115
125
|
const entry = manifest.get(name);
|
|
116
126
|
if (!entry) return null;
|
|
117
|
-
const { index, effective } = await openIndex(name, entry.subIndexes, options);
|
|
118
|
-
if (JSON.stringify(effective) !== JSON.stringify(entry.subIndexes)) await writeManifest(name, {
|
|
127
|
+
const { index, effective } = await openIndex(name, entry.subIndexes, options, entry.metadata);
|
|
128
|
+
if (JSON.stringify(effective) !== JSON.stringify(entry.subIndexes)) await writeManifest(name, {
|
|
129
|
+
...entry,
|
|
130
|
+
subIndexes: effective
|
|
131
|
+
});
|
|
119
132
|
indexes.set(name, index);
|
|
120
133
|
return index;
|
|
121
134
|
});
|
|
@@ -124,25 +137,31 @@ async function createSqliteIndexer(options) {
|
|
|
124
137
|
ensureOpen();
|
|
125
138
|
return manifest.has(name);
|
|
126
139
|
},
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
deleteIndex(name) {
|
|
141
|
+
return runExclusive(async () => {
|
|
142
|
+
ensureOpen();
|
|
143
|
+
const live = indexes.get(name);
|
|
144
|
+
if (live) {
|
|
145
|
+
await live.deleteIndex();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const entry = manifest.get(name);
|
|
149
|
+
if (entry) await dropIndexTables(name, entry.subIndexes);
|
|
150
|
+
});
|
|
136
151
|
},
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
152
|
+
flush() {
|
|
153
|
+
return runExclusive(async () => {
|
|
154
|
+
ensureOpen();
|
|
155
|
+
for (const index of indexes.values()) await index.flush();
|
|
156
|
+
});
|
|
140
157
|
},
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
158
|
+
close() {
|
|
159
|
+
return runExclusive(async () => {
|
|
160
|
+
if (closed) return;
|
|
161
|
+
closed = true;
|
|
162
|
+
for (const index of [...indexes.values()]) await index.close().catch(() => void 0);
|
|
163
|
+
indexes.clear();
|
|
164
|
+
});
|
|
146
165
|
}
|
|
147
166
|
};
|
|
148
167
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-sqlite-indexer.js","names":[],"sources":["../src/create-sqlite-indexer.ts"],"sourcesContent":["import type { Db } from \"@statewalker/db-api\";\nimport type {\n AnySubIndexBinding,\n CreateIndexParams,\n GetIndexOptions,\n Index,\n Indexer,\n IndexInfo,\n} from \"@statewalker/indexer-api\";\nimport { createCompositeIndex, createSerialiser } from \"@statewalker/indexer-core\";\nimport { type BlockIds, openBlockIds } from \"./block-ids.js\";\nimport type { SqliteModalityContext, SqliteModalityProvider } from \"./types.js\";\n\nexport interface SqliteIndexerOptions {\n /** The database. The indexer never closes it. */\n db: Db;\n /** One provider per modality type, e.g. FTS5, vector, n-gram. */\n providers: SqliteModalityProvider[];\n}\n\ntype SubSpec = { type: string } & Record<string, unknown>;\ninterface IndexManifest {\n subIndexes: Record<string, SubSpec>;\n}\n\nconst MANIFEST_TABLE = \"__indexer_manifest\";\n\n/** Escapes everything but lower-case letters and digits: SQLite table names fold case, so \"Docs\" and \"docs\" must not collide. */\nconst sanitizePrefix = (name: string) =>\n name.replace(/[^a-z0-9]/g, (ch) => `_${ch.charCodeAt(0)}_`);\n\nconst indexPrefix = (name: string) => `idx_${sanitizePrefix(name)}__`;\n\n/**\n * `Indexer` over one SQLite `Db`. Keeps a manifest row per Index, one shared\n * `{indexPrefix}blocks` id table per Index, and opens each sub-index through\n * its provider. Search fuses sub-indexes by RRF (`createCompositeIndex`).\n */\nexport async function createSqliteIndexer(options: SqliteIndexerOptions): Promise<Indexer> {\n const { db } = options;\n const providers = new Map(options.providers.map((p) => [p.type, p]));\n const indexes = new Map<string, Index>();\n const manifest = new Map<string, IndexManifest>();\n const runExclusive = createSerialiser();\n let closed = false;\n\n await db.exec(\n `CREATE TABLE IF NOT EXISTS ${MANIFEST_TABLE} (name TEXT PRIMARY KEY, config TEXT NOT NULL)`,\n );\n for (const row of await db.query<{ name: string; config: string }>(\n `SELECT name, config FROM ${MANIFEST_TABLE}`,\n )) {\n manifest.set(row.name, JSON.parse(row.config) as IndexManifest);\n }\n\n const ensureOpen = () => {\n if (closed) throw new Error(\"Indexer is closed\");\n };\n\n const contextFor = (name: string, sub: string, blockIds: BlockIds): SqliteModalityContext => ({\n db,\n prefix: `${indexPrefix(name)}${sanitizePrefix(sub)}_`,\n blockIds,\n });\n\n async function writeManifest(name: string, entry: IndexManifest): Promise<void> {\n await db.query(\n `INSERT INTO ${MANIFEST_TABLE} (name, config) VALUES (?, ?) ON CONFLICT (name) DO UPDATE SET config = excluded.config`,\n [name, JSON.stringify(entry)],\n );\n manifest.set(name, entry);\n }\n\n async function dropIndexTables(name: string, subIndexes: Record<string, SubSpec>): Promise<void> {\n const blockIds = await openBlockIds(db, `${indexPrefix(name)}blocks`);\n for (const [sub, spec] of Object.entries(subIndexes)) {\n await providers.get(spec.type)?.drop(contextFor(name, sub, blockIds));\n }\n await db.exec(`DROP TABLE IF EXISTS ${blockIds.table}`);\n await db.query(`DELETE FROM ${MANIFEST_TABLE} WHERE name = ?`, [name]);\n manifest.delete(name);\n }\n\n async function openIndex(\n name: string,\n subIndexes: Record<string, SubSpec>,\n options?: GetIndexOptions,\n ): Promise<{ index: Index; effective: Record<string, SubSpec> }> {\n const onMissing = options?.onMissingProvider ?? \"throw\";\n const blockIds = await openBlockIds(db, `${indexPrefix(name)}blocks`);\n const bindings: AnySubIndexBinding[] = [];\n const effective: Record<string, SubSpec> = {};\n\n for (const [sub, saved] of Object.entries(subIndexes)) {\n const action = options?.subIndexes?.[sub];\n if (action && \"skip\" in action && action.skip === true) {\n effective[sub] = saved;\n continue;\n }\n const ctx = contextFor(name, sub, blockIds);\n const spec = action && !(\"skip\" in action) ? (action as SubSpec) : saved;\n const provider = providers.get(spec.type);\n if (provider && spec !== saved) await providers.get(saved.type)?.drop(ctx);\n if (!provider) {\n if (onMissing === \"throw\") {\n throw new Error(\n `Cannot load index: sub-index \"${sub}\" requires modality \"${spec.type}\" but only [${[\n ...providers.keys(),\n ]\n .map((t) => `\"${t}\"`)\n .join(\", \")}] are registered.`,\n );\n }\n const g = globalThis as { console?: { warn(...args: unknown[]): void } };\n g.console?.warn(`Sub-index \"${sub}\" (type \"${spec.type}\") has no provider; skipping.`);\n effective[sub] = saved;\n continue;\n }\n const { type, ...config } = spec;\n bindings.push({ name: sub, type, config, index: await provider.open(ctx, config) });\n effective[sub] = spec;\n }\n\n const index = createCompositeIndex({\n name,\n bindings,\n onDeleteIndex: async () => {\n // Bound sub-indexes dropped themselves; skipped ones still own tables.\n for (const [sub, spec] of Object.entries(manifest.get(name)?.subIndexes ?? {})) {\n await providers.get(spec.type)?.drop(contextFor(name, sub, blockIds));\n }\n await db.exec(`DROP TABLE IF EXISTS ${blockIds.table}`);\n await db.query(`DELETE FROM ${MANIFEST_TABLE} WHERE name = ?`, [name]);\n manifest.delete(name);\n indexes.delete(name);\n },\n });\n return { index, effective };\n }\n\n return {\n async getIndexNames(): Promise<IndexInfo[]> {\n ensureOpen();\n return [...manifest.keys()].map((name) => ({ name }));\n },\n\n createIndex(params: CreateIndexParams): Promise<Index> {\n return runExclusive(async () => {\n ensureOpen();\n const { name, subIndexes, overwrite, metadata } = params;\n if (!subIndexes || Object.keys(subIndexes).length === 0) {\n throw new Error(\"Cannot create index without any subIndexes\");\n }\n const existing = manifest.get(name);\n if (existing || indexes.has(name)) {\n if (!overwrite) throw new Error(`Index \"${name}\" already exists`);\n await indexes\n .get(name)\n ?.close()\n .catch(() => undefined);\n indexes.delete(name);\n await dropIndexTables(name, existing?.subIndexes ?? {});\n }\n const { index, effective } = await openIndex(name, subIndexes);\n await writeManifest(name, { subIndexes: effective });\n if (metadata !== undefined) (index as { metadata?: unknown }).metadata = metadata;\n indexes.set(name, index);\n return index;\n });\n },\n\n getIndex(name: string, options?: GetIndexOptions): Promise<Index | null> {\n // Serialised with createIndex so concurrent first opens open once\n // (a second reinit would otherwise drop tables the first just wrote).\n return runExclusive(async () => {\n ensureOpen();\n const cached = indexes.get(name);\n if (cached) return cached;\n const entry = manifest.get(name);\n if (!entry) return null;\n const { index, effective } = await openIndex(name, entry.subIndexes, options);\n if (JSON.stringify(effective) !== JSON.stringify(entry.subIndexes)) {\n await writeManifest(name, { subIndexes: effective });\n }\n indexes.set(name, index);\n return index;\n });\n },\n\n async hasIndex(name: string): Promise<boolean> {\n ensureOpen();\n return manifest.has(name);\n },\n\n async deleteIndex(name: string): Promise<void> {\n ensureOpen();\n const live = indexes.get(name);\n if (live) {\n await live.deleteIndex();\n return;\n }\n const entry = manifest.get(name);\n if (entry) await dropIndexTables(name, entry.subIndexes);\n },\n\n async flush(): Promise<void> {\n ensureOpen();\n for (const index of indexes.values()) await index.flush();\n },\n\n async close(): Promise<void> {\n if (closed) return;\n closed = true;\n for (const index of indexes.values()) await index.close().catch(() => undefined);\n indexes.clear();\n },\n } satisfies Indexer;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"create-sqlite-indexer.js","names":[],"sources":["../src/create-sqlite-indexer.ts"],"sourcesContent":["import type { Db } from \"@statewalker/db-api\";\nimport type {\n AnySubIndexBinding,\n CreateIndexParams,\n GetIndexOptions,\n Index,\n Indexer,\n IndexInfo,\n Metadata,\n} from \"@statewalker/indexer-api\";\nimport { createCompositeIndex, createSerialiser } from \"@statewalker/indexer-core\";\nimport { type BlockIds, openBlockIds } from \"./block-ids.js\";\nimport type { SqliteModalityContext, SqliteModalityProvider } from \"./types.js\";\n\nexport interface SqliteIndexerOptions {\n /** The database. The indexer never closes it. */\n db: Db;\n /** One provider per modality type, e.g. FTS5, vector, n-gram. */\n providers: SqliteModalityProvider[];\n}\n\ntype SubSpec = { type: string } & Record<string, unknown>;\ninterface IndexManifest {\n subIndexes: Record<string, SubSpec>;\n /** Index-level metadata from `createIndex` (absent in manifests written before it was saved). */\n metadata?: Metadata;\n}\n\nconst MANIFEST_TABLE = \"__indexer_manifest\";\n\n/** Escapes everything but lower-case letters and digits: SQLite table names fold case, so \"Docs\" and \"docs\" must not collide. */\nconst sanitizePrefix = (name: string) =>\n name.replace(/[^a-z0-9]/g, (ch) => `_${ch.charCodeAt(0)}_`);\n\nconst indexPrefix = (name: string) => `idx_${sanitizePrefix(name)}__`;\n\n/**\n * `Indexer` over one SQLite `Db`. Keeps a manifest row per Index, one shared\n * `{indexPrefix}blocks` id table per Index, and opens each sub-index through\n * its provider. Search fuses sub-indexes by RRF (`createCompositeIndex`).\n */\nexport async function createSqliteIndexer(options: SqliteIndexerOptions): Promise<Indexer> {\n const { db } = options;\n const providers = new Map(options.providers.map((p) => [p.type, p]));\n const indexes = new Map<string, Index>();\n const manifest = new Map<string, IndexManifest>();\n const runExclusive = createSerialiser();\n let closed = false;\n\n await db.exec(\n `CREATE TABLE IF NOT EXISTS ${MANIFEST_TABLE} (name TEXT PRIMARY KEY, config TEXT NOT NULL)`,\n );\n for (const row of await db.query<{ name: string; config: string }>(\n `SELECT name, config FROM ${MANIFEST_TABLE}`,\n )) {\n manifest.set(row.name, JSON.parse(row.config) as IndexManifest);\n }\n\n const ensureOpen = () => {\n if (closed) throw new Error(\"Indexer is closed\");\n };\n\n const contextFor = (name: string, sub: string, blockIds: BlockIds): SqliteModalityContext => ({\n db,\n prefix: `${indexPrefix(name)}${sanitizePrefix(sub)}_`,\n blockIds,\n });\n\n async function writeManifest(name: string, entry: IndexManifest): Promise<void> {\n await db.query(\n `INSERT INTO ${MANIFEST_TABLE} (name, config) VALUES (?, ?) ON CONFLICT (name) DO UPDATE SET config = excluded.config`,\n [name, JSON.stringify(entry)],\n );\n manifest.set(name, entry);\n }\n\n async function dropIndexTables(name: string, subIndexes: Record<string, SubSpec>): Promise<void> {\n const blockIds = await openBlockIds(db, `${indexPrefix(name)}blocks`);\n for (const [sub, spec] of Object.entries(subIndexes)) {\n await providers.get(spec.type)?.drop(contextFor(name, sub, blockIds));\n }\n await db.exec(`DROP TABLE IF EXISTS ${blockIds.table}`);\n await db.query(`DELETE FROM ${MANIFEST_TABLE} WHERE name = ?`, [name]);\n manifest.delete(name);\n }\n\n async function openIndex(\n name: string,\n subIndexes: Record<string, SubSpec>,\n options?: GetIndexOptions,\n metadata?: Metadata,\n ): Promise<{ index: Index; effective: Record<string, SubSpec> }> {\n const onMissing = options?.onMissingProvider ?? \"throw\";\n const blockIds = await openBlockIds(db, `${indexPrefix(name)}blocks`);\n const bindings: AnySubIndexBinding[] = [];\n const effective: Record<string, SubSpec> = {};\n\n for (const [sub, saved] of Object.entries(subIndexes)) {\n const action = options?.subIndexes?.[sub];\n if (action && \"skip\" in action && action.skip === true) {\n effective[sub] = saved;\n continue;\n }\n const ctx = contextFor(name, sub, blockIds);\n const spec = action && !(\"skip\" in action) ? (action as SubSpec) : saved;\n const provider = providers.get(spec.type);\n if (provider && spec !== saved) await providers.get(saved.type)?.drop(ctx);\n if (!provider) {\n if (onMissing === \"throw\") {\n throw new Error(\n `Cannot load index: sub-index \"${sub}\" requires modality \"${spec.type}\" but only [${[\n ...providers.keys(),\n ]\n .map((t) => `\"${t}\"`)\n .join(\", \")}] are registered.`,\n );\n }\n const g = globalThis as { console?: { warn(...args: unknown[]): void } };\n g.console?.warn(`Sub-index \"${sub}\" (type \"${spec.type}\") has no provider; skipping.`);\n effective[sub] = saved;\n continue;\n }\n const { type, ...config } = spec;\n bindings.push({ name: sub, type, config, index: await provider.open(ctx, config) });\n effective[sub] = spec;\n }\n\n const index: Index = createCompositeIndex({\n name,\n metadata,\n bindings,\n // A closed instance leaves the cache, so getIndex opens a new one.\n onClose: async () => {\n if (indexes.get(name) === index) indexes.delete(name);\n },\n onDeleteIndex: async () => {\n // Bound sub-indexes dropped themselves; skipped ones still own tables.\n for (const [sub, spec] of Object.entries(manifest.get(name)?.subIndexes ?? {})) {\n await providers.get(spec.type)?.drop(contextFor(name, sub, blockIds));\n }\n await db.exec(`DROP TABLE IF EXISTS ${blockIds.table}`);\n await db.query(`DELETE FROM ${MANIFEST_TABLE} WHERE name = ?`, [name]);\n manifest.delete(name);\n indexes.delete(name);\n },\n });\n return { index, effective };\n }\n\n return {\n async getIndexNames(): Promise<IndexInfo[]> {\n ensureOpen();\n return [...manifest.entries()].map(([name, entry]) =>\n entry.metadata !== undefined ? { name, metadata: entry.metadata } : { name },\n );\n },\n\n createIndex(params: CreateIndexParams): Promise<Index> {\n return runExclusive(async () => {\n ensureOpen();\n const { name, subIndexes, overwrite, metadata } = params;\n if (!subIndexes || Object.keys(subIndexes).length === 0) {\n throw new Error(\"Cannot create index without any subIndexes\");\n }\n const existing = manifest.get(name);\n if (existing || indexes.has(name)) {\n if (!overwrite) throw new Error(`Index \"${name}\" already exists`);\n await indexes\n .get(name)\n ?.close()\n .catch(() => undefined);\n indexes.delete(name);\n await dropIndexTables(name, existing?.subIndexes ?? {});\n }\n const { index, effective } = await openIndex(name, subIndexes, undefined, metadata);\n await writeManifest(name, {\n subIndexes: effective,\n ...(metadata !== undefined ? { metadata } : {}),\n });\n indexes.set(name, index);\n return index;\n });\n },\n\n getIndex(name: string, options?: GetIndexOptions): Promise<Index | null> {\n // Serialised with createIndex so concurrent first opens open once\n // (a second reinit would otherwise drop tables the first just wrote).\n return runExclusive(async () => {\n ensureOpen();\n const cached = indexes.get(name);\n if (cached) return cached;\n const entry = manifest.get(name);\n if (!entry) return null;\n const { index, effective } = await openIndex(\n name,\n entry.subIndexes,\n options,\n entry.metadata,\n );\n if (JSON.stringify(effective) !== JSON.stringify(entry.subIndexes)) {\n await writeManifest(name, { ...entry, subIndexes: effective });\n }\n indexes.set(name, index);\n return index;\n });\n },\n\n async hasIndex(name: string): Promise<boolean> {\n ensureOpen();\n return manifest.has(name);\n },\n\n deleteIndex(name: string): Promise<void> {\n return runExclusive(async () => {\n ensureOpen();\n const live = indexes.get(name);\n if (live) {\n await live.deleteIndex();\n return;\n }\n const entry = manifest.get(name);\n if (entry) await dropIndexTables(name, entry.subIndexes);\n });\n },\n\n flush(): Promise<void> {\n return runExclusive(async () => {\n ensureOpen();\n for (const index of indexes.values()) await index.flush();\n });\n },\n\n close(): Promise<void> {\n return runExclusive(async () => {\n if (closed) return;\n closed = true;\n for (const index of [...indexes.values()]) await index.close().catch(() => undefined);\n indexes.clear();\n });\n },\n } satisfies Indexer;\n}\n"],"mappings":"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"}
|
package/dist/enumeration.d.ts
CHANGED
|
@@ -12,7 +12,10 @@ export interface Enumeration {
|
|
|
12
12
|
paths(prefix?: DocumentPath): AsyncGenerator<DocumentPath>;
|
|
13
13
|
refs(prefix?: DocumentPath): AsyncGenerator<BlockReference>;
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* `memberIdsSql` is a `SELECT … AS id` listing the sub-index's ids, e.g. `SELECT rowid AS id FROM t`.
|
|
17
|
+
* `ensureOpen` runs before every call, so a closed sub-index throws like the other backends.
|
|
18
|
+
*/
|
|
19
|
+
export declare function newEnumeration(db: Db, blockIds: BlockIds, memberIdsSql: string, ensureOpen?: () => void): Enumeration;
|
|
17
20
|
//#endregion
|
|
18
21
|
//# sourceMappingURL=enumeration.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enumeration.d.ts","names":[],"sources":["../src/enumeration.ts"],"mappings":";;;;iBAKiB,mBAAmB;EAClC;;;iBAIe;EACf,KAAK,SAAS,eAAe;EAC7B,QAAQ,SAAS,eAAe,QAAQ;EACxC,MAAM,SAAS,eAAe,eAAe;EAC7C,KAAK,SAAS,eAAe,eAAe
|
|
1
|
+
{"version":3,"file":"enumeration.d.ts","names":[],"sources":["../src/enumeration.ts"],"mappings":";;;;iBAKiB,mBAAmB;EAClC;;;iBAIe;EACf,KAAK,SAAS,eAAe;EAC7B,QAAQ,SAAS,eAAe,QAAQ;EACxC,MAAM,SAAS,eAAe,eAAe;EAC7C,KAAK,SAAS,eAAe,eAAe;;;;;;wBAO9B,eACd,IAAI,IACJ,UAAU,UACV,sBACA,0BACC"}
|
package/dist/enumeration.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { pathPrefixSql } from "./path-sql.js";
|
|
2
2
|
//#region src/enumeration.ts
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* `memberIdsSql` is a `SELECT … AS id` listing the sub-index's ids, e.g. `SELECT rowid AS id FROM t`.
|
|
5
|
+
* `ensureOpen` runs before every call, so a closed sub-index throws like the other backends.
|
|
6
|
+
*/
|
|
7
|
+
function newEnumeration(db, blockIds, memberIdsSql, ensureOpen = () => {}) {
|
|
5
8
|
const from = (prefix) => {
|
|
6
9
|
const scope = pathPrefixSql("b.path", prefix ? [prefix] : []);
|
|
7
10
|
return {
|
|
@@ -11,11 +14,13 @@ function newEnumeration(db, blockIds, memberIdsSql) {
|
|
|
11
14
|
};
|
|
12
15
|
return {
|
|
13
16
|
async size(prefix) {
|
|
17
|
+
ensureOpen();
|
|
14
18
|
const f = from(prefix);
|
|
15
19
|
const rows = await db.query(`SELECT COUNT(*) AS n ${f.sql}`, f.params);
|
|
16
20
|
return Number(rows[0]?.n ?? 0);
|
|
17
21
|
},
|
|
18
22
|
async entries(prefix) {
|
|
23
|
+
ensureOpen();
|
|
19
24
|
const f = from(prefix);
|
|
20
25
|
return (await db.query(`SELECT b.id, b.path, b.block_id ${f.sql} ORDER BY b.id`, f.params)).map((r) => ({
|
|
21
26
|
id: Number(r.id),
|
|
@@ -24,6 +29,7 @@ function newEnumeration(db, blockIds, memberIdsSql) {
|
|
|
24
29
|
}));
|
|
25
30
|
},
|
|
26
31
|
async *paths(prefix) {
|
|
32
|
+
ensureOpen();
|
|
27
33
|
const f = from(prefix);
|
|
28
34
|
const rows = await db.query(`SELECT DISTINCT b.path ${f.sql}`, f.params);
|
|
29
35
|
for (const r of rows) yield r.path;
|
package/dist/enumeration.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enumeration.js","names":[],"sources":["../src/enumeration.ts"],"sourcesContent":["import type { Db } from \"@statewalker/db-api\";\nimport type { BlockReference, DocumentPath } from \"@statewalker/indexer-api\";\nimport type { BlockIds } from \"./block-ids.js\";\nimport { pathPrefixSql } from \"./path-sql.js\";\n\nexport interface BlockEntry extends BlockReference {\n id: number;\n}\n\n/** `SearchIndex` enumeration over a sub-index's member ids joined with the blocks table. */\nexport interface Enumeration {\n size(prefix?: DocumentPath): Promise<number>;\n entries(prefix?: DocumentPath): Promise<BlockEntry[]>;\n paths(prefix?: DocumentPath): AsyncGenerator<DocumentPath>;\n refs(prefix?: DocumentPath): AsyncGenerator<BlockReference>;\n}\n\n
|
|
1
|
+
{"version":3,"file":"enumeration.js","names":[],"sources":["../src/enumeration.ts"],"sourcesContent":["import type { Db } from \"@statewalker/db-api\";\nimport type { BlockReference, DocumentPath } from \"@statewalker/indexer-api\";\nimport type { BlockIds } from \"./block-ids.js\";\nimport { pathPrefixSql } from \"./path-sql.js\";\n\nexport interface BlockEntry extends BlockReference {\n id: number;\n}\n\n/** `SearchIndex` enumeration over a sub-index's member ids joined with the blocks table. */\nexport interface Enumeration {\n size(prefix?: DocumentPath): Promise<number>;\n entries(prefix?: DocumentPath): Promise<BlockEntry[]>;\n paths(prefix?: DocumentPath): AsyncGenerator<DocumentPath>;\n refs(prefix?: DocumentPath): AsyncGenerator<BlockReference>;\n}\n\n/**\n * `memberIdsSql` is a `SELECT … AS id` listing the sub-index's ids, e.g. `SELECT rowid AS id FROM t`.\n * `ensureOpen` runs before every call, so a closed sub-index throws like the other backends.\n */\nexport function newEnumeration(\n db: Db,\n blockIds: BlockIds,\n memberIdsSql: string,\n ensureOpen: () => void = () => {},\n): Enumeration {\n const from = (prefix?: DocumentPath) => {\n const scope = pathPrefixSql(\"b.path\", prefix ? [prefix] : []);\n return {\n sql: `FROM (${memberIdsSql}) m JOIN ${blockIds.table} b ON b.id = m.id WHERE ${scope.sql}`,\n params: scope.params,\n };\n };\n return {\n async size(prefix) {\n ensureOpen();\n const f = from(prefix);\n const rows = await db.query<{ n: number }>(`SELECT COUNT(*) AS n ${f.sql}`, f.params);\n return Number(rows[0]?.n ?? 0);\n },\n async entries(prefix) {\n ensureOpen();\n const f = from(prefix);\n const rows = await db.query<{ id: number; path: string; block_id: string }>(\n `SELECT b.id, b.path, b.block_id ${f.sql} ORDER BY b.id`,\n f.params,\n );\n return rows.map((r) => ({\n id: Number(r.id),\n path: r.path as DocumentPath,\n blockId: r.block_id,\n }));\n },\n async *paths(prefix) {\n ensureOpen();\n const f = from(prefix);\n const rows = await db.query<{ path: string }>(`SELECT DISTINCT b.path ${f.sql}`, f.params);\n for (const r of rows) yield r.path as DocumentPath;\n },\n async *refs(prefix) {\n for (const e of await this.entries(prefix)) yield { path: e.path, blockId: e.blockId };\n },\n };\n}\n"],"mappings":";;;;;;AAqBA,SAAgB,eACd,IACA,UACA,cACA,mBAA+B,CAAC,GACnB;CACb,MAAM,QAAQ,WAA0B;EACtC,MAAM,QAAQ,cAAc,UAAU,SAAS,CAAC,MAAM,IAAI,CAAC,CAAC;EAC5D,OAAO;GACL,KAAK,SAAS,aAAa,WAAW,SAAS,MAAM,0BAA0B,MAAM;GACrF,QAAQ,MAAM;EAChB;CACF;CACA,OAAO;EACL,MAAM,KAAK,QAAQ;GACjB,WAAW;GACX,MAAM,IAAI,KAAK,MAAM;GACrB,MAAM,OAAO,MAAM,GAAG,MAAqB,wBAAwB,EAAE,OAAO,EAAE,MAAM;GACpF,OAAO,OAAO,KAAK,EAAE,EAAE,KAAK,CAAC;EAC/B;EACA,MAAM,QAAQ,QAAQ;GACpB,WAAW;GACX,MAAM,IAAI,KAAK,MAAM;GAKrB,QAAO,MAJY,GAAG,MACpB,mCAAmC,EAAE,IAAI,iBACzC,EAAE,MACJ,EAAA,CACY,KAAK,OAAO;IACtB,IAAI,OAAO,EAAE,EAAE;IACf,MAAM,EAAE;IACR,SAAS,EAAE;GACb,EAAE;EACJ;EACA,OAAO,MAAM,QAAQ;GACnB,WAAW;GACX,MAAM,IAAI,KAAK,MAAM;GACrB,MAAM,OAAO,MAAM,GAAG,MAAwB,0BAA0B,EAAE,OAAO,EAAE,MAAM;GACzF,KAAK,MAAM,KAAK,MAAM,MAAM,EAAE;EAChC;EACA,OAAO,KAAK,QAAQ;GAClB,KAAK,MAAM,KAAK,MAAM,KAAK,QAAQ,MAAM,GAAG,MAAM;IAAE,MAAM,EAAE;IAAM,SAAS,EAAE;GAAQ;EACvF;CACF;AACF"}
|
package/dist/fts-provider.js
CHANGED
|
@@ -27,7 +27,7 @@ function newSqliteFullTextProvider() {
|
|
|
27
27
|
const table = `${prefix}fts`;
|
|
28
28
|
const blocks = blockIds.table;
|
|
29
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}
|
|
30
|
+
const list = newEnumeration(db, blockIds, `SELECT rowid AS id FROM ${table}`, () => ensureOpen());
|
|
31
31
|
let closed = false;
|
|
32
32
|
const ensureOpen = () => {
|
|
33
33
|
if (closed) throw new Error("FullTextIndex is closed");
|
|
@@ -116,6 +116,7 @@ function newSqliteFullTextProvider() {
|
|
|
116
116
|
getDocumentPaths: (p) => list.paths(p),
|
|
117
117
|
getDocumentBlocksRefs: (p) => list.refs(p),
|
|
118
118
|
async *getDocumentsBlocks(p) {
|
|
119
|
+
ensureOpen();
|
|
119
120
|
const scope = pathPrefixSql("b.path", p ? [p] : []);
|
|
120
121
|
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
122
|
for (const r of rows) yield {
|
package/dist/fts-provider.js.map
CHANGED
|
@@ -1 +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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"}
|
|
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 ensureOpen(),\n );\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 ensureOpen();\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/ngram-provider.js
CHANGED
|
@@ -49,7 +49,7 @@ function newSqliteNgramProvider() {
|
|
|
49
49
|
store: storeKind
|
|
50
50
|
});
|
|
51
51
|
await store.init();
|
|
52
|
-
const list = newEnumeration(db, blockIds, `SELECT id FROM ${store.idTable}
|
|
52
|
+
const list = newEnumeration(db, blockIds, `SELECT id FROM ${store.idTable}`, () => ensureOpen());
|
|
53
53
|
let closed = false;
|
|
54
54
|
const ensureOpen = () => {
|
|
55
55
|
if (closed) throw new Error("NgramIndex is closed");
|
|
@@ -113,6 +113,7 @@ function newSqliteNgramProvider() {
|
|
|
113
113
|
getDocumentPaths: (p) => list.paths(p),
|
|
114
114
|
getDocumentBlocksRefs: (p) => list.refs(p),
|
|
115
115
|
async *getDocumentsBlocks(p) {
|
|
116
|
+
ensureOpen();
|
|
116
117
|
const entries = await list.entries(p);
|
|
117
118
|
const texts = await store.getTexts(entries.map((e) => e.id));
|
|
118
119
|
for (const e of entries) {
|
|
@@ -1 +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}
|
|
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 ensureOpen(),\n );\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 ensureOpen();\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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"}
|
package/dist/vector-provider.js
CHANGED
|
@@ -23,7 +23,7 @@ function newSqliteVectorProvider() {
|
|
|
23
23
|
metric: "cosine"
|
|
24
24
|
});
|
|
25
25
|
await engine.init();
|
|
26
|
-
const list = newEnumeration(db, blockIds, `SELECT id FROM ${engine.idTable}
|
|
26
|
+
const list = newEnumeration(db, blockIds, `SELECT id FROM ${engine.idTable}`, () => ensureOpen());
|
|
27
27
|
let closed = false;
|
|
28
28
|
const ensureOpen = () => {
|
|
29
29
|
if (closed) throw new Error("VectorIndex is closed");
|
|
@@ -39,6 +39,7 @@ function newSqliteVectorProvider() {
|
|
|
39
39
|
},
|
|
40
40
|
async *search(params) {
|
|
41
41
|
ensureOpen();
|
|
42
|
+
for (const q of params.embeddings ?? []) validateDimensionality(info, q);
|
|
42
43
|
const k = params.topK ?? 100;
|
|
43
44
|
const allowed = params.paths?.length ? new Set(await blockIds.select(db, params.paths.map((path) => ({ path })))) : void 0;
|
|
44
45
|
const filter = allowed ? (id) => allowed.has(id) : void 0;
|
|
@@ -85,6 +86,7 @@ function newSqliteVectorProvider() {
|
|
|
85
86
|
getDocumentPaths: (p) => list.paths(p),
|
|
86
87
|
getDocumentBlocksRefs: (p) => list.refs(p),
|
|
87
88
|
async *getDocumentsBlocks(p) {
|
|
89
|
+
ensureOpen();
|
|
88
90
|
const entries = await list.entries(p);
|
|
89
91
|
const vectors = await engine.getVectors(entries.map((e) => e.id));
|
|
90
92
|
for (const e of entries) {
|
|
@@ -1 +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}
|
|
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 ensureOpen(),\n );\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 for (const q of params.embeddings ?? []) validateDimensionality(info, q);\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 ensureOpen();\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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/indexer-sqlite",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "SQLite-backed indexer: FTS5, vector and n-gram/grep sub-indexes in one @statewalker/db-api Db (Node, browser worker, Durable Objects).",
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
"@statewalker/db-api": "^0.1.0",
|
|
35
35
|
"@statewalker/db-sqlite": "^0.1.0",
|
|
36
36
|
"@statewalker/db-sqlite-index": "^0.1.0",
|
|
37
|
-
"@statewalker/indexer-api": "^0.1.
|
|
38
|
-
"@statewalker/indexer-
|
|
39
|
-
"@statewalker/indexer-
|
|
40
|
-
"@statewalker/indexer-ngram": "^0.1.
|
|
41
|
-
"@statewalker/indexer-vector": "^0.1.
|
|
37
|
+
"@statewalker/indexer-api": "^0.1.4",
|
|
38
|
+
"@statewalker/indexer-core": "^0.1.4",
|
|
39
|
+
"@statewalker/indexer-fulltext": "^0.1.5",
|
|
40
|
+
"@statewalker/indexer-ngram": "^0.1.1",
|
|
41
|
+
"@statewalker/indexer-vector": "^0.1.5"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@statewalker/db-sqlite-do": "^0.1.0",
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
Index,
|
|
7
7
|
Indexer,
|
|
8
8
|
IndexInfo,
|
|
9
|
+
Metadata,
|
|
9
10
|
} from "@statewalker/indexer-api";
|
|
10
11
|
import { createCompositeIndex, createSerialiser } from "@statewalker/indexer-core";
|
|
11
12
|
import { type BlockIds, openBlockIds } from "./block-ids.js";
|
|
@@ -21,6 +22,8 @@ export interface SqliteIndexerOptions {
|
|
|
21
22
|
type SubSpec = { type: string } & Record<string, unknown>;
|
|
22
23
|
interface IndexManifest {
|
|
23
24
|
subIndexes: Record<string, SubSpec>;
|
|
25
|
+
/** Index-level metadata from `createIndex` (absent in manifests written before it was saved). */
|
|
26
|
+
metadata?: Metadata;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
const MANIFEST_TABLE = "__indexer_manifest";
|
|
@@ -85,6 +88,7 @@ export async function createSqliteIndexer(options: SqliteIndexerOptions): Promis
|
|
|
85
88
|
name: string,
|
|
86
89
|
subIndexes: Record<string, SubSpec>,
|
|
87
90
|
options?: GetIndexOptions,
|
|
91
|
+
metadata?: Metadata,
|
|
88
92
|
): Promise<{ index: Index; effective: Record<string, SubSpec> }> {
|
|
89
93
|
const onMissing = options?.onMissingProvider ?? "throw";
|
|
90
94
|
const blockIds = await openBlockIds(db, `${indexPrefix(name)}blocks`);
|
|
@@ -121,9 +125,14 @@ export async function createSqliteIndexer(options: SqliteIndexerOptions): Promis
|
|
|
121
125
|
effective[sub] = spec;
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
const index = createCompositeIndex({
|
|
128
|
+
const index: Index = createCompositeIndex({
|
|
125
129
|
name,
|
|
130
|
+
metadata,
|
|
126
131
|
bindings,
|
|
132
|
+
// A closed instance leaves the cache, so getIndex opens a new one.
|
|
133
|
+
onClose: async () => {
|
|
134
|
+
if (indexes.get(name) === index) indexes.delete(name);
|
|
135
|
+
},
|
|
127
136
|
onDeleteIndex: async () => {
|
|
128
137
|
// Bound sub-indexes dropped themselves; skipped ones still own tables.
|
|
129
138
|
for (const [sub, spec] of Object.entries(manifest.get(name)?.subIndexes ?? {})) {
|
|
@@ -141,7 +150,9 @@ export async function createSqliteIndexer(options: SqliteIndexerOptions): Promis
|
|
|
141
150
|
return {
|
|
142
151
|
async getIndexNames(): Promise<IndexInfo[]> {
|
|
143
152
|
ensureOpen();
|
|
144
|
-
return [...manifest.
|
|
153
|
+
return [...manifest.entries()].map(([name, entry]) =>
|
|
154
|
+
entry.metadata !== undefined ? { name, metadata: entry.metadata } : { name },
|
|
155
|
+
);
|
|
145
156
|
},
|
|
146
157
|
|
|
147
158
|
createIndex(params: CreateIndexParams): Promise<Index> {
|
|
@@ -161,9 +172,11 @@ export async function createSqliteIndexer(options: SqliteIndexerOptions): Promis
|
|
|
161
172
|
indexes.delete(name);
|
|
162
173
|
await dropIndexTables(name, existing?.subIndexes ?? {});
|
|
163
174
|
}
|
|
164
|
-
const { index, effective } = await openIndex(name, subIndexes);
|
|
165
|
-
await writeManifest(name, {
|
|
166
|
-
|
|
175
|
+
const { index, effective } = await openIndex(name, subIndexes, undefined, metadata);
|
|
176
|
+
await writeManifest(name, {
|
|
177
|
+
subIndexes: effective,
|
|
178
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
179
|
+
});
|
|
167
180
|
indexes.set(name, index);
|
|
168
181
|
return index;
|
|
169
182
|
});
|
|
@@ -178,9 +191,14 @@ export async function createSqliteIndexer(options: SqliteIndexerOptions): Promis
|
|
|
178
191
|
if (cached) return cached;
|
|
179
192
|
const entry = manifest.get(name);
|
|
180
193
|
if (!entry) return null;
|
|
181
|
-
const { index, effective } = await openIndex(
|
|
194
|
+
const { index, effective } = await openIndex(
|
|
195
|
+
name,
|
|
196
|
+
entry.subIndexes,
|
|
197
|
+
options,
|
|
198
|
+
entry.metadata,
|
|
199
|
+
);
|
|
182
200
|
if (JSON.stringify(effective) !== JSON.stringify(entry.subIndexes)) {
|
|
183
|
-
await writeManifest(name, { subIndexes: effective });
|
|
201
|
+
await writeManifest(name, { ...entry, subIndexes: effective });
|
|
184
202
|
}
|
|
185
203
|
indexes.set(name, index);
|
|
186
204
|
return index;
|
|
@@ -192,27 +210,33 @@ export async function createSqliteIndexer(options: SqliteIndexerOptions): Promis
|
|
|
192
210
|
return manifest.has(name);
|
|
193
211
|
},
|
|
194
212
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
213
|
+
deleteIndex(name: string): Promise<void> {
|
|
214
|
+
return runExclusive(async () => {
|
|
215
|
+
ensureOpen();
|
|
216
|
+
const live = indexes.get(name);
|
|
217
|
+
if (live) {
|
|
218
|
+
await live.deleteIndex();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const entry = manifest.get(name);
|
|
222
|
+
if (entry) await dropIndexTables(name, entry.subIndexes);
|
|
223
|
+
});
|
|
204
224
|
},
|
|
205
225
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
226
|
+
flush(): Promise<void> {
|
|
227
|
+
return runExclusive(async () => {
|
|
228
|
+
ensureOpen();
|
|
229
|
+
for (const index of indexes.values()) await index.flush();
|
|
230
|
+
});
|
|
209
231
|
},
|
|
210
232
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
233
|
+
close(): Promise<void> {
|
|
234
|
+
return runExclusive(async () => {
|
|
235
|
+
if (closed) return;
|
|
236
|
+
closed = true;
|
|
237
|
+
for (const index of [...indexes.values()]) await index.close().catch(() => undefined);
|
|
238
|
+
indexes.clear();
|
|
239
|
+
});
|
|
216
240
|
},
|
|
217
241
|
} satisfies Indexer;
|
|
218
242
|
}
|
package/src/enumeration.ts
CHANGED
|
@@ -15,8 +15,16 @@ export interface Enumeration {
|
|
|
15
15
|
refs(prefix?: DocumentPath): AsyncGenerator<BlockReference>;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* `memberIdsSql` is a `SELECT … AS id` listing the sub-index's ids, e.g. `SELECT rowid AS id FROM t`.
|
|
20
|
+
* `ensureOpen` runs before every call, so a closed sub-index throws like the other backends.
|
|
21
|
+
*/
|
|
22
|
+
export function newEnumeration(
|
|
23
|
+
db: Db,
|
|
24
|
+
blockIds: BlockIds,
|
|
25
|
+
memberIdsSql: string,
|
|
26
|
+
ensureOpen: () => void = () => {},
|
|
27
|
+
): Enumeration {
|
|
20
28
|
const from = (prefix?: DocumentPath) => {
|
|
21
29
|
const scope = pathPrefixSql("b.path", prefix ? [prefix] : []);
|
|
22
30
|
return {
|
|
@@ -26,11 +34,13 @@ export function newEnumeration(db: Db, blockIds: BlockIds, memberIdsSql: string)
|
|
|
26
34
|
};
|
|
27
35
|
return {
|
|
28
36
|
async size(prefix) {
|
|
37
|
+
ensureOpen();
|
|
29
38
|
const f = from(prefix);
|
|
30
39
|
const rows = await db.query<{ n: number }>(`SELECT COUNT(*) AS n ${f.sql}`, f.params);
|
|
31
40
|
return Number(rows[0]?.n ?? 0);
|
|
32
41
|
},
|
|
33
42
|
async entries(prefix) {
|
|
43
|
+
ensureOpen();
|
|
34
44
|
const f = from(prefix);
|
|
35
45
|
const rows = await db.query<{ id: number; path: string; block_id: string }>(
|
|
36
46
|
`SELECT b.id, b.path, b.block_id ${f.sql} ORDER BY b.id`,
|
|
@@ -43,6 +53,7 @@ export function newEnumeration(db: Db, blockIds: BlockIds, memberIdsSql: string)
|
|
|
43
53
|
}));
|
|
44
54
|
},
|
|
45
55
|
async *paths(prefix) {
|
|
56
|
+
ensureOpen();
|
|
46
57
|
const f = from(prefix);
|
|
47
58
|
const rows = await db.query<{ path: string }>(`SELECT DISTINCT b.path ${f.sql}`, f.params);
|
|
48
59
|
for (const r of rows) yield r.path as DocumentPath;
|
package/src/fts-provider.ts
CHANGED
|
@@ -47,7 +47,9 @@ export function newSqliteFullTextProvider(): SqliteModalityProvider<FullTextConf
|
|
|
47
47
|
await db.exec(
|
|
48
48
|
`CREATE VIRTUAL TABLE IF NOT EXISTS ${table} USING fts5(content, metadata UNINDEXED, tokenize='${tokenizerFor(config.language)}')`,
|
|
49
49
|
);
|
|
50
|
-
const list = newEnumeration(db, blockIds, `SELECT rowid AS id FROM ${table}
|
|
50
|
+
const list = newEnumeration(db, blockIds, `SELECT rowid AS id FROM ${table}`, () =>
|
|
51
|
+
ensureOpen(),
|
|
52
|
+
);
|
|
51
53
|
let closed = false;
|
|
52
54
|
const ensureOpen = () => {
|
|
53
55
|
if (closed) throw new Error("FullTextIndex is closed");
|
|
@@ -135,6 +137,7 @@ export function newSqliteFullTextProvider(): SqliteModalityProvider<FullTextConf
|
|
|
135
137
|
getDocumentBlocksRefs: (p?: DocumentPath) => list.refs(p),
|
|
136
138
|
|
|
137
139
|
async *getDocumentsBlocks(p?: DocumentPath): AsyncGenerator<FullTextBlock> {
|
|
140
|
+
ensureOpen();
|
|
138
141
|
const scope = pathPrefixSql("b.path", p ? [p] : []);
|
|
139
142
|
const rows = await db.query<{
|
|
140
143
|
path: string;
|
package/src/ngram-provider.ts
CHANGED
|
@@ -55,7 +55,9 @@ export function newSqliteNgramProvider(): SqliteModalityProvider<NgramConfig, Ng
|
|
|
55
55
|
const storeKind = config.store ?? "fts5";
|
|
56
56
|
const store = new NgramStore({ db, prefix: `${prefix}ng_`, store: storeKind });
|
|
57
57
|
await store.init();
|
|
58
|
-
const list = newEnumeration(db, blockIds, `SELECT id FROM ${store.idTable}
|
|
58
|
+
const list = newEnumeration(db, blockIds, `SELECT id FROM ${store.idTable}`, () =>
|
|
59
|
+
ensureOpen(),
|
|
60
|
+
);
|
|
59
61
|
let closed = false;
|
|
60
62
|
const ensureOpen = () => {
|
|
61
63
|
if (closed) throw new Error("NgramIndex is closed");
|
|
@@ -130,6 +132,7 @@ export function newSqliteNgramProvider(): SqliteModalityProvider<NgramConfig, Ng
|
|
|
130
132
|
getDocumentBlocksRefs: (p?: DocumentPath) => list.refs(p),
|
|
131
133
|
|
|
132
134
|
async *getDocumentsBlocks(p?: DocumentPath): AsyncGenerator<NgramBlock> {
|
|
135
|
+
ensureOpen();
|
|
133
136
|
const entries = await list.entries(p);
|
|
134
137
|
const texts = await store.getTexts(entries.map((e) => e.id));
|
|
135
138
|
for (const e of entries) {
|
package/src/vector-provider.ts
CHANGED
|
@@ -34,7 +34,9 @@ export function newSqliteVectorProvider(): SqliteModalityProvider<VectorConfig,
|
|
|
34
34
|
metric: "cosine",
|
|
35
35
|
});
|
|
36
36
|
await engine.init();
|
|
37
|
-
const list = newEnumeration(db, blockIds, `SELECT id FROM ${engine.idTable}
|
|
37
|
+
const list = newEnumeration(db, blockIds, `SELECT id FROM ${engine.idTable}`, () =>
|
|
38
|
+
ensureOpen(),
|
|
39
|
+
);
|
|
38
40
|
let closed = false;
|
|
39
41
|
const ensureOpen = () => {
|
|
40
42
|
if (closed) throw new Error("VectorIndex is closed");
|
|
@@ -52,6 +54,7 @@ export function newSqliteVectorProvider(): SqliteModalityProvider<VectorConfig,
|
|
|
52
54
|
|
|
53
55
|
async *search(params: VectorQuery): AsyncGenerator<VectorResult> {
|
|
54
56
|
ensureOpen();
|
|
57
|
+
for (const q of params.embeddings ?? []) validateDimensionality(info, q);
|
|
55
58
|
const k = params.topK ?? 100;
|
|
56
59
|
const allowed = params.paths?.length
|
|
57
60
|
? new Set(
|
|
@@ -109,6 +112,7 @@ export function newSqliteVectorProvider(): SqliteModalityProvider<VectorConfig,
|
|
|
109
112
|
getDocumentBlocksRefs: (p?: DocumentPath) => list.refs(p),
|
|
110
113
|
|
|
111
114
|
async *getDocumentsBlocks(p?: DocumentPath): AsyncGenerator<VectorBlock> {
|
|
115
|
+
ensureOpen();
|
|
112
116
|
const entries = await list.entries(p);
|
|
113
117
|
const vectors = await engine.getVectors(entries.map((e) => e.id));
|
|
114
118
|
for (const e of entries) {
|