MCPcopy Create free account
hub / github.com/colbymchenry/codegraph / searchNodesFTS

Method searchNodesFTS

src/db/queries.ts:1377–1440  ·  view source on GitHub ↗

* FTS5 search with prefix matching

(query: string, options: SearchOptions)

Source from the content-addressed store, hash-verified

1375 * FTS5 search with prefix matching
1376 */
1377 private searchNodesFTS(query: string, options: SearchOptions): SearchResult[] {
1378 const { kinds, languages, limit = 100, offset = 0 } = options;
1379
1380 // Add prefix wildcard for better matching (e.g., "auth" matches "AuthService", "authenticate")
1381 // Escape special FTS5 characters and add prefix wildcard.
1382 //
1383 // `::` is a qualifier separator in Rust/C++/Ruby, not a token char,
1384 // so treat it as whitespace before the strip step. Otherwise queries
1385 // like `stage_apply::run` collapse to `stage_applyrun` (the colons
1386 // are stripped without splitting) and find nothing. See #173.
1387 const ftsQuery = query
1388 .replace(/::/g, ' ') // Rust/C++/Ruby qualifier separator
1389 .replace(/['"*():^]/g, '') // Remove FTS5 special chars
1390 .split(/\s+/)
1391 .filter(term => term.length > 0)
1392 // Strip FTS5 boolean operators to prevent query manipulation
1393 .filter(term => !/^(AND|OR|NOT|NEAR)$/i.test(term))
1394 .map(term => `"${term}"*`) // Prefix match each term
1395 .join(' OR ');
1396
1397 if (!ftsQuery) {
1398 return [];
1399 }
1400
1401 // BM25 column weights: id=0, name=20, qualified_name=5, docstring=1, signature=2
1402 // Heavy name weight ensures exact/prefix name matches rank above incidental
1403 // mentions in long docstrings or qualified names of nested symbols.
1404 // Fetch 5x requested limit so post-hoc rescoring (kindBonus, pathRelevance,
1405 // nameMatchBonus) can promote results that BM25 alone undervalues.
1406 const ftsLimit = Math.max(limit * 5, 100);
1407
1408 let sql = `
1409 SELECT nodes.*, bm25(nodes_fts, 0, 20, 5, 1, 2) as score
1410 FROM nodes_fts
1411 JOIN nodes ON nodes_fts.id = nodes.id
1412 WHERE nodes_fts MATCH ?
1413 `;
1414
1415 const params: (string | number)[] = [ftsQuery];
1416
1417 if (kinds && kinds.length > 0) {
1418 sql += ` AND nodes.kind IN (${kinds.map(() => '?').join(',')})`;
1419 params.push(...kinds);
1420 }
1421
1422 if (languages && languages.length > 0) {
1423 sql += ` AND nodes.language IN (${languages.map(() => '?').join(',')})`;
1424 params.push(...languages);
1425 }
1426
1427 sql += ' ORDER BY score LIMIT ? OFFSET ?';
1428 params.push(ftsLimit, offset);
1429
1430 try {
1431 const rows = this.db.prepare(sql).all(...params) as (NodeRow & { score: number })[];
1432 return rows.map((row) => ({
1433 node: rowToNode(row),
1434 score: Math.abs(row.score), // bm25 returns negative scores

Callers 1

searchNodesMethod · 0.95

Calls 4

rowToNodeFunction · 0.85
allMethod · 0.65
prepareMethod · 0.65
joinMethod · 0.45

Tested by

no test coverage detected