use crate::fts::bm25::Bm25Params;
use crate::fts::index::{DEFAULT_FIELD, Document, FieldId, LexicalIndex, SegmentIndex};
use crate::fts::prune::{Strategy, search_pruned};
use crate::fts::search::{FieldWeights, TermQuery, search};
use crate::fts::tokenizer::{Analyzer, Profile};
const SMALL_MATCHES: usize = 64;
const LARGE_MATCHES: usize = 1_024;
const GROWTH_ALLOWANCE: u64 = 32;
fn corpus(matches: usize) -> LexicalIndex {
#[expect(
clippy::expect_used,
reason = "a fixed profile is valid; a failure here is a broken build, not bad input"
)]
let analyzer = Analyzer::new(Profile::Code.config()).expect("valid profile");
let mut segment = SegmentIndex::new();
for ordinal in 0..matches {
let text = format!("alpha filler{ordinal} beta gamma delta");
#[expect(
clippy::expect_used,
reason = "the fixture text is analyzable by construction"
)]
segment
.push_document(&analyzer, &Document::with_text(&text))
.expect("indexable fixture");
}
let mut index = LexicalIndex::new();
#[expect(
clippy::expect_used,
reason = "sealing a fixture segment cannot fail; a failure is a broken writer"
)]
index.push_segment(segment).expect("seals");
index
}
fn exhaustive_allocations(matches: usize) -> u64 {
let index = corpus(matches);
let query = TermQuery::flat(vec![b"alpha".to_vec()], &[DEFAULT_FIELD]);
drop(search(&index, &query, 10, Bm25Params::default()));
let (result, report) = crate::allocation_audit::audit_engine_path(|| {
search(&index, &query, 10, Bm25Params::default())
});
drop(result);
report.allocations
}
#[test]
fn exhaustive_query_allocations_do_not_scale_with_postings() {
let small = exhaustive_allocations(SMALL_MATCHES);
let large = exhaustive_allocations(LARGE_MATCHES);
assert!(
large <= small.saturating_add(GROWTH_ALLOWANCE),
"the exhaustive scorer allocates per posting: {small} allocations at \
{SMALL_MATCHES} matches, {large} at {LARGE_MATCHES}"
);
}
fn pruned_allocations(matches: usize) -> u64 {
let index = corpus(matches);
let query = TermQuery::flat(vec![b"alpha".to_vec()], &[DEFAULT_FIELD]);
drop(search_pruned(
&index,
&query,
10,
Bm25Params::default(),
Strategy::BlockMaxWand,
));
let (result, report) = crate::allocation_audit::audit_engine_path(|| {
search_pruned(
&index,
&query,
10,
Bm25Params::default(),
Strategy::BlockMaxWand,
)
});
drop(result);
report.allocations
}
const TITLE: FieldId = FieldId(0);
const BODY: FieldId = FieldId(1);
fn multifield_corpus(matches: usize) -> LexicalIndex {
#[expect(
clippy::expect_used,
reason = "a fixed profile is valid; a failure here is a broken build, not bad input"
)]
let analyzer = Analyzer::new(Profile::Code.config()).expect("valid profile");
let mut segment = SegmentIndex::new();
for ordinal in 0..matches {
let mut document = Document::new();
document.set(TITLE, &format!("alpha title{ordinal}"));
document.set(BODY, &format!("alpha filler{ordinal} beta gamma delta"));
#[expect(
clippy::expect_used,
reason = "the fixture text is analyzable by construction"
)]
segment
.push_document(&analyzer, &document)
.expect("indexable fixture");
}
let mut index = LexicalIndex::new();
#[expect(
clippy::expect_used,
reason = "sealing a fixture segment cannot fail; a failure is a broken writer"
)]
index.push_segment(segment).expect("seals");
index
}
fn multifield_allocations(matches: usize) -> u64 {
let index = multifield_corpus(matches);
let query = TermQuery {
terms: vec![b"alpha".to_vec()],
fields: FieldWeights::flat(&[TITLE, BODY]),
};
drop(search(&index, &query, 10, Bm25Params::default()));
let (result, report) = crate::allocation_audit::audit_engine_path(|| {
search(&index, &query, 10, Bm25Params::default())
});
drop(result);
report.allocations
}
#[test]
fn cross_field_union_allocations_do_not_scale_with_postings() {
let small = multifield_allocations(SMALL_MATCHES);
let large = multifield_allocations(LARGE_MATCHES);
assert!(
large <= small.saturating_add(GROWTH_ALLOWANCE),
"the cross-field union allocates per posting: {small} allocations at \
{SMALL_MATCHES} matches, {large} at {LARGE_MATCHES}"
);
}
#[test]
fn pruned_query_allocations_do_not_scale_with_postings() {
let small = pruned_allocations(SMALL_MATCHES);
let large = pruned_allocations(LARGE_MATCHES);
assert!(
large <= small.saturating_add(GROWTH_ALLOWANCE),
"the pruned scorer allocates per posting: {small} allocations at \
{SMALL_MATCHES} matches, {large} at {LARGE_MATCHES}"
);
}