@lincoln504/pi-research 1.6.19 → 1.6.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/agent-skill/pi-research/SKILL.md +1 -1
- package/dist/cli.mjs +343 -55
- package/dist/thread-worker.mjs +7 -0
- package/docs/CHANGELOG.md +16 -0
- package/docs/CONFIGURATION.md +3 -1
- package/docs/KNOWLEDGE-STORE.md +38 -4
- package/package.json +10 -10
- package/src/cli.ts +60 -7
- package/src/config.ts +35 -15
- package/src/core/pi-version.ts +12 -3
- package/src/healthcheck/index.ts +18 -0
- package/src/index.ts +3 -2
- package/src/infrastructure/browser/thread-worker.mjs +7 -0
- package/src/infrastructure/knowledge-store-service.ts +29 -4
- package/src/knowledge/availability.ts +33 -10
- package/src/knowledge/index.ts +17 -6
- package/src/knowledge/store-operations.ts +177 -0
- package/src/knowledge/store-schema.ts +58 -0
- package/src/knowledge/store.ts +118 -21
- package/src/research-config.ts +58 -18
|
@@ -46,17 +46,36 @@ export interface KnowledgeStoreAvailability {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* The packages the store cannot run without
|
|
50
|
-
*
|
|
51
|
-
*
|
|
49
|
+
* The packages the store cannot run without, per retrieval mode.
|
|
50
|
+
*
|
|
51
|
+
* Every mode needs `@lancedb/lancedb` — it backs the store in ALL modes (its
|
|
52
|
+
* native FTS/Tantivy engine provides BM25 ranking in lexical mode too).
|
|
53
|
+
*
|
|
54
|
+
* Only the 'vector' (hybrid) retrieval mode additionally needs
|
|
55
|
+
* `@huggingface/transformers` (the only embedding backend — see
|
|
56
|
+
* transformers-loader.ts, there is no remote-embedding fallback). Selecting
|
|
57
|
+
* retrieval='bm25' is a HARD dependency boundary: the embedding package is
|
|
58
|
+
* never resolved, loaded, or downloaded in lexical mode, so a bm25-only host
|
|
59
|
+
* (e.g. an install with --omit=optional) runs the store fully.
|
|
52
60
|
*/
|
|
53
|
-
|
|
61
|
+
function requiredPackages(retrieval: KnowledgeStoreRetrieval): readonly string[] {
|
|
62
|
+
return retrieval === 'bm25'
|
|
63
|
+
? ['@lancedb/lancedb']
|
|
64
|
+
: ['@lancedb/lancedb', '@huggingface/transformers'];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Knowledge store retrieval strategy. 'vector' = hybrid dense+sparse (default,
|
|
68
|
+
* embedding-backed); 'bm25' = pure lexical, no embedding model. */
|
|
69
|
+
export type KnowledgeStoreRetrieval = 'vector' | 'bm25';
|
|
54
70
|
|
|
55
71
|
// Memoized per-process: resolution touches the filesystem on every miss, the
|
|
56
72
|
// answer cannot change within a live process without a reinstall mid-flight,
|
|
57
73
|
// and the probe sits on settings/health paths that run often. Tests reset it
|
|
58
74
|
// via clearAvailabilityCache.
|
|
59
75
|
let _cached: KnowledgeStoreAvailability | null = null;
|
|
76
|
+
// The retrieval mode the cached probe answer was computed for — the cache must
|
|
77
|
+
// be keyed on it because the required-package set differs between modes.
|
|
78
|
+
let _cachedRetrieval: KnowledgeStoreRetrieval | null = null;
|
|
60
79
|
|
|
61
80
|
/** Resolver signature — injectable so tests never need the real filesystem. */
|
|
62
81
|
export type PackageResolver = (specifier: string) => string;
|
|
@@ -75,10 +94,10 @@ const defaultResolver: PackageResolver = (specifier) =>
|
|
|
75
94
|
* Probe whether the knowledge store's required packages are resolvable.
|
|
76
95
|
* Memoized after the first call; {@link clearAvailabilityCache} resets.
|
|
77
96
|
*/
|
|
78
|
-
export function probeKnowledgeStoreAvailability(resolver: PackageResolver = defaultResolver): KnowledgeStoreAvailability {
|
|
79
|
-
if (cachedIsValidFor(resolver)) return _cached!;
|
|
97
|
+
export function probeKnowledgeStoreAvailability(retrieval: KnowledgeStoreRetrieval = 'vector', resolver: PackageResolver = defaultResolver): KnowledgeStoreAvailability {
|
|
98
|
+
if (cachedIsValidFor(retrieval, resolver)) return _cached!;
|
|
80
99
|
const missing: string[] = [];
|
|
81
|
-
for (const pkg of
|
|
100
|
+
for (const pkg of requiredPackages(retrieval)) {
|
|
82
101
|
try {
|
|
83
102
|
resolver(pkg);
|
|
84
103
|
} catch {
|
|
@@ -88,17 +107,21 @@ export function probeKnowledgeStoreAvailability(resolver: PackageResolver = defa
|
|
|
88
107
|
}
|
|
89
108
|
}
|
|
90
109
|
const result: KnowledgeStoreAvailability = { available: missing.length === 0, missing };
|
|
91
|
-
if (resolver === defaultResolver)
|
|
110
|
+
if (resolver === defaultResolver) {
|
|
111
|
+
_cached = result;
|
|
112
|
+
_cachedRetrieval = retrieval;
|
|
113
|
+
}
|
|
92
114
|
return result;
|
|
93
115
|
}
|
|
94
116
|
|
|
95
|
-
function cachedIsValidFor(resolver: PackageResolver): boolean {
|
|
96
|
-
return _cached !== null && resolver === defaultResolver;
|
|
117
|
+
function cachedIsValidFor(retrieval: KnowledgeStoreRetrieval, resolver: PackageResolver): boolean {
|
|
118
|
+
return _cached !== null && _cachedRetrieval === retrieval && resolver === defaultResolver;
|
|
97
119
|
}
|
|
98
120
|
|
|
99
121
|
/** Reset the memoized probe result (test isolation). */
|
|
100
122
|
export function clearAvailabilityCache(): void {
|
|
101
123
|
_cached = null;
|
|
124
|
+
_cachedRetrieval = null;
|
|
102
125
|
}
|
|
103
126
|
|
|
104
127
|
/**
|
package/src/knowledge/index.ts
CHANGED
|
@@ -33,7 +33,9 @@ function getMigrationStrategy(config: Config): MigrationStrategy | undefined {
|
|
|
33
33
|
* Result of knowledge store initialization
|
|
34
34
|
*/
|
|
35
35
|
export interface KnowledgeStoreComponents {
|
|
36
|
-
|
|
36
|
+
/** null in bm25 (lexical) retrieval mode — the store runs with NO embedding
|
|
37
|
+
* model; non-null in 'vector' (hybrid) mode. */
|
|
38
|
+
embedder: IEmbedder | null;
|
|
37
39
|
store: IKnowledgeStore;
|
|
38
40
|
writerQueue: WriterQueue;
|
|
39
41
|
}
|
|
@@ -51,7 +53,7 @@ export async function createKnowledgeStoreComponents(
|
|
|
51
53
|
withLock?: <T>(fn: () => Promise<T>) => Promise<T>,
|
|
52
54
|
config: Config = getConfig(),
|
|
53
55
|
workspace: string = process.cwd(),
|
|
54
|
-
availability: KnowledgeStoreAvailability = probeKnowledgeStoreAvailability()
|
|
56
|
+
availability: KnowledgeStoreAvailability = probeKnowledgeStoreAvailability(config.KNOWLEDGE_STORE_RETRIEVAL)
|
|
55
57
|
): Promise<KnowledgeStoreComponents | null> {
|
|
56
58
|
if (config.KNOWLEDGE_STORE_MODE === 'none') {
|
|
57
59
|
logger.debug('[knowledge] Knowledge store is disabled in configuration');
|
|
@@ -76,15 +78,20 @@ export async function createKnowledgeStoreComponents(
|
|
|
76
78
|
for (let attempt = 1; attempt <= MAX_INIT_RETRIES; attempt++) {
|
|
77
79
|
let store: KnowledgeStore | undefined;
|
|
78
80
|
try {
|
|
79
|
-
logger.info(`[knowledge] Creating Knowledge Store components (attempt ${attempt}/${MAX_INIT_RETRIES})...`);
|
|
81
|
+
logger.info(`[knowledge] Creating Knowledge Store components (attempt ${attempt}/${MAX_INIT_RETRIES}, retrieval=${config.KNOWLEDGE_STORE_RETRIEVAL})...`);
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
// Hard dependency boundary: in bm25 (lexical) retrieval mode the embedder
|
|
84
|
+
// factory is NEVER invoked — no @huggingface/transformers resolution, no
|
|
85
|
+
// model download, no initialization, no inference. The store runs purely
|
|
86
|
+
// on LanceDB's FTS/BM25 engine.
|
|
87
|
+
const embedder = config.KNOWLEDGE_STORE_RETRIEVAL === 'bm25' ? null : await embedderFactory();
|
|
82
88
|
const migrationStrategy = getMigrationStrategy(config);
|
|
83
89
|
|
|
84
90
|
store = new KnowledgeStore({
|
|
85
91
|
dbDir: getDbDir(config, workspace),
|
|
86
92
|
embedder,
|
|
87
93
|
modelName: config.EMBEDDING_MODEL,
|
|
94
|
+
retrieval: config.KNOWLEDGE_STORE_RETRIEVAL,
|
|
88
95
|
migrationStrategy: migrationStrategy,
|
|
89
96
|
reconnectFactory,
|
|
90
97
|
withLock,
|
|
@@ -149,8 +156,12 @@ export async function forceDeleteKnowledgeStore(config?: Config, workspace?: str
|
|
|
149
156
|
const manifestPath = path.join(dbDir, 'store-manifest.json');
|
|
150
157
|
try {
|
|
151
158
|
if (fs.existsSync(manifestPath)) {
|
|
152
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as { activeTableName?: string };
|
|
153
|
-
|
|
159
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as { activeTableName?: string; activeBm25TableName?: string };
|
|
160
|
+
// Read the manifest key that belongs to the CONFIGURED retrieval mode —
|
|
161
|
+
// each mode owns its own key so a shared dbDir never cross-wires them.
|
|
162
|
+
const key = config?.KNOWLEDGE_STORE_RETRIEVAL === 'bm25' ? 'activeBm25TableName' : 'activeTableName';
|
|
163
|
+
const saved = key === 'activeBm25TableName' ? manifest.activeBm25TableName : manifest.activeTableName;
|
|
164
|
+
if (saved) tableName = saved;
|
|
154
165
|
}
|
|
155
166
|
} catch (err) {
|
|
156
167
|
logger.warn(`[knowledge] Failed to read manifest before forced delete, defaulting to table '${tableName}':`, err);
|
|
@@ -188,6 +188,183 @@ export async function searchStore(
|
|
|
188
188
|
return filteredResults;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Add documents to the BM25 (lexical) table — the vector write path minus the
|
|
193
|
+
* embedding step. Rows carry no vector at all; lexical ranking comes from the
|
|
194
|
+
* Tantivy FTS indexes (see store-schema.ts). Everything else — row shape,
|
|
195
|
+
* scope columns, ingestion_type fallback, metrics — matches addDocumentsToStore
|
|
196
|
+
* so the two backends stay interchangeable behind IKnowledgeStore.
|
|
197
|
+
*/
|
|
198
|
+
export async function addDocumentsBm25ToStore(
|
|
199
|
+
table: lancedb.Table,
|
|
200
|
+
docs: StoreDocument[],
|
|
201
|
+
isClosing: () => boolean,
|
|
202
|
+
workspace: string,
|
|
203
|
+
isGlobal: boolean
|
|
204
|
+
): Promise<AddDocumentsOutcome> {
|
|
205
|
+
if (docs.length === 0) return 'empty';
|
|
206
|
+
if (isClosing()) {
|
|
207
|
+
logger.warn('[store] Ignoring addDocuments during close');
|
|
208
|
+
return 'ignored_closing';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const startTime = Date.now();
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const data = docs.map((doc) => ({
|
|
215
|
+
url: doc.url,
|
|
216
|
+
text: doc.text,
|
|
217
|
+
content: doc.content ?? null,
|
|
218
|
+
metadata: JSON.stringify(doc.metadata),
|
|
219
|
+
workspace,
|
|
220
|
+
is_global: isGlobal,
|
|
221
|
+
ingestion_type: doc.ingestion_type || (doc.metadata['ingestionType'] as string) || 'unknown',
|
|
222
|
+
timestamp: BigInt(doc.timestamp),
|
|
223
|
+
}));
|
|
224
|
+
|
|
225
|
+
await table.add(data);
|
|
226
|
+
const duration = Date.now() - startTime;
|
|
227
|
+
metrics.observe('knowledge_store_add_documents_duration_ms', duration, { retrieval: 'bm25' });
|
|
228
|
+
metrics.increment('knowledge_store_chunks_added_total', docs.length);
|
|
229
|
+
logger.log(`[store] Added ${docs.length} chunk(s) for ${docs[0]?.url} [retrieval=bm25, workspace=${workspace}, global=${isGlobal}]`);
|
|
230
|
+
return 'written';
|
|
231
|
+
} catch (err) {
|
|
232
|
+
const duration = Date.now() - startTime;
|
|
233
|
+
metrics.observe('knowledge_store_add_documents_duration_ms', duration, { status: 'error', retrieval: 'bm25' });
|
|
234
|
+
// Same accounting posture as the vector path: attempt-level failures are
|
|
235
|
+
// thrown; outcome accounting belongs to KnowledgeStore.addDocuments.
|
|
236
|
+
logger.debug(`[store] bm25 add attempt failed after ${duration}ms: ${err instanceof Error ? err.message : String(err)}`);
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Search the BM25 (lexical) table — pure full-text retrieval, no embedding.
|
|
243
|
+
*
|
|
244
|
+
* Runs the SAME lexical half the hybrid path already uses: Tantivy BM25 over
|
|
245
|
+
* BOTH 'text' (synthesis descriptions) and 'content' (full article body),
|
|
246
|
+
* ranked by BM25 score alone (no nearestTo, no RRF fusion — there is no second
|
|
247
|
+
* signal to fuse). Filters, result mapping, and metrics match searchStore so
|
|
248
|
+
* callers see identical StoreDocument results either way.
|
|
249
|
+
*/
|
|
250
|
+
export async function searchStoreBm25(
|
|
251
|
+
table: lancedb.Table,
|
|
252
|
+
query: string,
|
|
253
|
+
limit: number,
|
|
254
|
+
scopeFilter?: string
|
|
255
|
+
): Promise<StoreDocument[]> {
|
|
256
|
+
const startTime = Date.now();
|
|
257
|
+
|
|
258
|
+
const rowCount = scopeFilter ? await table.countRows(scopeFilter) : await table.countRows();
|
|
259
|
+
if (rowCount === 0) {
|
|
260
|
+
metrics.increment('knowledge_store_search_total', 1, { status: 'empty', retrieval: 'bm25' });
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let filter = "ingestion_type = 'synthesis-description'";
|
|
265
|
+
if (scopeFilter) {
|
|
266
|
+
filter = `(${filter}) AND (${scopeFilter})`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Pure FTS query: no nearestTo() — a Query (not a VectorQuery) ranked by BM25.
|
|
270
|
+
const results = await table
|
|
271
|
+
.query()
|
|
272
|
+
.where(filter)
|
|
273
|
+
.fullTextSearch(query, { columns: ['text', 'content'] })
|
|
274
|
+
.limit(limit)
|
|
275
|
+
.toArray();
|
|
276
|
+
|
|
277
|
+
const filteredResults = results
|
|
278
|
+
.map(r => {
|
|
279
|
+
let metadata: Record<string, unknown>;
|
|
280
|
+
try {
|
|
281
|
+
metadata = JSON.parse(r.metadata as string) as Record<string, unknown>;
|
|
282
|
+
} catch {
|
|
283
|
+
logger.debug('[store] Corrupted metadata in searchStoreBm25, skipping row');
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
url: r.url as string,
|
|
288
|
+
text: r.text as string,
|
|
289
|
+
content: (r.content as string | null) ?? undefined,
|
|
290
|
+
metadata,
|
|
291
|
+
timestamp: Number(r.timestamp),
|
|
292
|
+
};
|
|
293
|
+
})
|
|
294
|
+
.filter((doc): doc is NonNullable<typeof doc> => doc !== null);
|
|
295
|
+
|
|
296
|
+
const duration = Date.now() - startTime;
|
|
297
|
+
metrics.observe('knowledge_store_search_duration_ms', duration);
|
|
298
|
+
metrics.increment('knowledge_store_search_total', 1, { status: 'success', retrieval: 'bm25' });
|
|
299
|
+
metrics.increment('knowledge_store_search_results_total', filteredResults.length);
|
|
300
|
+
|
|
301
|
+
return filteredResults;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Find relevant URLs in the BM25 (lexical) table — the vector variant minus
|
|
306
|
+
* the embedding step: BM25 ranking over 'text' + 'content', same filters,
|
|
307
|
+
* same provenance decoration, same result shape as findRelevantUrls.
|
|
308
|
+
*/
|
|
309
|
+
export async function findRelevantUrlsBm25(
|
|
310
|
+
table: lancedb.Table,
|
|
311
|
+
query: string,
|
|
312
|
+
limit: number,
|
|
313
|
+
scopeFilter?: string
|
|
314
|
+
): Promise<{ url: string; description: string; provenance?: string }[]> {
|
|
315
|
+
const startTime = Date.now();
|
|
316
|
+
const rowCount = scopeFilter ? await table.countRows(scopeFilter) : await table.countRows();
|
|
317
|
+
if (rowCount === 0) {
|
|
318
|
+
metrics.increment('knowledge_store_find_urls_total', 1, { status: 'empty', retrieval: 'bm25' });
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let filter = "ingestion_type = 'synthesis-description'";
|
|
323
|
+
if (scopeFilter) {
|
|
324
|
+
filter = `(${filter}) AND (${scopeFilter})`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const results = await table
|
|
328
|
+
.query()
|
|
329
|
+
.where(filter)
|
|
330
|
+
.fullTextSearch(query, { columns: ['text', 'content'] })
|
|
331
|
+
.limit(limit)
|
|
332
|
+
.toArray();
|
|
333
|
+
|
|
334
|
+
const entries: { url: string; description: string; provenance?: string }[] = [];
|
|
335
|
+
const seen = new Set<string>();
|
|
336
|
+
for (const r of results) {
|
|
337
|
+
const url = r.url as string;
|
|
338
|
+
if (seen.has(url)) continue;
|
|
339
|
+
seen.add(url);
|
|
340
|
+
|
|
341
|
+
let description = '';
|
|
342
|
+
let provenance = 'description-unverified';
|
|
343
|
+
try {
|
|
344
|
+
const meta = JSON.parse(r.metadata as string);
|
|
345
|
+
description = meta.description as string ?? '';
|
|
346
|
+
// Surface provenance metadata so consumers can prefer verified entries
|
|
347
|
+
if (meta.provenance) provenance = meta.provenance;
|
|
348
|
+
else if (meta.hasContent === true) provenance = 'scraped-verified';
|
|
349
|
+
} catch { logger.debug('[store] Corrupted metadata in findRelevantUrlsBm25, skipping row'); }
|
|
350
|
+
|
|
351
|
+
if (!description) description = (r.text as string ?? '').substring(0, 300);
|
|
352
|
+
|
|
353
|
+
// Add store scope to provenance
|
|
354
|
+
const scope = r.is_global === true ? 'Global Store' : `Local Project (${r.workspace || 'unknown'})`;
|
|
355
|
+
const finalProvenance = `${provenance} [${scope}]`;
|
|
356
|
+
|
|
357
|
+
entries.push({ url, description, provenance: finalProvenance });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const duration = Date.now() - startTime;
|
|
361
|
+
metrics.observe('knowledge_store_find_urls_duration_ms', duration);
|
|
362
|
+
metrics.increment('knowledge_store_find_urls_total', 1, { status: 'success', retrieval: 'bm25' });
|
|
363
|
+
metrics.increment('knowledge_store_urls_found_total', entries.length);
|
|
364
|
+
|
|
365
|
+
return entries;
|
|
366
|
+
}
|
|
367
|
+
|
|
191
368
|
/**
|
|
192
369
|
* Find documents by URL
|
|
193
370
|
*/
|
|
@@ -40,6 +40,38 @@ function createStoreSchema(dim: number, modelName: string): Schema {
|
|
|
40
40
|
]));
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Schema metadata for BM25 (lexical) tables.
|
|
45
|
+
*
|
|
46
|
+
* BM25 tables carry NO vector column at all — rows are stored without any
|
|
47
|
+
* embedding and ranking comes entirely from the Tantivy FTS inverted indexes
|
|
48
|
+
* over 'text' and 'content' (computed at query time, nothing score-shaped is
|
|
49
|
+
* persisted). The schema is otherwise identical to the vector schema, so the
|
|
50
|
+
* same store operations, scope filters, and result mapping apply.
|
|
51
|
+
*/
|
|
52
|
+
export const BM25_TABLE_NAME = 'knowledge-bm25';
|
|
53
|
+
export const BM25_EMBEDDING_MODEL_TAG = 'bm25-lexical';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create the BM25 (lexical) store table schema — the vector schema minus the
|
|
57
|
+
* 'vector' column, tagged so the two table kinds are distinguishable on disk.
|
|
58
|
+
*/
|
|
59
|
+
function createBm25StoreSchema(): Schema {
|
|
60
|
+
return new Schema([
|
|
61
|
+
new Field('url', new Utf8(), false),
|
|
62
|
+
new Field('text', new Utf8(), false),
|
|
63
|
+
new Field('content', new Utf8(), true), // full page markdown, nullable
|
|
64
|
+
new Field('metadata', new Utf8(), false), // JSON stringified
|
|
65
|
+
new Field('workspace', new Utf8(), false), // local workspace path or 'global'
|
|
66
|
+
new Field('is_global', new Bool(), false), // boolean indicating if it's shared globally
|
|
67
|
+
new Field('ingestion_type', new Utf8(), false), // e.g. 'synthesis-description'
|
|
68
|
+
new Field('timestamp', new Int64(), false),
|
|
69
|
+
], new Map([
|
|
70
|
+
['embedding_model', BM25_EMBEDDING_MODEL_TAG],
|
|
71
|
+
['schema_version', CURRENT_SCHEMA_VERSION]
|
|
72
|
+
]));
|
|
73
|
+
}
|
|
74
|
+
|
|
43
75
|
/**
|
|
44
76
|
* Create an empty table with the knowledge store schema
|
|
45
77
|
*/
|
|
@@ -50,6 +82,32 @@ export async function createStoreTable(
|
|
|
50
82
|
modelName: string
|
|
51
83
|
): Promise<lancedb.Table> {
|
|
52
84
|
const schema = createStoreSchema(dim, modelName);
|
|
85
|
+
return createKnowledgeTable(db, name, schema);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create an empty BM25 (lexical) table — same indexes as the vector table, no
|
|
90
|
+
* vector column. Used by retrieval mode 'bm25' (separate table so existing
|
|
91
|
+
* vector stores are never touched — no migration either way).
|
|
92
|
+
*/
|
|
93
|
+
export async function createBm25StoreTable(
|
|
94
|
+
db: lancedb.Connection,
|
|
95
|
+
name: string = BM25_TABLE_NAME
|
|
96
|
+
): Promise<lancedb.Table> {
|
|
97
|
+
const schema = createBm25StoreSchema();
|
|
98
|
+
return createKnowledgeTable(db, name, schema);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Shared table creation: FTS indexes for lexical search + btree scalar indexes
|
|
103
|
+
* for scope/eviction performance. Identical for both table kinds — the ONLY
|
|
104
|
+
* difference between them is the presence of the 'vector' column.
|
|
105
|
+
*/
|
|
106
|
+
async function createKnowledgeTable(
|
|
107
|
+
db: lancedb.Connection,
|
|
108
|
+
name: string,
|
|
109
|
+
schema: Schema
|
|
110
|
+
): Promise<lancedb.Table> {
|
|
53
111
|
const { Index } = await getLancedb();
|
|
54
112
|
|
|
55
113
|
// Create empty table with schema and metadata
|