use std::sync::Arc;
use redb::Database;
use nodedb_fts::FtsSearchParams;
use nodedb_fts::posting::QueryMode;
use nodedb_types::{Surrogate, TenantId};
use super::core::InvertedIndex;
const DB: u64 = 0;
const T: TenantId = TenantId::new(1);
fn open_temp() -> (InvertedIndex, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test-inverted.redb");
let db = Arc::new(Database::create(&path).unwrap());
let idx = InvertedIndex::open(db).unwrap();
(idx, dir)
}
#[test]
fn index_and_search() {
let (idx, _dir) = open_temp();
idx.index_document(
DB,
T,
"docs",
Surrogate::new(1),
"The quick brown fox jumps over the lazy dog",
)
.unwrap();
idx.index_document(
DB,
T,
"docs",
Surrogate::new(2),
"A fast brown dog runs across the field",
)
.unwrap();
idx.index_document(
DB,
T,
"docs",
Surrogate::new(3),
"Rust programming language for systems",
)
.unwrap();
let results = idx
.search(
DB,
T,
"docs",
FtsSearchParams {
query: "brown fox",
top_k: 10,
fuzzy_enabled: false,
mode: QueryMode::And,
prefilter: None,
},
)
.unwrap();
assert!(!results.is_empty());
assert_eq!(results[0].doc_id, Surrogate::new(1));
}
#[test]
fn search_with_stemming() {
let (idx, _dir) = open_temp();
idx.index_document(
DB,
T,
"docs",
Surrogate::new(1),
"running distributed databases",
)
.unwrap();
idx.index_document(DB, T, "docs", Surrogate::new(2), "the cat sat on a mat")
.unwrap();
let results = idx
.search(
DB,
T,
"docs",
FtsSearchParams {
query: "database distribution",
top_k: 10,
fuzzy_enabled: false,
mode: QueryMode::And,
prefilter: None,
},
)
.unwrap();
assert!(!results.is_empty());
assert_eq!(results[0].doc_id, Surrogate::new(1));
}
#[test]
fn fuzzy_search() {
let (idx, _dir) = open_temp();
idx.index_document(
DB,
T,
"docs",
Surrogate::new(1),
"distributed database systems",
)
.unwrap();
let results = idx
.search(
DB,
T,
"docs",
FtsSearchParams {
query: "databse",
top_k: 10,
fuzzy_enabled: true,
mode: QueryMode::And,
prefilter: None,
},
)
.unwrap();
assert!(!results.is_empty());
assert!(results[0].fuzzy);
}
#[test]
fn remove_document() {
let (idx, _dir) = open_temp();
idx.index_document(DB, T, "docs", Surrogate::new(1), "hello world")
.unwrap();
idx.index_document(DB, T, "docs", Surrogate::new(2), "hello rust")
.unwrap();
idx.remove_document(DB, T, "docs", Surrogate::new(1))
.unwrap();
let results = idx
.search(
DB,
T,
"docs",
FtsSearchParams {
query: "hello",
top_k: 10,
fuzzy_enabled: false,
mode: QueryMode::And,
prefilter: None,
},
)
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].doc_id, Surrogate::new(2));
}
#[test]
fn empty_query() {
let (idx, _dir) = open_temp();
idx.index_document(DB, T, "docs", Surrogate::new(1), "some text here")
.unwrap();
let results = idx
.search(
DB,
T,
"docs",
FtsSearchParams {
query: "the a is",
top_k: 10,
fuzzy_enabled: false,
mode: QueryMode::And,
prefilter: None,
},
)
.unwrap();
assert!(results.is_empty());
}
#[test]
fn collections_isolated() {
let (idx, _dir) = open_temp();
idx.index_document(DB, T, "col_a", Surrogate::new(1), "alpha bravo charlie")
.unwrap();
idx.index_document(DB, T, "col_b", Surrogate::new(1), "delta echo foxtrot")
.unwrap();
let results = idx
.search(
DB,
T,
"col_a",
FtsSearchParams {
query: "alpha",
top_k: 10,
fuzzy_enabled: false,
mode: QueryMode::And,
prefilter: None,
},
)
.unwrap();
assert_eq!(results.len(), 1);
let results = idx
.search(
DB,
T,
"col_b",
FtsSearchParams {
query: "alpha",
top_k: 10,
fuzzy_enabled: false,
mode: QueryMode::And,
prefilter: None,
},
)
.unwrap();
assert!(results.is_empty());
}
#[test]
fn reindex_same_surrogate_identical_content_does_not_double_count_stats() {
let (idx, _dir) = open_temp();
idx.index_document(DB, T, "docs", Surrogate::new(1), "alpha bravo charlie")
.unwrap();
let (count, avg_len) = idx.corpus_stats(DB, T, "docs").unwrap();
assert_eq!(count, 1, "first index must count the doc once");
assert_eq!(avg_len, 3.0, "avg doc len == the single doc's length");
idx.index_document(DB, T, "docs", Surrogate::new(1), "alpha bravo charlie")
.unwrap();
let (count, avg_len) = idx.corpus_stats(DB, T, "docs").unwrap();
assert_eq!(
count, 1,
"replaying an already-indexed doc must not bump doc count"
);
assert_eq!(
avg_len, 3.0,
"replaying an already-indexed doc must not change total token sum"
);
}
#[test]
fn reindex_same_surrogate_different_length_adjusts_total_by_delta() {
let (idx, _dir) = open_temp();
idx.index_document(DB, T, "docs", Surrogate::new(1), "alpha bravo charlie")
.unwrap();
let (count, avg_len) = idx.corpus_stats(DB, T, "docs").unwrap();
assert_eq!(count, 1);
assert_eq!(avg_len, 3.0);
idx.index_document(
DB,
T,
"docs",
Surrogate::new(1),
"alpha bravo charlie delta echo",
)
.unwrap();
let (count, avg_len) = idx.corpus_stats(DB, T, "docs").unwrap();
assert_eq!(count, 1, "re-indexing must not create a second doc count");
assert_eq!(
avg_len, 5.0,
"total must reflect the new length, not the sum of old + new"
);
}
#[test]
fn remove_document_decrements_stats() {
let (idx, _dir) = open_temp();
idx.index_document(DB, T, "docs", Surrogate::new(1), "alpha bravo charlie")
.unwrap();
idx.index_document(DB, T, "docs", Surrogate::new(2), "delta echo")
.unwrap();
let (count, avg_len) = idx.corpus_stats(DB, T, "docs").unwrap();
assert_eq!(count, 2);
assert_eq!(avg_len, 2.5);
idx.remove_document(DB, T, "docs", Surrogate::new(1))
.unwrap();
let (count, avg_len) = idx.corpus_stats(DB, T, "docs").unwrap();
assert_eq!(count, 1, "remove must decrement doc count");
assert_eq!(
avg_len, 2.0,
"remove must subtract the removed doc's length"
);
}
#[test]
fn purge_tenant_structurally_drops_data() {
let (idx, _dir) = open_temp();
let t1 = TenantId::new(1);
let t2 = TenantId::new(2);
idx.index_document(DB, t1, "docs", Surrogate::new(1), "alpha bravo")
.unwrap();
idx.index_document(DB, t2, "docs", Surrogate::new(1), "alpha bravo")
.unwrap();
idx.purge_tenant(DB, t1).unwrap();
assert!(
idx.search(
DB,
t1,
"docs",
FtsSearchParams {
query: "alpha",
top_k: 10,
fuzzy_enabled: false,
mode: QueryMode::And,
prefilter: None
}
)
.unwrap()
.is_empty()
);
assert!(
!idx.search(
DB,
t2,
"docs",
FtsSearchParams {
query: "alpha",
top_k: 10,
fuzzy_enabled: false,
mode: QueryMode::And,
prefilter: None
}
)
.unwrap()
.is_empty()
);
}