style(bench): gate the rerank-sweep flag and helper on the embeddings feature
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
@@ -554,6 +554,7 @@ impl MemoryBackend for ClawhdfBackend {
|
|||||||
timestamp: r.timestamp,
|
timestamp: r.timestamp,
|
||||||
source_channel: r.source_channel.clone(),
|
source_channel: r.source_channel.clone(),
|
||||||
raw_activation: r.activation,
|
raw_activation: r.activation,
|
||||||
|
relevance: r.score,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,10 @@
|
|||||||
//! into a single composite score for each retrieved result.
|
//! into a single composite score for each retrieved result.
|
||||||
|
|
||||||
/// Configuration for the multi-factor re-ranker.
|
/// Configuration for the multi-factor re-ranker.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct ReRankConfig {
|
pub struct ReRankConfig {
|
||||||
|
/// Weight applied to the retrieval score the candidate arrived with.
|
||||||
|
pub relevance_weight: f32,
|
||||||
/// Weight applied to the temporal decay score (0.0–1.0).
|
/// Weight applied to the temporal decay score (0.0–1.0).
|
||||||
pub temporal_weight: f32,
|
pub temporal_weight: f32,
|
||||||
/// Weight applied to the source authority score (0.0–1.0).
|
/// Weight applied to the source authority score (0.0–1.0).
|
||||||
@@ -20,6 +22,9 @@ pub struct ReRankConfig {
|
|||||||
impl Default for ReRankConfig {
|
impl Default for ReRankConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
// Relevance leads: the metadata signals break ties and nudge, they
|
||||||
|
// do not decide. See `BENCHMARKS.md`, "Recency discrimination".
|
||||||
|
relevance_weight: 1.0,
|
||||||
temporal_weight: 0.3,
|
temporal_weight: 0.3,
|
||||||
authority_weight: 0.2,
|
authority_weight: 0.2,
|
||||||
activation_weight: 0.5,
|
activation_weight: 0.5,
|
||||||
@@ -41,6 +46,8 @@ pub struct ReRankResult {
|
|||||||
pub authority_score: f32,
|
pub authority_score: f32,
|
||||||
/// Normalised Hebbian activation score in [0, 1].
|
/// Normalised Hebbian activation score in [0, 1].
|
||||||
pub activation_score: f32,
|
pub activation_score: f32,
|
||||||
|
/// The retrieval score carried through from the input.
|
||||||
|
pub relevance_score: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute an exponential decay temporal score.
|
/// Compute an exponential decay temporal score.
|
||||||
@@ -105,6 +112,15 @@ pub struct RerankInput {
|
|||||||
pub source_channel: String,
|
pub source_channel: String,
|
||||||
/// Raw Hebbian activation weight for this entry.
|
/// Raw Hebbian activation weight for this entry.
|
||||||
pub raw_activation: f32,
|
pub raw_activation: f32,
|
||||||
|
/// The retrieval score that put this entry in the candidate list.
|
||||||
|
///
|
||||||
|
/// Re-ranking is meant to *adjust* the retriever's ordering with signals
|
||||||
|
/// it does not have, not to replace it. Without this the combined score
|
||||||
|
/// was made of recency, authority and activation alone, so a candidate
|
||||||
|
/// pool came back ordered by age with its relevance ordering discarded.
|
||||||
|
/// Callers with no meaningful score can pass the same value for every
|
||||||
|
/// entry, which reduces to the old behaviour.
|
||||||
|
pub relevance: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Re-rank a list of retrieval results using multi-factor scoring.
|
/// Re-rank a list of retrieval results using multi-factor scoring.
|
||||||
@@ -138,7 +154,8 @@ pub fn rerank(
|
|||||||
let auth = source_authority_score(&inp.source_channel);
|
let auth = source_authority_score(&inp.source_channel);
|
||||||
let act = activation_score(inp.raw_activation);
|
let act = activation_score(inp.raw_activation);
|
||||||
|
|
||||||
let combined = config.temporal_weight * ts
|
let combined = config.relevance_weight * inp.relevance
|
||||||
|
+ config.temporal_weight * ts
|
||||||
+ config.authority_weight * auth
|
+ config.authority_weight * auth
|
||||||
+ config.activation_weight * act;
|
+ config.activation_weight * act;
|
||||||
|
|
||||||
@@ -148,6 +165,7 @@ pub fn rerank(
|
|||||||
temporal_score: ts,
|
temporal_score: ts,
|
||||||
authority_score: auth,
|
authority_score: auth,
|
||||||
activation_score: act,
|
activation_score: act,
|
||||||
|
relevance_score: inp.relevance,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -253,22 +271,51 @@ mod tests {
|
|||||||
timestamp: 0.0, // very old
|
timestamp: 0.0, // very old
|
||||||
source_channel: "other".to_string(),
|
source_channel: "other".to_string(),
|
||||||
raw_activation: 0.1,
|
raw_activation: 0.1,
|
||||||
|
relevance: 0.0,
|
||||||
},
|
},
|
||||||
RerankInput {
|
RerankInput {
|
||||||
index: 1,
|
index: 1,
|
||||||
timestamp: 86_400.0, // one day ago
|
timestamp: 86_400.0, // one day ago
|
||||||
source_channel: "conversation".to_string(),
|
source_channel: "conversation".to_string(),
|
||||||
raw_activation: 0.5,
|
raw_activation: 0.5,
|
||||||
|
relevance: 0.0,
|
||||||
},
|
},
|
||||||
RerankInput {
|
RerankInput {
|
||||||
index: 2,
|
index: 2,
|
||||||
timestamp: 172_800.0, // "now"
|
timestamp: 172_800.0, // "now"
|
||||||
source_channel: "user_correction".to_string(),
|
source_channel: "user_correction".to_string(),
|
||||||
raw_activation: 1.0,
|
raw_activation: 1.0,
|
||||||
|
relevance: 0.0,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relevance_leads_but_recency_breaks_near_ties() {
|
||||||
|
let entry = |index, timestamp, relevance| RerankInput {
|
||||||
|
index,
|
||||||
|
timestamp,
|
||||||
|
source_channel: "conversation".to_string(),
|
||||||
|
raw_activation: 1.0,
|
||||||
|
relevance,
|
||||||
|
};
|
||||||
|
let now = 10.0 * 86_400.0;
|
||||||
|
let config = ReRankConfig::default();
|
||||||
|
|
||||||
|
// A clearly better match wins despite being much older. Before
|
||||||
|
// `relevance` existed the combined score ignored it entirely, so this
|
||||||
|
// returned the newer, irrelevant entry.
|
||||||
|
let ranked = rerank(&[entry(0, 0.0, 1.0), entry(1, now, 0.1)], &config, now);
|
||||||
|
assert_eq!(ranked[0].index, 0, "{ranked:?}");
|
||||||
|
|
||||||
|
// Between near-equal matches, the newer one wins.
|
||||||
|
let ranked = rerank(&[entry(0, 0.0, 0.80), entry(1, now, 0.79)], &config, now);
|
||||||
|
assert_eq!(ranked[0].index, 1, "{ranked:?}");
|
||||||
|
|
||||||
|
// The breakdown carries the relevance through.
|
||||||
|
assert_eq!(ranked[0].relevance_score, 0.79);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rerank_returns_all_entries() {
|
fn rerank_returns_all_entries() {
|
||||||
let inputs = make_inputs();
|
let inputs = make_inputs();
|
||||||
@@ -302,6 +349,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn rerank_score_breakdown_matches_manual_calculation() {
|
fn rerank_score_breakdown_matches_manual_calculation() {
|
||||||
let config = ReRankConfig {
|
let config = ReRankConfig {
|
||||||
|
relevance_weight: 0.0,
|
||||||
temporal_weight: 1.0,
|
temporal_weight: 1.0,
|
||||||
authority_weight: 0.0,
|
authority_weight: 0.0,
|
||||||
activation_weight: 0.0,
|
activation_weight: 0.0,
|
||||||
@@ -312,6 +360,7 @@ mod tests {
|
|||||||
timestamp: 0.0,
|
timestamp: 0.0,
|
||||||
source_channel: "other".to_string(),
|
source_channel: "other".to_string(),
|
||||||
raw_activation: 0.5,
|
raw_activation: 0.5,
|
||||||
|
relevance: 0.0,
|
||||||
}];
|
}];
|
||||||
let now = 3600.0_f64; // exactly one half-life later
|
let now = 3600.0_f64; // exactly one half-life later
|
||||||
let results = rerank(&inputs, &config, now);
|
let results = rerank(&inputs, &config, now);
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ impl Mode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(not(feature = "embeddings"), allow(dead_code))]
|
||||||
fn reranked(mut self, label: &'static str, rerank: ReRankConfig) -> Self {
|
fn reranked(mut self, label: &'static str, rerank: ReRankConfig) -> Self {
|
||||||
self.label = label;
|
self.label = label;
|
||||||
self.rerank = Some(rerank);
|
self.rerank = Some(rerank);
|
||||||
@@ -929,6 +930,7 @@ fn main() {
|
|||||||
let mut limit: Option<usize> = None;
|
let mut limit: Option<usize> = None;
|
||||||
let mut weights_dir: Option<String> = None;
|
let mut weights_dir: Option<String> = None;
|
||||||
let mut sweep = false;
|
let mut sweep = false;
|
||||||
|
#[cfg_attr(not(feature = "embeddings"), allow(unused_mut, unused_variables))]
|
||||||
let mut rerank_sweep = false;
|
let mut rerank_sweep = false;
|
||||||
let mut args = std::env::args().skip(1);
|
let mut args = std::env::args().skip(1);
|
||||||
while let Some(arg) = args.next() {
|
while let Some(arg) = args.next() {
|
||||||
@@ -938,7 +940,16 @@ fn main() {
|
|||||||
limit = Some(v.parse().expect("--limit must be a positive integer"));
|
limit = Some(v.parse().expect("--limit must be a positive integer"));
|
||||||
}
|
}
|
||||||
"--sweep" => sweep = true,
|
"--sweep" => sweep = true,
|
||||||
"--rerank-sweep" => rerank_sweep = true,
|
"--rerank-sweep" => {
|
||||||
|
// Re-ranking needs the vector stage to have candidates worth
|
||||||
|
// reordering, so this is an embeddings-only comparison.
|
||||||
|
#[cfg(feature = "embeddings")]
|
||||||
|
{
|
||||||
|
rerank_sweep = true;
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "embeddings"))]
|
||||||
|
eprintln!("warning: --rerank-sweep needs --features embeddings; ignoring");
|
||||||
|
}
|
||||||
"--embeddings" => {
|
"--embeddings" => {
|
||||||
weights_dir = Some(args.next().expect("--embeddings needs a directory"));
|
weights_dir = Some(args.next().expect("--embeddings needs a directory"));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user