Skip to content

Commit 9406529

Browse files
devwhodevsclaude
andcommitted
feat: 3-lane RRF — graph agent as third search lane
Graph lane expands seed results from semantic + FTS by following wikilinks. Weight 0.8 (slightly below direct lanes). Detail field on LaneContribution shows hop info in --explain output. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bcd50e6 commit 9406529

2 files changed

Lines changed: 34 additions & 4 deletions

File tree

src/fusion.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
/// rrf_score = sum( weight_i / (k + rank_i) )
77
///
88
/// A ranked result from a single search lane.
9+
#[derive(Clone)]
910
pub struct RankedResult {
1011
pub file_path: String,
1112
pub file_id: i64,
@@ -32,6 +33,7 @@ pub struct LaneContribution {
3233
pub rank: usize,
3334
pub raw_score: f64,
3435
pub weighted_contribution: f64,
36+
pub detail: Option<String>, // e.g., "1-hop from BRE-2579"
3537
}
3638

3739
use std::collections::HashMap;
@@ -93,6 +95,7 @@ pub fn rrf_fuse(lanes: &[(&str, &[RankedResult], f64)], k: usize) -> Vec<FusedRe
9395
rank,
9496
raw_score: r.score,
9597
weighted_contribution: contribution,
98+
detail: None,
9699
});
97100
}
98101
}
@@ -124,10 +127,15 @@ pub fn rrf_fuse(lanes: &[(&str, &[RankedResult], f64)], k: usize) -> Vec<FusedRe
124127
pub fn format_explain(result: &FusedResult) -> String {
125128
let mut out = format!(" RRF: {:.4}\n", result.rrf_score);
126129
for lc in &result.lane_contributions {
127-
out.push_str(&format!(
128-
" {}: rank #{}, raw {:.2}, +{:.4}\n",
129-
lc.lane_name, lc.rank, lc.raw_score, lc.weighted_contribution,
130-
));
130+
let detail_str = lc
131+
.detail
132+
.as_deref()
133+
.map(|d| format!(" ({})", d))
134+
.unwrap_or_default();
135+
out += &format!(
136+
" {}: rank #{}, raw {:.2}{}, +{:.4}\n",
137+
lc.lane_name, lc.rank, lc.raw_score, detail_str, lc.weighted_contribution
138+
);
131139
}
132140
out
133141
}
@@ -234,12 +242,14 @@ mod tests {
234242
rank: 1,
235243
raw_score: 0.87,
236244
weighted_contribution: 0.0164,
245+
detail: None,
237246
},
238247
LaneContribution {
239248
lane_name: "fts".to_string(),
240249
rank: 3,
241250
raw_score: 5.23,
242251
weighted_contribution: 0.0159,
252+
detail: None,
243253
},
244254
],
245255
};

src/search.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use serde_json::json;
66

77
use crate::embedder::Embedder;
88
use crate::fusion::{self, RankedResult};
9+
use crate::graph;
910
use crate::hnsw::HnswIndex;
1011
use crate::store::{Store, StoreStats};
1112

@@ -127,12 +128,31 @@ pub fn run_search(
127128
.unwrap_or(std::cmp::Ordering::Equal)
128129
});
129130

131+
// --- Graph lane ---
132+
// Combine seeds from semantic + FTS (deduplicated by file_path, take higher score)
133+
let combined_seeds: Vec<RankedResult> = {
134+
let mut by_file: HashMap<String, RankedResult> = HashMap::new();
135+
for r in semantic_results.iter().chain(fts_results.iter()) {
136+
match by_file.get(&r.file_path) {
137+
Some(existing) if r.score <= existing.score => {}
138+
_ => {
139+
by_file.insert(r.file_path.clone(), r.clone());
140+
}
141+
}
142+
}
143+
by_file.into_values().collect()
144+
};
145+
146+
let graph_results =
147+
graph::graph_expand(&store, &combined_seeds, query, 2, 20).unwrap_or_default();
148+
130149
// --- RRF Fusion ---
131150
const RRF_K: usize = 60;
132151
let fused = fusion::rrf_fuse(
133152
&[
134153
("semantic", &semantic_results, 1.0),
135154
("fts", &fts_results, 1.0),
155+
("graph", &graph_results, 0.8),
136156
],
137157
RRF_K,
138158
);

0 commit comments

Comments
 (0)