Evaluate frozen relevance hypothesis once on held-out Google CPU queries

This commit is contained in:
karl committed 2026-10-13 12:23:31 +02:00
1 parent e7245fe15e
commit 193ab08f62
3 files changed
+128 -24

No files matched your search

+40
View File
@@ -108,3 +108,43 @@ Policy SHA-256:
`df0a887b97dd7fda6aeff1b6c966fe7558521f40dbe1c911e91cfd312dca7461`. `df0a887b97dd7fda6aeff1b6c966fe7558521f40dbe1c911e91cfd312dca7461`.
Ignored analysis: `.searchi/qa/calibration-cosine-policy-v1.json`. Ignored analysis: `.searchi/qa/calibration-cosine-policy-v1.json`.
Held-out remains unevaluated at this freeze. Product ranking is unchanged. Held-out remains unevaluated at this freeze. Product ranking is unchanged.
## Held-out result and disposition, 2026-10-13
After the policy freeze was committed and pushed, actual Google CPU inference
completed all 24 held-out queries plus six Code-scope repeats. The fixture,
model, policy and threshold were unchanged. The explicit runner option requires
the exact pre-evaluation policy SHA; default execution remains calibration-only.
Four structural Rust tests and strict all-target Clippy passed.
| Scope | Unfiltered semantic / hybrid positive top-1 | Positive top results retained at 0.70 | Absent requests with retained semantic neighbours |
| --- | --- | --- | --- |
| All files | 16/16 | 15/16 | 0/8 |
| Additional Code scope | 4/4 | 4/4 | 0/2 |
The lost request is `version-wildcard-de`: its correct All files semantic
candidate scores **0.6766**, below the frozen threshold. It has no lexical
results, so preserving lexical retrieval would not rescue it. The same request
under Code scope scores **0.7388** and survives. The highest absent All files
top score is approximately **0.6954**, exceeding that positive score: there is
no single raw-cosine threshold separating these held-out top results.
**Reject deployment as a universal default cutoff.** Do not lower this policy
after seeing held-out outcomes or claim that its strong calibration result
proved safe abstention. Product ranking remains unchanged. The next relevance
work should preserve access to weak-but-correct results and evaluate richer
evidence on a new independent corpus; this sample is no longer unseen data.
`scores.hypothesis_semantic` measures retained semantic candidates only.
The recorded production hybrid request is unmodified; no filtered hybrid
fusion or UI confidence behavior was executed. This small bilingual corpus
does not establish general customer quality or media abstention.
Raw ignored report: `.searchi/qa/held-out-cosine-v1.json`, SHA-256
`8f6baceac27e90c719c2bbb522eeb2d1623056e2f8334f31f7dd67a912c0fccd`.
Diagnostic runner SHA-256:
`db685085f7f53d02cc85ef7c534e8ac02f4c5c806940bd4ff44ccbeae76e5762`.
Worker capabilities record Google EmbeddingGemma 2, CPU, one thread, query
role, context 512 and 768 dimensions. Semantic indexing took 73,991.6605 ms;
query startup took 7,153.4587 ms. These ordered debug setup measurements are
not UI latency or warm p95. Owned workers exited; the installed app was retained.
+14 -5
View File
@@ -1,10 +1,11 @@
# Calibration-only relevance diagnostic # Frozen relevance diagnostic
This internal runner uses searchi's shared Rust engine and genuine Google This internal runner uses searchi's shared Rust engine and genuine Google
EmbeddingGemma 2. It has **no held-out execution option**. It indexes all 14 frozen EmbeddingGemma 2. By default it indexes all 14 frozen source files as the fixed
source files as the fixed distractor library, but evaluates only the 24 distractor library and evaluates only the 24 calibration queries. Held-out
calibration queries. Held-out queries stay unevaluated until a policy and its execution requires `--held-out-policy` with the exact SHA-256 of the hypothesis
parameters are frozen using calibration evidence. frozen in commit `e7245fe`, before held-out inference. Changed policy bytes are
rejected. The two splits are selected independently and never mixed in one run.
First verify all frozen labels, source bytes, licenses and split integrity: First verify all frozen labels, source bytes, licenses and split integrity:
@@ -21,6 +22,14 @@ $env:SEARCHI_FORCE_CPU = '1'
tools/relevance-eval/target/release/searchi-relevance-eval.exe fixtures/relevance-v2 C:/path/to/verified-models C:/path/to/searchi-gemma-worker.exe .searchi/qa/calibration-v2.json tools/relevance-eval/target/release/searchi-relevance-eval.exe fixtures/relevance-v2 C:/path/to/verified-models C:/path/to/searchi-gemma-worker.exe .searchi/qa/calibration-v2.json
``` ```
For the one frozen held-out evaluation, append
`--held-out-policy tools/relevance-eval/policies/cosine-v1.json` and use a new
held-out report name. Raw lexical, semantic and production hybrid results are
retained. `scores.hypothesis_semantic` evaluates raw semantic candidates after
the frozen cutoff; it is not a newly executed filtered hybrid fusion. This
diagnostic does not deploy a threshold into the shared engine. Do not retune
this policy using the held-out outcomes.
Supply an existing output directory and a new output filename. Existing evidence Supply an existing output directory and a new output filename. Existing evidence
is never overwritten: the output is reserved with `create_new` before costly is never overwritten: the output is reserved with `create_new` before costly
work and subsequent phase/row checkpoints replace JSON atomically. The runner verifies fixture SHA-256, exact source bytes, work and subsequent phase/row checkpoints replace JSON atomically. The runner verifies fixture SHA-256, exact source bytes,
+74 -19
View File
@@ -1,4 +1,4 @@
//! Calibration-only diagnostics through the production shared engine. //! Frozen calibration and policy-gated held-out diagnostics through the shared engine.
use anyhow::{ensure, Context, Result}; use anyhow::{ensure, Context, Result};
use searchi_core::{SearchIndex, SearchOptions, SearchResult}; use searchi_core::{SearchIndex, SearchOptions, SearchResult};
use searchi_service::HybridSearch; use searchi_service::HybridSearch;
@@ -26,6 +26,14 @@ fn digest(path: &Path) -> Result<String> {
} }
fn calibration_queries(fixture: &Value) -> Result<Vec<&Value>> { fn calibration_queries(fixture: &Value) -> Result<Vec<&Value>> {
split_queries(fixture, "calibration")
}
fn split_queries<'a>(fixture: &'a Value, split: &str) -> Result<Vec<&'a Value>> {
ensure!(
matches!(split, "calibration" | "held_out"),
"Unknown requested split"
);
let queries = fixture["queries"].as_array().context("Missing queries")?; let queries = fixture["queries"].as_array().context("Missing queries")?;
ensure!( ensure!(
queries queries
@@ -33,13 +41,10 @@ fn calibration_queries(fixture: &Value) -> Result<Vec<&Value>> {
.all(|q| matches!(q["split"].as_str(), Some("calibration" | "held_out"))), .all(|q| matches!(q["split"].as_str(), Some("calibration" | "held_out"))),
"Unknown split" "Unknown split"
); );
let selected: Vec<_> = queries let selected: Vec<_> = queries.iter().filter(|q| q["split"] == split).collect();
.iter()
.filter(|q| q["split"] == "calibration")
.collect();
ensure!( ensure!(
selected.len() == 24, selected.len() == 24,
"Expected exactly 24 frozen calibration queries" "Expected exactly 24 frozen queries in selected split"
); );
Ok(selected) Ok(selected)
} }
@@ -56,7 +61,11 @@ fn checkpoint(path: &Path, report: &Value) -> Result<()> {
Ok(()) Ok(())
} }
fn score(hits: &[SearchResult], query: &Value, fixture: &Value) -> Result<Value> { fn score<'a>(
hits: impl IntoIterator<Item = &'a SearchResult>,
query: &Value,
fixture: &Value,
) -> Result<Value> {
let expected = query["expected_ids"] let expected = query["expected_ids"]
.as_array() .as_array()
.context("Missing expected labels")?; .context("Missing expected labels")?;
@@ -89,8 +98,8 @@ fn score(hits: &[SearchResult], query: &Value, fixture: &Value) -> Result<Value>
fn main() -> Result<()> { fn main() -> Result<()> {
let args: Vec<_> = std::env::args_os().skip(1).collect(); let args: Vec<_> = std::env::args_os().skip(1).collect();
ensure!( ensure!(
args.len() == 4, args.len() == 4 || (args.len() == 6 && args[4] == "--held-out-policy"),
"Supply FIXTURE MODEL_DIR WORKER OUTPUT_JSON; held-out execution is unsupported" "Supply FIXTURE MODEL_DIR WORKER OUTPUT_JSON [--held-out-policy FROZEN_POLICY]"
); );
ensure!( ensure!(
std::env::var("SEARCHI_FORCE_CPU").as_deref() == Ok("1"), std::env::var("SEARCHI_FORCE_CPU").as_deref() == Ok("1"),
@@ -100,12 +109,27 @@ fn main() -> Result<()> {
let models = PathBuf::from(&args[1]).canonicalize()?; let models = PathBuf::from(&args[1]).canonicalize()?;
let worker = PathBuf::from(&args[2]).canonicalize()?; let worker = PathBuf::from(&args[2]).canonicalize()?;
let output = PathBuf::from(&args[3]); let output = PathBuf::from(&args[3]);
let policy = if args.len() == 6 {
let path = PathBuf::from(&args[5]);
ensure!(
digest(&path)? == "df0a887b97dd7fda6aeff1b6c966fe7558521f40dbe1c911e91cfd312dca7461",
"Policy differs from frozen pre-evaluation hypothesis"
);
Some(serde_json::from_slice::<Value>(&fs::read(path)?)?)
} else {
None
};
let split = if policy.is_some() {
"held_out"
} else {
"calibration"
};
fs::OpenOptions::new() fs::OpenOptions::new()
.write(true) .write(true)
.create_new(true) .create_new(true)
.open(&output) .open(&output)
.context("Cannot reserve output; existing evidence is never overwritten")?; .context("Cannot reserve output; existing evidence is never overwritten")?;
let mut report = json!({"complete":false,"phase":"verifying","split":"calibration","held_out_evaluated":false,"cpu_threads":1,"rows":[]}); let mut report = json!({"complete":false,"phase":"verifying","split":split,"held_out_evaluated":policy.is_some(),"policy":policy,"cpu_threads":1,"rows":[]});
checkpoint(&output, &report)?; checkpoint(&output, &report)?;
let fixture_path = fixture_root.join("fixture.json"); let fixture_path = fixture_root.join("fixture.json");
let fixture: Value = serde_json::from_slice(&fs::read(&fixture_path)?)?; let fixture: Value = serde_json::from_slice(&fs::read(&fixture_path)?)?;
@@ -119,7 +143,11 @@ fn main() -> Result<()> {
lock["sha256"]["fixture.json"] == digest(&fixture_path)?, lock["sha256"]["fixture.json"] == digest(&fixture_path)?,
"Frozen fixture changed" "Frozen fixture changed"
); );
let queries = calibration_queries(&fixture)?; let queries = if policy.is_some() {
split_queries(&fixture, split)?
} else {
calibration_queries(&fixture)?
};
let manifest_path = let manifest_path =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../embeddinggemma2/model-manifest.json"); PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../embeddinggemma2/model-manifest.json");
let manifest: Value = serde_json::from_slice(&fs::read(&manifest_path)?)?; let manifest: Value = serde_json::from_slice(&fs::read(&manifest_path)?)?;
@@ -145,7 +173,7 @@ fn main() -> Result<()> {
let temporary = tempfile::tempdir()?; let temporary = tempfile::tempdir()?;
let corpus = temporary.path().join("corpus"); let corpus = temporary.path().join("corpus");
fs::create_dir(&corpus)?; fs::create_dir(&corpus)?;
// Index both splits as the fixed distractor library, but execute calibration queries only. // Index both splits as the fixed distractor library, execute only the selected split.
for asset in fixture["assets"].as_array().context("Missing assets")? { for asset in fixture["assets"].as_array().context("Missing assets")? {
let source = fixture_root let source = fixture_root
.join(asset["path"].as_str().context("Missing asset path")?) .join(asset["path"].as_str().context("Missing asset path")?)
@@ -189,18 +217,19 @@ fn main() -> Result<()> {
let query_startup_ms = start.elapsed().as_secs_f64() * 1000.0; let query_startup_ms = start.elapsed().as_secs_f64() * 1000.0;
report["worker_info"] = json!(query_session.worker_info()); report["worker_info"] = json!(query_session.worker_info());
report["query_startup_ms"] = json!(query_startup_ms); report["query_startup_ms"] = json!(query_startup_ms);
report["phase"] = json!("calibration_queries"); report["phase"] = json!(format!("{split}_queries"));
checkpoint(&output, &report)?; checkpoint(&output, &report)?;
for query in queries { for query in queries {
ensure!( ensure!(query["split"] == split, "Query outside selected split");
query["split"] == "calibration",
"Held-out execution forbidden"
);
let text = query["query"].as_str().context("Missing query text")?; let text = query["query"].as_str().context("Missing query text")?;
let code_intent = query["id"] let code_intent = query["id"]
.as_str() .as_str()
.context("Missing query ID")? .context("Missing query ID")?
.starts_with("pattern-"); .starts_with(if split == "held_out" {
"version-"
} else {
"pattern-"
});
for code_only in [false, true] for code_only in [false, true]
.into_iter() .into_iter()
.filter(|code_only| !*code_only || code_intent) .filter(|code_only| !*code_only || code_intent)
@@ -215,7 +244,15 @@ fn main() -> Result<()> {
let semantic_hits = query_session.search_semantic(text, 80, &options)?; let semantic_hits = query_session.search_semantic(text, 80, &options)?;
let semantic_ms = start.elapsed().as_secs_f64() * 1000.0; let semantic_ms = start.elapsed().as_secs_f64() * 1000.0;
let hybrid_hits = query_session.search_measured(text, 20, &options, true)?; let hybrid_hits = query_session.search_measured(text, 20, &options, true)?;
let scores = json!({"lexical":score(&lexical_hits.results,query,&fixture)?,"semantic":score(&semantic_hits,query,&fixture)?,"hybrid":score(&hybrid_hits.results,query,&fixture)?}); let retained: Vec<_> = semantic_hits
.iter()
.filter(|hit| {
policy
.as_ref()
.is_none_or(|p| hit.score >= p["semantic_cosine_minimum"].as_f64().unwrap())
})
.collect();
let scores = json!({"lexical":score(&lexical_hits.results,query,&fixture)?,"semantic":score(&semantic_hits,query,&fixture)?,"hybrid":score(&hybrid_hits.results,query,&fixture)?,"hypothesis_semantic":score(retained,query,&fixture)?});
report["rows"].as_array_mut().context("Missing output rows")?.push(json!({"query":query,"scope":if code_only {"code"} else {"all_files"},"lexical":lexical_hits,"semantic_raw":semantic_hits,"semantic_ms":semantic_ms,"hybrid":hybrid_hits,"scores":scores,"order_note":"lexical then semantic then hybrid; hybrid reuses the preceding query-vector cache. Raw semantic scores are cosine, hybrid scores are RRF, not confidence."})); report["rows"].as_array_mut().context("Missing output rows")?.push(json!({"query":query,"scope":if code_only {"code"} else {"all_files"},"lexical":lexical_hits,"semantic_raw":semantic_hits,"semantic_ms":semantic_ms,"hybrid":hybrid_hits,"scores":scores,"order_note":"lexical then semantic then hybrid; hybrid reuses the preceding query-vector cache. Raw semantic scores are cosine, hybrid scores are RRF, not confidence."}));
checkpoint(&output, &report)?; checkpoint(&output, &report)?;
} }
@@ -262,6 +299,24 @@ mod tests {
assert!(calibration_queries(&json!({"queries":[{"split":"held_out"}]})).is_err()); assert!(calibration_queries(&json!({"queries":[{"split":"held_out"}]})).is_err());
} }
#[test]
fn held_out_selection_is_disjoint_and_has_six_code_requests() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/relevance-v2/fixture.json");
let fixture: Value = serde_json::from_slice(&fs::read(path).unwrap()).unwrap();
let selected = split_queries(&fixture, "held_out").unwrap();
assert_eq!(selected.len(), 24);
assert!(selected.iter().all(|q| q["split"] == "held_out"));
assert_eq!(
selected
.iter()
.filter(|q| q["id"].as_str().unwrap().starts_with("version-"))
.count(),
6
);
assert!(split_queries(&fixture, "unknown").is_err());
}
#[test] #[test]
fn frozen_calibration_has_six_code_requests_and_disjoint_query_ids() { fn frozen_calibration_has_six_code_requests_and_disjoint_query_ids() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))