mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
fix(bm25): return FTS-matched symbols instead of arbitrary LIMIT 3 nodes
Previously, bm25Search fetched up to 3 arbitrary symbols from the matched file using MATCH (n) WHERE n.filePath = $filePath LIMIT 3 (no ORDER BY). This meant the specific function or class that actually scored highest in the BM25 index could be completely absent from the results. Fix: propagate nodeId from each FTS hit through searchFTSFromLbug, then use those nodeIds in bm25Search to look up the exact matched nodes via WHERE n.id IN $nodeIds. Falls back to the old filePath-based lookup when nodeIds are unavailable. Also switches the per-file score aggregation from naive sum-of-all to sum-of-top-3, which prevents files with many mediocre matches (e.g. test files) from outranking files with a single highly-relevant symbol.
This commit is contained in:
parent
9f4109a33f
commit
aac7c36918
2 changed files with 45 additions and 19 deletions
|
|
@ -11,6 +11,7 @@ export interface BM25SearchResult {
|
|||
filePath: string;
|
||||
score: number;
|
||||
rank: number;
|
||||
nodeIds?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -23,7 +24,7 @@ async function queryFTSViaExecutor(
|
|||
indexName: string,
|
||||
query: string,
|
||||
limit: number,
|
||||
): Promise<Array<{ filePath: string; score: number }>> {
|
||||
): Promise<Array<{ filePath: string; score: number; nodeId: string }>> {
|
||||
// Escape single quotes and backslashes to prevent Cypher injection
|
||||
const escapedQuery = query.replace(/\\/g, '\\\\').replace(/'/g, "''");
|
||||
const cypher = `
|
||||
|
|
@ -40,6 +41,7 @@ async function queryFTSViaExecutor(
|
|||
return {
|
||||
filePath: node.filePath || '',
|
||||
score: typeof score === 'number' ? score : parseFloat(score) || 0,
|
||||
nodeId: node.id || '',
|
||||
};
|
||||
});
|
||||
} catch {
|
||||
|
|
@ -99,17 +101,13 @@ export const searchFTSFromLbug = async (
|
|||
);
|
||||
}
|
||||
|
||||
// Merge results by filePath, summing scores for same file
|
||||
const merged = new Map<string, { filePath: string; score: number }>();
|
||||
// Collect all node scores per filePath to track which nodes actually matched
|
||||
const fileNodeScores = new Map<string, Array<{ score: number; nodeId: string }>>();
|
||||
|
||||
const addResults = (results: any[]) => {
|
||||
for (const r of results) {
|
||||
const existing = merged.get(r.filePath);
|
||||
if (existing) {
|
||||
existing.score += r.score;
|
||||
} else {
|
||||
merged.set(r.filePath, { filePath: r.filePath, score: r.score });
|
||||
}
|
||||
if (!fileNodeScores.has(r.filePath)) fileNodeScores.set(r.filePath, []);
|
||||
fileNodeScores.get(r.filePath)!.push({ score: r.score, nodeId: r.nodeId });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -119,6 +117,19 @@ export const searchFTSFromLbug = async (
|
|||
addResults(methodResults);
|
||||
addResults(interfaceResults);
|
||||
|
||||
// Sum the top-3 highest-scoring nodes per file and collect their nodeIds.
|
||||
// Summing all nodes naively inflates scores for files with many mediocre
|
||||
// matches (e.g. test files) over files with a single highly-relevant symbol.
|
||||
const merged = new Map<string, { filePath: string; score: number; nodeIds: string[] }>();
|
||||
for (const [filePath, entries] of fileNodeScores) {
|
||||
const top3 = entries.sort((a, b) => b.score - a.score).slice(0, 3);
|
||||
merged.set(filePath, {
|
||||
filePath,
|
||||
score: top3.reduce((acc, e) => acc + e.score, 0),
|
||||
nodeIds: top3.map(e => e.nodeId).filter(id => id),
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by score descending and add rank
|
||||
const sorted = Array.from(merged.values())
|
||||
.sort((a, b) => b.score - a.score)
|
||||
|
|
@ -128,5 +139,6 @@ export const searchFTSFromLbug = async (
|
|||
filePath: r.filePath,
|
||||
score: r.score,
|
||||
rank: index + 1,
|
||||
nodeIds: r.nodeIds,
|
||||
}));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -775,16 +775,30 @@ export class LocalBackend {
|
|||
for (const bm25Result of bm25Results) {
|
||||
const fullPath = bm25Result.filePath;
|
||||
try {
|
||||
const symbols = await executeParameterized(
|
||||
repo.id,
|
||||
`
|
||||
MATCH (n)
|
||||
WHERE n.filePath = $filePath
|
||||
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
|
||||
LIMIT 3
|
||||
`,
|
||||
{ filePath: fullPath },
|
||||
);
|
||||
// Prefer direct nodeId lookup (exact FTS-matched nodes) over filePath fallback.
|
||||
// Without this, LIMIT 3 on filePath returns arbitrary symbols rather than
|
||||
// the nodes that actually scored highest in the BM25 index.
|
||||
const nodeIds = bm25Result.nodeIds?.length ? bm25Result.nodeIds : null;
|
||||
const symbols = nodeIds
|
||||
? await executeParameterized(
|
||||
repo.id,
|
||||
`
|
||||
MATCH (n)
|
||||
WHERE n.id IN $nodeIds
|
||||
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
|
||||
`,
|
||||
{ nodeIds },
|
||||
)
|
||||
: await executeParameterized(
|
||||
repo.id,
|
||||
`
|
||||
MATCH (n)
|
||||
WHERE n.filePath = $filePath
|
||||
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
|
||||
LIMIT 3
|
||||
`,
|
||||
{ filePath: fullPath },
|
||||
);
|
||||
|
||||
if (symbols.length > 0) {
|
||||
for (const sym of symbols) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue