diff --git a/crates/storage/src/database.rs b/crates/storage/src/database.rs index 420d93d..dddc9bb 100644 --- a/crates/storage/src/database.rs +++ b/crates/storage/src/database.rs @@ -888,6 +888,61 @@ mod tests { assert!(deleted.is_none()); } + #[tokio::test] + async fn search_transcripts_uses_fts5_and_ranks_by_relevance() { + // Locks in the FTS5 behaviour contract: MATCH-based substring-like + // search (case-insensitive tokens) with rank-ordered results, so + // anyone swapping `search_transcripts` out for embeddings later + // knows what invariants they must preserve. + let pool = test_pool().await; + + let rows = [ + ("t1", "The Parakeet speech model runs on sherpa-onnx."), + ( + "t2", + "Parakeet parakeet parakeet dominates English benchmarks.", + ), + ("t3", "Whisper large-v3 remains the multilingual champion."), + ]; + + for (id, text) in rows { + insert_transcript( + &pool, + &InsertTranscriptParams { + id, + text, + source: "microphone", + profile_id: crate::DEFAULT_PROFILE_ID, + title: None, + audio_path: None, + duration: 1.0, + engine: Some("whisper"), + model_id: Some("whisper-tiny-en"), + inference_ms: None, + sample_rate: None, + audio_channels: None, + format_mode: None, + remove_fillers: false, + british_english: false, + anti_hallucination: false, + }, + ) + .await + .unwrap(); + } + + let hits = search_transcripts(&pool, "parakeet", 10).await.unwrap(); + let ids: Vec<&str> = hits.iter().map(|row| row.id.as_str()).collect(); + assert_eq!( + ids, + vec!["t2", "t1"], + "t3 has no 'parakeet' token; t2 outranks t1 on repetition" + ); + + let no_match = search_transcripts(&pool, "moonshine", 10).await.unwrap(); + assert!(no_match.is_empty()); + } + #[tokio::test] async fn task_crud_roundtrip() { let pool = test_pool().await;