Ship floating launcher, automatic discovery and bounded media search improvements

This commit is contained in:
karl committed 2026-10-14 11:44:15 +02:00
1 parent 01279a6b25
commit 521966a2ac
43 files changed
+3165 -267

No files matched your search

+83 -2
View File
@@ -11,6 +11,7 @@ pub use speech::{speech_policy_key, BudgetedSpeechProvider, SpeechExcerpt, Speec
mod prepared;
pub use prepared::PreparedGraphs;
mod pipeline;
mod relevance;
mod routing;
pub use routing::SemanticOrigin;
mod store;
@@ -280,6 +281,12 @@ pub struct HybridSearch {
}
impl HybridSearch {
/// Prepare storage before background discovery starts. No model is loaded.
pub fn prepare_database(db: impl AsRef<Path>) -> Result<()> {
let _ = SearchIndex::open(&db)?;
let _ = Store::open(db.as_ref(), GEMMA_DIMENSIONS)?;
Ok(())
}
/// Attach shared admission without replacing the warmed model or query LRU.
pub fn with_budget(
mut self,
@@ -485,7 +492,11 @@ impl HybridSearch {
/// Execute real query inference and load the active ANN generation before
/// foreground use. Call on a background worker after assets are verified.
pub fn warmup(&mut self) -> Result<()> {
self.lexical.prewarm()?;
if let Err(error) = self.lexical.prewarm() {
if !is_busy_sqlite_error(&error) {
return Err(error);
}
}
let vector = self.embedder.embed_query("warm up local file search")?;
let _ = self.store.search(
&vector,
@@ -580,8 +591,14 @@ impl HybridSearch {
promote_identifiers(query.trim(), &mut lexical)
});
let vector = self.query_vector(query, options.code_only, trace, parent)?;
let semantic =
let mut semantic =
self.routed_semantic_traced(query, &vector, candidates, options, trace, parent)?;
trace.timed("hybrid.semantic_shortlist", parent, |trace, span| {
trace.counter(span, "candidates_before", semantic.len() as u64);
let removed = relevance::prune_semantic_tail(&mut semantic);
trace.counter(span, "tail_candidates_pruned", removed as u64);
trace.counter(span, "candidates_after", semantic.len() as u64);
});
let results = trace.timed("hybrid.rank_fusion", parent, |trace, span| {
trace.counter(span, "lexical_files", lexical.len() as u64);
trace.counter(span, "semantic_files", semantic.len() as u64);
@@ -684,6 +701,11 @@ impl HybridSearch {
}
}
fn is_busy_sqlite_error(error: &anyhow::Error) -> bool {
matches!(error.downcast_ref::<rusqlite::Error>(), Some(rusqlite::Error::SqliteFailure(error, _))
if matches!(error.code, rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked))
}
fn bounded_title(title: &str) -> &str {
let mut end = title.len().min(512);
while !title.is_char_boundary(end) {
@@ -995,3 +1017,62 @@ mod tests {
#[cfg(test)]
mod coverage_tests;
#[cfg(test)]
mod startup_lock_tests {
use super::*;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
struct QueryProbe(Arc<AtomicUsize>);
impl EmbeddingProvider for QueryProbe {
fn fingerprint(&self) -> &str {
"query-path-probe"
}
fn dimensions(&self) -> usize {
GEMMA_DIMENSIONS
}
fn split_windows(&mut self, _: &str, _: &str) -> Result<Vec<TextWindow>> {
anyhow::bail!("unused")
}
fn embed_documents(&mut self, _: &[&str], _: &str) -> Result<Vec<Vec<f32>>> {
anyhow::bail!("unused")
}
fn embed_query(&mut self, _: &str) -> Result<Vec<f32>> {
self.0.fetch_add(1, Ordering::SeqCst);
anyhow::bail!("Reached query inference; no fabricated vector")
}
}
#[test]
fn optional_lexical_cache_busy_does_not_abort_query_preparation() -> Result<()> {
let temporary = tempfile::tempdir()?;
let db = temporary.path().join("index.db");
HybridSearch::prepare_database(&db)?;
let mut writer = rusqlite::Connection::open(&db)?;
let transaction =
writer.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
let calls = Arc::new(AtomicUsize::new(0));
let mut service = HybridSearch::with_provider(&db, Box::new(QueryProbe(calls.clone())))?;
assert!(service
.warmup()
.unwrap_err()
.to_string()
.contains("Reached query inference"));
assert_eq!(calls.load(Ordering::SeqCst), 1);
transaction.rollback()?;
Ok(())
}
#[test]
fn non_busy_sqlite_cache_errors_remain_visible() -> Result<()> {
let temporary = tempfile::tempdir()?;
let db = temporary.path().join("index.db");
HybridSearch::prepare_database(&db)?;
let calls = Arc::new(AtomicUsize::new(0));
let mut service = HybridSearch::with_provider(&db, Box::new(QueryProbe(calls.clone())))?;
rusqlite::Connection::open(&db)?.execute("DROP TABLE name_statistics_state", [])?;
assert!(service.warmup().is_err());
assert_eq!(calls.load(Ordering::SeqCst), 0);
Ok(())
}
}
+101
View File
@@ -0,0 +1,101 @@
//! Hybrid result shortlisting, not a confidence or no-answer classifier.
use searchi_core::SearchResult;
// Rounded upward from the largest adjacent gap (0.0650497) observed in the
// eight frozen calibration absence requests. A flat/noisy list stays intact;
// the strongest semantic candidate and every independent lexical match remain
// accessible. Raw semantic diagnostics are deliberately not changed.
const MIN_CLEAR_GAP: f64 = 0.07;
const NUMERIC_TIE: f64 = 2.0 * f32::EPSILON as f64;
/// Keep the strongest semantic cluster only when one clear score gap separates
/// it from the tail. Inputs are already source-checked, filtered and file-deduped.
pub(super) fn prune_semantic_tail(results: &mut Vec<SearchResult>) -> usize {
if results.len() < 2
|| results.iter().any(|result| !result.score.is_finite())
|| results.windows(2).any(|pair| pair[0].score < pair[1].score)
{
return 0;
}
let mut largest = 0.0;
let mut second = 0.0;
let mut boundary = results.len();
for (index, pair) in results.windows(2).enumerate() {
let gap = pair[0].score - pair[1].score;
if gap > largest {
second = largest;
largest = gap;
boundary = index + 1;
} else if gap > second {
second = gap;
}
}
// Equal large gaps indicate no unique cluster boundary, e.g. a uniform
// staircase. Do not pick an arbitrary split due to f32 rounding.
if largest <= MIN_CLEAR_GAP || largest - second <= NUMERIC_TIE {
return 0;
}
let removed = results.len() - boundary;
results.truncate(boundary);
removed
}
#[cfg(test)]
mod tests {
use super::*;
fn candidates(scores: &[f64]) -> Vec<SearchResult> {
scores
.iter()
.enumerate()
.map(|(index, score)| SearchResult {
path: format!("candidate-{index}"),
filename: format!("candidate-{index}"),
score: *score,
snippet: String::new(),
match_kind: "semantic".into(),
source: None,
extraction_error: None,
})
.collect()
}
#[test]
fn weak_correct_best_and_multiple_strong_candidates_survive() {
let mut weak = candidates(&[0.6766, 0.50, 0.49]);
assert_eq!(prune_semantic_tail(&mut weak), 2);
assert_eq!(weak[0].score, 0.6766);
let mut multiple = candidates(&[0.82, 0.79, 0.57, 0.55]);
assert_eq!(prune_semantic_tail(&mut multiple), 2);
assert_eq!(multiple.len(), 2);
}
#[test]
fn small_flat_and_ambiguous_lists_are_preserved() {
for scores in [
&[][..],
&[0.55],
&[0.61, 0.60],
&[0.65, 0.64, 0.63],
&[0.9, 0.8, 0.7, 0.6],
&[0.61, 0.61001, 0.60],
&[0.60, 0.599999, 0.599998],
] {
let mut results = candidates(scores);
assert_eq!(prune_semantic_tail(&mut results), 0, "{scores:?}");
assert_eq!(results.len(), scores.len());
}
let mut two = candidates(&[0.82, 0.52]);
assert_eq!(prune_semantic_tail(&mut two), 1);
assert_eq!(two.len(), 1);
}
#[test]
fn independent_exact_lexical_match_survives_semantic_tail_pruning() {
let mut semantic = candidates(&[0.82, 0.52]);
let mut exact = candidates(&[0.52]).remove(0);
exact.path = "candidate-1".into();
exact.match_kind = "exact_filename".into();
assert_eq!(prune_semantic_tail(&mut semantic), 1);
let results = crate::fuse(vec![exact], semantic, 10);
assert_eq!(results.len(), 2);
assert_eq!(results[0].path, "candidate-1");
assert_eq!(results[0].match_kind, "exact_filename");
}
}
+67 -16
View File
@@ -24,7 +24,7 @@ impl SemanticOrigin {
}
/// A conservative intent hint, not a language understanding model. Explicit
/// document/visual/code cues veto it; ordinary topic and instrument queries do not
/// document/code cues veto it; ordinary topic and instrument queries do not
/// silently restrict modality. Word boundaries avoid filename substrings.
fn preferred_origin(query: &str) -> Option<SemanticOrigin> {
if query.split_whitespace().count() == 1 && Path::new(query).extension().is_some() {
@@ -37,18 +37,10 @@ fn preferred_origin(query: &str) -> Option<SemanticOrigin> {
.collect();
let has = |values: &[&str]| words.iter().any(|word| values.contains(word));
if has(&[
"image",
"photo",
"picture",
"screenshot",
"diagram",
"document",
"pdf",
"text",
"paper",
"bild",
"foto",
"screenshot",
"dokument",
"textdatei",
"code",
@@ -57,9 +49,33 @@ fn preferred_origin(query: &str) -> Option<SemanticOrigin> {
"quellcode",
"funktion",
"implementierung",
"algorithm",
"algorithms",
"algorithmus",
"algorithmen",
]) {
return None;
}
// Explicit visual requests should not be answered by unrelated audio or
// document neighbors. Check these before speech/audio words: a photo of a
// speaker and a screenshot of a waveform still request visual evidence.
let visual = has(&[
"image",
"photo",
"picture",
"screenshot",
"diagram",
"bild",
"foto",
"fotografie",
]);
let video = has(&["video", "movie", "clip", "film"]);
if visual && video {
return None;
}
if visual {
return Some(SemanticOrigin::Image);
}
if has(&[
"spoken",
"speech",
@@ -86,7 +102,6 @@ fn preferred_origin(query: &str) -> Option<SemanticOrigin> {
if has(&[
"audio",
"recording",
"sound",
"music",
"geräusch",
"geraeusch",
@@ -98,9 +113,13 @@ fn preferred_origin(query: &str) -> Option<SemanticOrigin> {
"hoerbarer",
"hoerbare",
"tonaufnahme",
]) {
]) || (has(&["sound"]) && has(&["of", "effect", "effects"]))
{
return Some(SemanticOrigin::Audio);
}
if video {
return Some(SemanticOrigin::Video);
}
None
}
@@ -130,7 +149,7 @@ impl HybridSearch {
} else {
preferred_origin(query)
} {
trace.counter(parent, "audio_preference", 1);
trace.counter(parent, "explicit_source_channel", 1);
let preferred = self.store.search_origin_traced(
vector,
&self.generation_fingerprint,
@@ -157,6 +176,12 @@ impl HybridSearch {
return Ok(raw);
}
}
// An empty requested channel is useful evidence. Falling back
// to every source type turns an absent recording/photo into an
// unrelated result; lexical filename/content hits remain in
// the independently fused lane.
trace.counter(parent, "requested_channel_empty", 1);
return Ok(Vec::new());
}
trace.counter(parent, "general_lane", 1);
self.store.search_origin_traced(
@@ -226,7 +251,7 @@ mod tests {
}
#[test]
fn explicit_spoken_intent_is_conservative_and_bilingual() {
fn explicit_media_intent_is_conservative_and_bilingual() {
for query in [
"where someone mentions a contract",
"a speaker says the date",
@@ -234,6 +259,7 @@ mod tests {
"jemand erwähnt das Budget",
"Wo spricht jemand über Kündigung?",
"Gespräch über die neue Wohnung",
"video where someone mentions a contract",
] {
assert_eq!(
preferred_origin(query),
@@ -241,6 +267,29 @@ mod tests {
"{query}"
);
}
for query in [
"photo of someone speaking",
"Bild von einem Sprecher",
"a screenshot of an audio waveform",
"Foto mit Bergen",
] {
assert_eq!(
preferred_origin(query),
Some(SemanticOrigin::Image),
"{query}"
);
}
for query in [
"video of a boat",
"Film mit einem Wasserfall",
"a movie with a blue car",
] {
assert_eq!(
preferred_origin(query),
Some(SemanticOrigin::Video),
"{query}"
);
}
for query in [
"audio of a waterfall",
"hörbarer Regen",
@@ -256,17 +305,19 @@ mod tests {
for query in [
"contract deadline",
"violin solo",
"photo of someone speaking",
"Bild von einem Sprecher",
"a document that someone mentioned",
"speech.png",
"a screenshot of an audio waveform",
"code that chooses audio search when a query mentions speech",
"an implementation for identifying music",
"a function that decodes audio samples",
"Quellcode für die Suche nach gesprochenen Themen",
"eine Funktion für die Verarbeitung von Musik",
"Implementierung für die Erkennung von Geräuschen",
"photo or video of a boat",
"Bild oder Film vom Wasserfall",
"image compression algorithm",
"Algorithmus zur Bildkomprimierung",
"sound financial advice",
] {
assert_eq!(preferred_origin(query), None, "{query}");
}
+224 -48
View File
@@ -18,6 +18,99 @@ use usearch::{Index, IndexOptions, MetricKind, ScalarKind};
pub(crate) const MAX_SCAN_CHUNKS: usize = 100_000;
pub(crate) const MIN_COSINE: f32 = 0.25;
const SCHEMA_VERSION: i64 = 9;
fn current_schema_ready(connection: &Connection) -> Result<bool> {
const TABLES: &[(&str, &[&str])] = &[
(
"semantic_meta",
&[
"schema_version",
"active_generation",
"corpus_revision",
"dirty_floor",
],
),
(
"semantic_generations",
&[
"id",
"fingerprint",
"dimensions",
"corpus_revision",
"ann_blob",
"ann_sha256",
"incremental",
],
),
(
"semantic_chunks",
&[
"generation",
"passage_id",
"ordinal",
"path",
"kind",
"number",
"content_hash",
"start_byte",
"end_byte",
"snippet",
"vector",
"source_type",
"file_size",
"file_mtime",
"speech_end_ms",
"task_key",
"file_change_revision",
],
),
("semantic_dirty_paths", &["path", "revision"]),
("semantic_media_errors", &["generation", "path", "message"]),
(
"semantic_media_metadata_revision",
&["singleton", "revision"],
),
(
"semantic_tasks",
&[
"generation",
"task_key",
"path",
"source_type",
"source_hash",
"title_hash",
"policy",
"passage_id",
"state",
"attempts",
"expected_chunks",
"message",
],
),
];
for (table, required) in TABLES {
let mut query = connection.prepare("SELECT name FROM pragma_table_info(?1)")?;
let columns = query
.query_map([table], |row| row.get::<_, String>(0))?
.collect::<rusqlite::Result<HashSet<_>>>()?;
if required.iter().any(|name| !columns.contains(*name)) {
return Ok(false);
}
}
let triggers: i64 = connection.query_row(
"SELECT count(*) FROM sqlite_schema WHERE type='trigger' AND name IN ('semantic_files_insert','semantic_files_update','semantic_files_delete','semantic_media_metadata_insert','semantic_media_metadata_update','semantic_media_metadata_delete')", [], |row| row.get(0),
)?;
let indexes: i64 = connection.query_row(
"SELECT count(*) FROM sqlite_schema WHERE type='index' AND name IN ('semantic_dirty_revision','semantic_chunk_path','semantic_task_reuse','semantic_chunk_task')", [], |row| row.get(0),
)?;
let metadata_row: bool = connection.query_row(
"SELECT EXISTS(SELECT 1 FROM semantic_media_metadata_revision WHERE singleton=1)",
[],
|row| row.get(0),
)?;
Ok(triggers == 6 && indexes == 4 && metadata_row)
}
// Disjoint negative namespaces preserve existing document/raw-media IDs without a table rebuild.
pub(crate) fn media_passage_id(file_id: i64, source_type: &str) -> Result<i64> {
@@ -122,24 +215,46 @@ impl Store {
let mut connection = Connection::open(db)?;
connection.busy_timeout(std::time::Duration::from_secs(5))?;
connection.execute_batch("PRAGMA foreign_keys=ON;")?;
let initialization =
connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
initialization.execute_batch(
let has_meta: bool = connection.query_row(
"SELECT EXISTS(SELECT 1 FROM sqlite_schema WHERE type='table' AND name='semantic_meta')",
[], |row| row.get(0),
)?;
let current_version = if has_meta {
let version: i64 = connection.query_row(
"SELECT schema_version FROM semantic_meta WHERE singleton=1",
[],
|row| row.get(0),
)?;
ensure!(
(1..=SCHEMA_VERSION).contains(&version),
"Unsupported semantic schema version {version}"
);
Some(version)
} else {
None
};
// WAL readers must not acquire a writer lock just to reopen an already
// prepared index. Some earlier schema-9 builds added columns without a
// version bump, so incomplete legacy shapes still run normal migration.
if current_version != Some(SCHEMA_VERSION) || !current_schema_ready(&connection)? {
let initialization =
connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
initialization.execute_batch(
"CREATE TABLE IF NOT EXISTS semantic_meta (
singleton INTEGER PRIMARY KEY CHECK(singleton=1), schema_version INTEGER NOT NULL,
active_generation INTEGER, corpus_revision INTEGER NOT NULL DEFAULT 0, dirty_floor INTEGER NOT NULL DEFAULT 0);
INSERT OR IGNORE INTO semantic_meta(singleton,schema_version) VALUES(1,5);",
)?;
let version: i64 = initialization.query_row(
"SELECT schema_version FROM semantic_meta WHERE singleton=1",
[],
|row| row.get(0),
)?;
ensure!(
(1..=9).contains(&version),
"Unsupported semantic schema version {version}"
);
initialization.execute_batch("
let version: i64 = initialization.query_row(
"SELECT schema_version FROM semantic_meta WHERE singleton=1",
[],
|row| row.get(0),
)?;
ensure!(
(1..=9).contains(&version),
"Unsupported semantic schema version {version}"
);
initialization.execute_batch("
CREATE TABLE IF NOT EXISTS semantic_generations(id INTEGER PRIMARY KEY, fingerprint TEXT NOT NULL, dimensions INTEGER NOT NULL, corpus_revision INTEGER NOT NULL DEFAULT 0, ann_blob BLOB, ann_sha256 TEXT);
CREATE TABLE IF NOT EXISTS semantic_chunks(
generation INTEGER NOT NULL REFERENCES semantic_generations(id) ON DELETE CASCADE,
@@ -148,8 +263,8 @@ impl Store {
content_hash TEXT NOT NULL, start_byte INTEGER NOT NULL, end_byte INTEGER NOT NULL,
snippet TEXT NOT NULL, vector BLOB NOT NULL,
PRIMARY KEY(generation,passage_id,ordinal));")?;
if version < 5 {
for (table,column,alter) in [
if version < 5 {
for (table,column,alter) in [
("semantic_meta","corpus_revision","ALTER TABLE semantic_meta ADD COLUMN corpus_revision INTEGER NOT NULL DEFAULT 0"),
("semantic_meta","dirty_floor","ALTER TABLE semantic_meta ADD COLUMN dirty_floor INTEGER NOT NULL DEFAULT 0"),
("semantic_generations","dimensions","ALTER TABLE semantic_generations ADD COLUMN dimensions INTEGER NOT NULL DEFAULT 384"),
@@ -160,10 +275,10 @@ impl Store {
let exists:bool=initialization.query_row("SELECT EXISTS(SELECT 1 FROM pragma_table_info(?1) WHERE name=?2)",params![table,column],|row| row.get(0))?;
if !exists { initialization.execute_batch(alter)?; }
}
initialization.execute_batch("DROP TRIGGER IF EXISTS semantic_files_insert;
initialization.execute_batch("DROP TRIGGER IF EXISTS semantic_files_insert;
DROP TRIGGER IF EXISTS semantic_files_update; DROP TRIGGER IF EXISTS semantic_files_delete;")?;
}
initialization.execute_batch(
}
initialization.execute_batch(
"CREATE TABLE IF NOT EXISTS semantic_dirty_paths(path TEXT PRIMARY KEY, revision INTEGER NOT NULL);
CREATE INDEX IF NOT EXISTS semantic_dirty_revision ON semantic_dirty_paths(revision);
CREATE INDEX IF NOT EXISTS semantic_chunk_path ON semantic_chunks(generation,path);
@@ -183,45 +298,50 @@ impl Store {
ON CONFLICT(path) DO UPDATE SET revision=excluded.revision; END;
UPDATE semantic_meta SET schema_version=6 WHERE singleton=1;",
)?;
for (column, declaration) in [
("source_type", "TEXT NOT NULL DEFAULT 'text'"),
("file_size", "INTEGER"),
("file_mtime", "INTEGER"),
] {
let exists: bool = initialization.query_row(
for (column, declaration) in [
("source_type", "TEXT NOT NULL DEFAULT 'text'"),
("file_size", "INTEGER"),
("file_mtime", "INTEGER"),
] {
let exists: bool = initialization.query_row(
"SELECT EXISTS(SELECT 1 FROM pragma_table_info('semantic_chunks') WHERE name=?1)",
[column],
|r| r.get(0),
)?;
if !exists {
initialization.execute_batch(&format!(
"ALTER TABLE semantic_chunks ADD COLUMN {column} {declaration}"
))?;
if !exists {
initialization.execute_batch(&format!(
"ALTER TABLE semantic_chunks ADD COLUMN {column} {declaration}"
))?;
}
}
}
initialization.execute_batch("CREATE TABLE IF NOT EXISTS semantic_media_errors(generation INTEGER NOT NULL REFERENCES semantic_generations(id) ON DELETE CASCADE,path TEXT NOT NULL,message TEXT NOT NULL,PRIMARY KEY(generation,path))")?;
let has_speech_end:bool=initialization.query_row("SELECT EXISTS(SELECT 1 FROM pragma_table_info('semantic_chunks') WHERE name='speech_end_ms')",[],|row|row.get(0))?;
if !has_speech_end {
initialization.execute(
"ALTER TABLE semantic_chunks ADD COLUMN speech_end_ms INTEGER",
[],
)?;
}
// Probe publication changes filter eligibility, not source discovery identity.
// Keep its epoch separate so indexing does not invalidate its own corpus snapshot.
initialization.execute_batch("CREATE TABLE IF NOT EXISTS semantic_media_metadata_revision(singleton INTEGER PRIMARY KEY CHECK(singleton=1), revision INTEGER NOT NULL);
initialization.execute_batch("CREATE TABLE IF NOT EXISTS semantic_media_errors(generation INTEGER NOT NULL REFERENCES semantic_generations(id) ON DELETE CASCADE,path TEXT NOT NULL,message TEXT NOT NULL,PRIMARY KEY(generation,path))")?;
let has_speech_end:bool=initialization.query_row("SELECT EXISTS(SELECT 1 FROM pragma_table_info('semantic_chunks') WHERE name='speech_end_ms')",[],|row|row.get(0))?;
if !has_speech_end {
initialization.execute(
"ALTER TABLE semantic_chunks ADD COLUMN speech_end_ms INTEGER",
[],
)?;
}
// Probe publication changes filter eligibility, not source discovery identity.
// Keep its epoch separate so indexing does not invalidate its own corpus snapshot.
initialization.execute_batch("CREATE TABLE IF NOT EXISTS semantic_media_metadata_revision(singleton INTEGER PRIMARY KEY CHECK(singleton=1), revision INTEGER NOT NULL);
INSERT OR IGNORE INTO semantic_media_metadata_revision(singleton,revision) VALUES(1,0);")?;
for (name, event) in [
("insert", "INSERT"),
("update", "UPDATE"),
("delete", "DELETE"),
] {
initialization.execute_batch(&format!("DROP TRIGGER IF EXISTS semantic_media_metadata_{name};
for (name, event) in [
("insert", "INSERT"),
("update", "UPDATE"),
("delete", "DELETE"),
] {
initialization.execute_batch(&format!("DROP TRIGGER IF EXISTS semantic_media_metadata_{name};
CREATE TRIGGER semantic_media_metadata_{name} AFTER {event} ON media_metadata BEGIN
UPDATE semantic_media_metadata_revision SET revision=revision+1 WHERE singleton=1; END;"))?;
}
incremental::migrate(&initialization)?;
initialization.commit()?;
ensure!(
current_schema_ready(&connection)?,
"Semantic schema initialization incomplete"
);
}
incremental::migrate(&initialization)?;
initialization.commit()?;
let canonical = db.canonicalize()?;
let lock_path = PathBuf::from(format!("{}.semantic.lock", canonical.to_string_lossy()));
Ok(Self {
@@ -2068,3 +2188,59 @@ mod tests {
#[cfg(test)]
#[path = "ann_benchmark.rs"]
mod ann_benchmark;
#[cfg(test)]
mod reopen_tests {
use super::*;
#[test]
fn initialized_wal_store_reopens_during_an_owned_write_transaction() -> Result<()> {
let temporary = tempfile::tempdir()?;
let db = temporary.path().join("index.db");
crate::HybridSearch::prepare_database(&db)?;
let mut writer = Connection::open(&db)?;
let transaction =
writer.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
transaction.execute(
"UPDATE semantic_meta SET corpus_revision=corpus_revision+1 WHERE singleton=1",
[],
)?;
let started = std::time::Instant::now();
let reader = Store::open(&db, 768)?;
assert!(started.elapsed() < std::time::Duration::from_secs(2));
assert_eq!(
reader.connection.query_row(
"SELECT corpus_revision FROM semantic_meta WHERE singleton=1",
[],
|row| row.get::<_, i64>(0)
)?,
0
);
transaction.rollback()?;
Ok(())
}
#[test]
fn legacy_schema_nine_additions_are_repaired_and_future_versions_rejected() -> Result<()> {
let temporary = tempfile::tempdir()?;
let db = temporary.path().join("index.db");
crate::HybridSearch::prepare_database(&db)?;
let connection = Connection::open(&db)?;
connection.execute_batch("ALTER TABLE semantic_chunks DROP COLUMN speech_end_ms; DROP TRIGGER semantic_media_metadata_update;")?;
assert!(!current_schema_ready(&connection)?);
drop(Store::open(&db, 768)?);
assert!(current_schema_ready(&connection)?);
connection.execute("UPDATE semantic_meta SET schema_version=10", [])?;
let error = match Store::open(&db, 768) {
Ok(_) => panic!("future schema accepted"),
Err(error) => error,
};
assert!(error
.to_string()
.contains("Unsupported semantic schema version 10"));
assert_eq!(
connection.query_row("SELECT schema_version FROM semantic_meta", [], |row| row
.get::<_, i64>(0))?,
10
);
Ok(())
}
}
@@ -0,0 +1,122 @@
//! Actual query retrieval against the owned, verified README fixture index.
use anyhow::{Context, Result};
use searchi_service::{HybridSearch, SearchOptions};
use std::path::PathBuf;
fn required(name: &str) -> Result<PathBuf> {
std::env::var_os(name)
.map(PathBuf::from)
.with_context(|| format!("Set {name}"))
}
#[test]
#[ignore = "Requires verified owned README fixture database and actual Google CPU model/runtime"]
fn real_google_hybrid_shortlist_removes_unrelated_media_without_changing_raw_search() -> Result<()>
{
assert_eq!(std::env::var("SEARCHI_FORCE_CPU")?, "1");
let db = required("SEARCHI_TEST_RELEVANCE_DATABASE")?;
let model = required("SEARCHI_GEMMA_MODEL_DIR")?;
let worker = required("SEARCHI_GEMMA_WORKER")?;
let mut search = HybridSearch::open_query(db, model, worker, 1)?;
let options = SearchOptions::default();
let contract = "cancel the supplier partnership";
let raw = search.search_semantic(contract, 50, &options)?;
assert_eq!(raw[0].filename, "Supplier Agreement.pdf");
assert!(raw
.iter()
.any(|result| result.filename == "Mountain Lake.jpg"));
let measured = search.search_measured(contract, 50, &options, true)?;
assert_eq!(measured.results[0].filename, "Supplier Agreement.pdf");
assert!(measured.results.iter().all(
|result| result.filename != "Mountain Lake.jpg" && result.filename != "Night Pier.jpg"
));
assert!(measured
.metrics
.trace
.as_ref()
.context("debug trace")?
.iter()
.any(|span| span.name == "hybrid.semantic_shortlist"
&& span
.counters
.get("tail_candidates_pruned")
.is_some_and(|count| *count > 0)));
let raw_after = search.search_semantic(contract, 50, &options)?;
assert_eq!(raw_after.len(), raw.len());
assert_eq!(raw_after[0].score, raw[0].score);
let image = search.search("mountains reflected in calm water", 50, &options)?;
assert_eq!(image[0].filename, "Mountain Lake.jpg");
assert!(image
.iter()
.all(|result| result.match_kind == "semantic_image"));
assert_eq!(
search.search("Mountain Lake.jpg", 1, &options)?[0].match_kind,
"exact_filename"
);
let pdf = SearchOptions {
extension: Some("pdf".into()),
..Default::default()
};
assert!(search
.search(contract, 50, &pdf)?
.iter()
.all(|result| result.filename == "Supplier Agreement.pdf"));
assert_eq!(
search.search(contract, 1, &options)?[0].filename,
"Supplier Agreement.pdf"
);
let excluded = SearchOptions {
max_size: Some(1),
..Default::default()
};
assert!(search.search(contract, 50, &excluded)?.is_empty());
eprintln!(
"actual raw contract candidates={}, hybrid={}, image={}",
raw.len(),
measured.results.len(),
image.len()
);
Ok(())
}
#[test]
#[ignore = "Requires the owned verified README fixture database and real Google CPU runtime"]
fn real_google_query_warms_during_an_owned_wal_writer() -> Result<()> {
assert_eq!(std::env::var("SEARCHI_FORCE_CPU")?, "1");
let db = required("SEARCHI_TEST_RELEVANCE_DATABASE")?;
let model = required("SEARCHI_GEMMA_MODEL_DIR")?;
let worker = required("SEARCHI_GEMMA_WORKER")?;
let mut writer = rusqlite::Connection::open(&db)?;
let old_dirty: i64 = writer.query_row(
"SELECT dirty FROM name_statistics_state WHERE singleton=1",
[],
|row| row.get(0),
)?;
writer.execute(
"UPDATE name_statistics_state SET dirty=1 WHERE singleton=1",
[],
)?;
let transaction = writer.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
let outcome = (|| -> Result<()> {
let mut search = HybridSearch::open_query(&db, model, worker, 1)?;
search.warmup()?;
let hits = search.search_semantic(
"cancel the supplier partnership",
1,
&SearchOptions::default(),
)?;
assert_eq!(hits[0].filename, "Supplier Agreement.pdf");
assert_eq!(
search.worker_info().context("real worker info")?.device,
"cpu"
);
eprintln!("Actual Google CPU query initialization, warmup and prepared retrieval succeeded with the WAL writer held");
Ok(())
})();
transaction.rollback()?;
writer.execute(
"UPDATE name_statistics_state SET dirty=?1 WHERE singleton=1",
[old_dirty],
)?;
outcome
}
@@ -3,6 +3,69 @@ use searchi_core::SearchIndex;
use searchi_service::{FfmpegTools, HybridSearch, MediaLimits, SearchOptions, SemanticOrigin};
use std::{fs, path::PathBuf};
#[test]
#[ignore = "Requires verified Google CPU worker/model and FFmpeg fixtures"]
fn explicit_media_requests_do_not_fall_back_to_unrelated_channels() -> Result<()> {
assert_eq!(std::env::var("SEARCHI_FORCE_CPU")?, "1");
let model = std::env::var_os("SEARCHI_GEMMA_MODEL_DIR").expect("Set model directory");
let worker = std::env::var_os("SEARCHI_GEMMA_WORKER").expect("Set worker path");
let tools = FfmpegTools {
ffmpeg: std::env::var_os("SEARCHI_TEST_FFMPEG")
.expect("Set FFmpeg")
.into(),
ffprobe: std::env::var_os("SEARCHI_TEST_FFPROBE")
.expect("Set FFprobe")
.into(),
};
let fixture =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/media/assets/image01.jpg");
let temp = tempfile::tempdir()?;
let corpus = temp.path().join("corpus");
fs::create_dir(&corpus)?;
fs::copy(fixture, corpus.join("cat.jpg"))?;
let database = temp.path().join("index.db");
let mut lexical = SearchIndex::open(&database)?;
lexical.index_folder(&corpus)?;
let mut indexer = HybridSearch::open(&database, &model, &worker, 1)?;
let report = indexer.index_embeddings_with_media(&tools, &MediaLimits::default())?;
assert!(report.failures.is_empty(), "{report:?}");
assert_eq!(report.media_files, 1);
drop(indexer);
let mut search = HybridSearch::open_query(&database, &model, &worker, 1)?;
let options = SearchOptions::default();
// The unfiltered real model does return the nearest photo for these requests.
// The correction rejects its wrong channel, not its similarity score.
for query in [
"music with violin",
"audio of thunder",
"someone mentions a contract",
"video of a boat",
"Musik mit Klavier",
"Film mit einem Wasserfall",
] {
assert!(
!search
.search_semantic_with_origin(query, 10, &options, SemanticOrigin::All)?
.is_empty(),
"{query}"
);
assert!(
search.search_semantic(query, 10, &options)?.is_empty(),
"{query}"
);
}
for query in ["photo of a cat indoors", "Bild einer Katze", "cat indoors"] {
let hits = search.search_semantic(query, 10, &options)?;
assert_eq!(hits[0].filename, "cat.jpg", "{query}");
assert_eq!(hits[0].match_kind, "semantic_image", "{query}");
}
assert_eq!(
search.search("cat.jpg", 1, &options)?[0].match_kind,
"exact_filename"
);
Ok(())
}
#[test]
#[ignore = "Requires verified Google CPU worker/model and FFmpeg fixtures"]
fn explicit_code_intent_preserves_code_among_real_audio_candidates() -> Result<()> {