mod incremental; pub(crate) use incremental::SourceTask; use std::{ collections::{HashMap, HashSet}, fs::{File, OpenOptions}, path::{Path, PathBuf}, sync::Arc, }; use anyhow::{ensure, Context, Result}; use fs2::FileExt; use rusqlite::{params, Connection, OptionalExtension}; use searchi_core::metrics::{MetricsMode, SpanToken, TraceCollector}; use searchi_core::{IndexedPassage, SearchOptions, SearchResult, SourceLocation}; use searchi_semantic::TextWindow; use sha2::{Digest, Sha256}; use usearch::{Index, IndexOptions, MetricKind, ScalarKind}; pub(crate) const MAX_SCAN_CHUNKS: usize = 100_000; pub(crate) const MIN_COSINE: f32 = 0.25; #[derive(Clone, PartialEq, Eq)] struct FilterKey { folder: Option, extension: Option, min_size: Option, max_size: Option, modified_after: Option, modified_before: Option, min_duration: Option, max_duration: Option, source_type: Option, code_only: bool, } struct EligibilityCache { generation: i64, revision: i64, filter: FilterKey, keys: Arc>, by_path: HashMap>, } pub(crate) struct Store { connection: Connection, lock_path: PathBuf, dimensions: usize, ann_cache: Option<(i64, Index)>, eligibility_cache: Option, } pub(crate) struct BuildLock(File, PathBuf); impl Drop for BuildLock { fn drop(&mut self) { let _ = FileExt::unlock(&self.0); } } pub(crate) fn content_hash(text: &str) -> String { #[cfg(test)] HASH_CALLS.with(|count| count.set(count.get() + 1)); format!("{:x}", Sha256::digest(text.as_bytes())) } #[cfg(test)] thread_local! { static HASH_CALLS:std::cell::Cell=const {std::cell::Cell::new(0)}; } #[cfg(test)] thread_local! { static MEDIA_STAT_CALLS:std::cell::Cell=const {std::cell::Cell::new(0)}; } impl Store { pub(crate) fn active_fingerprint(&self) -> Result> { Ok(self.connection.query_row("SELECT g.fingerprint FROM semantic_meta m JOIN semantic_generations g ON g.id=m.active_generation WHERE m.singleton=1",[],|r|r.get(0)).optional()?) } pub(crate) fn open(db: &Path, dimensions: usize) -> Result { ensure!( (1..=4096).contains(&dimensions), "Embedding dimensions must be between 1 and 4096" ); ensure!( db != Path::new(":memory:"), "Hybrid search requires a persistent database path" ); 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( "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..=7).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, passage_id INTEGER NOT NULL, ordinal INTEGER NOT NULL, path TEXT NOT NULL, kind TEXT NOT NULL, number INTEGER NOT NULL, 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 [ ("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"), ("semantic_generations","corpus_revision","ALTER TABLE semantic_generations ADD COLUMN corpus_revision INTEGER NOT NULL DEFAULT 0"), ("semantic_generations","ann_blob","ALTER TABLE semantic_generations ADD COLUMN ann_blob BLOB"), ("semantic_generations","ann_sha256","ALTER TABLE semantic_generations ADD COLUMN ann_sha256 TEXT"), ] { 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; DROP TRIGGER IF EXISTS semantic_files_update; DROP TRIGGER IF EXISTS semantic_files_delete;")?; } 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); CREATE TRIGGER IF NOT EXISTS semantic_files_insert AFTER INSERT ON files BEGIN UPDATE semantic_meta SET corpus_revision=corpus_revision+1 WHERE singleton=1; INSERT INTO semantic_dirty_paths(path,revision) VALUES(new.path,(SELECT corpus_revision FROM semantic_meta WHERE singleton=1)) ON CONFLICT(path) DO UPDATE SET revision=excluded.revision; END; CREATE TRIGGER IF NOT EXISTS semantic_files_update AFTER UPDATE ON files BEGIN UPDATE semantic_meta SET corpus_revision=corpus_revision+1 WHERE singleton=1; INSERT INTO semantic_dirty_paths(path,revision) VALUES(old.path,(SELECT corpus_revision FROM semantic_meta WHERE singleton=1)) ON CONFLICT(path) DO UPDATE SET revision=excluded.revision; INSERT INTO semantic_dirty_paths(path,revision) VALUES(new.path,(SELECT corpus_revision FROM semantic_meta WHERE singleton=1)) ON CONFLICT(path) DO UPDATE SET revision=excluded.revision; END; CREATE TRIGGER IF NOT EXISTS semantic_files_delete AFTER DELETE ON files BEGIN UPDATE semantic_meta SET corpus_revision=corpus_revision+1 WHERE singleton=1; INSERT INTO semantic_dirty_paths(path,revision) VALUES(old.path,(SELECT corpus_revision FROM semantic_meta WHERE singleton=1)) 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( "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}" ))?; } } 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))")?; incremental::migrate(&initialization)?; initialization.commit()?; let canonical = db.canonicalize()?; let lock_path = PathBuf::from(format!("{}.semantic.lock", canonical.to_string_lossy())); Ok(Self { connection, lock_path, dimensions, ann_cache: None, eligibility_cache: None, }) } pub(crate) fn build_lock(&self) -> Result { let file = OpenOptions::new() .read(true) .write(true) .create(true) .truncate(false) .open(&self.lock_path)?; file.try_lock_exclusive() .context("Another semantic rebuild is running")?; Ok(BuildLock(file, self.lock_path.clone())) } #[cfg(test)] pub(crate) fn begin(&mut self, fingerprint: &str) -> Result { let transaction = self.connection.transaction()?; // Only called while holding the cross-process lock: abandoned staging // generations may be removed safely after a crash. transaction.execute("DELETE FROM semantic_generations WHERE id != COALESCE((SELECT active_generation FROM semantic_meta WHERE singleton=1),-1)", [])?; transaction.execute( "INSERT INTO semantic_generations(fingerprint,dimensions,corpus_revision) SELECT ?1,?2,corpus_revision FROM semantic_meta WHERE singleton=1", params![fingerprint, self.dimensions as i64], )?; let id = transaction.last_insert_rowid(); transaction.commit()?; Ok(id) } pub(crate) fn media_batch(&self, after: i64) -> Result> { let mut q = self.connection.prepare( "SELECT rowid,path,size,mtime FROM files WHERE rowid>?1 ORDER BY rowid LIMIT 64", )?; let rows = q .query_map([after], |r| { Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)) })? .collect::>>()?; Ok(rows) } pub(crate) fn media_failure(&self, generation: i64, path: &str, message: &str) -> Result<()> { self.connection.execute( "DELETE FROM semantic_chunks WHERE generation=?1 AND path=?2 AND source_type!='text'", params![generation, path], )?; self.connection.execute("INSERT OR REPLACE INTO semantic_media_errors(generation,path,message) VALUES(?1,?2,?3)",params![generation,path,message.chars().take(2048).collect::()])?; Ok(()) } #[allow(clippy::too_many_arguments)] pub(crate) fn insert_media( &self, generation: i64, file_id: i64, ordinal: usize, identity: &searchi_media::MediaIdentity, source_type: &str, timestamp: u32, vector: &[f32], ) -> Result<()> { ensure!( vector.len() == self.dimensions && vector.iter().all(|v| v.is_finite()), "Invalid media embedding" ); let norm = vector.iter().map(|v| v * v).sum::().sqrt(); ensure!( (norm - 1.).abs() < 1e-3, "Media embeddings must be normalized" ); let path = identity.path.to_string_lossy(); let valid:bool=self.connection.query_row("SELECT EXISTS(SELECT 1 FROM files WHERE rowid=?1 AND path=?2 AND size=?3 AND mtime=?4)",params![file_id,path,identity.bytes as i64,identity.modified_ns],|r|r.get(0))?; ensure!(valid, "Media file changed since filesystem indexing"); let kind = if source_type == "image" { "image" } else { "timestamp_ms" }; let snippet = if source_type == "image" { "Image content".to_string() } else { format!( "{} content at {:.3} seconds", source_type, f64::from(timestamp) / 1000. ) }; let bytes: Vec = vector.iter().flat_map(|v| v.to_le_bytes()).collect(); self.connection.execute("INSERT INTO semantic_chunks(generation,passage_id,ordinal,path,kind,number,content_hash,start_byte,end_byte,snippet,vector,source_type,file_size,file_mtime) VALUES(?1,?2,?3,?4,?5,?6,?7,0,0,?8,?9,?10,?11,?12)",params![generation,-file_id,ordinal as i64,path,kind,timestamp,identity.sha256,snippet,bytes,source_type,identity.bytes as i64,identity.modified_ns])?; Ok(()) } pub(crate) fn insert( &mut self, generation: i64, passage: &IndexedPassage, first_ordinal: usize, windows: &[TextWindow], vectors: &[Vec], ) -> Result<()> { ensure!( windows.len() == vectors.len(), "Window and embedding counts differ" ); let hash = content_hash(&passage.text); let transaction = self.connection.transaction()?; for (i, (window, vector)) in windows.iter().zip(vectors).enumerate() { ensure!( vector.len() == self.dimensions && vector.iter().all(|v| v.is_finite()), "Invalid persisted vector" ); let norm = vector.iter().map(|value| value * value).sum::().sqrt(); ensure!( norm.is_finite() && (norm - 1.0).abs() < 1e-3, "Persisted embeddings must be L2 normalized" ); ensure!( passage.text.get(window.start_byte..window.end_byte) == Some(window.text.as_str()), "Window does not match passage source range" ); let bytes: Vec = vector .iter() .flat_map(|value| value.to_le_bytes()) .collect(); transaction.execute("INSERT INTO semantic_chunks(generation,passage_id,ordinal,path,kind,number,content_hash,start_byte,end_byte,snippet,vector) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11)", params![generation, passage.id, (first_ordinal+i) as i64, passage.path, passage.source.kind, passage.source.number, hash, window.start_byte as i64, window.end_byte as i64, window.text, bytes])?; } transaction.commit()?; Ok(()) } #[cfg(test)] pub(crate) fn activate(&mut self, generation: i64) -> Result<()> { self.activate_revision(generation, None) } fn activate_revision(&mut self, generation: i64, revision: Option) -> Result<()> { // A failed graph build or serialization leaves the old generation active. let ann = self.build_ann(generation)?; let length = ann.serialized_length(); ensure!( length <= 512 * 1024 * 1024, "Serialized ANN exceeds 512 MiB generation budget" ); let mut serialized = vec![0_u8; length]; ann.save_to_buffer(&mut serialized)?; let checksum = format!("{:x}", Sha256::digest(&serialized)); let transaction = self .connection .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?; if let Some(expected) = revision { let current: i64 = transaction.query_row( "SELECT corpus_revision FROM semantic_meta WHERE singleton=1", [], |r| r.get(0), )?; ensure!( current == expected, "Indexed corpus changed during semantic rebuild; retry required" ); incremental::validate_completion(&transaction, generation)?; } ensure!( transaction.query_row( "SELECT EXISTS(SELECT 1 FROM semantic_generations WHERE id=?1)", [generation], |row| row.get::<_, bool>(0) )?, "Missing staging generation" ); transaction.execute( "UPDATE semantic_generations SET ann_blob=?1,ann_sha256=?2 WHERE id=?3", params![serialized, checksum, generation], )?; transaction.execute( "UPDATE semantic_meta SET active_generation=?1 WHERE singleton=1", [generation], )?; transaction.execute( "DELETE FROM semantic_generations WHERE id!=?1", [generation], )?; transaction.execute_batch("UPDATE semantic_meta SET dirty_floor=max(dirty_floor,corpus_revision-100000) WHERE singleton=1; DELETE FROM semantic_dirty_paths WHERE revision<(SELECT dirty_floor FROM semantic_meta WHERE singleton=1);")?; transaction.commit()?; self.ann_cache = Some((generation, ann)); self.eligibility_cache = None; Ok(()) } fn new_ann(&self) -> Result { Ok(Index::new(&IndexOptions { dimensions: self.dimensions, metric: MetricKind::Cos, quantization: ScalarKind::F32, connectivity: 16, expansion_add: 128, expansion_search: 64, multi: false, })?) } fn build_ann(&self, generation: i64) -> Result { let count: usize = self.connection.query_row( "SELECT count(*) FROM semantic_chunks WHERE generation=?1", [generation], |row| row.get(0), )?; ensure!( count <= MAX_SCAN_CHUNKS, "ANN generation exceeds {MAX_SCAN_CHUNKS} chunks" ); let ann = self.new_ann()?; ann.reserve_capacity_and_threads(count.max(1), 1)?; let mut statement = self.connection.prepare( "SELECT rowid,vector FROM semantic_chunks WHERE generation=?1 ORDER BY rowid", )?; let mut rows = statement.query([generation])?; while let Some(row) = rows.next()? { let key: i64 = row.get(0)?; let bytes: Vec = row.get(1)?; ensure!( bytes.len() == self.dimensions * 4, "Invalid staging embedding length" ); let vector: Vec = bytes .as_chunks::<4>() .0 .iter() .map(|bytes| f32::from_le_bytes(*bytes)) .collect(); ensure!( vector.iter().all(|v| v.is_finite()), "Invalid staging embedding values" ); ann.add(u64::try_from(key)?, &vector)?; } Ok(ann) } #[cfg(test)] pub(crate) fn abandon(&mut self, generation: i64) -> Result<()> { self.connection.execute("DELETE FROM semantic_generations WHERE id=?1 AND id!=COALESCE((SELECT active_generation FROM semantic_meta WHERE singleton=1),-1)", [generation])?; Ok(()) } pub(crate) fn search( &mut self, query: &[f32], fingerprint: &str, limit: usize, options: &SearchOptions, ) -> Result> { self.search_origin(query, fingerprint, limit, options, None) } pub(crate) fn search_origin( &mut self, query: &[f32], fingerprint: &str, limit: usize, options: &SearchOptions, source_type: Option<&str>, ) -> Result> { let mut trace = TraceCollector::new(MetricsMode::Basic); self.search_origin_traced( query, fingerprint, limit, options, source_type, &mut trace, None, ) } #[allow(clippy::too_many_arguments)] pub(crate) fn search_origin_traced( &mut self, query: &[f32], fingerprint: &str, limit: usize, options: &SearchOptions, source_type: Option<&str>, trace: &mut TraceCollector, parent: Option, ) -> Result> { trace.timed("semantic.retrieval", parent, |trace, parent| { self.search_inner( query, fingerprint, limit, options, source_type, trace, parent, ) }) } #[allow(clippy::too_many_arguments)] fn search_inner( &mut self, query: &[f32], fingerprint: &str, limit: usize, options: &SearchOptions, source_type: Option<&str>, trace: &mut TraceCollector, parent: Option, ) -> Result> { ensure!( query.len() == self.dimensions && query.iter().all(|v| v.is_finite()), "Invalid query vector" ); if limit == 0 { return Ok(Vec::new()); } // A consistent read snapshot covers eligibility and materialization. let generation_span = trace.begin("semantic.generation_snapshot", parent); let snapshot = self.connection.unchecked_transaction()?; let active:Option=snapshot.query_row("SELECT g.id FROM semantic_generations g JOIN semantic_meta m ON m.active_generation=g.id WHERE g.fingerprint=?1 AND g.dimensions=?2", params![fingerprint,self.dimensions as i64],|row|row.get(0)).optional()?; let Some(active) = active else { return Ok(Vec::new()); }; trace.end(generation_span, Default::default()); let ann_load = trace.begin("semantic.ann_load", parent); trace.counter( ann_load, "cache_hit", u64::from( self.ann_cache .as_ref() .is_some_and(|(generation, _)| *generation == active), ), ); if !self .ann_cache .as_ref() .is_some_and(|(generation, _)| *generation == active) { let blob_span = trace.begin("semantic.ann_blob_read", ann_load); let length: Option = snapshot.query_row( "SELECT length(ann_blob) FROM semantic_generations WHERE id=?1", [active], |row| row.get(0), )?; ensure!( length.is_some_and(|bytes| bytes <= 512 * 1024 * 1024), "Missing or oversized ANN generation; rebuild embeddings explicitly" ); let (serialized, checksum): (Vec, String) = snapshot.query_row( "SELECT ann_blob,ann_sha256 FROM semantic_generations WHERE id=?1", [active], |row| Ok((row.get(0)?, row.get(1)?)), )?; trace.counter(blob_span, "bytes", serialized.len() as u64); trace.end(blob_span, Default::default()); let checksum_span = trace.begin("semantic.ann_checksum", ann_load); ensure!( format!("{:x}", Sha256::digest(&serialized)) == checksum, "ANN generation SHA256 mismatch; rebuild embeddings explicitly" ); trace.end(checksum_span, Default::default()); let deserialize_span = trace.begin("semantic.ann_deserialize", ann_load); let ann = self.new_ann()?; ann.load_from_buffer(&serialized)?; ensure!( ann.dimensions() == self.dimensions, "ANN dimensions differ from embedding provider" ); ann.reserve_capacity_and_threads(ann.size().max(1), 1)?; trace.counter(deserialize_span, "chunks", ann.size() as u64); trace.end(deserialize_span, Default::default()); self.ann_cache = Some((active, ann)); } trace.end(ann_load, Default::default()); let filter_span = trace.begin("semantic.filter_normalization", parent); let folder = options .folder .as_ref() .map(std::fs::canonicalize) .transpose()? .map(|p| { format!( "{}{}", p.to_string_lossy().trim_end_matches(['/', '\\']), std::path::MAIN_SEPARATOR ) }); let extension = options .extension .as_ref() .map(|ext| ext.trim_start_matches('.').to_ascii_lowercase()); let min_size = options.min_size.map(i64::try_from).transpose()?; let max_size = options.max_size.map(i64::try_from).transpose()?; let filter = FilterKey { folder, extension, min_size, max_size, modified_after: options.modified_after_unix_ms, modified_before: options.modified_before_unix_ms, min_duration: options.min_duration_ms, max_duration: options.max_duration_ms, source_type: source_type.map(str::to_owned), code_only: options.code_only, }; trace.end(filter_span, Default::default()); let eligibility_span = trace.begin("semantic.eligibility", parent); let (revision, floor): (i64, i64) = snapshot.query_row( "SELECT corpus_revision,dirty_floor FROM semantic_meta WHERE singleton=1", [], |row| Ok((row.get(0)?, row.get(1)?)), )?; let existing = self.eligibility_cache.take(); trace.counter( eligibility_span, "cache_hit", u64::from(existing.as_ref().is_some_and(|c| { c.generation == active && c.filter == filter && c.revision == revision })), ); let mut cache = match existing { Some(mut cache) if cache.generation == active && cache.filter == filter && cache.revision >= floor => { if cache.revision != revision { let mut statement=snapshot.prepare("SELECT path FROM semantic_dirty_paths WHERE revision>?1 AND revision<=?2 ORDER BY path LIMIT 1001")?; let paths = statement .query_map(params![cache.revision, revision], |row| { row.get::<_, String>(0) })? .collect::>>()?; trace.counter(eligibility_span, "dirty_paths", paths.len() as u64); if paths.len() > 1000 { cache = collect_eligibility_traced( &snapshot, active, revision, &filter, None, trace, eligibility_span, )?; } else { for path in paths { if let Some(old) = cache.by_path.remove(&path) { for key in old { Arc::make_mut(&mut cache.keys).remove(&key); } } let changed = collect_eligibility_traced( &snapshot, active, revision, &filter, Some(&path), trace, eligibility_span, )?; Arc::make_mut(&mut cache.keys).extend(changed.keys.iter().copied()); cache.by_path.extend(changed.by_path); } cache.revision = revision; } } cache } _ => collect_eligibility_traced( &snapshot, active, revision, &filter, None, trace, eligibility_span, )?, }; ensure!( cache.keys.len() <= MAX_SCAN_CHUNKS, "Eligibility cache exceeds chunk resource budget" ); trace.counter(eligibility_span, "eligible_chunks", cache.keys.len() as u64); trace.end(eligibility_span, Default::default()); cache.revision = revision; let eligible = Arc::clone(&cache.keys); self.eligibility_cache = Some(cache); if eligible.is_empty() { return Ok(Vec::new()); } let ann = &self.ann_cache.as_ref().expect("loaded ANN cache").1; let mut candidate_count = limit.saturating_mul(4).min(eligible.len()).max(1); let mut results; let mut filesystem_validity = HashMap::::new(); let mut passes = 0u64; loop { passes += 1; let pass_span = trace.begin("semantic.ann_pass", parent); trace.counter(pass_span, "requested_chunks", candidate_count as u64); let ann_span = trace.begin("semantic.ann_search", pass_span); let matches = ann.filtered_search(query, candidate_count, |key| eligible.contains(&key))?; trace.counter(ann_span, "returned_chunks", matches.keys.len() as u64); trace.end(ann_span, Default::default()); let materialization_span = trace.begin("semantic.parent_materialization", pass_span); let mut ranked: Vec<_> = matches.keys.into_iter().zip(matches.distances).collect(); ranked.sort_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(&b.0))); let mut best = HashMap::::new(); let mut materialize=snapshot.prepare("SELECT c.path,f.filename,c.snippet,c.kind,c.number,c.source_type,c.file_size,c.file_mtime FROM semantic_chunks c JOIN files f ON f.path=c.path WHERE c.rowid=?1 AND c.generation=?2")?; for (key, distance) in ranked { let cosine = 1.0 - distance; ensure!(cosine.is_finite(), "ANN produced nonfinite distance"); if cosine < MIN_COSINE { continue; } let (result, size, mtime) = materialize.query_row(params![i64::try_from(key)?, active], |row| { Ok(( SearchResult { path: row.get(0)?, filename: row.get(1)?, snippet: row.get(2)?, score: f64::from(cosine), match_kind: match row.get::<_, String>(5)?.as_str() { "text" => "semantic".into(), other => format!("semantic_{other}"), }, source: Some(SourceLocation { kind: row.get(3)?, number: row.get(4)?, }), extraction_error: None, }, row.get::<_, Option>(6)?, row.get::<_, Option>(7)?, )) })?; if let (Some(size), Some(mtime)) = (size, mtime) { let valid = *filesystem_validity .entry(result.path.clone()) .or_insert_with(|| { #[cfg(test)] MEDIA_STAT_CALLS.with(|count| count.set(count.get() + 1)); let validation_span = trace.begin( "semantic.parent_filesystem_validation", materialization_span, ); let valid = std::fs::metadata(&result.path) .ok() .is_some_and(|metadata| { metadata.is_file() && i64::try_from(metadata.len()).ok() == Some(size) && metadata .modified() .ok() .and_then(|t| { t.duration_since(std::time::UNIX_EPOCH).ok() }) .and_then(|d| i64::try_from(d.as_nanos()).ok()) == Some(mtime) }); trace.counter(validation_span, "valid", u64::from(valid)); trace.end(validation_span, Default::default()); valid }); if !valid { continue; } } if best.contains_key(&result.path) { continue; } best.insert(result.path.clone(), result); } results = best.into_values().collect::>(); trace.counter(materialization_span, "unique_parents", results.len() as u64); trace.counter( materialization_span, "filesystem_checks", filesystem_validity.len() as u64, ); trace.end(materialization_span, Default::default()); trace.end(pass_span, Default::default()); if results.len() >= limit || candidate_count == eligible.len() { break; } candidate_count = candidate_count.saturating_mul(2).min(eligible.len()); } results.sort_by(|a, b| { b.score .total_cmp(&a.score) .then_with(|| a.path.cmp(&b.path)) }); results.truncate(limit); trace.counter(parent, "ann_passes", passes); trace.counter(parent, "result_files", results.len() as u64); trace.counter( parent, "filesystem_checks", filesystem_validity.len() as u64, ); snapshot.commit()?; Ok(results) } #[cfg(test)] fn exact_search( &self, query: &[f32], fingerprint: &str, limit: usize, options: &SearchOptions, ) -> Result> { ensure!( query.len() == self.dimensions && query.iter().all(|v| v.is_finite()), "Invalid query vector" ); let active: Option = self.connection.query_row("SELECT g.id FROM semantic_generations g JOIN semantic_meta m ON m.active_generation=g.id WHERE g.fingerprint=?1 AND g.dimensions=?2", params![fingerprint,self.dimensions as i64], |row| row.get(0)).optional()?; let Some(active) = active else { return Ok(Vec::new()); }; let folder = options .folder .as_ref() .map(std::fs::canonicalize) .transpose()? .map(|p| { format!( "{}{}", p.to_string_lossy().trim_end_matches(['/', '\\']), std::path::MAIN_SEPARATOR ) }); let extension = options .extension .as_ref() .map(|ext| ext.trim_start_matches('.').to_ascii_lowercase()); let min_size = options.min_size.map(i64::try_from).transpose()?; let max_size = options.max_size.map(i64::try_from).transpose()?; let mut statement = self.connection.prepare("SELECT c.passage_id,c.path,f.filename,c.snippet,c.vector,c.content_hash,p.text,c.kind,c.number FROM semantic_chunks c JOIN passages p ON p.rowid=c.passage_id AND p.file_path=c.path AND p.kind=c.kind AND p.number=c.number JOIN files f ON f.path=c.path WHERE c.generation=?1 AND (?2 IS NULL OR substr(f.path,1,length(?2))=?2) AND (?3 IS NULL OR f.extension=?3) AND (?4 IS NULL OR f.size>=?4) AND (?5 IS NULL OR f.size<=?5) AND (?6 IS NULL OR f.mtime/1000000>=?6) AND (?7 IS NULL OR f.mtime/1000000<=?7) ORDER BY c.passage_id,c.ordinal LIMIT ?8")?; // Fetch one extra to detect the baseline cap instead of silently hiding // candidates. Metadata filtering precedes this cap and all top-k limits. let mut rows = statement.query(params![ active, folder, extension, min_size, max_size, options.modified_after_unix_ms, options.modified_before_unix_ms, MAX_SCAN_CHUNKS as i64 + 1 ])?; let mut hashes = HashMap::::new(); let mut best = HashMap::::new(); let mut scanned = 0; while let Some(row) = rows.next()? { scanned += 1; ensure!( scanned <= MAX_SCAN_CHUNKS, "Exact-vector baseline exceeds {MAX_SCAN_CHUNKS} chunks; ANN integration required" ); let passage_id: i64 = row.get(0)?; let current_hash = if let Some(hash) = hashes.get(&passage_id) { hash.clone() } else { let hash = content_hash(&row.get::<_, String>(6)?); hashes.insert(passage_id, hash.clone()); hash }; if current_hash != row.get::<_, String>(5)? { continue; } let bytes: Vec = row.get(4)?; ensure!( bytes.len() == self.dimensions * 4, "Corrupt stored embedding length" ); let mut cosine = 0.0; for (dimension, value) in bytes.as_chunks::<4>().0.iter().enumerate() { let value = f32::from_le_bytes(*value); ensure!(value.is_finite(), "Corrupt stored embedding value"); cosine += value * query[dimension]; } if cosine < MIN_COSINE { continue; } let path: String = row.get(1)?; if best .get(&path) .is_some_and(|result| result.score >= f64::from(cosine)) { continue; } best.insert( path.clone(), SearchResult { path, filename: row.get(2)?, snippet: row.get(3)?, score: f64::from(cosine), match_kind: "semantic".into(), source: Some(SourceLocation { kind: row.get(7)?, number: row.get(8)?, }), extraction_error: None, }, ); } let mut results: Vec<_> = best.into_values().collect(); results.sort_by(|a, b| { b.score .total_cmp(&a.score) .then_with(|| a.path.cmp(&b.path)) }); results.truncate(limit); Ok(results) } } #[allow(clippy::too_many_arguments)] fn collect_eligibility_traced( connection: &Connection, generation: i64, revision: i64, filter: &FilterKey, path: Option<&str>, trace: &mut TraceCollector, parent: Option, ) -> Result { trace.timed( "semantic.eligibility_filtered_scan_and_hash", parent, |trace, span| { let result = collect_eligibility(connection, generation, revision, filter, path); if let Ok(cache) = &result { trace.counter(span, "eligible_chunks", cache.keys.len() as u64); } trace.counter(span, "single_dirty_file", u64::from(path.is_some())); result }, ) } fn collect_eligibility( connection: &Connection, generation: i64, revision: i64, filter: &FilterKey, path: Option<&str>, ) -> Result { let code = if filter.code_only { format!(" AND {}", searchi_core::code_file_predicate()) } else { String::new() }; let mut statement=connection.prepare(&format!("SELECT c.rowid,c.passage_id,c.content_hash,p.text,c.path,c.source_type FROM semantic_chunks c LEFT JOIN passages p ON c.source_type='text' AND p.rowid=c.passage_id AND p.file_path=c.path AND p.kind=c.kind AND p.number=c.number JOIN files f ON f.path=c.path WHERE c.generation=?1 AND ((c.source_type='text' AND p.rowid IS NOT NULL) OR (c.source_type!='text' AND c.file_size=f.size AND c.file_mtime=f.mtime)) AND (?2 IS NULL OR substr(f.path,1,length(?2))=?2) AND (?3 IS NULL OR f.extension=?3) AND (?4 IS NULL OR f.size>=?4) AND (?5 IS NULL OR f.size<=?5) AND (?6 IS NULL OR f.mtime/1000000>=?6) AND (?7 IS NULL OR f.mtime/1000000<=?7) AND (?10 IS NULL OR EXISTS(SELECT 1 FROM media_metadata m WHERE m.path=f.path AND m.duration_ms>=?10)) AND (?11 IS NULL OR EXISTS(SELECT 1 FROM media_metadata m WHERE m.path=f.path AND m.duration_ms<=?11)) AND (?12 IS NULL OR c.source_type=?12) AND (?9 IS NULL OR c.path=?9) {code} ORDER BY c.passage_id,c.ordinal LIMIT ?8"))?; let mut rows = statement.query(params![ generation, filter.folder, filter.extension, filter.min_size, filter.max_size, filter.modified_after, filter.modified_before, MAX_SCAN_CHUNKS as i64 + 1, path, filter.min_duration, filter.max_duration, filter.source_type ])?; let mut hashes = HashMap::::new(); let mut keys = HashSet::new(); let mut by_path = HashMap::>::new(); let mut count = 0; while let Some(row) = rows.next()? { count += 1; ensure!( count <= MAX_SCAN_CHUNKS, "Corpus exceeds {MAX_SCAN_CHUNKS} chunk resource budget" ); let passage_id: i64 = row.get(1)?; let hash = if row.get::<_, String>(5)? != "text" { row.get::<_, String>(2)? } else if let Some(hash) = hashes.get(&passage_id) { hash.clone() } else { let hash = content_hash(&row.get::<_, String>(3)?); hashes.insert(passage_id, hash.clone()); hash }; if hash == row.get::<_, String>(2)? { let key = u64::try_from(row.get::<_, i64>(0)?)?; keys.insert(key); by_path.entry(row.get(4)?).or_default().push(key); } } Ok(EligibilityCache { generation, revision, filter: filter.clone(), keys: Arc::new(keys), by_path, }) } #[cfg(test)] mod tests { use super::*; use searchi_core::SearchIndex; const DIMENSIONS: usize = 768; #[test] fn code_filter_precedes_ann_top_k_and_cache_distinguishes_scope() -> Result<()> { // Retrieval geometry only: production vectors come from Google. let directory = tempfile::tempdir()?; let root = directory.path().join("sources"); std::fs::create_dir(&root)?; for n in 0..40 { std::fs::write( root.join(format!("document-{n}.md")), "parse JSON and retry requests", )?; } std::fs::write( root.join("network.rs"), "fn retry_request() { parse_json(); }", )?; std::fs::write(root.join("Makefile"), "build: compile_sources\n")?; let db = directory.path().join("index.db"); let mut lexical = SearchIndex::open(&db)?; lexical.index_folder(&root)?; let mut store = Store::open(&db, DIMENSIONS)?; let generation = store.begin("code-filter-proof")?; let mut query = vec![0.; DIMENSIONS]; query[0] = 1.; let mut weaker = query.clone(); weaker[0] = 0.9; weaker[1] = (1.0_f32 - 0.9_f32.powi(2)).sqrt(); for passage in lexical.passage_batch(0, 100)? { let vector = if searchi_core::is_code_path(Path::new(&passage.path)) { weaker.clone() } else { query.clone() }; let window = TextWindow { text: passage.text.clone(), start_byte: 0, end_byte: passage.text.len(), }; store.insert(generation, &passage, 0, &[window], &[vector])?; } store.activate(generation)?; assert!( store.search(&query, "code-filter-proof", 1, &SearchOptions::default())?[0] .filename .ends_with(".md") ); let code = SearchOptions { code_only: true, ..Default::default() }; let hits = store.search(&query, "code-filter-proof", 2, &code)?; assert_eq!(hits.len(), 2); assert!(hits .iter() .all(|h| searchi_core::is_code_path(Path::new(&h.path)))); let incompatible = SearchOptions { code_only: true, extension: Some("md".into()), ..Default::default() }; assert!(store .search(&query, "code-filter-proof", 2, &incompatible)? .is_empty()); std::fs::remove_file(root.join("network.rs"))?; lexical.index_folder(&root)?; assert_eq!( store.search(&query, "code-filter-proof", 2, &code)?.len(), 1 ); Ok(()) } #[test] fn many_video_windows_do_not_crowd_out_other_parent_files() -> Result<()> { // Geometry verifies grouped retrieval, not model inference or codecs. let directory = tempfile::tempdir()?; let root = directory.path().join("media"); std::fs::create_dir(&root)?; for name in ["long.mp4", "other.mp4", "excluded.png"] { std::fs::write(root.join(name), b"parent-grouping identity fixture")?; } let db = directory.path().join("index.db"); let mut lexical = SearchIndex::open(&db)?; lexical.index_folder(&root)?; let mut store = Store::open(&db, DIMENSIONS)?; let generation = store.begin("grouped-retrieval-proof")?; let mut query = vec![0.; DIMENSIONS]; query[0] = 1.; let mut second = query.clone(); second[0] = 0.95; second[1] = (1.0_f32 - 0.95_f32.powi(2)).sqrt(); for (id, path, _, _) in store.media_batch(0)? { let identity = searchi_media::MediaIdentity::read(Path::new(&path), 1024)?; if path.ends_with("long.mp4") { for ordinal in 0..96 { store.insert_media( generation, id, ordinal, &identity, "video", ordinal as u32 * 2000, &query, )?; } } else if path.ends_with("other.mp4") { store.insert_media(generation, id, 0, &identity, "video", 4000, &second)?; } else { store.insert_media(generation, id, 0, &identity, "image", 0, &query)?; } } store.activate(generation)?; let options = SearchOptions { extension: Some("mp4".into()), ..Default::default() }; MEDIA_STAT_CALLS.with(|count| count.set(0)); let mut trace = TraceCollector::new(MetricsMode::Debug); let results = store.search_origin_traced( &query, "grouped-retrieval-proof", 2, &options, None, &mut trace, None, )?; assert_eq!(results.len(), 2); assert!(results[0].path.ends_with("long.mp4")); assert!(results[1].path.ends_with("other.mp4")); assert_eq!(results[1].source.as_ref().unwrap().number, 4000); assert_eq!( MEDIA_STAT_CALLS.with(std::cell::Cell::get), 2, "Each parent is stat-checked once despite repeated ANN expansions" ); let metrics = trace.finish(results.len()); let spans = metrics.trace.unwrap(); assert!( spans .iter() .filter(|s| s.name == "semantic.ann_pass") .count() > 1 ); assert_eq!( spans .iter() .filter(|s| s.name == "semantic.parent_filesystem_validation") .count(), 2 ); assert!( spans .iter() .find(|s| s.name == "semantic.retrieval") .unwrap() .counters["ann_passes"] > 1 ); for span in &spans { if let Some(parent) = span.parent { let parent = &spans[parent]; assert!(span.start_ms >= parent.start_ms); assert!( span.start_ms + span.duration_ms <= parent.start_ms + parent.duration_ms + 1e-6 ); } } Ok(()) } #[test] fn media_origins_persist_filters_and_changes_remove_stale_candidates() -> Result<()> { let directory = tempfile::tempdir()?; let root = directory.path().join("media"); std::fs::create_dir(&root)?; let file = root.join("clip.mp4"); std::fs::write(&file, b"storage identity fixture; not an inference test")?; let db = directory.path().join("index.db"); let mut lexical = SearchIndex::open(&db)?; lexical.index_folder(&root)?; let mut store = Store::open(&db, DIMENSIONS)?; let (id, path, _, _) = store.media_batch(0)?.remove(0); let identity = searchi_media::MediaIdentity::read(Path::new(&path), 1024)?; lexical.set_media_metadata( &path, "artist: real extracted metadata fixture", 65000, identity.bytes, identity.modified_ns, )?; let mut vector = vec![0.; DIMENSIONS]; vector[0] = 1.; let generation = store.begin("media-storage-proof")?; store.insert_media(generation, id, 0, &identity, "video", 2345, &vector)?; store.insert_media(generation, id, 1, &identity, "audio", 28000, &vector)?; store.activate(generation)?; drop(store); let mut store = Store::open(&db, DIMENSIONS)?; let mut cold = TraceCollector::new(MetricsMode::Debug); let results = store.search_origin_traced( &vector, "media-storage-proof", 5, &SearchOptions::default(), None, &mut cold, None, )?; let cold = cold.finish(results.len()).trace.unwrap(); assert_eq!( cold.iter() .find(|s| s.name == "semantic.ann_load") .unwrap() .counters["cache_hit"], 0 ); assert!(cold.iter().any(|s| s.name == "semantic.ann_checksum")); let mut warm = TraceCollector::new(MetricsMode::Debug); let warm_results = store.search_origin_traced( &vector, "media-storage-proof", 5, &SearchOptions::default(), None, &mut warm, None, )?; assert_eq!( serde_json::to_value(&results)?, serde_json::to_value(&warm_results)? ); let warm = warm.finish(warm_results.len()).trace.unwrap(); assert_eq!( warm.iter() .find(|s| s.name == "semantic.ann_load") .unwrap() .counters["cache_hit"], 1 ); assert_eq!( warm.iter() .find(|s| s.name == "semantic.eligibility") .unwrap() .counters["cache_hit"], 1 ); assert!(!warm.iter().any(|s| s.name == "semantic.ann_blob_read")); assert_eq!(results.len(), 1); assert_eq!(results[0].source.as_ref().unwrap().kind, "timestamp_ms"); assert!([2345, 28000].contains(&results[0].source.as_ref().unwrap().number)); let audio = store.search_origin( &vector, "media-storage-proof", 5, &SearchOptions::default(), Some("audio"), )?; assert_eq!(audio.len(), 1); assert_eq!(audio[0].match_kind, "semantic_audio"); assert_eq!(audio[0].source.as_ref().unwrap().number, 28000); let video = store.search_origin( &vector, "media-storage-proof", 5, &SearchOptions::default(), Some("video"), )?; assert_eq!(video.len(), 1); assert_eq!(video[0].match_kind, "semantic_video"); assert_eq!(video[0].source.as_ref().unwrap().number, 2345); assert!(store .search_origin( &vector, "media-storage-proof", 5, &SearchOptions::default(), Some("image") )? .is_empty()); assert!( results[0].match_kind == "semantic_video" || results[0].match_kind == "semantic_audio" ); let filter = SearchOptions { extension: Some("png".into()), ..SearchOptions::default() }; assert!(store .search(&vector, "media-storage-proof", 5, &filter)? .is_empty()); let duration_filter = SearchOptions { min_duration_ms: Some(66000), ..Default::default() }; assert!(store .search(&vector, "media-storage-proof", 5, &duration_filter)? .is_empty()); let duration_filter = SearchOptions { min_duration_ms: Some(64000), max_duration_ms: Some(66000), ..Default::default() }; assert_eq!( store .search(&vector, "media-storage-proof", 5, &duration_filter)? .len(), 1 ); std::fs::write(&file, b"changed")?; // Foreground hits check only actual candidate metadata, before watcher // reconciliation; they never rehash the complete filesystem corpus. assert!(store .search(&vector, "media-storage-proof", 5, &SearchOptions::default())? .is_empty()); lexical.index_folder(&root)?; assert!(store .search(&vector, "media-storage-proof", 5, &SearchOptions::default())? .is_empty()); assert!(store .insert_media(generation, id, 2, &identity, "video", 0, &vector) .is_err()); std::fs::remove_file(&file)?; lexical.index_folder(&root)?; assert!(store .search(&vector, "media-storage-proof", 5, &SearchOptions::default())? .is_empty()); Ok(()) } #[test] fn media_failure_removes_partial_vectors_and_persists_reason() -> Result<()> { let directory = tempfile::tempdir()?; let file = directory.path().join("picture.png"); std::fs::write(&file, b"identity fixture")?; let db = directory.path().join("index.db"); let mut lexical = SearchIndex::open(&db)?; lexical.index_folder(directory.path())?; let mut store = Store::open(&db, DIMENSIONS)?; let (id, path, _, _) = store .media_batch(0)? .into_iter() .find(|(_, p, _, _)| p.ends_with("picture.png")) .unwrap(); let identity = searchi_media::MediaIdentity::read(Path::new(&path), 1024)?; let mut vector = vec![0.; DIMENSIONS]; vector[0] = 1.; let generation = store.begin("failure-proof")?; store.insert_media(generation, id, 0, &identity, "image", 0, &vector)?; store.media_failure(generation, &path, "Decoder rejected corrupt image")?; let chunks: usize = store.connection.query_row( "SELECT count(*) FROM semantic_chunks WHERE generation=?1", [generation], |r| r.get(0), )?; assert_eq!(chunks, 0); let reason: String = store.connection.query_row( "SELECT message FROM semantic_media_errors WHERE generation=?1", [generation], |r| r.get(0), )?; assert!(reason.contains("corrupt")); Ok(()) } #[test] fn deleted_media_without_reconciliation_does_not_hide_later_valid_top_k() -> Result<()> { let directory = tempfile::tempdir()?; let root = directory.path().join("images"); std::fs::create_dir(&root)?; for name in ["a.png", "b.png"] { std::fs::write(root.join(name), b"storage fixture")?; } let db = directory.path().join("index.db"); let mut lexical = SearchIndex::open(&db)?; lexical.index_folder(&root)?; let mut store = Store::open(&db, DIMENSIONS)?; let generation = store.begin("candidate-stat-proof")?; let mut vector = vec![0.; DIMENSIONS]; vector[0] = 1.; for (id, path, _, _) in store.media_batch(0)? { let identity = searchi_media::MediaIdentity::read(Path::new(&path), 1024)?; store.insert_media(generation, id, 0, &identity, "image", 0, &vector)?; } store.activate(generation)?; std::fs::remove_file(root.join("a.png"))?; let results = store.search( &vector, "candidate-stat-proof", 1, &SearchOptions::default(), )?; assert_eq!(results.len(), 1); assert!(results[0].path.ends_with("b.png")); std::fs::remove_file(root.join("b.png"))?; assert!(store .search( &vector, "candidate-stat-proof", 1, &SearchOptions::default() )? .is_empty()); Ok(()) } #[test] fn generation_persists_failed_staging_preserves_active_and_changes_hide_stale_vectors( ) -> Result<()> { let directory = tempfile::tempdir()?; let root = directory.path().join("docs"); std::fs::create_dir(&root)?; let db = directory.path().join("index.db"); std::fs::write(root.join("notes.txt"), "original relevant content")?; let mut index = SearchIndex::open(&db)?; index.index_folder(&root)?; let passage = index.passage_batch(0, 1)?.remove(0); let mut vector = vec![0.0; DIMENSIONS]; vector[0] = 1.0; let windows = [TextWindow { text: passage.text.clone(), start_byte: 0, end_byte: passage.text.len(), }]; let mut store = Store::open(&db, DIMENSIONS)?; let generation = store.begin("test-storage-vectors")?; store.insert(generation, &passage, 0, &windows, &[vector.clone()])?; store.activate(generation)?; assert_eq!( store.exact_search( &vector, "test-storage-vectors", 10, &SearchOptions::default() )?[0] .path, store.search( &vector, "test-storage-vectors", 10, &SearchOptions::default() )?[0] .path ); drop(store); let mut store = Store::open(&db, DIMENSIONS)?; assert_eq!( store .search( &vector, "test-storage-vectors", 10, &SearchOptions::default() )? .len(), 1 ); assert!(store .search(&vector, "different-model", 10, &SearchOptions::default())? .is_empty()); let staging = store.begin("new-model")?; assert_eq!( store .search( &vector, "test-storage-vectors", 10, &SearchOptions::default() )? .len(), 1 ); store.abandon(staging)?; std::fs::write( root.join("notes.txt"), "replacement unrelated content longer than before", )?; index.index_folder(&root)?; assert!(store .search( &vector, "test-storage-vectors", 10, &SearchOptions::default() )? .is_empty()); std::fs::remove_file(root.join("notes.txt"))?; index.index_folder(&root)?; assert!(store .search( &vector, "test-storage-vectors", 10, &SearchOptions::default() )? .is_empty()); Ok(()) } #[test] fn ann_filters_before_top_k_checks_checksum_and_status_tracks_corpus_changes() -> Result<()> { let directory = tempfile::tempdir()?; let root = directory.path().join("docs"); std::fs::create_dir(&root)?; let db = directory.path().join("index.db"); std::fs::write(root.join("closest.md"), "closest metadata excluded source")?; std::fs::write(root.join("allowed.txt"), "allowed metadata source")?; let mut lexical = SearchIndex::open(&db)?; lexical.index_folder(&root)?; let passages = lexical.passage_batch(0, 10)?; let mut store = Store::open(&db, DIMENSIONS)?; let fingerprint = crate::generation_fingerprint(searchi_semantic::GEMMA_FINGERPRINT); let generation = store.begin(&fingerprint)?; let mut query = vec![0.0; DIMENSIONS]; query[0] = 1.0; for passage in &passages { let mut vector = query.clone(); if passage.filename == "allowed.txt" { vector[0] = 0.8; vector[1] = 0.6; } let windows = [TextWindow { text: passage.text.clone(), start_byte: 0, end_byte: passage.text.len(), }]; store.insert(generation, passage, 0, &windows, &[vector])?; } store.activate(generation)?; let options = SearchOptions { extension: Some("txt".into()), ..Default::default() }; assert_eq!( store.search(&query, &fingerprint, 1, &options)?[0].filename, "allowed.txt" ); let warmed_hashes = HASH_CALLS.with(std::cell::Cell::get); let _ = store.search(&query, &fingerprint, 1, &options)?; assert_eq!( HASH_CALLS.with(std::cell::Cell::get), warmed_hashes, "Warm query rehashed unchanged corpus" ); assert_eq!( store.exact_search(&query, &fingerprint, 1, &options)?[0].filename, "allowed.txt" ); assert!(!crate::HybridSearch::embedding_status(&db)?.stale); std::fs::write( root.join("allowed.txt"), "replacement changes the generation revision", )?; lexical.index_folder(&root)?; let status = crate::HybridSearch::embedding_status(&db)?; let before_delta = HASH_CALLS.with(std::cell::Cell::get); assert!(store.search(&query, &fingerprint, 1, &options)?.is_empty()); assert!( HASH_CALLS.with(std::cell::Cell::get) - before_delta <= 1, "Single file change rehashed unrelated passages" ); assert!(status.stale); assert_eq!(status.pending, 2); store.connection.execute( "UPDATE semantic_generations SET ann_blob=x'010203' WHERE id=?1", [generation], )?; drop(store); let mut store = Store::open(&db, DIMENSIONS)?; assert!(store .search(&query, &fingerprint, 1, &SearchOptions::default()) .unwrap_err() .to_string() .contains("SHA256 mismatch")); Ok(()) } } #[cfg(test)] #[path = "ann_benchmark.rs"] mod ann_benchmark;