use searchez::{Document, MemoryBackend, Query, Searchable, SearchEngine};
#[derive(Clone)]
struct Article {
id: u64,
title: String,
body: String,
published: bool,
}
impl Searchable for Article {
fn index_name() -> &'static str {
"articles"
}
fn search_id(&self) -> String {
self.id.to_string()
}
fn to_document(&self) -> Document {
Document::new()
.field("title", self.title.clone())
.field("body", self.body.clone())
.field("published", self.published)
}
}
fn engine() -> SearchEngine {
SearchEngine::new(MemoryBackend::new())
}
fn article(id: u64, title: &str, body: &str, published: bool) -> Article {
Article { id, title: title.into(), body: body.into(), published }
}
#[tokio::test]
async fn index_search_and_remove() {
let e = engine();
e.index(&article(1, "Rust ownership", "borrow checker basics", true))
.await
.unwrap();
assert_eq!(e.count::<Article>().await.unwrap(), 1);
let hits = e.search::<Article>("ownership").await.unwrap();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].id, "1");
assert_eq!(hits[0].document.get("title").unwrap(), "Rust ownership");
e.remove_id::<Article>("1").await.unwrap();
assert!(e.search::<Article>("ownership").await.unwrap().is_empty());
assert_eq!(e.count::<Article>().await.unwrap(), 0);
}
#[tokio::test]
async fn reindexing_a_record_supersedes_the_old_text() {
let e = engine();
e.index(&article(1, "coffee", "dark roast", true)).await.unwrap();
e.index(&article(1, "tea", "green leaves", true)).await.unwrap();
assert!(e.search::<Article>("coffee").await.unwrap().is_empty());
assert_eq!(e.search::<Article>("tea").await.unwrap().len(), 1);
assert_eq!(e.count::<Article>().await.unwrap(), 1, "still one document, not two");
}
#[tokio::test]
async fn bm25_ranks_the_stronger_match_first() {
let e = engine();
e.index(&article(
1,
"Programming languages",
"there are many languages and rust is one of the systems languages people use",
true,
))
.await
.unwrap();
e.index(&article(2, "Rust", "rust rust systems language", true))
.await
.unwrap();
let hits = e.search::<Article>("rust").await.unwrap();
assert_eq!(hits.len(), 2);
assert_eq!(hits[0].id, "2", "the denser, shorter match ranks first");
assert!(hits[0].score > hits[1].score, "scores must be ordered");
assert!(hits[1].score > 0.0, "the weaker match still scores > 0");
}
#[tokio::test]
async fn filters_narrow_and_combine_with_text() {
let e = engine();
e.index(&article(1, "rust guide", "draft", false)).await.unwrap();
e.index(&article(2, "rust guide", "live", true)).await.unwrap();
let hits = e
.search::<Article>(Query::text("rust").filter("published", true))
.await
.unwrap();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].id, "2");
let all_drafts = e
.search::<Article>(Query::all().filter("published", false))
.await
.unwrap();
assert_eq!(all_drafts.len(), 1);
assert_eq!(all_drafts[0].id, "1");
assert_eq!(all_drafts[0].score, 0.0);
}
#[tokio::test]
async fn paging_is_stable_across_offset() {
let e = engine();
for i in 1..=5 {
e.index(&article(i, "same title", "shared body text", true)).await.unwrap();
}
let page1 = e.search::<Article>(Query::text("title").limit(2).offset(0)).await.unwrap();
let page2 = e.search::<Article>(Query::text("title").limit(2).offset(2)).await.unwrap();
let ids1: Vec<_> = page1.iter().map(|h| &h.id).collect();
let ids2: Vec<_> = page2.iter().map(|h| &h.id).collect();
assert_eq!(ids1, vec!["1", "2"]);
assert_eq!(ids2, vec!["3", "4"]);
}
#[tokio::test]
async fn reindex_rebuilds_from_a_full_set() {
let e = engine();
e.index(&article(99, "stale", "orphan document", true)).await.unwrap();
let fresh = vec![
article(1, "alpha", "first", true),
article(2, "beta", "second", true),
];
let n = e.reindex(&fresh).await.unwrap();
assert_eq!(n, 2);
assert_eq!(e.count::<Article>().await.unwrap(), 2);
assert!(e.search::<Article>("orphan").await.unwrap().is_empty());
assert_eq!(e.search::<Article>("alpha").await.unwrap()[0].id, "1");
}
#[tokio::test]
async fn search_ids_drives_hydration_in_rank_order() {
let e = engine();
e.index(&article(1, "weak rust mention buried in words words words", "x", true)).await.unwrap();
e.index(&article(2, "rust", "rust", true)).await.unwrap();
let ids = e.search_ids::<Article>("rust").await.unwrap();
assert_eq!(ids, vec!["2".to_string(), "1".to_string()]);
}