Library-wide full-text search (SQLite FTS5), in both UIs (3.x)
A new FTS5 index (`video_search`) over every video's title, channel, and description — searchable across the whole library, not just the loaded grid. `search_meta` tracks each video's mtime so a routine rescan only re-reads a description sidecar when the video actually changed; vanished videos are evicted. The index is refreshed after every scan in both front-ends via the shared `library::build_search_entries`. - database.rs: schema + `sync_search_index` (mtime-gated, one txn) + `search_videos` (ranked, with a highlighted snippet) + a safe prefix MATCH builder. Unit-tested (index/search/prefix/AND/evict/garbage). - web.rs: `GET /api/search?q=&limit=`; index synced after the initial scan, rescan, and maintenance-remove. - Web UI: a 🔍 header button + `f` shortcut open a debounced search modal with ranked results and highlighted description snippets; clicking a result jumps to it. - Desktop (egui): a 🔎 Search button opens a floating results window querying the same index; clicking a result plays the video. Closes the desktop/web parity gap for search. Integration test seeds a real video + description, rescans, and asserts title / prefix / description-only matches hit and unrelated misses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3031b5b0e5
commit
a47c7991b4
6 changed files with 447 additions and 1 deletions
33
tests/api.rs
33
tests/api.rs
|
|
@ -318,3 +318,36 @@ fn backup_db_returns_sqlite() {
|
|||
assert_eq!(code, 200);
|
||||
assert!(body.starts_with("SQLite format 3"), "backup is a SQLite file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_text_search_indexes_titles_and_descriptions() {
|
||||
if !have_curl() { eprintln!("skip: no curl"); return; }
|
||||
let s = Server::start();
|
||||
|
||||
// Seed a real video on disk: <dir>/ch/channels/TestChan/<Title> [id].mkv
|
||||
// plus a .description sidecar (the description word isn't in the title).
|
||||
let chan = s.dir.join("ch/channels/TestChan");
|
||||
std::fs::create_dir_all(&chan).unwrap();
|
||||
std::fs::write(chan.join("Cooking Sourdough [vid123].mkv"), b"").unwrap();
|
||||
std::fs::write(chan.join("Cooking Sourdough [vid123].description"),
|
||||
b"an artisan bread tutorial").unwrap();
|
||||
|
||||
// Rescan so the server picks up the new file and reindexes it.
|
||||
let (code, _) = s.post("/api/rescan", "");
|
||||
assert_eq!(code, 200);
|
||||
|
||||
// Title term hits.
|
||||
let (code, body) = s.get("/api/search?q=sourdough");
|
||||
assert_eq!(code, 200, "{body}");
|
||||
assert!(body.contains("vid123"), "title search should hit: {body}");
|
||||
|
||||
// Prefix / type-ahead hits.
|
||||
assert!(s.get("/api/search?q=sourd").1.contains("vid123"), "prefix search should hit");
|
||||
|
||||
// A word only in the description hits (proves the sidecar got indexed).
|
||||
assert!(s.get("/api/search?q=artisan").1.contains("vid123"), "description search should hit");
|
||||
|
||||
// An unrelated query does not.
|
||||
assert!(!s.get("/api/search?q=quantumchromodynamics").1.contains("vid123"),
|
||||
"unrelated query must not hit");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue