From f48b2ad895c8028210fa031f3c56fff1dd79052d Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sun, 18 Oct 2026 02:39:08 +0200 Subject: [PATCH] Search words with an unknown field prefix as plain text (#2099) Found while testing crates/catalog/src/query.rs:146-177 (token_matches) Co-authored-by: Claude Haiku 5.5 --- crates/catalog/src/query.rs | 11 +++++++++-- crates/catalog/src/tests.rs | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/crates/catalog/src/query.rs b/crates/catalog/src/query.rs index d1548f8bd..311f6dcde 100644 --- a/crates/catalog/src/query.rs +++ b/crates/catalog/src/query.rs @@ -173,11 +173,18 @@ fn token_matches(p: &Photo, tok: &str) -> bool { "date" => p.captured.as_deref().is_some_and(|c| c.starts_with(val)), "copy" | "virtual" => (val == "true" || val == "yes") == p.copy_of.is_some(), "name" | "file" => p.file_name.to_lowercase().contains(val), - _ => false, + // Not a known field: the colon is part of a plain word ("10:30", "Re:"). + _ => text_matches(p, &t), }; } + text_matches(p, &t) +} + +/// Whether the lower-cased word `t` is in the file name, title, caption, camera, lens, location, +/// format or a keyword of `p`. +fn text_matches(p: &Photo, t: &str) -> bool { let hay = [&p.file_name, &p.meta.title, &p.meta.caption, &p.meta.camera, &p.meta.lens, &p.meta.location, &p.format]; - hay.iter().any(|h| h.to_lowercase().contains(&t)) || p.meta.keywords.iter().any(|k| k.to_lowercase().contains(&t)) + hay.iter().any(|h| h.to_lowercase().contains(t)) || p.meta.keywords.iter().any(|k| k.to_lowercase().contains(t)) } /// Whether `p` has a named face region called `name` (case-insensitive). diff --git a/crates/catalog/src/tests.rs b/crates/catalog/src/tests.rs index 8f1aaf151..255417098 100644 --- a/crates/catalog/src/tests.rs +++ b/crates/catalog/src/tests.rs @@ -123,6 +123,27 @@ fn filter_search_sort() { assert_eq!(c.keywords(), vec![("ocean".to_string(), 1), ("summer".to_string(), 1)]); } +/// A search word with a colon whose left part is not a known field ("10:30", "Re:") is plain +/// text, not a fielded token that matches nothing. +#[test] +fn free_text_with_a_colon_searches_the_text_fields() { + let mut c = Catalog::new(); + let a = photo(&mut c, "meeting.jpg", "2026-05-01T10:00:00"); + let _b = photo(&mut c, "other.jpg", "2026-04-01T10:00:00"); + let mut m = c.photo(a).unwrap().meta.clone(); + m.caption = "Meeting at 10:30 with Re: notes".into(); + m.iso = Some(1600); + c.apply(Op::SetMeta { id: a, meta: Box::new(m) }).unwrap(); + let q = |text: &str| c.query(&Filter { text: text.into(), ..Default::default() }, &Sort::default()); + assert_eq!(q("meeting"), vec![a]); + assert_eq!(q("10:30"), vec![a], "a free-text token with a colon"); + assert_eq!(q("re:"), vec![a]); + assert_eq!(q("RE: notes"), vec![a], "case-insensitive, one token per word"); + assert_eq!(q("iso:>800"), vec![a], "known fields stay fielded"); + assert_eq!(q("iso:>2000"), Vec::::new()); + assert_eq!(q("11:45"), Vec::::new(), "an unknown field matching no text finds nothing"); +} + /// A `captured` string that is not ASCII (the Lightroom importer copies `captureTime` into an /// `AddPhoto` op verbatim, and a JSON snapshot is valid UTF-8 whatever it holds) must not be /// sliced on a byte index in `date_groups`: grouping counts the well-formed dates and ignores