MCPcopy
hub / github.com/colbymchenry/codegraph / searchNodes

Method searchNodes

src/db/queries.ts:775–892  ·  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

773 * 3. Score results based on match quality
774 */
775 searchNodes(query: string, options: SearchOptions = {}): SearchResult[] {
776 const { limit = 100, offset = 0 } = options;
777
778 // Parse field-qualified bits out of the raw query (kind:, lang:,
779 // path:, name:). Anything not recognised stays in `text` and goes
780 // to FTS unchanged. Filters compose with the SearchOptions arg —
781 // both are applied (intersection-style).
782 const parsed = parseQuery(query);
783 const mergedKinds =
784 parsed.kinds.length > 0
785 ? Array.from(new Set([...(options.kinds ?? []), ...parsed.kinds]))
786 : options.kinds;
787 const mergedLanguages =
788 parsed.languages.length > 0
789 ? Array.from(new Set([...(options.languages ?? []), ...parsed.languages]))
790 : options.languages;
791 const pathFilters = parsed.pathFilters;
792 const nameFilters = parsed.nameFilters;
793 // The text portion drives FTS/LIKE; if all the user typed was
794 // filters (`kind:function`), we still need *some* candidate set,
795 // so synthesise an empty-text path that returns everything matching
796 // the filters.
797 const text = parsed.text;
798 const kinds = mergedKinds;
799 const languages = mergedLanguages;
800
801 // First try FTS5 with prefix matching
802 let results = text
803 ? this.searchNodesFTS(text, { kinds, languages, limit, offset })
804 // Over-fetch by 5× when running filter-only (no text). The
805 // post-scoring path: + name: filters can be very selective, so
806 // a smaller multiplier risks returning fewer than `limit`
807 // results despite the DB having plenty of matches.
808 : this.searchAllByFilters({ kinds, languages, limit: limit * 5 });
809
810 // If no FTS results, try LIKE-based substring search
811 if (results.length === 0 && text.length >= 2) {
812 results = this.searchNodesLike(text, { kinds, languages, limit, offset });
813 }
814
815 // Final fuzzy fallback: scan all known names and keep those within
816 // a tight Levenshtein distance. Only fires when both FTS and LIKE
817 // returned nothing AND there's a text portion long enough to be
818 // worth fuzzing (1-char queries would match too much).
819 if (results.length === 0 && text.length >= 3) {
820 results = this.searchNodesFuzzy(text, { kinds, languages, limit });
821 }
822
823 // Supplement: ensure exact name matches are always candidates.
824 // BM25 can bury short exact-match names (e.g. "getBean") under hundreds of
825 // compound names (e.g. "getBeanDescriptor") in large codebases,
826 // pushing them past the FTS fetch limit before post-hoc scoring can help.
827 // Use the max BM25 score as the base so the nameMatchBonus (exact=30 vs
828 // prefix=20) actually differentiates them after rescoring.
829 if (results.length > 0 && query) {
830 const existingIds = new Set(results.map(r => r.node.id));
831 const maxFtsScore = Math.max(...results.map(r => r.score));
832 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
joinMethod · 0.80
hasMethod · 0.80
allMethod · 0.65

Tested by

no test coverage detected