Filter exact music identities before lexical and semantic candidate limits
This commit is contained in:
1 parent
3ac1201909
commit
2eace15a33
7 files changed
+458
-19
No files matched your search
@@ -41,3 +41,9 @@ Audio timestamps represent segment starts, not exact word locations. Smaller
|
||||
windows alone have not demonstrated accurate spoken navigation. No-answer
|
||||
calibration, music discrimination, exact spoken positions, process isolation,
|
||||
unchanged-vector reuse and production platform checks remain release gates.
|
||||
|
||||
Exact artist/title constraints use the core's normalized structured probe keys
|
||||
before ANN limits and in the eligibility cache key. Metadata-only writes advance
|
||||
the corpus revision and dirty path so warmed readers cannot retain old eligibility.
|
||||
Empty metadata-only hybrid requests use core results without model inference.
|
||||
These explicit constraints do not establish abstention for free-form topic queries.
|
||||
@@ -513,7 +513,7 @@ impl HybridSearch {
|
||||
.lexical
|
||||
.search_with_options_traced(query, candidates, options, trace, parent)?;
|
||||
if query.trim().is_empty() || limit == 0 {
|
||||
return Ok(Vec::new());
|
||||
return Ok(lexical.into_iter().take(limit.min(1000)).collect());
|
||||
}
|
||||
trace.timed("hybrid.identifier_priority", parent, |_, _| {
|
||||
promote_identifiers(query.trim(), &mut lexical)
|
||||
@@ -776,6 +776,24 @@ mod tests {
|
||||
let directory = tempfile::tempdir()?;
|
||||
let db = directory.path().join("index.db");
|
||||
let mut service = HybridSearch::with_provider(&db, Box::new(FailingProvider))?;
|
||||
// The failure-only provider proves metadata-only searches never require inference.
|
||||
let metadata = service.search_measured(
|
||||
"",
|
||||
5,
|
||||
&SearchOptions {
|
||||
media_artist: Some("Absent Artist".into()),
|
||||
..Default::default()
|
||||
},
|
||||
true,
|
||||
)?;
|
||||
assert!(metadata.results.is_empty());
|
||||
assert!(!metadata
|
||||
.metrics
|
||||
.trace
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|span| span.name == "semantic.query_embedding"));
|
||||
|
||||
let mut trace = TraceCollector::new(MetricsMode::Debug);
|
||||
assert!(service
|
||||
.search_traced(
|
||||
|
||||
@@ -190,15 +190,9 @@ impl HybridSearch {
|
||||
metadata: &MediaProbe,
|
||||
change_revision: i64,
|
||||
) -> Result<()> {
|
||||
let tags = metadata
|
||||
.tags
|
||||
.iter()
|
||||
.map(|(k, v)| format!("{k}: {v}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
self.lexical.set_media_metadata_at_revision(
|
||||
self.lexical.set_structured_media_metadata_at_revision(
|
||||
&path.to_string_lossy(),
|
||||
&tags,
|
||||
&metadata.tags,
|
||||
metadata.duration_ms,
|
||||
identity.bytes,
|
||||
identity.modified_ns,
|
||||
|
||||
@@ -31,6 +31,8 @@ struct FilterKey {
|
||||
max_duration: Option<u32>,
|
||||
source_type: Option<String>,
|
||||
code_only: bool,
|
||||
media_artist: Option<String>,
|
||||
media_title: Option<String>,
|
||||
}
|
||||
|
||||
struct EligibilityCache {
|
||||
@@ -184,6 +186,17 @@ impl Store {
|
||||
}
|
||||
}
|
||||
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))")?;
|
||||
// Metadata-only writes change strict eligibility even when file bytes do not change.
|
||||
for (name, event, row) in [
|
||||
("insert", "INSERT", "new"),
|
||||
("update", "UPDATE", "new"),
|
||||
("delete", "DELETE", "old"),
|
||||
] {
|
||||
initialization.execute_batch(&format!("CREATE TRIGGER IF NOT EXISTS semantic_media_metadata_{name} AFTER {event} ON media_metadata BEGIN
|
||||
UPDATE semantic_meta SET corpus_revision=corpus_revision+1 WHERE singleton=1;
|
||||
INSERT INTO semantic_dirty_paths(path,revision) VALUES({row}.path,(SELECT corpus_revision FROM semantic_meta WHERE singleton=1))
|
||||
ON CONFLICT(path) DO UPDATE SET revision=excluded.revision; END;"))?;
|
||||
}
|
||||
incremental::migrate(&initialization)?;
|
||||
initialization.commit()?;
|
||||
let canonical = db.canonicalize()?;
|
||||
@@ -644,6 +657,16 @@ impl Store {
|
||||
max_duration: options.max_duration_ms,
|
||||
source_type: source_type.map(str::to_owned),
|
||||
code_only: options.code_only,
|
||||
media_artist: options
|
||||
.media_artist
|
||||
.as_deref()
|
||||
.map(searchi_core::normalize_media_field)
|
||||
.transpose()?,
|
||||
media_title: options
|
||||
.media_title
|
||||
.as_deref()
|
||||
.map(searchi_core::normalize_media_field)
|
||||
.transpose()?,
|
||||
};
|
||||
trace.end(filter_span, Default::default());
|
||||
let eligibility_span = trace.begin("semantic.eligibility", parent);
|
||||
@@ -883,6 +906,8 @@ impl Store {
|
||||
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 (?9 IS NULL OR EXISTS(SELECT 1 FROM media_metadata m WHERE m.path=f.path AND m.artist_key=?9))
|
||||
AND (?10 IS NULL OR EXISTS(SELECT 1 FROM media_metadata m WHERE m.path=f.path AND m.title_key=?10))
|
||||
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.
|
||||
@@ -894,7 +919,17 @@ impl Store {
|
||||
max_size,
|
||||
options.modified_after_unix_ms,
|
||||
options.modified_before_unix_ms,
|
||||
MAX_SCAN_CHUNKS as i64 + 1
|
||||
MAX_SCAN_CHUNKS as i64 + 1,
|
||||
options
|
||||
.media_artist
|
||||
.as_deref()
|
||||
.map(searchi_core::normalize_media_field)
|
||||
.transpose()?,
|
||||
options
|
||||
.media_title
|
||||
.as_deref()
|
||||
.map(searchi_core::normalize_media_field)
|
||||
.transpose()?
|
||||
])?;
|
||||
let mut hashes = HashMap::<i64, String>::new();
|
||||
let mut best = HashMap::<String, SearchResult>::new();
|
||||
@@ -1008,6 +1043,8 @@ fn collect_eligibility(
|
||||
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 (?13 IS NULL OR EXISTS(SELECT 1 FROM media_metadata m WHERE m.path=f.path AND m.artist_key=?13))
|
||||
AND (?14 IS NULL OR EXISTS(SELECT 1 FROM media_metadata m WHERE m.path=f.path AND m.title_key=?14))
|
||||
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![
|
||||
@@ -1022,7 +1059,9 @@ fn collect_eligibility(
|
||||
path,
|
||||
filter.min_duration,
|
||||
filter.max_duration,
|
||||
filter.source_type
|
||||
filter.source_type,
|
||||
filter.media_artist,
|
||||
filter.media_title
|
||||
])?;
|
||||
let mut hashes = HashMap::<i64, String>::new();
|
||||
let mut keys = HashSet::new();
|
||||
@@ -1293,6 +1332,108 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_metadata_filters_apply_before_ann_and_invalidate_warmed_eligibility() -> Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let root = dir.path().join("media");
|
||||
std::fs::create_dir(&root)?;
|
||||
std::fs::write(root.join("first.ogg"), b"metadata ANN eligibility fixture")?;
|
||||
std::fs::write(root.join("other.ogg"), b"metadata distractor fixture")?;
|
||||
let db = dir.path().join("index.db");
|
||||
let mut lexical = SearchIndex::open(&db)?;
|
||||
lexical.index_folder(&root)?;
|
||||
let mut store = Store::open(&db, DIMENSIONS)?;
|
||||
let mut files = store.media_batch(0)?;
|
||||
files.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
let (other_id, other_path, _, _) = files.pop().context("distractor fixture")?;
|
||||
let other_identity = searchi_media::MediaIdentity::read(Path::new(&other_path), 1024)?;
|
||||
let (id, path, _, _) = files.remove(0);
|
||||
let identity = searchi_media::MediaIdentity::read(Path::new(&path), 1024)?;
|
||||
let revision =
|
||||
lexical.media_change_revision(&path, identity.bytes, identity.modified_ns)?;
|
||||
let mut tags = std::collections::BTreeMap::from([
|
||||
("artist".into(), "Björk".into()),
|
||||
("title".into(), "Jóga".into()),
|
||||
]);
|
||||
lexical.set_structured_media_metadata_at_revision(
|
||||
&path,
|
||||
&tags,
|
||||
1000,
|
||||
identity.bytes,
|
||||
identity.modified_ns,
|
||||
revision,
|
||||
)?;
|
||||
// Deterministic geometry tests storage/filter behavior, not model quality.
|
||||
let mut vector = vec![0.; DIMENSIONS];
|
||||
vector[0] = 1.;
|
||||
let generation = store.begin("metadata-filter-proof")?;
|
||||
let mut weaker = vector.clone();
|
||||
weaker[0] = 0.8;
|
||||
weaker[1] = 0.6;
|
||||
store.insert_media(generation, id, 0, &identity, "audio", 0, &weaker)?;
|
||||
store.insert_media(
|
||||
generation,
|
||||
other_id,
|
||||
0,
|
||||
&other_identity,
|
||||
"audio",
|
||||
0,
|
||||
&vector,
|
||||
)?;
|
||||
store.activate(generation)?;
|
||||
let mut options = SearchOptions {
|
||||
media_artist: Some("BJÖRK".into()),
|
||||
media_title: Some("JÓGA".into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
store
|
||||
.search(&vector, "metadata-filter-proof", 1, &options)?
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
options.media_artist = Some("Absent".into());
|
||||
assert!(store
|
||||
.search(&vector, "metadata-filter-proof", 1, &options)?
|
||||
.is_empty());
|
||||
options.media_artist = Some("BJÖRK".into());
|
||||
assert_eq!(
|
||||
store
|
||||
.search(&vector, "metadata-filter-proof", 1, &options)?
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
tags.insert("artist".into(), "Replacement".into());
|
||||
lexical.set_structured_media_metadata_at_revision(
|
||||
&path,
|
||||
&tags,
|
||||
1000,
|
||||
identity.bytes,
|
||||
identity.modified_ns,
|
||||
revision,
|
||||
)?;
|
||||
assert!(store
|
||||
.search(&vector, "metadata-filter-proof", 1, &options)?
|
||||
.is_empty());
|
||||
options.media_artist = Some("Replacement".into());
|
||||
assert_eq!(
|
||||
store
|
||||
.search(&vector, "metadata-filter-proof", 1, &options)?
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
lexical.index_paths_cancellable(
|
||||
&root,
|
||||
&[std::path::PathBuf::from(&path)],
|
||||
&searchi_core::IndexOptions::default(),
|
||||
&std::sync::atomic::AtomicBool::new(false),
|
||||
)?;
|
||||
assert!(store
|
||||
.search(&vector, "metadata-filter-proof", 1, &options)?
|
||||
.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_origins_persist_filters_and_changes_remove_stale_candidates() -> Result<()> {
|
||||
let directory = tempfile::tempdir()?;
|
||||
|
||||
Reference in new issue
Block a user