Add shared-engine calibration-only relevance diagnostics

This commit is contained in:
karl committed 2026-10-13 10:25:35 +02:00
1 parent 4e60038c2f
commit 43c43ebdc9
5 files changed
+1668

No files matched your search

+6
View File
@@ -235,6 +235,12 @@ Completion requires recorded evidence; targets are not measured results.
returned candidates. See [mixed quality](MIXED-QUALITY.md). returned candidates. See [mixed quality](MIXED-QUALITY.md).
- [ ] Measure task fusion and cache cost against independently reviewed - [ ] Measure task fusion and cache cost against independently reviewed
held-out data, preserving the frozen diagnostic for regression comparison. held-out data, preserving the frozen diagnostic for regression comparison.
- [x] Freeze disjoint bilingual text/code calibration and held-out labels
after independent AI review; verify exact Git-preserved source/label bytes.
- [x] Add a shared-engine calibration-only runner retaining raw cosine,
lexical provenance, hybrid ranks and atomic incomplete evidence.
- [ ] Execute calibration, freeze the proposed relevance policy, then
evaluate held-out queries without tuning against their results.
- [x] Explicit English/German code intent vetoes exclusive audio routing; - [x] Explicit English/German code intent vetoes exclusive audio routing;
actual Google CPU regression preserves code among qualifying audio candidates. actual Google CPU regression preserves code among qualifying audio candidates.
- [ ] Validate code search UI interaction on native Windows/macOS/Linux and - [ ] Validate code search UI interaction on native Windows/macOS/Linux and
+1311
View File
File diff suppressed because it is too large. Load diff
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "searchi-relevance-eval"
version = "0.1.0"
edition = "2021"
publish = false
[workspace]
[dependencies]
anyhow = "1"
serde_json = "1"
sha2 = "0.10"
searchi-core = { path = "../../crates/searchi-core" }
searchi-service = { path = "../../crates/searchi-service" }
tempfile = "3"
+50
View File
@@ -0,0 +1,50 @@
# Calibration-only relevance diagnostic
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
source files as the fixed distractor library, but evaluates only the 24
calibration queries. Held-out queries stay unevaluated until a policy and its
parameters are frozen using calibration evidence.
First verify all frozen labels, source bytes, licenses and split integrity:
```powershell
python fixtures/relevance-v2/verify.py
cargo test --manifest-path tools/relevance-eval/Cargo.toml --locked
cargo build --release --manifest-path tools/relevance-eval/Cargo.toml --locked
```
Only run when the shared CPU inference slot is free:
```powershell
$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
```
Supply an existing output directory and a new output filename. Existing evidence
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,
and both pinned Google assets before indexing. It records runner/worker/model
hashes, actual worker capabilities, indexing/startup timings, all lexical
candidates (including match kinds and source locations), raw semantic cosine
scores, and final hybrid results with detailed engine traces. Code intents run
both All files and Code scope; all lanes use the same strict scope filters.
The final 20-result hybrid request uses an 80-candidate budget. The diagnostic
therefore records 80 lexical/semantic candidates separately. Semantic candidates
are the production routed, per-file lane, not all passage vectors. Hybrid scores
are reciprocal-rank-fusion scores, **not confidence**. No arbitrary cosine cutoff
is applied, no vectors are simulated, and no quality acceptance claim follows
from a successful run.
Queries execute lexical → semantic → hybrid in a retained query-role session.
Hybrid consequently reuses the preceding semantic query vector. These timings
are ordered diagnostic timings, not cold first input, UI latency, independent
warm p95, or production performance evidence. Partial completed rows are saved
after each request; an interrupted/failed report remains `complete: false`.
This corpus is small: fictional customer documents, two real source files and
independent AI annotation. It does not cover media or production scale. Preserve
labels and original raw output. A later policy experiment must fix all routing,
abstention and parameter choices on calibration only before exposing held-out
queries, and must retain exact filename priority and source provenance.
+286
View File
@@ -0,0 +1,286 @@
//! Calibration-only diagnostics through the production shared engine.
use anyhow::{ensure, Context, Result};
use searchi_core::{SearchIndex, SearchOptions, SearchResult};
use searchi_service::HybridSearch;
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::{
fs,
io::{Read, Write},
path::{Path, PathBuf},
time::Instant,
};
fn digest(path: &Path) -> Result<String> {
let mut stream = fs::File::open(path)?;
let mut hash = Sha256::new();
let mut buffer = [0; 65536];
loop {
let count = stream.read(&mut buffer)?;
if count == 0 {
break;
}
hash.update(&buffer[..count]);
}
Ok(format!("{:x}", hash.finalize()))
}
fn calibration_queries(fixture: &Value) -> Result<Vec<&Value>> {
let queries = fixture["queries"].as_array().context("Missing queries")?;
ensure!(
queries
.iter()
.all(|q| matches!(q["split"].as_str(), Some("calibration" | "held_out"))),
"Unknown split"
);
let selected: Vec<_> = queries
.iter()
.filter(|q| q["split"] == "calibration")
.collect();
ensure!(
selected.len() == 24,
"Expected exactly 24 frozen calibration queries"
);
Ok(selected)
}
fn checkpoint(path: &Path, report: &Value) -> Result<()> {
let parent = path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or(Path::new("."));
let mut staged = tempfile::NamedTempFile::new_in(parent)?;
staged.write_all(&serde_json::to_vec_pretty(report)?)?;
staged.as_file().sync_all()?;
staged.persist(path)?;
Ok(())
}
fn score(hits: &[SearchResult], query: &Value, fixture: &Value) -> Result<Value> {
let expected = query["expected_ids"]
.as_array()
.context("Missing expected labels")?;
let mut ids = Vec::new();
for hit in hits {
let asset = fixture["assets"]
.as_array()
.context("Missing assets")?
.iter()
.find(|asset| {
Path::new(asset["path"].as_str().unwrap_or_default())
.file_name()
.is_some_and(|name| name == hit.filename.as_str())
})
.context("Result outside fixed corpus")?;
if !ids.contains(&asset["id"]) {
ids.push(asset["id"].clone());
}
}
let rank = ids
.iter()
.position(|id| expected.contains(id))
.map(|rank| rank + 1);
let absent = expected.is_empty();
Ok(
json!({"asset_ranking":ids,"first_relevant_rank":rank,"top1":if absent {None} else {Some(rank == Some(1))},"recall_at_5":if absent {None} else {Some(ids.iter().take(5).filter(|id| expected.contains(id)).count() as f64 / expected.len() as f64)},"absent_artifact_returned_neighbors":if absent {Some(!ids.is_empty())} else {None}}),
)
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args_os().skip(1).collect();
ensure!(
args.len() == 4,
"Supply FIXTURE MODEL_DIR WORKER OUTPUT_JSON; held-out execution is unsupported"
);
ensure!(
std::env::var("SEARCHI_FORCE_CPU").as_deref() == Ok("1"),
"Set SEARCHI_FORCE_CPU=1"
);
let fixture_root = PathBuf::from(&args[0]).canonicalize()?;
let models = PathBuf::from(&args[1]).canonicalize()?;
let worker = PathBuf::from(&args[2]).canonicalize()?;
let output = PathBuf::from(&args[3]);
fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&output)
.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":[]});
checkpoint(&output, &report)?;
let fixture_path = fixture_root.join("fixture.json");
let fixture: Value = serde_json::from_slice(&fs::read(&fixture_path)?)?;
ensure!(
digest(&fixture_path)?
== "000047fd17accd315b6138093bbdc9f353cd877b4bf1958fbe336e4a6b10cd2e",
"This runner accepts only the reviewed frozen v2 fixture"
);
let lock: Value = serde_json::from_slice(&fs::read(fixture_root.join("fixture.lock.json"))?)?;
ensure!(
lock["sha256"]["fixture.json"] == digest(&fixture_path)?,
"Frozen fixture changed"
);
let queries = calibration_queries(&fixture)?;
let manifest_path =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../embeddinggemma2/model-manifest.json");
let manifest: Value = serde_json::from_slice(&fs::read(&manifest_path)?)?;
for asset in manifest["files"]
.as_array()
.context("Missing model assets")?
{
let path = models.join(asset["path"].as_str().context("Missing model path")?);
ensure!(
fs::metadata(&path)?.len() == asset["bytes"].as_u64().context("Missing model size")?,
"Model size mismatch"
);
ensure!(digest(&path)? == asset["sha256"], "Model hash mismatch");
}
report["fixture_sha256"] = json!(digest(&fixture_path)?);
report["model_manifest_sha256"] = json!(digest(&manifest_path)?);
report["model"] = manifest;
report["assets"] = fixture["assets"].clone();
report["worker_sha256"] = json!(digest(&worker)?);
report["runner_sha256"] = json!(digest(&std::env::current_exe()?)?);
report["phase"] = json!("materializing_and_lexical_index");
checkpoint(&output, &report)?;
let temporary = tempfile::tempdir()?;
let corpus = temporary.path().join("corpus");
fs::create_dir(&corpus)?;
// Index both splits as the fixed distractor library, but execute calibration queries only.
for asset in fixture["assets"].as_array().context("Missing assets")? {
let source = fixture_root
.join(asset["path"].as_str().context("Missing asset path")?)
.canonicalize()?;
ensure!(
source.starts_with(&fixture_root),
"Source escapes frozen fixture"
);
ensure!(
fs::metadata(&source)?.len()
== asset["bytes"].as_u64().context("Missing asset size")?
&& digest(&source)? == asset["sha256"],
"Source changed"
);
let destination = corpus.join(source.file_name().context("Missing filename")?);
ensure!(!destination.exists(), "Duplicate materialized filename");
fs::copy(source, destination)?;
}
let db = temporary.path().join("index.sqlite");
let mut lexical = SearchIndex::open(&db)?;
let start = Instant::now();
let lexical_report = lexical.index_folder(&corpus)?;
let lexical_ms = start.elapsed().as_secs_f64() * 1000.0;
report["lexical_report"] = json!(lexical_report);
report["lexical_index_ms"] = json!(lexical_ms);
report["phase"] = json!("semantic_index");
checkpoint(&output, &report)?;
let start = Instant::now();
let mut indexing = HybridSearch::open(&db, &models, &worker, 1)?;
let index_report = indexing.index_embeddings()?;
let semantic_index_ms = start.elapsed().as_secs_f64() * 1000.0;
report["index_report"] = json!(index_report);
report["semantic_index_ms"] = json!(semantic_index_ms);
report["phase"] = json!("query_warmup");
checkpoint(&output, &report)?;
ensure!(index_report.failures.is_empty(), "Semantic indexing failed");
drop(indexing);
let start = Instant::now();
let mut query_session = HybridSearch::open_query(&db, &models, &worker, 1)?;
query_session.warmup()?;
let query_startup_ms = start.elapsed().as_secs_f64() * 1000.0;
report["worker_info"] = json!(query_session.worker_info());
report["query_startup_ms"] = json!(query_startup_ms);
report["phase"] = json!("calibration_queries");
checkpoint(&output, &report)?;
for query in queries {
ensure!(
query["split"] == "calibration",
"Held-out execution forbidden"
);
let text = query["query"].as_str().context("Missing query text")?;
let code_intent = query["id"]
.as_str()
.context("Missing query ID")?
.starts_with("pattern-");
for code_only in [false, true]
.into_iter()
.filter(|code_only| !*code_only || code_intent)
{
let options = SearchOptions {
code_only,
..Default::default()
};
// Match the production hybrid candidate budget for a final limit of 20.
let lexical_hits = lexical.search_with_options_measured(text, 80, &options, true)?;
let start = Instant::now();
let semantic_hits = query_session.search_semantic(text, 80, &options)?;
let semantic_ms = start.elapsed().as_secs_f64() * 1000.0;
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)?});
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)?;
}
}
report["complete"] = json!(true);
report["phase"] = json!("complete");
checkpoint(&output, &report)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn checkpoint_replaces_complete_json_and_existing_evidence_is_reserved() {
let temporary = tempfile::tempdir().unwrap();
let path = temporary.path().join("evidence.json");
fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
.unwrap();
checkpoint(&path, &json!({"complete":false,"rows":[1]})).unwrap();
assert!(fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
.is_err());
checkpoint(&path, &json!({"complete":true,"rows":[1,2]})).unwrap();
let report: Value = serde_json::from_slice(&fs::read(path).unwrap()).unwrap();
assert_eq!(report["rows"], json!([1, 2]));
assert_eq!(fs::read_dir(temporary.path()).unwrap().count(), 1);
}
#[test]
fn rejects_unknown_split_and_never_selects_held_out() {
let mut queries = vec![json!({"split":"calibration"}); 24];
queries.push(json!({"split":"held_out","query":"DO NOT EXECUTE"}));
let fixture = json!({"queries":queries});
assert!(calibration_queries(&fixture)
.unwrap()
.iter()
.all(|q| q["split"] == "calibration"));
assert!(calibration_queries(&json!({"queries":[{"split":"unknown"}]})).is_err());
assert!(calibration_queries(&json!({"queries":[{"split":"held_out"}]})).is_err());
}
#[test]
fn frozen_calibration_has_six_code_requests_and_disjoint_query_ids() {
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 = calibration_queries(&fixture).unwrap();
assert_eq!(
selected
.iter()
.filter(|q| q["id"].as_str().unwrap().starts_with("pattern-"))
.count(),
6
);
for query in selected {
assert!(!fixture["queries"]
.as_array()
.unwrap()
.iter()
.any(|held| held["split"] == "held_out" && held["id"] == query["id"]));
}
}
}