///|
/// A disjunction of scored terms with an optimized WAND Top-K entry point.
pub struct BlockMaxWandQuery {
terms : ReadOnlyArray[Term]
}
///|
pub fn BlockMaxWandQuery::new(terms : Array[Term]) -> BlockMaxWandQuery {
let unique : Array[Term] = []
for term in terms {
if unique.search_by(existing => existing == term) is None {
unique.push(term)
}
}
{ terms: ReadOnlyArray::from_array(unique) }
}
///|
pub(all) struct WandSearchResult {
hits : ReadOnlyArray[SearchHit]
evaluated_candidates : Int
skipped_candidates : Int
} derive(@debug.Debug)
///|
priv struct WandWeight {
child : &Weight
}
///|
pub impl Query for BlockMaxWandQuery with fn weight(self, statistics) {
let clauses : Array[BooleanClause] = []
for term in self.terms {
clauses.push(
BooleanClause::new(Occur::Should, TermQuery::new(term) as &Query),
)
}
WandWeight::{ child: Query::weight(BooleanQuery::new(clauses), statistics) }
as &Weight
}
///|
impl Weight for WandWeight with fn scorer(self, segment) {
self.child.scorer(segment)
}
///|
priv struct WandTermState {
cursor : PostingCursor
bm25 : Bm25Scorer
upper_bound : Double
}
///|
fn wand_term_state(
segment : Segment,
term : Term,
statistics : SearchStatistics,
) -> WandTermState? {
let postings = segment.postings_for(term)
if postings.length() == 0 {
return None
}
let bm25 = Bm25Scorer::from_statistics(statistics, term)
let mut upper_bound = 0.0
for posting in postings {
let score = bm25.score(posting, segment)
if score > upper_bound {
upper_bound = score
}
}
let cursor = segment.posting_cursor(term)
if !cursor.advance() {
return None
}
Some({ cursor, bm25, upper_bound })
}
///|
/// Runs safe WAND pruning using exact per-term upper bounds. Returned hits are
/// identical to the ordinary disjunction and deterministic Top-K tie-break.
pub fn Searcher::search_wand(
self : Searcher,
query : BlockMaxWandQuery,
collector : TopKCollector,
) -> WandSearchResult {
let heap = TopKHeap::new(collector.limit)
let mut evaluated_candidates = 0
let mut skipped_candidates = 0
for segment_ord in 0.. states.push(state)
None => ()
}
}
while true {
let active : Array[Int] = []
for index in 0..= 0 {
active.push(index)
}
}
if active.length() == 0 {
break
}
active.sort_by((left, right) => {
states[left].cursor.doc().value.compare(
states[right].cursor.doc().value,
)
})
let threshold = match heap.competitive_score() {
Some(score) => score
None => -1.0
}
let mut upper_sum = 0.0
let mut pivot_offset = -1
for offset in 0.. threshold {
pivot_offset = offset
break
}
}
if pivot_offset < 0 {
break
}
let pivot_doc = states[active[pivot_offset]].cursor.doc()
let first_doc = states[active[0]].cursor.doc()
if first_doc == pivot_doc {
let mut score = 0.0
for state_index in active {
if states[state_index].cursor.doc() == pivot_doc {
match states[state_index].cursor.posting() {
Some(posting) =>
score += states[state_index].bm25.score(
posting,
snapshot.segment,
)
None => ()
}
}
}
evaluated_candidates += 1
if !snapshot.is_deleted(pivot_doc) {
heap.offer({ address: DocAddress::new(segment_ord, pivot_doc), score })
}
for state_index in active {
if states[state_index].cursor.doc() == pivot_doc {
ignore(states[state_index].cursor.advance())
}
}
} else {
for offset in 0.. before + 1 {
skipped_candidates += after - before - 1
}
}
}
}
}
{ hits: collector.finish(heap), evaluated_candidates, skipped_candidates }
}