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

Method searchNodes

src/db/queries.ts:1163–1280  ·  view source on GitHub ↗

* Search nodes by name using FTS with fallback to LIKE for better matching * * Search strategy: * 1. Try FTS5 prefix match (query*) for word-start matching * 2. If no results, try LIKE for substring matching (e.g., "signIn" finds "signInWithGoogle") * 3. Score results based on match q

(query: string, options: SearchOptions = {})

Source from the content-addressed store, hash-verified

1161 * 3. Score results based on match quality
1162 */
1163 searchNodes(query: string, options: SearchOptions = {}): SearchResult[] {
1164 const { limit = 100, offset = 0 } = options;
1165
1166 // Parse field-qualified bits out of the raw query (kind:, lang:,
1167 // path:, name:). Anything not recognised stays in `text` and goes
1168 // to FTS unchanged. Filters compose with the SearchOptions arg —
1169 // both are applied (intersection-style).
1170 const parsed = parseQuery(query);
1171 const mergedKinds =
1172 parsed.kinds.length > 0
1173 ? Array.from(new Set([...(options.kinds ?? []), ...parsed.kinds]))
1174 : options.kinds;
1175 const mergedLanguages =
1176 parsed.languages.length > 0
1177 ? Array.from(new Set([...(options.languages ?? []), ...parsed.languages]))
1178 : options.languages;
1179 const pathFilters = parsed.pathFilters;
1180 const nameFilters = parsed.nameFilters;
1181 // The text portion drives FTS/LIKE; if all the user typed was
1182 // filters (`kind:function`), we still need *some* candidate set,
1183 // so synthesise an empty-text path that returns everything matching
1184 // the filters.
1185 const text = parsed.text;
1186 const kinds = mergedKinds;
1187 const languages = mergedLanguages;
1188
1189 // First try FTS5 with prefix matching
1190 let results = text
1191 ? this.searchNodesFTS(text, { kinds, languages, limit, offset })
1192 // Over-fetch by 5× when running filter-only (no text). The
1193 // post-scoring path: + name: filters can be very selective, so
1194 // a smaller multiplier risks returning fewer than `limit`
1195 // results despite the DB having plenty of matches.
1196 : this.searchAllByFilters({ kinds, languages, limit: limit * 5 });
1197
1198 // If no FTS results, try LIKE-based substring search
1199 if (results.length === 0 && text.length >= 2) {
1200 results = this.searchNodesLike(text, { kinds, languages, limit, offset });
1201 }
1202
1203 // Final fuzzy fallback: scan all known names and keep those within
1204 // a tight Levenshtein distance. Only fires when both FTS and LIKE
1205 // returned nothing AND there's a text portion long enough to be
1206 // worth fuzzing (1-char queries would match too much).
1207 if (results.length === 0 && text.length >= 3) {
1208 results = this.searchNodesFuzzy(text, { kinds, languages, limit });
1209 }
1210
1211 // Supplement: ensure exact name matches are always candidates.
1212 // BM25 can bury short exact-match names (e.g. "getBean") under hundreds of
1213 // compound names (e.g. "getBeanDescriptor") in large codebases,
1214 // pushing them past the FTS fetch limit before post-hoc scoring can help.
1215 // Use the max BM25 score as the base so the nameMatchBonus (exact=30 vs
1216 // prefix=20) actually differentiates them after rescoring.
1217 if (results.length > 0 && query) {
1218 const existingIds = new Set(results.map(r => r.node.id));
1219 const maxFtsScore = Math.max(...results.map(r => r.score));
1220 const terms = query.split(/\s+/).filter(t => t.length >= 2);

Callers 6

findRelevantContextMethod · 0.45
handleSearchMethod · 0.45
boundaryCandidatesMethod · 0.45
findSymbolMatchesMethod · 0.45
findAllSymbolsMethod · 0.45
mainFunction · 0.45

Calls 13

searchNodesFTSMethod · 0.95
searchAllByFiltersMethod · 0.95
searchNodesLikeMethod · 0.95
searchNodesFuzzyMethod · 0.95
parseQueryFunction · 0.90
kindBonusFunction · 0.90
scorePathRelevanceFunction · 0.90
nameMatchBonusFunction · 0.90
rowToNodeFunction · 0.85
hasMethod · 0.80
allMethod · 0.65
prepareMethod · 0.65

Tested by

no test coverage detected