Reuse completed Google embeddings and resume semantic staging
This commit is contained in:
1 parent
0b56ab553c
commit
5c18e7b49c
11 files changed
+1677
-148
No files matched your search
@@ -164,22 +164,31 @@ fn real_google_failed_policy_rebuild_preserves_live_reader_and_retry() -> Result
|
||||
connection.query_row("SELECT count(*) FROM semantic_generations", [], |row| {
|
||||
row.get(0)
|
||||
})?;
|
||||
assert_eq!(generations, 1, "Failed activation must discard staging");
|
||||
|
||||
connection.execute_batch("CREATE TRIGGER reject_generation_cleanup BEFORE DELETE ON semantic_generations WHEN old.id != (SELECT active_generation FROM semantic_meta WHERE singleton=1) BEGIN SELECT RAISE(ABORT,'rejected generation cleanup'); END;")?;
|
||||
assert_eq!(
|
||||
generations, 2,
|
||||
"Failed activation must retain resumable staging"
|
||||
);
|
||||
// A repeated publication failure must preserve the same completed staging
|
||||
// generation and the live reader; it must not create or abandon another one.
|
||||
let failure = service
|
||||
.index_embeddings_with_media(&tools, &limits)
|
||||
.unwrap_err();
|
||||
let message = format!("{failure:#}");
|
||||
assert!(message.contains("rejected generation activation"));
|
||||
assert!(message.contains("rejected generation cleanup"));
|
||||
assert!(format!("{failure:#}").contains("rejected generation activation"));
|
||||
assert_eq!(active(&connection)?, original);
|
||||
assert_reader(&mut service)?;
|
||||
connection.execute_batch(
|
||||
"DROP TRIGGER reject_generation_activation; DROP TRIGGER reject_generation_cleanup;",
|
||||
)?;
|
||||
assert_eq!(
|
||||
connection.query_row("SELECT count(*) FROM semantic_generations", [], |row| row
|
||||
.get::<_, i64>(
|
||||
0
|
||||
))?,
|
||||
2
|
||||
);
|
||||
connection.execute_batch("DROP TRIGGER reject_generation_activation;")?;
|
||||
let retry = service.index_embeddings_with_media(&tools, &limits)?;
|
||||
assert_eq!(retry.passages, 1);
|
||||
assert!(retry.resumed);
|
||||
assert_eq!(retry.reused_sources, 1);
|
||||
assert_eq!(retry.document_inference_calls, 0);
|
||||
assert_reader(&mut service)?;
|
||||
let media_active = active(&connection)?;
|
||||
assert_ne!(media_active.1, original.1);
|
||||
@@ -195,7 +204,9 @@ fn real_google_failed_policy_rebuild_preserves_live_reader_and_retry() -> Result
|
||||
assert_eq!(active(&connection)?, media_active);
|
||||
assert_reader(&mut service)?;
|
||||
connection.execute_batch("DROP TRIGGER reject_generation_activation;")?;
|
||||
service.index_embeddings()?;
|
||||
let reverse_retry = service.index_embeddings()?;
|
||||
assert!(reverse_retry.resumed);
|
||||
assert_eq!(reverse_retry.document_inference_calls, 0);
|
||||
assert_eq!(active(&connection)?.1, original.1);
|
||||
assert_reader(&mut service)?;
|
||||
Ok(())
|
||||
|
||||
@@ -0,0 +1,417 @@
|
||||
//! Incremental acceptance uses genuine Google vectors, never stand-in embeddings.
|
||||
use anyhow::Result;
|
||||
use rusqlite::Connection;
|
||||
use searchi_core::SearchIndex;
|
||||
use searchi_semantic::{GemmaEmbedder, TextWindow};
|
||||
use searchi_service::{EmbeddingProvider, HybridSearch, SearchOptions};
|
||||
use std::{
|
||||
fs,
|
||||
path::Path,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
struct Calls {
|
||||
documents: usize,
|
||||
images: usize,
|
||||
audio: usize,
|
||||
}
|
||||
struct CountedGoogle {
|
||||
inner: GemmaEmbedder,
|
||||
calls: Arc<Mutex<Calls>>,
|
||||
}
|
||||
impl EmbeddingProvider for CountedGoogle {
|
||||
fn fingerprint(&self) -> &str {
|
||||
EmbeddingProvider::fingerprint(&self.inner)
|
||||
}
|
||||
fn dimensions(&self) -> usize {
|
||||
EmbeddingProvider::dimensions(&self.inner)
|
||||
}
|
||||
fn split_windows(&mut self, text: &str, title: &str) -> Result<Vec<TextWindow>> {
|
||||
self.inner.split_windows(text, title)
|
||||
}
|
||||
fn embed_documents(&mut self, texts: &[&str], title: &str) -> Result<Vec<Vec<f32>>> {
|
||||
self.calls.lock().unwrap().documents += texts.len();
|
||||
self.inner.embed_documents(texts, title)
|
||||
}
|
||||
fn embed_query(&mut self, text: &str) -> Result<Vec<f32>> {
|
||||
self.inner.embed_query(text)
|
||||
}
|
||||
fn embed_code_query(&mut self, text: &str) -> Result<Vec<f32>> {
|
||||
self.inner.embed_code_query(text)
|
||||
}
|
||||
fn embed_images(&mut self, paths: &[&Path], budget: u32) -> Result<Vec<Vec<f32>>> {
|
||||
self.calls.lock().unwrap().images += paths.len();
|
||||
self.inner.embed_images_with_budget(paths, budget)
|
||||
}
|
||||
fn embed_audio(&mut self, pcm: &[f32]) -> Result<Vec<f32>> {
|
||||
self.calls.lock().unwrap().audio += 1;
|
||||
self.inner.embed_audio_pcm(pcm)
|
||||
}
|
||||
fn worker_info(&self) -> Option<searchi_semantic::WorkerInfo> {
|
||||
Some(self.inner.info.clone())
|
||||
}
|
||||
}
|
||||
fn google(db: &Path) -> Result<(HybridSearch, Arc<Mutex<Calls>>)> {
|
||||
assert_eq!(std::env::var("SEARCHI_FORCE_CPU")?, "1");
|
||||
let model = std::env::var_os("SEARCHI_GEMMA_MODEL_DIR")
|
||||
.expect("Verified Google model directory required");
|
||||
let worker =
|
||||
std::env::var_os("SEARCHI_GEMMA_WORKER").expect("Normal auto runtime worker required");
|
||||
let inner = GemmaEmbedder::load(model, worker, 1)?;
|
||||
assert_eq!(inner.info.device, "cpu");
|
||||
let calls = Arc::new(Mutex::new(Calls::default()));
|
||||
let service = HybridSearch::with_provider(
|
||||
db,
|
||||
Box::new(CountedGoogle {
|
||||
inner,
|
||||
calls: calls.clone(),
|
||||
}),
|
||||
)?;
|
||||
Ok((service, calls))
|
||||
}
|
||||
type Chunk = (String, String, i64, i64, i64, String, Vec<u8>);
|
||||
fn chunks(db: &Path) -> Result<Vec<Chunk>> {
|
||||
let connection = Connection::open(db)?;
|
||||
let mut query = connection.prepare("SELECT path,kind,number,start_byte,end_byte,snippet,vector FROM semantic_chunks WHERE generation=(SELECT active_generation FROM semantic_meta WHERE singleton=1) ORDER BY path,ordinal")?;
|
||||
let rows = query.query_map([], |r| {
|
||||
Ok((
|
||||
r.get(0)?,
|
||||
r.get(1)?,
|
||||
r.get(2)?,
|
||||
r.get(3)?,
|
||||
r.get(4)?,
|
||||
r.get(5)?,
|
||||
r.get(6)?,
|
||||
))
|
||||
})?;
|
||||
Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Requires verified Google assets and the normal native worker; CPU only"]
|
||||
fn genuine_google_reuses_text_code_and_remaps_moves_but_reencodes_titles() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let root = temp.path().join("sources");
|
||||
fs::create_dir(&root)?;
|
||||
let document = root.join("agreement.txt");
|
||||
fs::write(
|
||||
&document,
|
||||
"The supplier partnership may be ended with sixty days written notice.",
|
||||
)?;
|
||||
fs::write(
|
||||
root.join("retry.rs"),
|
||||
"pub fn exponential_backoff(attempt: u32) -> u64 { 100 * 2_u64.pow(attempt.min(8)) }\n",
|
||||
)?;
|
||||
let db = temp.path().join("index.db");
|
||||
let mut lexical = SearchIndex::open(&db)?;
|
||||
lexical.index_folder(&root)?;
|
||||
let (mut service, calls) = google(&db)?;
|
||||
service.index_embeddings()?;
|
||||
let first = chunks(&db)?;
|
||||
assert_eq!(first.len(), 2);
|
||||
let initial = calls.lock().unwrap().clone();
|
||||
assert_eq!(initial.documents, 2);
|
||||
service.index_embeddings()?;
|
||||
assert_eq!(
|
||||
*calls.lock().unwrap(),
|
||||
initial,
|
||||
"Unchanged groups must use zero inference"
|
||||
);
|
||||
assert_eq!(
|
||||
chunks(&db)?,
|
||||
first,
|
||||
"Real vector bytes and locations must survive reuse"
|
||||
);
|
||||
|
||||
let moved = root.join("nested");
|
||||
fs::create_dir(&moved)?;
|
||||
fs::rename(&document, moved.join("agreement.txt"))?;
|
||||
lexical.index_folder(&root)?;
|
||||
service.index_embeddings()?;
|
||||
assert_eq!(
|
||||
*calls.lock().unwrap(),
|
||||
initial,
|
||||
"Folder-only moves retain the Google title"
|
||||
);
|
||||
let after_move = chunks(&db)?;
|
||||
let original = first
|
||||
.iter()
|
||||
.find(|c| c.0.ends_with("agreement.txt"))
|
||||
.unwrap();
|
||||
let relocated = after_move
|
||||
.iter()
|
||||
.find(|c| c.0.ends_with("agreement.txt"))
|
||||
.unwrap();
|
||||
assert_eq!(relocated.6, original.6);
|
||||
assert!(Path::new(&relocated.0).starts_with(moved.canonicalize()?));
|
||||
let results = service.search_semantic(
|
||||
"How can we stop working with the vendor?",
|
||||
10,
|
||||
&SearchOptions::default(),
|
||||
)?;
|
||||
assert_eq!(results[0].filename, "agreement.txt");
|
||||
assert!(results[0].source.is_some());
|
||||
|
||||
fs::rename(
|
||||
moved.join("agreement.txt"),
|
||||
moved.join("supplier-contract.txt"),
|
||||
)?;
|
||||
lexical.index_folder(&root)?;
|
||||
service.index_embeddings()?;
|
||||
assert_eq!(
|
||||
calls.lock().unwrap().documents,
|
||||
initial.documents + 1,
|
||||
"Basename changes alter Google's document title prompt"
|
||||
);
|
||||
fs::write(
|
||||
moved.join("supplier-contract.txt"),
|
||||
"The supplier partnership may be ended with ninety days written notice.",
|
||||
)?;
|
||||
lexical.index_folder(&root)?;
|
||||
service.index_embeddings()?;
|
||||
assert_eq!(calls.lock().unwrap().documents, initial.documents + 2);
|
||||
let code_before = first.iter().find(|c| c.0.ends_with("retry.rs")).unwrap();
|
||||
let latest = chunks(&db)?;
|
||||
assert_eq!(
|
||||
latest.iter().find(|c| c.0.ends_with("retry.rs")).unwrap(),
|
||||
code_before
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ffmpeg(tools: &searchi_service::FfmpegTools, output: &Path, arguments: &[&str]) -> Result<()> {
|
||||
let result = std::process::Command::new(&tools.ffmpeg)
|
||||
.args([
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-nostdin",
|
||||
"-y",
|
||||
"-threads",
|
||||
"1",
|
||||
])
|
||||
.args(arguments)
|
||||
.arg(output)
|
||||
.output()?;
|
||||
anyhow::ensure!(
|
||||
result.status.success(),
|
||||
"CPU fixture encoding failed: {}",
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Requires verified Google assets, normal worker and CPU FFmpeg helpers"]
|
||||
fn genuine_google_mixed_media_reuse_checks_bytes_despite_identical_file_metadata() -> Result<()> {
|
||||
let tools = searchi_service::FfmpegTools {
|
||||
ffmpeg: std::env::var_os("SEARCHI_TEST_FFMPEG")
|
||||
.expect("Set SEARCHI_TEST_FFMPEG")
|
||||
.into(),
|
||||
ffprobe: std::env::var_os("SEARCHI_TEST_FFPROBE")
|
||||
.expect("Set SEARCHI_TEST_FFPROBE")
|
||||
.into(),
|
||||
};
|
||||
let temp = tempfile::tempdir()?;
|
||||
let root = temp.path().join("mixed");
|
||||
fs::create_dir(&root)?;
|
||||
fs::write(
|
||||
root.join("agreement.txt"),
|
||||
"The supplier may terminate the partnership with sixty days written notice.",
|
||||
)?;
|
||||
fs::write(
|
||||
root.join("retry.rs"),
|
||||
"pub fn retry_delay(attempt: u32) -> u64 { 100 * 2_u64.pow(attempt.min(8)) }\n",
|
||||
)?;
|
||||
let image = root.join("color.bmp");
|
||||
ffmpeg(
|
||||
&tools,
|
||||
&image,
|
||||
&[
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"color=c=red:s=64x64",
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-c:v",
|
||||
"bmp",
|
||||
],
|
||||
)?;
|
||||
ffmpeg(
|
||||
&tools,
|
||||
&root.join("tone.wav"),
|
||||
&[
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"sine=frequency=440:duration=1",
|
||||
"-c:a",
|
||||
"pcm_s16le",
|
||||
"-ar",
|
||||
"16000",
|
||||
"-ac",
|
||||
"1",
|
||||
],
|
||||
)?;
|
||||
ffmpeg(
|
||||
&tools,
|
||||
&root.join("scene.mp4"),
|
||||
&[
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"color=c=blue:s=64x64:r=2:d=1",
|
||||
"-an",
|
||||
"-c:v",
|
||||
"mpeg4",
|
||||
],
|
||||
)?;
|
||||
let db = temp.path().join("index.db");
|
||||
let mut lexical = SearchIndex::open(&db)?;
|
||||
lexical.index_folder(&root)?;
|
||||
let (mut service, calls) = google(&db)?;
|
||||
let limits = searchi_service::MediaLimits::default();
|
||||
service.index_embeddings_with_media(&tools, &limits)?;
|
||||
let first = chunks(&db)?;
|
||||
for name in [
|
||||
"agreement.txt",
|
||||
"retry.rs",
|
||||
"color.bmp",
|
||||
"tone.wav",
|
||||
"scene.mp4",
|
||||
] {
|
||||
assert!(
|
||||
first.iter().any(|chunk| chunk.0.ends_with(name)),
|
||||
"Missing actual source {name}"
|
||||
);
|
||||
}
|
||||
let initial = calls.lock().unwrap().clone();
|
||||
assert_eq!(initial.documents, 2);
|
||||
assert!(initial.images >= 2 && initial.audio >= 1);
|
||||
service.index_embeddings_with_media(&tools, &limits)?;
|
||||
assert_eq!(
|
||||
*calls.lock().unwrap(),
|
||||
initial,
|
||||
"All five complete source groups must avoid repeated inference"
|
||||
);
|
||||
assert_eq!(chunks(&db)?, first);
|
||||
|
||||
let metadata = fs::metadata(&image)?;
|
||||
let before = fs::read(&image)?;
|
||||
ffmpeg(
|
||||
&tools,
|
||||
&image,
|
||||
&[
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"color=c=green:s=64x64",
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-c:v",
|
||||
"bmp",
|
||||
],
|
||||
)?;
|
||||
assert_eq!(fs::metadata(&image)?.len(), metadata.len());
|
||||
assert_ne!(fs::read(&image)?, before);
|
||||
fs::File::options()
|
||||
.write(true)
|
||||
.open(&image)?
|
||||
.set_times(fs::FileTimes::new().set_modified(metadata.modified()?))?;
|
||||
assert_eq!(fs::metadata(&image)?.modified()?, metadata.modified()?);
|
||||
// Core intentionally sees unchanged size/mtime: semantic reuse must still
|
||||
// hash actual media bytes before accepting a completion manifest.
|
||||
lexical.index_folder(&root)?;
|
||||
service.index_embeddings_with_media(&tools, &limits)?;
|
||||
let changed = calls.lock().unwrap().clone();
|
||||
assert_eq!(changed.documents, initial.documents);
|
||||
assert_eq!(changed.audio, initial.audio);
|
||||
assert_eq!(changed.images, initial.images + 1);
|
||||
let latest = chunks(&db)?;
|
||||
for chunk in first.iter().filter(|chunk| !chunk.0.ends_with("color.bmp")) {
|
||||
assert!(
|
||||
latest.contains(chunk),
|
||||
"Unaffected vector and citation changed"
|
||||
);
|
||||
}
|
||||
assert_ne!(
|
||||
latest
|
||||
.iter()
|
||||
.find(|chunk| chunk.0.ends_with("color.bmp"))
|
||||
.unwrap()
|
||||
.6,
|
||||
first
|
||||
.iter()
|
||||
.find(|chunk| chunk.0.ends_with("color.bmp"))
|
||||
.unwrap()
|
||||
.6
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Requires verified Google assets and normal native worker; CPU only"]
|
||||
fn genuine_google_activation_failure_resumes_completed_vectors_after_reopen() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let root = temp.path().join("documents");
|
||||
fs::create_dir(&root)?;
|
||||
let file = root.join("agreement.txt");
|
||||
fs::write(
|
||||
&file,
|
||||
"The supplier agreement ends with sixty days written notice.",
|
||||
)?;
|
||||
let db = temp.path().join("index.db");
|
||||
let mut lexical = SearchIndex::open(&db)?;
|
||||
lexical.index_folder(&root)?;
|
||||
let (mut service, calls) = google(&db)?;
|
||||
service.index_embeddings()?;
|
||||
let connection = Connection::open(&db)?;
|
||||
let active = connection.query_row(
|
||||
"SELECT active_generation FROM semantic_meta WHERE singleton=1",
|
||||
[],
|
||||
|r| r.get::<_, i64>(0),
|
||||
)?;
|
||||
fs::write(
|
||||
&file,
|
||||
"The supplier agreement ends with ninety days written notice.",
|
||||
)?;
|
||||
lexical.index_folder(&root)?;
|
||||
connection.execute_batch("CREATE TRIGGER reject_incremental_activation BEFORE UPDATE OF active_generation ON semantic_meta BEGIN SELECT RAISE(ABORT,'injected incremental activation failure'); END;")?;
|
||||
let failure = service.index_embeddings().unwrap_err();
|
||||
assert!(format!("{failure:#}").contains("injected incremental activation failure"));
|
||||
assert_eq!(
|
||||
connection.query_row(
|
||||
"SELECT active_generation FROM semantic_meta WHERE singleton=1",
|
||||
[],
|
||||
|r| r.get::<_, i64>(0)
|
||||
)?,
|
||||
active
|
||||
);
|
||||
assert_eq!(calls.lock().unwrap().documents, 2);
|
||||
assert!(
|
||||
service
|
||||
.search_semantic("supplier agreement", 10, &SearchOptions::default())?
|
||||
.is_empty(),
|
||||
"Changed old source cannot remain readable"
|
||||
);
|
||||
drop(service);
|
||||
connection.execute_batch("DROP TRIGGER reject_incremental_activation;")?;
|
||||
let (mut reopened, resumed_calls) = google(&db)?;
|
||||
reopened.index_embeddings()?;
|
||||
assert_eq!(
|
||||
resumed_calls.lock().unwrap().documents,
|
||||
0,
|
||||
"Durable completed source must survive reopen after failed activation"
|
||||
);
|
||||
let results = reopened.search_semantic(
|
||||
"How much notice ends the supplier agreement?",
|
||||
10,
|
||||
&SearchOptions::default(),
|
||||
)?;
|
||||
assert_eq!(results.len(), 1);
|
||||
assert!(results[0].snippet.contains("ninety days"));
|
||||
assert!(results[0].source.is_some());
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user