Files
searchi/crates/searchi-service/examples/evaluate.rs
T
karl caf6a2f82e
Core CI / core (ubuntu-latest) (push) Failing after 3m27s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Successful in 5m45s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
feat: integrate Google semantic search and branded desktop launcher
2026-10-12 17:44:56 +02:00

112 lines
5.4 KiB
Rust

//! Evaluate the immutable seed corpus with the actual configured Google model.
//! Usage: cargo run -p searchi-service --example evaluate -- MODEL_DIR WORKER OUTPUT_JSON
use anyhow::{ensure, Context, Result};
use searchi_core::SearchIndex;
use searchi_service::{HybridSearch, SearchOptions, SearchResult};
use serde_json::{json, Value};
use std::{fs, path::PathBuf, time::Instant};
fn expected_rank(results: &[SearchResult], filename: &str) -> Option<usize> {
results
.iter()
.position(|result| result.filename == filename)
.map(|rank| rank + 1)
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args_os().skip(1).collect();
ensure!(
args.len() == 3,
"Supply MODEL_DIR WORKER OUTPUT_JSON; this command never downloads assets"
);
ensure!(
std::env::var("SEARCHI_FORCE_CPU").is_ok_and(|value| value == "1"),
"Windows VM evaluation requires SEARCHI_FORCE_CPU=1"
);
let fixture: Value =
serde_json::from_str(include_str!("../../../fixtures/relevance/fixture.json"))?;
let temp = tempfile::tempdir()?;
let root = temp.path().join("documents");
fs::create_dir(&root)?;
let documents = fixture["documents"]
.as_array()
.context("documents missing")?;
for document in documents {
let filename = document["filename"].as_str().context("filename missing")?;
ensure!(
PathBuf::from(filename).file_name() == Some(std::ffi::OsStr::new(filename)),
"Fixture filename must be a basename"
);
fs::write(
root.join(filename),
document["text"].as_str().context("text missing")?,
)?;
}
let db = temp.path().join("index.db");
let mut lexical = SearchIndex::open(&db)?;
lexical.index_folder(&root)?;
let start = Instant::now();
let mut hybrid = HybridSearch::open(&db, &args[0], &args[1], 1)?;
let load_ms = start.elapsed().as_secs_f64() * 1000.0;
let start = Instant::now();
let indexed = hybrid.index_embeddings()?;
let indexing_ms = start.elapsed().as_secs_f64() * 1000.0;
drop(hybrid);
let start = Instant::now();
let mut hybrid = HybridSearch::open(&db, &args[0], &args[1], 1)?;
let startup_init_ms = start.elapsed().as_secs_f64() * 1000.0;
let start = Instant::now();
hybrid.warmup()?;
let background_prewarm_ms = start.elapsed().as_secs_f64() * 1000.0;
let start = Instant::now();
let first = hybrid.search(
"How can we end our relationship with the supplier?",
20,
&SearchOptions::default(),
)?;
let first_foreground_ms = start.elapsed().as_secs_f64() * 1000.0;
let first_foreground_top1 = first.first().map(|hit| hit.filename.as_str());
let mut rankings = Vec::new();
let mut durations = Vec::new();
for query in fixture["queries"].as_array().context("queries missing")? {
let text = query["text"].as_str().context("query text missing")?;
let expected = query["expected"].as_str().context("expected id missing")?;
let filename = documents
.iter()
.find(|doc| doc["id"] == expected)
.context("Expected document missing")?["filename"]
.as_str()
.unwrap();
let lexical_hits = lexical.search(text, 20)?;
let semantic_hits = hybrid.search_semantic(text, 20, &SearchOptions::default())?;
let start = Instant::now();
let hybrid_hits = hybrid.search(text, 20, &SearchOptions::default())?;
durations.push(start.elapsed().as_secs_f64() * 1000.0);
rankings.push(json!({"query":text,"expected":expected,"language":query["language"],
"lexical_rank":expected_rank(&lexical_hits,filename),
"semantic_rank":expected_rank(&semantic_hits,filename),
"hybrid_rank":expected_rank(&hybrid_hits,filename),
"hybrid_top3":hybrid_hits.iter().take(3).map(|hit| json!({"filename":hit.filename,"match_kind":hit.match_kind,"source":hit.source})).collect::<Vec<_>>()}));
}
durations.sort_by(f64::total_cmp);
let metrics = |lane: &str| {
json!({
"top1_accuracy":rankings.iter().filter(|row| row[lane]==1).count() as f64/rankings.len() as f64,
"recall_at3":rankings.iter().filter(|row| row[lane].as_u64().is_some_and(|rank| rank<=3)).count() as f64/rankings.len() as f64,
"recall_at10":rankings.iter().filter(|row| row[lane].as_u64().is_some_and(|rank| rank<=10)).count() as f64/rankings.len() as f64,
})
};
let report = json!({"model":"google/embeddinggemma-2","device":"cpu","threads":1,
"documents":documents.len(),"queries":rankings.len(),"load_ms":load_ms,"indexing_ms":indexing_ms,
"startup_init_ms":startup_init_ms,"background_prewarm_ms":background_prewarm_ms,"first_foreground_ms":first_foreground_ms,"first_foreground_top1":first_foreground_top1,
"hybrid_p50_ms":durations[durations.len()/2],"hybrid_p95_ms":durations[(durations.len()*95/100).min(durations.len()-1)],
"embedding_report":indexed,"lexical":metrics("lexical_rank"),"semantic":metrics("semantic_rank"),"hybrid":metrics("hybrid_rank"),
"rankings":rankings,"limitation":"Synthetic seed corpus only; no production quality or large-corpus latency certification"});
fs::write(&args[2], serde_json::to_string_pretty(&report)?)?;
println!(
"{}",
json!({"lexical":report["lexical"],"semantic":report["semantic"],"hybrid":report["hybrid"],"p95_ms":report["hybrid_p95_ms"]})
);
Ok(())
}