( projectRoot: string, embeddingModel: string, query: string, queryEmbedding: number[], topK: number, idxForFallback: Index | null, semanticOnly = false, )
| 211 | * read it twice); pass null to force SQLite-only. |
| 212 | */ |
| 213 | export async function searchPersisted( |
| 214 | projectRoot: string, |
| 215 | embeddingModel: string, |
| 216 | query: string, |
| 217 | queryEmbedding: number[], |
| 218 | topK: number, |
| 219 | idxForFallback: Index | null, |
| 220 | semanticOnly = false, |
| 221 | ): Promise<ScoredChunk[]> { |
| 222 | // Try SQLite first. |
| 223 | try { |
| 224 | const { openSqliteIndex, searchSqlite, allChunksFromSqlite } = await import('./sqlite-index.js'); |
| 225 | const handle = await openSqliteIndex(projectRoot, embeddingModel); |
| 226 | if (handle) { |
| 227 | // Wide semantic pool from the quantized store… |
| 228 | const semantic = searchSqlite(handle, queryEmbedding, Math.max(topK * 3, 50)); |
| 229 | if (semanticOnly) { |
| 230 | handle.db.close(); |
| 231 | return semantic.slice(0, topK); |
| 232 | } |
| 233 | // …then fuse with BM25 over the full chunk set. |
| 234 | const allChunks = allChunksFromSqlite(handle); |
| 235 | const { hybridRank } = await import('./hybrid-search.js'); |
| 236 | const fused = hybridRank(allChunks, semantic, query, topK); |
| 237 | handle.db.close(); |
| 238 | return fused; |
| 239 | } |
| 240 | } catch { /* fall through to JSON */ } |
| 241 | |
| 242 | // JSON fallback. |
| 243 | if (!idxForFallback) return []; |
| 244 | if (semanticOnly) return rankChunks(queryEmbedding, idxForFallback.chunks, topK); |
| 245 | return rankChunksHybrid(queryEmbedding, query, idxForFallback.chunks, topK); |
| 246 | } |
| 247 | |
| 248 | export async function buildIndex( |
| 249 | projectRoot: string, |
no test coverage detected