Prepare text search during media setup and retain query sessions through promotion

This commit is contained in:
karl committed 2026-10-13 09:27:40 +02:00
1 parent af6ce29125
commit 9aae3f8216
15 files changed
+1235 -65

No files matched your search

@@ -0,0 +1,260 @@
//! Coverage/publication-control tests use empty ANN graphs, never model vectors.
//! Actual semantic retrieval is separately covered by early_text_google_cpu.
use super::*;
use std::fs;
struct RejectAllInference;
impl EmbeddingProvider for RejectAllInference {
fn fingerprint(&self) -> &str {
GEMMA_FINGERPRINT
}
fn dimensions(&self) -> usize {
GEMMA_DIMENSIONS
}
fn split_windows(&mut self, _: &str, _: &str) -> Result<Vec<TextWindow>> {
anyhow::bail!("Unexpected tokenization")
}
fn embed_documents(&mut self, _: &[&str], _: &str) -> Result<Vec<Vec<f32>>> {
anyhow::bail!("Unexpected document inference")
}
fn embed_query(&mut self, _: &str) -> Result<Vec<f32>> {
anyhow::bail!("No fabricated query vectors")
}
}
fn fixture() -> Result<(tempfile::TempDir, std::path::PathBuf, SearchIndex, Store)> {
let temp = tempfile::tempdir()?;
let root = temp.path().join("corpus");
fs::create_dir(&root)?;
fs::write(
root.join("supplier.txt"),
"Supplier cancellation requires written notice.",
)?;
// Filename/coverage control only, not an image inference fixture.
fs::write(root.join("photo.bmp"), b"not an image")?;
let db = temp.path().join("index.db");
let mut lexical = SearchIndex::open(&db)?;
lexical.index_folder(&root)?;
let store = Store::open(&db, GEMMA_DIMENSIONS)?;
Ok((temp, db, lexical, store))
}
fn empty_generation(store: &mut Store, policy: &str) -> Result<i64> {
let _lock = store.build_lock()?;
let generation = store.begin(policy)?;
store.activate(generation)?;
Ok(generation)
}
#[test]
fn coverage_status_distinguishes_media_pending_and_survives_mutation() -> Result<()> {
let (temp, db, mut lexical, mut store) = fixture()?;
let before = HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?;
assert_eq!(before.pending, 1);
assert_eq!(before.coverage, None);
assert_eq!(HybridSearch::embedding_status(&db)?.pending, 2);
let policy = format!("{}:text-only", generation_fingerprint(GEMMA_FINGERPRINT));
empty_generation(&mut store, &policy)?;
let text = HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?;
assert_eq!(text.coverage, Some(SemanticCoverage::TextOnly));
assert!(!text.stale);
assert_eq!(
text.pending, 0,
"Media must not trigger endless text-only rebuilds"
);
assert!(text.text_indexing_allowed);
let full = HybridSearch::embedding_status(&db)?;
assert!(full.stale);
assert_eq!(full.pending, 2);
fs::write(
temp.path().join("corpus/supplier.txt"),
"A revised supplier cancellation policy.",
)?;
lexical.index_folder(temp.path().join("corpus"))?;
let stale = HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?;
assert!(stale.stale);
assert_eq!(stale.pending, 1);
fs::remove_file(temp.path().join("corpus/supplier.txt"))?;
lexical.index_folder(temp.path().join("corpus"))?;
assert_eq!(
HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?.pending,
0
);
assert_eq!(HybridSearch::embedding_status(&db)?.pending, 1);
Ok(())
}
#[test]
fn guarded_text_setup_preserves_full_publication_even_after_stale_ui_snapshot() -> Result<()> {
let (_temp, db, _lexical, mut publisher) = fixture()?;
let mut early = HybridSearch::with_provider(&db, Box::new(RejectAllInference))?;
assert!(
HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?
.text_indexing_allowed
);
let lock = publisher.build_lock()?;
// A competing full publisher owns the same lock. Early setup must not
// perform source discovery, inference or stage deletion while it is held.
assert!(early
.index_text_embeddings_preserving_media()
.unwrap_err()
.to_string()
.contains("Another semantic rebuild"));
let generation = publisher.begin(&generation_fingerprint(GEMMA_FINGERPRINT))?;
publisher.activate(generation)?;
drop(lock);
assert!(early.index_text_embeddings_preserving_media()?.is_none());
let status = HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?;
assert_eq!(status.generation, Some(generation));
assert_eq!(status.coverage, Some(SemanticCoverage::FullMedia));
assert!(!status.text_indexing_allowed);
assert!(!status.stale);
Ok(())
}
#[test]
fn custom_full_policy_remains_protected_while_default_status_is_stale() -> Result<()> {
let (_temp, db, _lexical, mut store) = fixture()?;
let policy = format!(
"{}:custom-240-2000-120-10000-2000-1800-14400000-17179869184",
generation_fingerprint(GEMMA_FINGERPRINT)
);
let generation = empty_generation(&mut store, &policy)?;
let mut early = HybridSearch::with_provider(&db, Box::new(RejectAllInference))?;
assert!(early.index_text_embeddings_preserving_media()?.is_none());
let text = HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?;
assert_eq!(text.coverage, Some(SemanticCoverage::FullMedia));
assert_eq!(text.generation, Some(generation));
assert!(!text.stale);
assert!(!text.text_indexing_allowed);
assert!(
HybridSearch::embedding_status(&db)?.stale,
"Default full status keeps its exact-policy contract"
);
Ok(())
}
#[test]
fn active_policy_transition_rejects_other_models_windows_and_dimensions() -> Result<()> {
let (_temp, db, _lexical, mut publisher) = fixture()?;
let graphs = PreparedGraphs::default();
publisher.share_prepared_graphs(&graphs)?;
let expected = generation_fingerprint(GEMMA_FINGERPRINT);
let generation = empty_generation(&mut publisher, &format!("{expected}:text-only"))?;
let mut reader = Store::open(&db, GEMMA_DIMENSIONS)?;
reader.share_prepared_graphs(&graphs)?;
// Empty graph controls policy acceptance and prepared-graph handoff without
// inventing vectors or claiming search-quality evidence.
let mut trace = TraceCollector::new(MetricsMode::Debug);
reader.search_origin_traced(
&vec![0.0; GEMMA_DIMENSIONS],
&expected,
5,
&SearchOptions::default(),
None,
&mut trace,
None,
)?;
let spans = trace.finish(0).trace.unwrap();
assert!(spans
.iter()
.any(|s| s.name == "semantic.ann_load" && s.counters.get("cache_hit") == Some(&1)));
let connection = rusqlite::Connection::open(&db)?;
for policy in [
"different-model",
&expected.replace("byte64k-overlap512", "byte64k-overlap1024"),
] {
connection.execute(
"UPDATE semantic_generations SET fingerprint=?1 WHERE id=?2",
rusqlite::params![policy, generation],
)?;
let mut trace = TraceCollector::new(MetricsMode::Debug);
reader.search_origin_traced(
&vec![0.0; GEMMA_DIMENSIONS],
&expected,
5,
&SearchOptions::default(),
None,
&mut trace,
None,
)?;
assert!(!trace
.finish(0)
.trace
.unwrap()
.iter()
.any(|s| s.name == "semantic.ann_load"));
assert_eq!(
HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?.coverage,
None
);
}
connection.execute(
"UPDATE semantic_generations SET fingerprint=?1,dimensions=512 WHERE id=?2",
rusqlite::params![expected, generation],
)?;
let mut trace = TraceCollector::new(MetricsMode::Debug);
reader.search_origin_traced(
&vec![0.0; GEMMA_DIMENSIONS],
&expected,
5,
&SearchOptions::default(),
None,
&mut trace,
None,
)?;
assert!(!trace
.finish(0)
.trace
.unwrap()
.iter()
.any(|s| s.name == "semantic.ann_load"));
let incompatible =
HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?;
assert_eq!(incompatible.coverage, None);
assert!(
incompatible.text_indexing_allowed,
"Wrong dimensions must not claim compatible full coverage"
);
connection.execute(
"UPDATE semantic_generations SET dimensions=768,ann_blob=NULL WHERE id=?1",
[generation],
)?;
let unprepared = HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?;
assert_eq!(unprepared.coverage, None);
assert!(
!unprepared.text_indexing_allowed,
"Missing ANN must not permit dropping otherwise compatible full media sources"
);
Ok(())
}
#[test]
fn text_task_component_policy_preserves_model_and_window_identity() {
let base = generation_fingerprint(GEMMA_FINGERPRINT);
assert_eq!(policy_base(&format!("{base}:text-only")), base);
let custom = format!("{base}:custom-240-2000-120-10000-2000-1800-14400000-17179869184");
assert_eq!(policy_base(&custom), base);
assert!(compatible_query_policy(
&format!("{base}:text-only"),
&custom
));
assert!(compatible_query_policy(&custom, &base));
for suffix in [":text-only-v2", ":custom-unknown", ":custom-1-2-3-4"] {
let unknown = format!("{base}{suffix}");
assert_eq!(policy_base(&unknown), unknown);
assert!(!compatible_query_policy(&base, &unknown));
}
assert!(!compatible_query_policy(
&base,
&base.replace("byte64k-overlap512", "byte64k-overlap1024")
));
assert_eq!(
serde_json::to_string(&SemanticCoverage::TextOnly).unwrap(),
"\"text_only\""
);
assert_eq!(
serde_json::to_string(&SemanticCoverage::FullMedia).unwrap(),
"\"full_media\""
);
}
+100 -11
View File
@@ -53,6 +53,42 @@ fn reusable_policy(expected: &str, active: &str) -> bool {
matches!(values,Ok(v) if v.len()==8 && v[0]>=2 && (5000..=30000).contains(&v[3]) && v[4]<v[3] && v[5]>0 && v[6]<=u32::MAX as u64)
}
/// Resolve compatible publication coverage without broadening model or window
/// policy compatibility. A retained reader can start under either coverage.
fn policy_base(policy: &str) -> &str {
if let Some(base) = policy.strip_suffix(":text-only") {
return base;
}
if let Some((base, _)) = policy.rsplit_once(":custom-") {
if reusable_policy(base, policy) {
return base;
}
}
policy
}
fn compatible_query_policy(expected: &str, active: &str) -> bool {
reusable_policy(policy_base(expected), active)
}
fn policy_coverage(expected: &str, active: &str) -> Option<SemanticCoverage> {
compatible_query_policy(expected, active).then(|| {
if active.ends_with(":text-only") {
SemanticCoverage::TextOnly
} else {
SemanticCoverage::FullMedia
}
})
}
/// Requested indexing coverage is separate from warmed model readiness.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SemanticCoverage {
TextOnly,
FullMedia,
}
/// Model-specific query/document prompts and actual tokenizer windows belong
/// to the provider. Storage and ranking never assume a particular dimension.
pub trait EmbeddingProvider: Send {
@@ -181,6 +217,11 @@ pub struct EmbeddingReport {
#[derive(Debug, Serialize)]
pub struct EmbeddingStatus {
/// Coverage of a compatible, prepared active generation, if any.
pub coverage: Option<SemanticCoverage>,
pub requested_coverage: SemanticCoverage,
/// Early setup must preserve an existing compatible full generation.
pub text_indexing_allowed: bool,
pub failures: Vec<MediaFailure>,
pub generation: Option<i64>,
pub chunks: usize,
@@ -236,18 +277,31 @@ impl HybridSearch {
/// Cheap local status check. No model, runtime worker, or network is loaded.
/// Corpus revision triggers detect file inserts, updates, and deletions.
pub fn embedding_status(db: impl AsRef<Path>) -> Result<EmbeddingStatus> {
Self::embedding_status_for_coverage(db, SemanticCoverage::FullMedia)
}
pub fn embedding_status_for_coverage(
db: impl AsRef<Path>,
requested_coverage: SemanticCoverage,
) -> Result<EmbeddingStatus> {
let connection =
rusqlite::Connection::open_with_flags(db, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)?;
connection.busy_timeout(std::time::Duration::from_secs(5))?;
let exists: bool = connection.query_row(
let snapshot = connection.unchecked_transaction()?;
let exists: bool = snapshot.query_row(
"SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE name='semantic_meta')",
[],
|row| row.get(0),
)?;
let pending: usize =
connection.query_row("SELECT (SELECT count(*) FROM passages)+(SELECT count(*) FROM files WHERE extension IN ('png','jpg','jpeg','webp','bmp','mp4','mkv','mov','avi','webm','m4v','mpeg','mpg','mp3','flac','wav','m4a','ogg','opus','aac','aiff','aif'))", [], |row| row.get(0))?;
let pending: usize = match requested_coverage {
SemanticCoverage::TextOnly => snapshot.query_row("SELECT count(*) FROM passages", [], |row| row.get(0))?,
SemanticCoverage::FullMedia => snapshot.query_row("SELECT (SELECT count(*) FROM passages)+(SELECT count(*) FROM files WHERE extension IN ('png','jpg','jpeg','webp','bmp','mp4','mkv','mov','avi','webm','m4v','mpeg','mpg','mp3','flac','wav','m4a','ogg','opus','aac','aiff','aif'))", [], |row| row.get(0))?,
};
if !exists {
return Ok(EmbeddingStatus {
coverage: None,
requested_coverage,
text_indexing_allowed: true,
failures: Vec::new(),
generation: None,
chunks: 0,
@@ -256,13 +310,16 @@ impl HybridSearch {
pending,
});
}
let version: i64 = connection.query_row(
let version: i64 = snapshot.query_row(
"SELECT schema_version FROM semantic_meta WHERE singleton=1",
[],
|row| row.get(0),
)?;
if version < 6 {
return Ok(EmbeddingStatus {
coverage: None,
requested_coverage,
text_indexing_allowed: true,
failures: Vec::new(),
generation: None,
chunks: 0,
@@ -275,15 +332,15 @@ impl HybridSearch {
(6..=8).contains(&version),
"Unsupported semantic schema version {version}"
);
let (generation,fingerprint,revision,current):(Option<i64>,Option<String>,Option<i64>,i64)=connection.query_row(
"SELECT g.id,g.fingerprint,CASE WHEN g.ann_blob IS NOT NULL AND g.dimensions=768 THEN g.corpus_revision END,m.corpus_revision FROM semantic_meta m LEFT JOIN semantic_generations g ON g.id=m.active_generation WHERE m.singleton=1",[],
|row| Ok((row.get(0)?,row.get(1)?,row.get(2)?,row.get(3)?)))?;
let chunks: usize = connection.query_row(
let (generation,fingerprint,revision,current,dimensions):(Option<i64>,Option<String>,Option<i64>,i64,Option<usize>)=snapshot.query_row(
"SELECT g.id,g.fingerprint,CASE WHEN g.ann_blob IS NOT NULL AND g.dimensions=768 THEN g.corpus_revision END,m.corpus_revision,g.dimensions FROM semantic_meta m LEFT JOIN semantic_generations g ON g.id=m.active_generation WHERE m.singleton=1",[],
|row| Ok((row.get(0)?,row.get(1)?,row.get(2)?,row.get(3)?,row.get(4)?)))?;
let chunks: usize = snapshot.query_row(
"SELECT count(*) FROM semantic_chunks WHERE generation=?1",
[generation],
|row| row.get(0),
)?;
let mut failures_query=connection.prepare("SELECT path,message FROM semantic_media_errors WHERE generation=?1 ORDER BY path LIMIT 1000")?;
let mut failures_query=snapshot.prepare("SELECT path,message FROM semantic_media_errors WHERE generation=?1 ORDER BY path LIMIT 1000")?;
let failures = failures_query
.query_map([generation], |r| {
Ok(MediaFailure {
@@ -292,9 +349,25 @@ impl HybridSearch {
})
})?
.collect::<rusqlite::Result<Vec<_>>>()?;
let stale = revision != Some(current)
|| fingerprint.as_deref() != Some(generation_fingerprint(GEMMA_FINGERPRINT).as_str());
let expected = generation_fingerprint(GEMMA_FINGERPRINT);
let coverage = if dimensions == Some(GEMMA_DIMENSIONS) {
fingerprint
.as_deref()
.and_then(|active| policy_coverage(&expected, active))
} else {
None
};
let prepared_coverage = if revision.is_some() { coverage } else { None };
let policy_matches = match requested_coverage {
SemanticCoverage::TextOnly => prepared_coverage.is_some(),
// Keep the default full-media status's exact configured policy rule.
SemanticCoverage::FullMedia => fingerprint.as_deref() == Some(expected.as_str()),
};
let stale = revision != Some(current) || !policy_matches;
Ok(EmbeddingStatus {
coverage: prepared_coverage,
requested_coverage,
text_indexing_allowed: coverage != Some(SemanticCoverage::FullMedia),
failures,
generation,
chunks,
@@ -374,6 +447,19 @@ impl HybridSearch {
})
}
/// Early text setup never replaces a compatible full generation. The
/// decision is made under the publication build lock, not from UI status.
/// `None` means full-media indexing must resume after its assets are ready.
pub fn index_text_embeddings_preserving_media(&mut self) -> Result<Option<EmbeddingReport>> {
let fingerprint = format!(
"{}:text-only",
generation_fingerprint(self.embedder.fingerprint())
);
self.rebuild_generation_preserving_media(fingerprint, |service, generation, work| {
service.build(generation, work)
})
}
/// Filter each retrieval lane before ranking, preserve filename priority,
/// then fuse keyword and semantic ranks with reciprocal-rank fusion (k=60).
pub fn search(
@@ -827,3 +913,6 @@ mod tests {
Ok(())
}
}
#[cfg(test)]
mod coverage_tests;
+61 -2
View File
@@ -44,7 +44,7 @@ pub(super) fn text_task(passage: &IndexedPassage, policy: &str) -> Result<Source
source_type: "text".into(),
source_hash: content_hash(&passage.text),
title_hash: content_hash(bounded_title(&passage.filename)),
policy: policy.into(),
policy: policy_base(policy).into(),
passage_id: passage.id,
})
}
@@ -56,7 +56,29 @@ impl HybridSearch {
media_limits: Option<&searchi_media::MediaLimits>,
build: impl FnOnce(&mut Self, i64, &mut BuildWork) -> Result<()>,
) -> Result<EmbeddingReport> {
self.rebuild_generation_guarded(fingerprint, media_limits, false, build)?
.ok_or_else(|| anyhow::anyhow!("Explicit semantic rebuild was unexpectedly deferred"))
}
pub(super) fn rebuild_generation_preserving_media(
&mut self,
fingerprint: String,
build: impl FnOnce(&mut Self, i64, &mut BuildWork) -> Result<()>,
) -> Result<Option<EmbeddingReport>> {
self.rebuild_generation_guarded(fingerprint, None, true, build)
}
fn rebuild_generation_guarded(
&mut self,
fingerprint: String,
media_limits: Option<&searchi_media::MediaLimits>,
preserve_full_media: bool,
build: impl FnOnce(&mut Self, i64, &mut BuildWork) -> Result<()>,
) -> Result<Option<EmbeddingReport>> {
let lock = self.store.build_lock()?;
if preserve_full_media && self.store.has_compatible_full_generation(&fingerprint)? {
return Ok(None);
}
let (generation, resumed) = self.store.start_or_resume(&fingerprint, &lock)?;
// This revision covers the entire discovery/build/finalization pass.
// A concurrent lexical update aborts publication; next invocation
@@ -109,7 +131,7 @@ impl HybridSearch {
}
Ok(())
})?;
Ok(work.report)
Ok(Some(work.report))
});
if result.is_err() {
self.generation_fingerprint = previous_fingerprint;
@@ -356,4 +378,41 @@ mod tests {
assert_eq!(staging, 1, "Resume must retain the same staging generation");
Ok(())
}
#[test]
fn discovery_and_encoding_use_the_same_normalized_text_component_identity() -> Result<()> {
let temp = tempfile::tempdir()?;
let root = temp.path().join("docs");
std::fs::create_dir(&root)?;
std::fs::write(
root.join("notes.txt"),
"Exact source bytes before a deliberately failed encoder.",
)?;
let db = temp.path().join("index.db");
SearchIndex::open(&db)?.index_folder(&root)?;
let mut service = HybridSearch::with_provider(&db, Box::new(RejectInference))?;
let base = generation_fingerprint(service.embedder.fingerprint());
service.generation_fingerprint = format!("{base}:text-only");
let discovered = service.discover_tasks(None)?.remove(0);
assert_eq!(discovered.policy, base);
let error = service
.index_text_embeddings_preserving_media()
.unwrap_err();
assert!(format!("{error:#}").contains("injected primary inference failure"));
let connection = rusqlite::Connection::open(&db)?;
let encoded: (String, String, String, String) = connection.query_row(
"SELECT task_key,source_hash,title_hash,policy FROM semantic_tasks",
[],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
)?;
assert_eq!(
encoded,
(
discovered.key,
discovered.source_hash,
discovered.title_hash,
discovered.policy
)
);
Ok(())
}
}
+19 -4
View File
@@ -82,6 +82,15 @@ impl Store {
Ok(())
}
pub(crate) fn has_compatible_full_generation(&self, expected: &str) -> Result<bool> {
let active: Option<String> = 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 AND g.dimensions=?1",
[self.dimensions as i64], |row| row.get(0)).optional()?;
Ok(active.as_deref().is_some_and(|active| {
super::policy_coverage(expected, active) == Some(super::SemanticCoverage::FullMedia)
}))
}
pub(crate) fn active_fingerprint(&self) -> Result<Option<String>> {
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()?)
}
@@ -525,11 +534,16 @@ impl Store {
// 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<i64>=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 {
let active:Option<(i64,String)>=snapshot.query_row("SELECT g.id,g.fingerprint FROM semantic_generations g JOIN semantic_meta m ON m.active_generation=g.id WHERE g.dimensions=?1",
[self.dimensions as i64],|row|Ok((row.get(0)?,row.get(1)?))).optional()?;
let Some((active, active_fingerprint)) = active else {
trace.end(generation_span, Default::default());
return Ok(Vec::new());
};
if !super::compatible_query_policy(fingerprint, &active_fingerprint) {
trace.end(generation_span, Default::default());
return Ok(Vec::new());
}
trace.end(generation_span, Default::default());
if !self
.ann_cache
@@ -542,7 +556,8 @@ impl Store {
[active],
|row| row.get(0),
)?;
self.prepared_graphs.get(active, fingerprint, &checksum)
self.prepared_graphs
.get(active, &active_fingerprint, &checksum)
})?;
if let Some(graph) = prepared {
self.ann_cache = Some((active, graph));
+135 -2
View File
@@ -398,11 +398,48 @@ impl Store {
generation: i64,
task: &SourceTask,
) -> Result<Option<(i64, String, usize)>> {
// Cross-coverage reuse is admitted only for the active generation,
// with at most two metadata identities (active/current staging). Exact
// generation-policy reuse retains the existing all-generation lookup.
// Validate before LIMIT, not by scanning/rejecting matching task rows.
let mut metadata=self.connection.prepare("SELECT g.id,g.fingerprint,g.dimensions,d.fingerprint,d.dimensions FROM semantic_generations d JOIN semantic_generations g ON (g.id=d.id OR g.id=(SELECT active_generation FROM semantic_meta WHERE singleton=1)) WHERE d.id=?1")?;
let generations = metadata.query_map([generation], |row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, String>(1)?,
row.get::<_, usize>(2)?,
row.get::<_, String>(3)?,
row.get::<_, usize>(4)?,
))
})?;
let mut admitted = Vec::with_capacity(2);
for candidate in generations {
let (id, source_policy, source_dimensions, destination_policy, destination_dimensions) =
candidate?;
let compatible = source_dimensions == destination_dimensions
&& (source_policy == destination_policy
|| (task.source_type == "text"
&& super::super::policy_base(&destination_policy) == task.policy
&& super::super::reusable_policy(&task.policy, &destination_policy)
&& super::super::reusable_policy(&task.policy, &source_policy)));
if compatible {
admitted.push(id);
}
}
ensure!(
admitted.len() <= 2,
"Donor generation admission exceeds resource budget"
);
if admitted.is_empty() {
return Ok(None);
}
let first = admitted[0];
let second = admitted.get(1).copied().unwrap_or(first);
Ok(self.connection.query_row("SELECT t.generation,t.task_key,t.expected_chunks FROM semantic_tasks t JOIN semantic_generations g ON g.id=t.generation JOIN semantic_generations destination ON destination.id=?1
WHERE t.source_hash=?2 AND t.title_hash=?3 AND t.policy=?4 AND t.source_type=?5 AND t.state='done' AND t.expected_chunks IS NOT NULL
AND g.fingerprint=destination.fingerprint AND g.dimensions=destination.dimensions
AND (g.fingerprint=destination.fingerprint OR (?5='text' AND t.generation IN (?6,?7))) AND g.dimensions=destination.dimensions
AND t.expected_chunks=(SELECT count(*) FROM semantic_chunks c WHERE c.generation=t.generation AND c.task_key=t.task_key)
ORDER BY (t.generation=?1) DESC,t.generation DESC LIMIT 1",params![generation,task.source_hash,task.title_hash,task.policy,task.source_type],|r|Ok((r.get(0)?,r.get(1)?,r.get(2)?))).optional()?)
ORDER BY (t.generation=?1) DESC,t.generation DESC LIMIT 1",params![generation,task.source_hash,task.title_hash,task.policy,task.source_type,first,second],|r|Ok((r.get(0)?,r.get(1)?,r.get(2)?))).optional()?)
}
/// No tokenizer or neural call: only a completed, policy-compatible group
@@ -910,6 +947,102 @@ mod tests {
);
Ok(())
}
#[test]
fn text_donor_coverage_admission_preserves_strict_identity_and_exact_legacy_lookup(
) -> Result<()> {
// Storage geometry only; actual Google promotion is tested separately.
let dir = tempfile::tempdir()?;
let root = dir.path().join("docs");
std::fs::create_dir(&root)?;
std::fs::write(
root.join("notes.txt"),
"A real source with stable exact bytes.",
)?;
let db = dir.path().join("index.db");
let mut lexical = SearchIndex::open(&db)?;
lexical.index_folder(&root)?;
let passage = lexical.passage_batch(0, 1)?.remove(0);
let mut source_task = task(&passage);
let base = "storage-model:window-v1";
source_task.policy = base.into();
let mut store = Store::open(&db, 2)?;
let lock = store.build_lock()?;
let source = store.begin(&format!("{base}:text-only"))?;
insert_complete(&mut store, source, &passage, &source_task)?;
store.activate(source)?;
let destination = store.start_or_resume(base, &lock)?.0;
store.stage_task(destination, &source_task)?;
assert_eq!(
store.reusable_task(destination, &source_task)?.unwrap().0,
source
);
for wrong in [
"different-model:window-v1:text-only",
"storage-model:window-v2:text-only",
"storage-model:window-v1:custom-unknown",
] {
store.connection.execute(
"UPDATE semantic_generations SET fingerprint=?1 WHERE id=?2",
params![wrong, source],
)?;
assert!(
store.reusable_task(destination, &source_task)?.is_none(),
"Forged identical task policy must not admit generation {wrong}"
);
}
let custom = format!("{base}:custom-240-2000-120-10000-2000-1800-14400000-17179869184");
store.connection.execute(
"UPDATE semantic_generations SET fingerprint=?1 WHERE id=?2",
params![custom, source],
)?;
assert_eq!(
store.reusable_task(destination, &source_task)?.unwrap().0,
source
);
store.connection.execute(
"UPDATE semantic_generations SET dimensions=3 WHERE id=?1",
[source],
)?;
assert!(store.reusable_task(destination, &source_task)?.is_none());
store.connection.execute(
"UPDATE semantic_generations SET dimensions=2,fingerprint=?1 WHERE id=?2",
params![format!("{base}:text-only"), source],
)?;
let mut media_task = source_task.clone();
media_task.key = "media-control".into();
media_task.source_type = "media".into();
media_task.passage_id = -1;
store.stage_task(source, &media_task)?;
store.complete_source(source, &media_task, 0)?;
assert!(
store.reusable_task(destination, &media_task)?.is_none(),
"Media donors still require exact generation policy"
);
// Exact-policy donors outside the active/current pair remain eligible.
store.connection.execute("INSERT INTO semantic_generations(fingerprint,dimensions,corpus_revision) SELECT ?1,2,corpus_revision FROM semantic_meta WHERE singleton=1",[base])?;
let older = store.connection.last_insert_rowid();
insert_complete(&mut store, older, &passage, &source_task)?;
assert_eq!(
store.reusable_task(destination, &source_task)?.unwrap().0,
older
);
assert_eq!(
store.reuse_text_source(destination, &source_task, &passage)?,
Some(1)
);
let before: Vec<u8> = store.connection.query_row(
"SELECT vector FROM semantic_chunks WHERE generation=?1",
[older],
|row| row.get(0),
)?;
let after: Vec<u8> = store.connection.query_row(
"SELECT vector FROM semantic_chunks WHERE generation=?1",
[destination],
|row| row.get(0),
)?;
assert_eq!(before, after);
Ok(())
}
}
#[cfg(test)]
@@ -0,0 +1,197 @@
//! Genuine Google CPU proof: retained query session sees early text and full promotion.
use anyhow::Result;
use searchi_core::SearchIndex;
use searchi_semantic::{Device, GemmaEmbedder, TextWindow, WorkerInfo};
use searchi_service::{
EmbeddingProvider, FfmpegTools, HybridSearch, MediaLimits, PreparedGraphs, SearchOptions,
SemanticCoverage,
};
use std::{
collections::BTreeMap,
fs,
path::{Path, PathBuf},
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
struct CountedQuery {
google: GemmaEmbedder,
calls: Arc<AtomicUsize>,
}
impl EmbeddingProvider for CountedQuery {
fn fingerprint(&self) -> &str {
EmbeddingProvider::fingerprint(&self.google)
}
fn dimensions(&self) -> usize {
EmbeddingProvider::dimensions(&self.google)
}
fn split_windows(&mut self, text: &str, title: &str) -> Result<Vec<TextWindow>> {
self.google.split_windows(text, title)
}
fn embed_documents(&mut self, texts: &[&str], title: &str) -> Result<Vec<Vec<f32>>> {
self.google.embed_documents(texts, title)
}
fn embed_query(&mut self, query: &str) -> Result<Vec<f32>> {
self.calls.fetch_add(1, Ordering::Relaxed);
self.google.embed_query(query)
}
fn embed_query_debug(&mut self, query: &str) -> Result<Vec<f32>> {
self.calls.fetch_add(1, Ordering::Relaxed);
self.google.embed_query_debug(query)
}
fn query_diagnostics(&self) -> BTreeMap<String, u64> {
self.google.query_diagnostics()
}
fn worker_info(&self) -> Option<WorkerInfo> {
Some(self.google.info.clone())
}
}
fn text_vectors(db: &Path) -> Result<Vec<(String, Vec<u8>)>> {
let connection = rusqlite::Connection::open(db)?;
let mut statement=connection.prepare("SELECT path,vector FROM semantic_chunks WHERE generation=(SELECT active_generation FROM semantic_meta WHERE singleton=1) AND source_type='text' ORDER BY path,ordinal")?;
let rows = statement
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
.collect::<rusqlite::Result<_>>()?;
Ok(rows)
}
fn assert_retained_trace(response: &searchi_service::SearchResponse<searchi_core::SearchResult>) {
let trace = response.metrics.trace.as_ref().expect("Debug trace");
assert!(trace
.iter()
.any(|span| span.counters.get("query_cache_hit") == Some(&1)));
assert!(
trace
.iter()
.any(|span| span.name == "semantic.ann_load"
&& span.counters.get("cache_hit") == Some(&1))
);
assert!(!trace.iter().any(|span| matches!(
span.name.as_str(),
"semantic.ann_blob_read" | "semantic.ann_checksum" | "semantic.ann_deserialize"
)));
}
#[test]
#[ignore = "Requires verified Google pair, normal worker and CPU FFmpeg helpers"]
fn retained_query_reader_follows_early_text_and_full_media_without_reencoding_documents(
) -> Result<()> {
assert_eq!(std::env::var("SEARCHI_FORCE_CPU")?, "1");
let models = std::env::var_os("SEARCHI_GEMMA_MODEL_DIR").expect("Verified models");
let worker = std::env::var_os("SEARCHI_GEMMA_WORKER").expect("Normal worker");
let tools = FfmpegTools {
ffmpeg: PathBuf::from(std::env::var_os("SEARCHI_TEST_FFMPEG").expect("CPU ffmpeg")),
ffprobe: PathBuf::from(std::env::var_os("SEARCHI_TEST_FFPROBE").expect("CPU ffprobe")),
};
let temp = tempfile::tempdir()?;
let root = temp.path().join("corpus");
fs::create_dir(&root)?;
let supplier = root.join("supplier.txt");
fs::write(
&supplier,
"The supplier agreement may be terminated with sixty days written notice.",
)?;
fs::write(
root.join("garden.txt"),
"A vegetable bed and rainwater barrel are planned for the garden.",
)?;
fs::copy(
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../searchi-semantic/tests/fixtures/digup-photo.jpg"),
root.join("photo.jpg"),
)?;
let db = temp.path().join("index.db");
let mut lexical = SearchIndex::open(&db)?;
lexical.index_folder(&root)?;
let graphs = PreparedGraphs::default();
let calls = Arc::new(AtomicUsize::new(0));
let google = GemmaEmbedder::load_query_with_device(&models, &worker, 1, Device::Cpu)?;
let mut reader = HybridSearch::with_provider(
&db,
Box::new(CountedQuery {
google,
calls: calls.clone(),
}),
)?;
reader.share_prepared_graphs(&graphs)?;
reader.warmup()?; // No active generation yet.
let query = "Wie beenden wir die Zusammenarbeit mit dem Lieferanten?";
assert!(reader
.search(query, 5, &SearchOptions::default())?
.is_empty());
assert_eq!(calls.load(Ordering::Relaxed), 2);
let mut publisher = HybridSearch::open(&db, &models, &worker, 1)?;
publisher.share_prepared_graphs(&graphs)?;
let text = publisher
.index_text_embeddings_preserving_media()?
.expect("Initial text build");
assert_eq!(text.document_inference_calls, 2);
assert_eq!(text.media_files, 0);
let text_status = HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?;
assert!(!text_status.stale);
assert_eq!(text_status.pending, 0);
assert_eq!(text_status.coverage, Some(SemanticCoverage::TextOnly));
assert!(HybridSearch::embedding_status(&db)?.stale);
let early = reader.search_measured(query, 5, &SearchOptions::default(), true)?;
assert_eq!(early.results[0].filename, "supplier.txt");
assert!(early.results[0].snippet.contains("sixty days"));
assert_eq!(early.results[0].source.as_ref().unwrap().kind, "line");
assert_retained_trace(&early);
assert_eq!(calls.load(Ordering::Relaxed), 2);
let before = text_vectors(&db)?;
let full = publisher.index_embeddings_with_media(&tools, &MediaLimits::default())?;
assert_eq!(
full.document_inference_calls, 0,
"Promotion reuses text's identical encoder/window policy"
);
assert_eq!(full.image_inference_calls, 1);
assert_eq!(
text_vectors(&db)?,
before,
"Document vector bytes/citations survive promotion"
);
let full_status = HybridSearch::embedding_status(&db)?;
assert_eq!(full_status.coverage, Some(SemanticCoverage::FullMedia));
assert!(!full_status.stale);
assert!(!full_status.text_indexing_allowed);
let promoted = reader.search_measured(query, 5, &SearchOptions::default(), true)?;
assert_eq!(promoted.results[0].filename, "supplier.txt");
assert_retained_trace(&promoted);
assert_eq!(
calls.load(Ordering::Relaxed),
2,
"No repeated warmup, restart or cached query inference"
);
assert_eq!(reader.worker_info().unwrap().role, "query");
assert!(publisher
.index_text_embeddings_preserving_media()?
.is_none());
assert_eq!(
HybridSearch::embedding_status(&db)?.generation,
Some(full.generation)
);
fs::write(
&supplier,
"An updated supplier agreement has ninety days written notice.",
)?;
lexical.index_folder(&root)?;
let stale = reader.search_measured(query, 5, &SearchOptions::default(), true)?;
assert!(
stale
.results
.iter()
.all(|result| result.filename != "supplier.txt"),
"Old extraction/semantic chunks stay suppressed"
);
assert_eq!(calls.load(Ordering::Relaxed), 2);
assert!(
publisher
.index_text_embeddings_preserving_media()?
.is_none(),
"Projector repair must not downgrade stale full generation"
);
Ok(())
}