feat: add multimodal search and granular performance diagnostics
Core CI / core (ubuntu-latest) (push) Failing after 3m31s
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 5m39s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
Core CI / core (ubuntu-latest) (push) Failing after 3m31s
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 5m39s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
This commit is contained in:
1 parent
d3bb8c7618
commit
b8400c4fbf
110 files changed
+79395
-250
No files matched your search
@@ -0,0 +1,83 @@
|
||||
//! End-to-end corpus indexing and querying through actual Searchi APIs.
|
||||
use anyhow::{ensure, Result};
|
||||
use searchi_core::{SearchIndex, SearchOptions};
|
||||
use searchi_media::{FfmpegTools, MediaLimits};
|
||||
use searchi_service::HybridSearch;
|
||||
use serde_json::{json, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{fs, path::PathBuf, time::Instant};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args_os().skip(1).collect();
|
||||
ensure!(
|
||||
args.len() == 6 || args.len() == 7,
|
||||
"Supply CORPUS MODEL CPU_WORKER FFMPEG FFPROBE OUTPUT_JSON [AUDIO_WINDOW_SECONDS]"
|
||||
);
|
||||
ensure!(
|
||||
std::env::var("SEARCHI_FORCE_CPU").as_deref() == Ok("1"),
|
||||
"CPU only"
|
||||
);
|
||||
let root = PathBuf::from(&args[0]).canonicalize()?;
|
||||
let bytes = fs::read(root.join("fixture.json"))?;
|
||||
let hash = format!("{:x}", Sha256::digest(&bytes));
|
||||
let lock: Value = serde_json::from_slice(&fs::read(root.join("labels.lock.json"))?)?;
|
||||
ensure!(lock["fixture_sha256"] == hash, "Frozen labels changed");
|
||||
let fixture: Value = serde_json::from_slice(&bytes)?;
|
||||
for asset in fixture["assets"].as_array().unwrap() {
|
||||
let content = fs::read(root.join(asset["path"].as_str().unwrap()))?;
|
||||
ensure!(
|
||||
asset["sha256"] == format!("{:x}", Sha256::digest(content)),
|
||||
"Asset changed"
|
||||
);
|
||||
}
|
||||
let temporary = tempfile::tempdir()?;
|
||||
let db = temporary.path().join("index.sqlite");
|
||||
let mut lexical = SearchIndex::open(&db)?;
|
||||
let filename_index = lexical.index_folder(root.join("assets"))?;
|
||||
let mut hybrid = HybridSearch::open(&db, &args[1], &args[2], 1)?;
|
||||
let tools = FfmpegTools::new(PathBuf::from(&args[3]), PathBuf::from(&args[4]))?;
|
||||
let start = Instant::now();
|
||||
let mut limits = MediaLimits::default();
|
||||
if let Some(seconds) = args.get(6) {
|
||||
let seconds: u32 = seconds.to_string_lossy().parse()?;
|
||||
ensure!(
|
||||
(5..=30).contains(&seconds),
|
||||
"Audio window must be 5–30 seconds"
|
||||
);
|
||||
limits.audio_window_ms = seconds * 1000;
|
||||
}
|
||||
let index_report = hybrid.index_embeddings_with_media(&tools, &limits)?;
|
||||
let encoding_ms = start.elapsed().as_secs_f64() * 1000.0;
|
||||
println!("Actual pipeline indexed {} chunks", index_report.chunks);
|
||||
hybrid.warmup()?;
|
||||
let mut rows = Vec::new();
|
||||
for query in fixture["queries"].as_array().unwrap() {
|
||||
if query["filters"].get("time_range_s").is_some() {
|
||||
rows.push(json!({"query":query,"unsupported":"Production query API has no temporal-range filter yet"}));
|
||||
continue;
|
||||
}
|
||||
let mut options = SearchOptions::default();
|
||||
if let Some(extension) = query["filters"]["extension"].as_str() {
|
||||
options.extension = Some(extension.into());
|
||||
}
|
||||
let start = Instant::now();
|
||||
let hits = hybrid.search(query["text"].as_str().unwrap(), 20, &options)?;
|
||||
let query_ms = start.elapsed().as_secs_f64() * 1000.0;
|
||||
let file_rankings: Vec<_> = hits.iter().map(|hit| {
|
||||
let asset = fixture["assets"].as_array().unwrap().iter().find(|asset| PathBuf::from(asset["path"].as_str().unwrap()).file_name().unwrap().to_string_lossy() == hit.filename).unwrap();
|
||||
let modality = if hit.match_kind.contains("audio") { "audio" } else if hit.match_kind == "media_metadata" { "metadata" } else { "image" };
|
||||
let mut candidate = json!({"asset":asset["id"],"modality":modality,"match_kind":hit.match_kind,"source":hit.source});
|
||||
if let Some(source) = &hit.source { if source.kind == "timestamp_ms" {
|
||||
let stamp = f64::from(source.number)/1000.0;
|
||||
candidate["start_s"] = json!(stamp);
|
||||
candidate["end_s"] = json!((stamp + if modality == "audio" {f64::from(limits.audio_window_ms)/1000.0} else {2.0}).min(asset["duration_s"].as_f64().unwrap_or(stamp)));
|
||||
candidate["interval_note"] = json!("End inferred from configured pipeline window policy, actual API exposes only start timestamp");
|
||||
}}
|
||||
json!({"candidate":candidate,"score":hit.score})
|
||||
}).collect();
|
||||
rows.push(json!({"query":query,"segment_top5":file_rankings.iter().take(5).cloned().collect::<Vec<_>>(),"file_rankings":file_rankings,"query_ms":query_ms}));
|
||||
}
|
||||
let report = json!({"fixture_sha256":hash,"worker":hybrid.worker_info(),"index_report":index_report,"filename_index":filename_index,"encoding_ms":encoding_ms,"audio_window_ms":limits.audio_window_ms,"audio_overlap_ms":limits.audio_overlap_ms,"encoding_failures":index_report.failures,"rows":rows,"scope":"Actual Searchi hybrid pipeline and native extension filters; temporal-range filter unsupported; segment ends inferred from configured audio/frame policy, exact point timestamps come from actual result API"});
|
||||
fs::write(&args[5], serde_json::to_vec_pretty(&report)?)?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user