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
@@ -0,0 +1,671 @@
|
||||
//! Completed-source manifests and resumable staging. Legacy generations stay
|
||||
//! searchable but cannot supply vectors without explicit completion evidence.
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SourceTask {
|
||||
pub key: String,
|
||||
pub path: String,
|
||||
/// `text` for one extracted passage, `media` for the entire media file.
|
||||
pub source_type: String,
|
||||
pub source_hash: String,
|
||||
pub title_hash: String,
|
||||
pub policy: String,
|
||||
pub passage_id: i64,
|
||||
}
|
||||
|
||||
pub(super) fn migrate(connection: &Connection) -> Result<()> {
|
||||
for (table, name, declaration) in [
|
||||
(
|
||||
"semantic_generations",
|
||||
"incremental",
|
||||
"INTEGER NOT NULL DEFAULT 0",
|
||||
),
|
||||
("semantic_chunks", "task_key", "TEXT"),
|
||||
] {
|
||||
let exists: bool = connection.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM pragma_table_info(?1) WHERE name=?2)",
|
||||
params![table, name],
|
||||
|r| r.get(0),
|
||||
)?;
|
||||
if !exists {
|
||||
connection.execute_batch(&format!(
|
||||
"ALTER TABLE {table} ADD COLUMN {name} {declaration}"
|
||||
))?;
|
||||
}
|
||||
}
|
||||
connection.execute_batch("CREATE TABLE IF NOT EXISTS semantic_tasks(
|
||||
generation INTEGER NOT NULL REFERENCES semantic_generations(id) ON DELETE CASCADE,
|
||||
task_key TEXT NOT NULL,path TEXT NOT NULL,source_type TEXT NOT NULL,
|
||||
source_hash TEXT NOT NULL,title_hash TEXT NOT NULL,policy TEXT NOT NULL,
|
||||
passage_id INTEGER NOT NULL,state TEXT NOT NULL CHECK(state IN ('pending','running','done','failed')),
|
||||
attempts INTEGER NOT NULL DEFAULT 0,expected_chunks INTEGER,message TEXT,
|
||||
PRIMARY KEY(generation,task_key));
|
||||
CREATE INDEX IF NOT EXISTS semantic_task_reuse ON semantic_tasks(source_hash,title_hash,policy,source_type,state);
|
||||
CREATE INDEX IF NOT EXISTS semantic_chunk_task ON semantic_chunks(generation,task_key);
|
||||
UPDATE semantic_meta SET schema_version=7 WHERE singleton=1;")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn validate_completion(connection: &Connection, generation: i64) -> Result<()> {
|
||||
let incomplete: bool = connection.query_row("SELECT EXISTS(SELECT 1 FROM semantic_tasks t WHERE generation=?1 AND
|
||||
(state NOT IN ('done','failed') OR (state='done' AND (expected_chunks IS NULL OR expected_chunks!=(SELECT count(*) FROM semantic_chunks c WHERE c.generation=t.generation AND c.task_key=t.task_key))) OR
|
||||
(state='failed' AND EXISTS(SELECT 1 FROM semantic_chunks c WHERE c.generation=t.generation AND c.task_key=t.task_key))))",[generation], |r|r.get(0))?;
|
||||
ensure!(
|
||||
!incomplete,
|
||||
"Semantic generation contains incomplete source tasks"
|
||||
);
|
||||
let unowned: bool = connection.query_row("SELECT EXISTS(SELECT 1 FROM semantic_chunks c LEFT JOIN semantic_tasks t ON t.generation=c.generation AND t.task_key=c.task_key WHERE c.generation=?1 AND (t.state IS NULL OR t.state!='done'))",[generation],|r|r.get(0))?;
|
||||
ensure!(
|
||||
!unowned,
|
||||
"Semantic generation contains chunks without completed sources"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Store {
|
||||
pub(crate) fn revision(&self) -> Result<i64> {
|
||||
Ok(self.connection.query_row(
|
||||
"SELECT corpus_revision FROM semantic_meta WHERE singleton=1",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Caller must retain the OS lock through reconciliation and publication.
|
||||
pub(crate) fn start_or_resume(
|
||||
&mut self,
|
||||
fingerprint: &str,
|
||||
lock: &BuildLock,
|
||||
) -> Result<(i64, bool)> {
|
||||
ensure!(
|
||||
lock.1 == self.lock_path,
|
||||
"Build lock belongs to a different index"
|
||||
);
|
||||
ensure!(
|
||||
!fingerprint.is_empty() && fingerprint.len() <= 4096,
|
||||
"Invalid generation fingerprint"
|
||||
);
|
||||
let tx = self
|
||||
.connection
|
||||
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
|
||||
let generation: Option<i64>=tx.query_row("SELECT id FROM semantic_generations WHERE id!=COALESCE((SELECT active_generation FROM semantic_meta WHERE singleton=1),-1) AND fingerprint=?1 AND dimensions=?2 AND incremental=1 ORDER BY id DESC LIMIT 1",params![fingerprint,self.dimensions as i64],|r|r.get(0)).optional()?;
|
||||
let resumed = generation.is_some();
|
||||
let id = if let Some(id) = generation {
|
||||
id
|
||||
} else {
|
||||
tx.execute("INSERT INTO semantic_generations(fingerprint,dimensions,corpus_revision,incremental) SELECT ?1,?2,corpus_revision,1 FROM semantic_meta WHERE singleton=1",params![fingerprint,self.dimensions as i64])?;
|
||||
tx.last_insert_rowid()
|
||||
};
|
||||
tx.execute("DELETE FROM semantic_generations WHERE id!=?1 AND id!=COALESCE((SELECT active_generation FROM semantic_meta WHERE singleton=1),-1)",[id])?;
|
||||
// Untagged chunks are writes interrupted before the source transaction.
|
||||
tx.execute("UPDATE semantic_tasks SET state='pending',expected_chunks=NULL WHERE generation=?1 AND state='done' AND
|
||||
(expected_chunks IS NULL OR expected_chunks!=(SELECT count(*) FROM semantic_chunks c WHERE c.generation=semantic_tasks.generation AND c.task_key=semantic_tasks.task_key))",[id])?;
|
||||
tx.execute("DELETE FROM semantic_chunks WHERE generation=?1 AND NOT EXISTS(SELECT 1 FROM semantic_tasks t WHERE t.generation=semantic_chunks.generation AND t.task_key=semantic_chunks.task_key AND t.state='done')",[id])?;
|
||||
tx.execute("UPDATE semantic_tasks SET state='pending',expected_chunks=NULL,message=NULL WHERE generation=?1 AND state!='done'",[id])?;
|
||||
tx.commit()?;
|
||||
Ok((id, resumed))
|
||||
}
|
||||
|
||||
pub(crate) fn stage_task(&mut self, generation: i64, task: &SourceTask) -> Result<bool> {
|
||||
validate_task(task)?;
|
||||
let tx = self.connection.transaction()?;
|
||||
let same:bool=tx.query_row("SELECT EXISTS(SELECT 1 FROM semantic_tasks WHERE generation=?1 AND task_key=?2 AND source_hash=?3 AND title_hash=?4 AND policy=?5 AND source_type=?6 AND state='done')",params![generation,task.key,task.source_hash,task.title_hash,task.policy,task.source_type],|r|r.get(0))?;
|
||||
if !same {
|
||||
tx.execute(
|
||||
"DELETE FROM semantic_chunks WHERE generation=?1 AND task_key=?2",
|
||||
params![generation, task.key],
|
||||
)?;
|
||||
tx.execute("INSERT INTO semantic_tasks(generation,task_key,path,source_type,source_hash,title_hash,policy,passage_id,state,attempts)
|
||||
VALUES(?1,?2,?3,?4,?5,?6,?7,?8,'running',1)
|
||||
ON CONFLICT(generation,task_key) DO UPDATE SET path=excluded.path,source_type=excluded.source_type,source_hash=excluded.source_hash,title_hash=excluded.title_hash,policy=excluded.policy,passage_id=excluded.passage_id,state='running',attempts=semantic_tasks.attempts+1,expected_chunks=NULL,message=NULL",params![generation,task.key,task.path,task.source_type,task.source_hash,task.title_hash,task.policy,task.passage_id])?;
|
||||
}
|
||||
tx.commit()?;
|
||||
Ok(same)
|
||||
}
|
||||
|
||||
pub(crate) fn complete_source(
|
||||
&mut self,
|
||||
generation: i64,
|
||||
task: &SourceTask,
|
||||
expected_chunks: usize,
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
!task.source_hash.is_empty(),
|
||||
"Cannot complete a source without verified identity"
|
||||
);
|
||||
ensure!(
|
||||
expected_chunks <= MAX_SCAN_CHUNKS,
|
||||
"Source exceeds semantic chunk budget"
|
||||
);
|
||||
let tx = self
|
||||
.connection
|
||||
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
|
||||
let count:i64=tx.query_row("SELECT count(*) FROM semantic_chunks WHERE generation=?1 AND path=?2 AND ((?3='text' AND source_type='text' AND passage_id=?4) OR (?3='media' AND source_type!='text'))",params![generation,task.path,task.source_type,task.passage_id],|r|r.get(0))?;
|
||||
ensure!(
|
||||
usize::try_from(count)? == expected_chunks,
|
||||
"Incomplete source chunk membership"
|
||||
);
|
||||
let total: usize = tx.query_row(
|
||||
"SELECT count(*) FROM semantic_chunks WHERE generation=?1",
|
||||
[generation],
|
||||
|r| r.get(0),
|
||||
)?;
|
||||
ensure!(
|
||||
total <= MAX_SCAN_CHUNKS,
|
||||
"Generation exceeds semantic chunk budget"
|
||||
);
|
||||
tx.execute("UPDATE semantic_chunks SET task_key=?5 WHERE generation=?1 AND path=?2 AND ((?3='text' AND source_type='text' AND passage_id=?4) OR (?3='media' AND source_type!='text'))",params![generation,task.path,task.source_type,task.passage_id,task.key])?;
|
||||
let changed=tx.execute("UPDATE semantic_tasks SET state='done',expected_chunks=?3,path=?4,passage_id=?5,message=NULL WHERE generation=?1 AND task_key=?2 AND source_hash=?6 AND title_hash=?7 AND policy=?8 AND source_type=?9",params![generation,task.key,count,task.path,task.passage_id,task.source_hash,task.title_hash,task.policy,task.source_type])?;
|
||||
ensure!(changed == 1, "Missing matching source task");
|
||||
if task.source_type == "media" {
|
||||
tx.execute(
|
||||
"DELETE FROM semantic_media_errors WHERE generation=?1 AND path=?2",
|
||||
params![generation, task.path],
|
||||
)?;
|
||||
}
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn fail_source(
|
||||
&mut self,
|
||||
generation: i64,
|
||||
task: &SourceTask,
|
||||
message: &str,
|
||||
) -> Result<()> {
|
||||
let tx = self.connection.transaction()?;
|
||||
tx.execute("DELETE FROM semantic_chunks WHERE generation=?1 AND (task_key=?2 OR (path=?3 AND ((?4='text' AND source_type='text' AND passage_id=?5) OR (?4='media' AND source_type!='text'))))",params![generation,task.key,task.path,task.source_type,task.passage_id])?;
|
||||
tx.execute("UPDATE semantic_tasks SET state='failed',expected_chunks=NULL,message=?3 WHERE generation=?1 AND task_key=?2",params![generation,task.key,message.chars().take(2048).collect::<String>()])?;
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn reconcile_tasks(
|
||||
&mut self,
|
||||
generation: i64,
|
||||
keys: &[String],
|
||||
revision: i64,
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
keys.len() <= MAX_SCAN_CHUNKS,
|
||||
"Generation exceeds source task budget"
|
||||
);
|
||||
let tx = self
|
||||
.connection
|
||||
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
|
||||
let current: i64 = tx.query_row(
|
||||
"SELECT corpus_revision FROM semantic_meta WHERE singleton=1",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)?;
|
||||
ensure!(
|
||||
current == revision,
|
||||
"Indexed corpus changed during semantic reconciliation; retry required"
|
||||
);
|
||||
tx.execute_batch("CREATE TEMP TABLE IF NOT EXISTS semantic_current_tasks(task_key TEXT PRIMARY KEY); DELETE FROM semantic_current_tasks;")?;
|
||||
for key in keys {
|
||||
tx.execute(
|
||||
"INSERT OR IGNORE INTO semantic_current_tasks VALUES(?1)",
|
||||
[key],
|
||||
)?;
|
||||
}
|
||||
tx.execute("DELETE FROM semantic_chunks WHERE generation=?1 AND (task_key IS NULL OR task_key NOT IN (SELECT task_key FROM semantic_current_tasks))",[generation])?;
|
||||
tx.execute("DELETE FROM semantic_tasks WHERE generation=?1 AND task_key NOT IN (SELECT task_key FROM semantic_current_tasks)",[generation])?;
|
||||
tx.execute("DELETE FROM semantic_media_errors WHERE generation=?1 AND path NOT IN (SELECT path FROM semantic_tasks WHERE generation=?1)",[generation])?;
|
||||
tx.execute(
|
||||
"UPDATE semantic_generations SET corpus_revision=?2 WHERE id=?1",
|
||||
params![generation, revision],
|
||||
)?;
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn activate_checked(&mut self, generation: i64, revision: i64) -> Result<()> {
|
||||
validate_completion(&self.connection, generation)?;
|
||||
self.activate_revision(generation, Some(revision))
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_task(task: &SourceTask) -> Result<()> {
|
||||
ensure!(
|
||||
!task.key.is_empty()
|
||||
&& task.key.len() <= 256
|
||||
&& !task.path.is_empty()
|
||||
&& task.path.len() <= 32768,
|
||||
"Invalid source task identity"
|
||||
);
|
||||
ensure!(
|
||||
matches!(task.source_type.as_str(), "text" | "media"),
|
||||
"Invalid source task type"
|
||||
);
|
||||
ensure!(
|
||||
(!task.source_hash.is_empty() || task.source_type == "media")
|
||||
&& task.source_hash.len() <= 256
|
||||
&& task.title_hash.len() <= 256
|
||||
&& !task.policy.is_empty()
|
||||
&& task.policy.len() <= 4096,
|
||||
"Invalid source completion policy"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Store {
|
||||
fn reusable_task(
|
||||
&self,
|
||||
generation: i64,
|
||||
task: &SourceTask,
|
||||
) -> Result<Option<(i64, String, usize)>> {
|
||||
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 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()?)
|
||||
}
|
||||
|
||||
/// No tokenizer or neural call: only a completed, policy-compatible group
|
||||
/// whose persisted ranges still equal the current extracted bytes qualifies.
|
||||
pub(crate) fn reuse_text_source(
|
||||
&mut self,
|
||||
generation: i64,
|
||||
task: &SourceTask,
|
||||
passage: &IndexedPassage,
|
||||
) -> Result<Option<usize>> {
|
||||
ensure!(
|
||||
task.source_type == "text"
|
||||
&& task.passage_id == passage.id
|
||||
&& task.path == passage.path,
|
||||
"Text task mapping differs from current source"
|
||||
);
|
||||
ensure!(
|
||||
task.source_hash == content_hash(&passage.text),
|
||||
"Text task hash differs from source bytes"
|
||||
);
|
||||
let Some((source, key, count)) = self.reusable_task(generation, task)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
ensure!(
|
||||
count <= MAX_SCAN_CHUNKS,
|
||||
"Reusable source exceeds chunk budget"
|
||||
);
|
||||
{
|
||||
let mut stmt=self.connection.prepare("SELECT start_byte,end_byte,snippet,vector,source_type,ordinal,content_hash FROM semantic_chunks WHERE generation=?1 AND task_key=?2 ORDER BY ordinal")?;
|
||||
let mut rows = stmt.query(params![source, key])?;
|
||||
let mut seen = 0usize;
|
||||
while let Some(row) = rows.next()? {
|
||||
let start: usize = row.get(0)?;
|
||||
let end: usize = row.get(1)?;
|
||||
let snippet: String = row.get(2)?;
|
||||
ensure!(
|
||||
passage.text.get(start..end) == Some(snippet.as_str()),
|
||||
"Reusable text range differs from current source"
|
||||
);
|
||||
ensure!(
|
||||
row.get::<_, String>(4)? == "text"
|
||||
&& row.get::<_, usize>(5)? == seen
|
||||
&& row.get::<_, String>(6)? == task.source_hash,
|
||||
"Corrupt completed text source membership"
|
||||
);
|
||||
validate_vector(&row.get::<_, Vec<u8>>(3)?, self.dimensions)?;
|
||||
seen += 1;
|
||||
}
|
||||
ensure!(seen == count, "Reusable text completion count differs");
|
||||
}
|
||||
self.copy_completed(
|
||||
generation,
|
||||
task,
|
||||
source,
|
||||
&key,
|
||||
count,
|
||||
Some(&passage.source),
|
||||
None,
|
||||
)?;
|
||||
Ok(Some(count))
|
||||
}
|
||||
|
||||
pub(crate) fn reuse_media_source(
|
||||
&mut self,
|
||||
generation: i64,
|
||||
task: &SourceTask,
|
||||
file_id: i64,
|
||||
identity: &searchi_media::MediaIdentity,
|
||||
) -> Result<Option<usize>> {
|
||||
ensure!(
|
||||
task.source_type == "media"
|
||||
&& task.passage_id == -file_id
|
||||
&& task.path == identity.path.to_string_lossy()
|
||||
&& task.source_hash == identity.sha256,
|
||||
"Media task mapping differs from current source"
|
||||
);
|
||||
let Some((source, key, count)) = self.reusable_task(generation, task)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
ensure!(
|
||||
count <= MAX_SCAN_CHUNKS,
|
||||
"Reusable source exceeds chunk budget"
|
||||
);
|
||||
{
|
||||
let mut stmt=self.connection.prepare("SELECT vector,source_type,ordinal,content_hash FROM semantic_chunks WHERE generation=?1 AND task_key=?2 ORDER BY ordinal")?;
|
||||
let mut rows = stmt.query(params![source, key])?;
|
||||
let mut seen = 0usize;
|
||||
while let Some(row) = rows.next()? {
|
||||
ensure!(
|
||||
matches!(
|
||||
row.get::<_, String>(1)?.as_str(),
|
||||
"image" | "audio" | "video"
|
||||
) && row.get::<_, usize>(2)? == seen
|
||||
&& row.get::<_, String>(3)? == task.source_hash,
|
||||
"Corrupt completed media source membership"
|
||||
);
|
||||
validate_vector(&row.get::<_, Vec<u8>>(0)?, self.dimensions)?;
|
||||
seen += 1;
|
||||
}
|
||||
ensure!(seen == count, "Reusable media completion count differs");
|
||||
}
|
||||
self.copy_completed(generation, task, source, &key, count, None, Some(identity))?;
|
||||
Ok(Some(count))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn copy_completed(
|
||||
&mut self,
|
||||
generation: i64,
|
||||
task: &SourceTask,
|
||||
source: i64,
|
||||
key: &str,
|
||||
count: usize,
|
||||
location: Option<&SourceLocation>,
|
||||
identity: Option<&searchi_media::MediaIdentity>,
|
||||
) -> Result<()> {
|
||||
let tx = self
|
||||
.connection
|
||||
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
|
||||
let total:usize=tx.query_row("SELECT count(*) FROM semantic_chunks WHERE generation=?1 AND (task_key IS NULL OR task_key!=?2)",params![generation,task.key],|r|r.get(0))?;
|
||||
ensure!(
|
||||
total.saturating_add(count) <= MAX_SCAN_CHUNKS,
|
||||
"Generation exceeds semantic chunk budget"
|
||||
);
|
||||
// A temporary SQL table allows remapping a completed staging task onto
|
||||
// itself without retaining its old citations or allocating all vectors.
|
||||
tx.execute_batch("CREATE TEMP TABLE IF NOT EXISTS semantic_reuse_chunks AS SELECT * FROM semantic_chunks WHERE 0; DELETE FROM semantic_reuse_chunks;")?;
|
||||
tx.execute("INSERT INTO semantic_reuse_chunks SELECT * FROM semantic_chunks WHERE generation=?1 AND task_key=?2",params![source,key])?;
|
||||
tx.execute(
|
||||
"DELETE FROM semantic_chunks WHERE generation=?1 AND task_key=?2",
|
||||
params![generation, task.key],
|
||||
)?;
|
||||
tx.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,task_key)
|
||||
SELECT ?1,?2,ordinal,?3,COALESCE(?4,kind),COALESCE(?5,number),content_hash,start_byte,end_byte,snippet,vector,source_type,COALESCE(?6,file_size),COALESCE(?7,file_mtime),?8 FROM semantic_reuse_chunks",params![generation,task.passage_id,task.path,location.map(|s|s.kind.as_str()),location.map(|s|s.number),identity.map(|i|i.bytes as i64),identity.map(|i|i.modified_ns),task.key])?;
|
||||
let changed=tx.execute("UPDATE semantic_tasks SET state='done',expected_chunks=?3,path=?4,passage_id=?5,message=NULL WHERE generation=?1 AND task_key=?2 AND source_hash=?6 AND title_hash=?7 AND policy=?8 AND source_type=?9",params![generation,task.key,count as i64,task.path,task.passage_id,task.source_hash,task.title_hash,task.policy,task.source_type])?;
|
||||
ensure!(changed == 1, "Missing source task during reuse");
|
||||
if task.source_type == "media" {
|
||||
tx.execute(
|
||||
"DELETE FROM semantic_media_errors WHERE generation=?1 AND path=?2",
|
||||
params![generation, task.path],
|
||||
)?;
|
||||
}
|
||||
tx.execute_batch("DELETE FROM semantic_reuse_chunks;")?;
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_vector(bytes: &[u8], dimensions: usize) -> Result<()> {
|
||||
ensure!(
|
||||
bytes.len() == dimensions * 4,
|
||||
"Corrupt reusable vector length"
|
||||
);
|
||||
let mut norm = 0.0f32;
|
||||
for value in bytes.as_chunks::<4>().0 {
|
||||
let value = f32::from_le_bytes(*value);
|
||||
ensure!(value.is_finite(), "Corrupt reusable vector value");
|
||||
norm += value * value;
|
||||
}
|
||||
ensure!(
|
||||
norm.is_finite() && (norm.sqrt() - 1.0).abs() < 1e-3,
|
||||
"Reusable vector is not normalized"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use searchi_core::SearchIndex;
|
||||
|
||||
// Storage-only vectors exercise durability and membership. They are not
|
||||
// simulated production inference or evidence of semantic relevance.
|
||||
fn vector() -> Vec<f32> {
|
||||
vec![1.0, 0.0]
|
||||
}
|
||||
fn task(p: &IndexedPassage) -> SourceTask {
|
||||
SourceTask {
|
||||
key: format!("text-{}-{}", p.path, p.source.number),
|
||||
path: p.path.clone(),
|
||||
source_type: "text".into(),
|
||||
source_hash: content_hash(&p.text),
|
||||
title_hash: content_hash(&p.filename),
|
||||
policy: "storage-test-window-policy".into(),
|
||||
passage_id: p.id,
|
||||
}
|
||||
}
|
||||
fn insert_complete(
|
||||
store: &mut Store,
|
||||
g: i64,
|
||||
p: &IndexedPassage,
|
||||
t: &SourceTask,
|
||||
) -> Result<()> {
|
||||
store.stage_task(g, t)?;
|
||||
store.insert(
|
||||
g,
|
||||
p,
|
||||
0,
|
||||
&[TextWindow {
|
||||
text: p.text.clone(),
|
||||
start_byte: 0,
|
||||
end_byte: p.text.len(),
|
||||
}],
|
||||
&[vector()],
|
||||
)?;
|
||||
store.complete_source(g, t, 1)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_retains_completed_groups_and_discards_unfinished_vectors() -> Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let root = dir.path().join("docs");
|
||||
std::fs::create_dir(&root)?;
|
||||
std::fs::write(root.join("first.txt"), "complete source")?;
|
||||
std::fs::write(root.join("second.txt"), "interrupted source")?;
|
||||
let db = dir.path().join("index.db");
|
||||
let mut lexical = SearchIndex::open(&db)?;
|
||||
lexical.index_folder(&root)?;
|
||||
let passages = lexical.passage_batch(0, 64)?;
|
||||
let first = task(&passages[0]);
|
||||
let second = task(&passages[1]);
|
||||
let generation;
|
||||
{
|
||||
let mut store = Store::open(&db, 2)?;
|
||||
let lock = store.build_lock()?;
|
||||
generation = store.start_or_resume("storage-only-model", &lock)?.0;
|
||||
insert_complete(&mut store, generation, &passages[0], &first)?;
|
||||
store.stage_task(generation, &second)?;
|
||||
store.insert(
|
||||
generation,
|
||||
&passages[1],
|
||||
0,
|
||||
&[TextWindow {
|
||||
text: passages[1].text.clone(),
|
||||
start_byte: 0,
|
||||
end_byte: passages[1].text.len(),
|
||||
}],
|
||||
&[vector()],
|
||||
)?;
|
||||
assert!(store
|
||||
.activate_checked(generation, store.revision()?)
|
||||
.is_err());
|
||||
}
|
||||
let mut store = Store::open(&db, 2)?;
|
||||
let lock = store.build_lock()?;
|
||||
assert_eq!(
|
||||
store.start_or_resume("storage-only-model", &lock)?,
|
||||
(generation, true)
|
||||
);
|
||||
assert_eq!(
|
||||
store.connection.query_row(
|
||||
"SELECT count(*) FROM semantic_chunks WHERE generation=?1",
|
||||
[generation],
|
||||
|r| r.get::<_, usize>(0)
|
||||
)?,
|
||||
1
|
||||
);
|
||||
store.stage_task(generation, &first)?;
|
||||
assert_eq!(
|
||||
store.reuse_text_source(generation, &first, &passages[0])?,
|
||||
Some(1)
|
||||
);
|
||||
assert!(store
|
||||
.reuse_text_source(generation, &second, &passages[1])?
|
||||
.is_none());
|
||||
assert!(store
|
||||
.activate_checked(generation, store.revision()?)
|
||||
.is_err());
|
||||
store.reconcile_tasks(generation, &[first.key], store.revision()?)?;
|
||||
store.activate_checked(generation, store.revision()?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_vectors_remain_searchable_but_do_not_qualify_for_reuse() -> Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let root = dir.path().join("docs");
|
||||
std::fs::create_dir(&root)?;
|
||||
std::fs::write(root.join("notes.txt"), "legacy searchable source")?;
|
||||
let db = dir.path().join("index.db");
|
||||
let mut lexical = SearchIndex::open(&db)?;
|
||||
lexical.index_folder(&root)?;
|
||||
let p = lexical.passage_batch(0, 1)?.remove(0);
|
||||
let mut store = Store::open(&db, 2)?;
|
||||
let legacy = store.begin("legacy-policy")?;
|
||||
store.insert(
|
||||
legacy,
|
||||
&p,
|
||||
0,
|
||||
&[TextWindow {
|
||||
text: p.text.clone(),
|
||||
start_byte: 0,
|
||||
end_byte: p.text.len(),
|
||||
}],
|
||||
&[vector()],
|
||||
)?;
|
||||
store.activate(legacy)?;
|
||||
// Reopen a schema6 DB with legacy active chunks and no manifests.
|
||||
store.connection.execute_batch(
|
||||
"DROP INDEX semantic_chunk_task; ALTER TABLE semantic_chunks DROP COLUMN task_key;
|
||||
DROP TABLE semantic_tasks; ALTER TABLE semantic_generations DROP COLUMN incremental;
|
||||
UPDATE semantic_meta SET schema_version=6;",
|
||||
)?;
|
||||
drop(store);
|
||||
let mut store = Store::open(&db, 2)?;
|
||||
assert_eq!(
|
||||
store
|
||||
.search(&vector(), "legacy-policy", 1, &SearchOptions::default())?
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
let lock = store.build_lock()?;
|
||||
let (g, _) = store.start_or_resume("legacy-policy", &lock)?;
|
||||
let t = task(&p);
|
||||
store.stage_task(g, &t)?;
|
||||
assert_eq!(store.reuse_text_source(g, &t, &p)?, None);
|
||||
assert_eq!(
|
||||
store.active_fingerprint()?.as_deref(),
|
||||
Some("legacy-policy")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revision_race_cannot_publish_and_failed_activation_keeps_staging() -> Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let root = dir.path().join("docs");
|
||||
std::fs::create_dir(&root)?;
|
||||
std::fs::write(root.join("notes.txt"), "source before change")?;
|
||||
let db = dir.path().join("index.db");
|
||||
let mut lexical = SearchIndex::open(&db)?;
|
||||
lexical.index_folder(&root)?;
|
||||
let p = lexical.passage_batch(0, 1)?.remove(0);
|
||||
let mut store = Store::open(&db, 2)?;
|
||||
let lock = store.build_lock()?;
|
||||
let (g, _) = store.start_or_resume("checked-policy", &lock)?;
|
||||
let t = task(&p);
|
||||
insert_complete(&mut store, g, &p, &t)?;
|
||||
let old_revision = store.revision()?;
|
||||
std::fs::write(root.join("notes.txt"), "source after change longer")?;
|
||||
lexical.index_folder(&root)?;
|
||||
assert!(store.activate_checked(g, old_revision).is_err());
|
||||
assert_eq!(store.active_fingerprint()?, None);
|
||||
assert_eq!(store.start_or_resume("checked-policy", &lock)?, (g, true));
|
||||
assert_eq!(
|
||||
store.connection.query_row(
|
||||
"SELECT count(*) FROM semantic_chunks WHERE generation=?1",
|
||||
[g],
|
||||
|r| r.get::<_, usize>(0)
|
||||
)?,
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod completion_failure_tests {
|
||||
use super::*;
|
||||
use searchi_core::SearchIndex;
|
||||
|
||||
#[test]
|
||||
fn failed_completion_rolls_back_chunk_membership_and_malformed_ranges_never_reuse() -> Result<()>
|
||||
{
|
||||
// SQLite durability fixture, not neural inference evidence.
|
||||
let dir = tempfile::tempdir()?;
|
||||
let root = dir.path().join("docs");
|
||||
std::fs::create_dir(&root)?;
|
||||
std::fs::write(root.join("notes.txt"), "UTF-8 source with 中文 preserved")?;
|
||||
let db = dir.path().join("index.db");
|
||||
let mut lexical = SearchIndex::open(&db)?;
|
||||
lexical.index_folder(&root)?;
|
||||
let p = lexical.passage_batch(0, 1)?.remove(0);
|
||||
let t = SourceTask {
|
||||
key: "durability-task".into(),
|
||||
path: p.path.clone(),
|
||||
source_type: "text".into(),
|
||||
source_hash: content_hash(&p.text),
|
||||
title_hash: content_hash(&p.filename),
|
||||
policy: "exact-window-test".into(),
|
||||
passage_id: p.id,
|
||||
};
|
||||
let mut store = Store::open(&db, 2)?;
|
||||
let lock = store.build_lock()?;
|
||||
let (g, _) = store.start_or_resume("storage-geometry", &lock)?;
|
||||
store.stage_task(g, &t)?;
|
||||
store.insert(
|
||||
g,
|
||||
&p,
|
||||
0,
|
||||
&[TextWindow {
|
||||
text: p.text.clone(),
|
||||
start_byte: 0,
|
||||
end_byte: p.text.len(),
|
||||
}],
|
||||
&[vec![1.0, 0.0]],
|
||||
)?;
|
||||
assert!(store.complete_source(g, &t, 2).is_err());
|
||||
let (state,owned):(String,usize)=store.connection.query_row("SELECT state,(SELECT count(*) FROM semantic_chunks WHERE generation=?1 AND task_key IS NOT NULL) FROM semantic_tasks WHERE generation=?1 AND task_key=?2",params![g,t.key],|r|Ok((r.get(0)?,r.get(1)?)))?;
|
||||
assert_eq!(state, "running");
|
||||
assert_eq!(owned, 0);
|
||||
assert!(store.activate_checked(g, store.revision()?).is_err());
|
||||
store.complete_source(g, &t, 1)?;
|
||||
store.connection.execute("UPDATE semantic_chunks SET start_byte=1,snippet='different snippet' WHERE generation=?1",[g])?;
|
||||
assert!(store.reuse_text_source(g, &t, &p).is_err());
|
||||
assert!(store.active_fingerprint()?.is_none());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user