Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions mcp/src/graph/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ export async function get_edges(

export type OutputFormat = "snippet" | "json";

const RRF_K = 5;
// Standard reciprocal-rank-fusion constant (Cormack et al.); high k keeps
// contributions flat across ranks so neither retriever's head dominates.
const RRF_K = 60;

function sortByPagerank(nodes: Neo4jNode[]): Neo4jNode[] {
return [...nodes].sort((a, b) => {
Expand Down Expand Up @@ -167,21 +169,17 @@ export async function searchWithProvenance(
});

const merged = new Map<string, { node: Neo4jNode; score: number }>();
const ftMaxScore = fulltextResults.length > 0 ? Math.max(...fulltextResults.map(n => n.score || 1)) : 1;
const vecMaxScore = vectorResults.length > 0 ? Math.max(...vectorResults.map(n => n.score || 1)) : 1;

fulltextResults.forEach((node, i) => {
const key = node.properties.ref_id || node.properties.node_key;
const norm = (node.score || 0) / ftMaxScore;
merged.set(key, { node, score: (1 / (RRF_K + i + 1)) * (0.5 + 0.5 * norm) });
merged.set(key, { node, score: 1 / (RRF_K + i + 1) });
});
vectorResults.forEach((node, i) => {
const key = node.properties.ref_id || node.properties.node_key;
const norm = (node.score || 0) / vecMaxScore;
const rrfScore = (1 / (RRF_K + i + 1)) * (0.5 + 0.5 * norm);
const rrfScore = 1 / (RRF_K + i + 1);
const existing = merged.get(key);
if (existing) {
existing.score += rrfScore * 1.5;
existing.score += rrfScore;
} else {
merged.set(key, { node, score: rrfScore });
}
Expand Down
6 changes: 5 additions & 1 deletion mcp/src/graph/neo4j.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,9 @@ class Db {
const result = await session.run(Q.VECTOR_SEARCH_QUERY, {
embeddings,
limit,
// KNN k must exceed the requested limit: post-filters (score >= 0.4,
// labels, paths) consume neighbor slots and there is no refetch.
knn_k: Math.max(limit * 10, 100),
node_types,
skip_node_types,
extensions,
Expand Down Expand Up @@ -1308,7 +1311,7 @@ class Db {
limit: number,
repo_paths: string[] | null,
file_paths: string[],
): Promise<{ ref_id: string; description: string }[]> {
): Promise<{ ref_id: string; name: string; description: string }[]> {
const session = this.resilientSession();
try {
const result = await session.run(
Expand All @@ -1321,6 +1324,7 @@ class Db {
);
return result.records.map((record) => ({
ref_id: record.get("ref_id"),
name: record.get("name"),
description: record.get("description"),
}));
} finally {
Expand Down
5 changes: 3 additions & 2 deletions mcp/src/graph/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ ${NODE_TYPES}
`;

export const VECTOR_SEARCH_QUERY = `
CALL db.index.vector.queryNodes('${VECTOR_INDEX}', toInteger($limit), $embeddings)
CALL db.index.vector.queryNodes('${VECTOR_INDEX}', toInteger($knn_k), $embeddings)
YIELD node, score
WITH node, score
WHERE score >= 0.4
Expand Down Expand Up @@ -578,6 +578,7 @@ WHERE score >= 0.4
END
RETURN node, score
ORDER BY score DESC
LIMIT toInteger($limit)
`;

export const FIND_NODE_QUERY = `
Expand Down Expand Up @@ -979,7 +980,7 @@ WHERE (n:Class OR n:Endpoint OR n:Request OR n:Function OR n:Datamodel OR n:Page
AND (n.embeddings IS NULL)
AND ($repo_paths IS NULL OR size($repo_paths) = 0 OR ANY(repo IN $repo_paths WHERE n.file STARTS WITH repo))
AND ($file_paths IS NULL OR size($file_paths) = 0 OR ANY(path IN $file_paths WHERE n.file ENDS WITH path))
RETURN n.ref_id as ref_id, n.description as description
RETURN n.ref_id as ref_id, n.name as name, n.description as description
LIMIT toInteger($limit)
`;

Expand Down
10 changes: 8 additions & 2 deletions mcp/src/repo/descriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ function usageTotals(usage: AiUsageWithLegacy) {



// Text that gets embedded for a node: prepend the identifier so the vector
// carries the symbol name's semantics, not just the prose description.
function embedText(name: string | undefined, description: string): string {
return name ? `${name}: ${description}` : description;
}

function extractRepoPaths(repo_url?: string): string[] | null {
if (!repo_url || repo_url.trim() === "") return null;

Expand Down Expand Up @@ -221,7 +227,7 @@ ${content.slice(0, 2000)}`;
// Bulk write to Neo4j
if (results.length > 0) {
if (do_embed) {
const texts = results.map((r) => r.text);
const texts = results.map((r) => embedText(r.name, r.text));
const embeddings = await vectorizeBatch(texts);
await db.bulk_update_descriptions_and_embeddings(
results.map((r, i) => ({
Expand Down Expand Up @@ -315,7 +321,7 @@ export const embed_nodes_agent = async (req: Request, res: Response) => {
current_batch_size: nodes.length,
});

const texts = nodes.map((n) => n.description);
const texts = nodes.map((n) => embedText(n.name, n.description));
const embeddings = await vectorizeBatch(texts);

const batch = nodes.map((n, i) => ({
Expand Down
Loading