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

Method searchNodesFuzzy

src/db/queries.ts:1318–1372  ·  view source on GitHub ↗

* Fuzzy fallback: when zero FTS/LIKE hits, try an edit-distance * sweep over the distinct symbol-name set. Caps `maxDist` at 2 so * `getUssr` finds `getUser` but `process` doesn't match `prosody`. * Bounded edit distance keeps each comparison cheap; the per-query * scan is O(distinct-nam

(
    text: string,
    options: { kinds?: NodeKind[]; languages?: Language[]; limit: number }
  )

Source from the content-addressed store, hash-verified

1316 * node count on any real codebase.
1317 */
1318 private searchNodesFuzzy(
1319 text: string,
1320 options: { kinds?: NodeKind[]; languages?: Language[]; limit: number }
1321 ): SearchResult[] {
1322 const { kinds, languages, limit } = options;
1323 const lowered = text.toLowerCase();
1324 const maxDist = lowered.length <= 4 ? 1 : 2;
1325
1326 // Pull the distinct name list once. The set is cached on QueryBuilder
1327 // by getAllNodeNames(); even on a 200k-node project the distinct
1328 // name set is typically O(10k) because most names repeat. The
1329 // candidate-cap below bounds memory regardless.
1330 const allNames = this.getAllNodeNames();
1331 const candidates: Array<{ name: string; dist: number }> = [];
1332 for (const name of allNames) {
1333 const dist = boundedEditDistance(name.toLowerCase(), lowered, maxDist);
1334 if (dist <= maxDist) candidates.push({ name, dist });
1335 }
1336 candidates.sort((a, b) => a.dist - b.dist);
1337
1338 // Cap the per-name follow-up queries. Each survivor triggers a
1339 // separate `SELECT * FROM nodes WHERE name = ?`; without this cap
1340 // a project with many similar names (`getUser1`, `getUser2`...)
1341 // could fan out far beyond `limit` queries before the inner-loop
1342 // limit kicks in.
1343 const FUZZY_FOLLOWUP_CAP = Math.max(limit * 2, 50);
1344 const cappedCandidates = candidates.slice(0, FUZZY_FOLLOWUP_CAP);
1345
1346 const results: SearchResult[] = [];
1347 const seen = new Set<string>();
1348 for (const c of cappedCandidates) {
1349 if (results.length >= limit) break;
1350 let sql = 'SELECT * FROM nodes WHERE name = ?';
1351 const params: (string | number)[] = [c.name];
1352 if (kinds && kinds.length > 0) {
1353 sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
1354 params.push(...kinds);
1355 }
1356 if (languages && languages.length > 0) {
1357 sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
1358 params.push(...languages);
1359 }
1360 sql += ' LIMIT 5';
1361 const rows = this.db.prepare(sql).all(...params) as NodeRow[];
1362 for (const row of rows) {
1363 if (seen.has(row.id)) continue;
1364 seen.add(row.id);
1365 // Lower the score for each edit step away from the query so
1366 // exact-match fallbacks (dist 0) outrank dist-2 typos.
1367 results.push({ node: rowToNode(row), score: 1 / (1 + c.dist) });
1368 if (results.length >= limit) break;
1369 }
1370 }
1371 return results;
1372 }
1373
1374 /**
1375 * FTS5 search with prefix matching

Callers 1

searchNodesMethod · 0.95

Calls 7

getAllNodeNamesMethod · 0.95
boundedEditDistanceFunction · 0.90
rowToNodeFunction · 0.85
hasMethod · 0.80
allMethod · 0.65
prepareMethod · 0.65
joinMethod · 0.45

Tested by

no test coverage detected