///|
/// Describes matching intent and builds query-and-snapshot-specific state.
///
/// The trait is open so applications can add query types without changing
/// Searcher. Concrete queries create one Weight from global SearchStatistics,
/// and each Weight creates a comparable segment-local Scorer.
pub(open) trait Query {
fn weight(Self, SearchStatistics) -> &Weight
}
///|
/// Query state that can create a Scorer for an immutable segment.
pub(open) trait Weight {
fn scorer(Self, Segment) -> &Scorer
}
///|
/// Ordered iterator over matching segment-local documents and their scores.
pub(open) trait Scorer {
fn advance(Self) -> Bool
fn doc(Self) -> DocId
fn score(Self) -> Double
}
///|
/// Exact field-qualified term query.
///
/// The term is already analyzed. QueryParser is the higher-level entry point
/// for raw field text; callers can keep using TermQuery for exact control.
pub struct TermQuery {
term : Term
}
///|
pub fn TermQuery::new(term : Term) -> TermQuery {
{ term, }
}
///|
pub fn TermQuery::term(self : TermQuery) -> Term {
self.term
}
///|
priv struct TermWeight {
term : Term
bm25 : Bm25Scorer
}
///|
pub impl Query for TermQuery with fn weight(self, statistics) {
TermWeight::{
term: self.term,
bm25: Bm25Scorer::from_statistics(statistics, self.term),
}
as &Weight
}
///|
impl Weight for TermWeight with fn scorer(self, segment) {
let postings = segment.postings_for(self.term)
let scored_docs : Array[ScoredDoc] = []
for posting in postings {
scored_docs.push({
doc_id: posting.doc_id,
score: self.bm25.score(posting, segment),
})
}
ArrayScorer::new(scored_docs) as &Scorer
}
///|
/// Multiplies every score produced by a child query.
pub struct BoostQuery {
query : &Query
boost : Double
}
///|
pub fn BoostQuery::new(query : &Query, boost : Double) -> BoostQuery {
{ query, boost }
}
///|
priv struct BoostWeight {
weight : &Weight
boost : Double
}
///|
pub impl Query for BoostQuery with fn weight(self, statistics) {
BoostWeight::{ weight: self.query.weight(statistics), boost: self.boost }
as &Weight
}
///|
impl Weight for BoostWeight with fn scorer(self, segment) {
let child = self.weight.scorer(segment)
let scored_docs : Array[ScoredDoc] = []
while child.advance() {
scored_docs.push({ doc_id: child.doc(), score: child.score() * self.boost })
}
ArrayScorer::new(scored_docs) as &Scorer
}
///|
/// Boolean occurrence semantics modeled after Lucene and Tantivy.
pub(all) enum Occur {
Must
Should
MustNot
} derive(Eq, @debug.Debug)
///|
/// One query and its occurrence rule in a BooleanQuery.
pub struct BooleanClause {
occur : Occur
query : &Query
}
///|
pub fn BooleanClause::new(occur : Occur, query : &Query) -> BooleanClause {
{ occur, query }
}
///|
pub struct BooleanQuery {
clauses : ReadOnlyArray[BooleanClause]
}
///|
pub fn BooleanQuery::new(clauses : Array[BooleanClause]) -> BooleanQuery {
let frozen : Array[BooleanClause] = []
for clause in clauses {
frozen.push(clause)
}
{ clauses: ReadOnlyArray::from_array(frozen) }
}
///|
priv struct WeightedClause {
occur : Occur
weight : &Weight
}
///|
priv struct BooleanWeight {
clauses : ReadOnlyArray[WeightedClause]
}
///|
pub impl Query for BooleanQuery with fn weight(self, statistics) {
let clauses : Array[WeightedClause] = []
for clause in self.clauses {
clauses.push({
occur: clause.occur,
weight: clause.query.weight(statistics),
})
}
BooleanWeight::{ clauses: ReadOnlyArray::from_array(clauses) } as &Weight
}
///|
impl Weight for BooleanWeight with fn scorer(self, segment) {
let document_count = segment.doc_count()
let must_matches = Array::make(document_count, 0)
let should_matches = Array::make(document_count, false)
let prohibited = Array::make(document_count, false)
let scores = Array::make(document_count, 0.0)
let mut must_clause_count = 0
for clause in self.clauses {
if clause.occur == Occur::Must {
must_clause_count += 1
}
let child = clause.weight.scorer(segment)
while child.advance() {
let doc_index = child.doc().value
if doc_index < 0 || doc_index >= document_count {
continue
}
match clause.occur {
Occur::Must => {
must_matches[doc_index] += 1
scores[doc_index] += child.score()
}
Occur::Should => {
should_matches[doc_index] = true
scores[doc_index] += child.score()
}
Occur::MustNot => prohibited[doc_index] = true
}
}
}
let scored_docs : Array[ScoredDoc] = []
for doc_index in 0.. 0 {
true
} else {
should_matches[doc_index]
}
if has_required && has_positive && !prohibited[doc_index] {
scored_docs.push({
doc_id: DocId::new(doc_index),
score: scores[doc_index],
})
}
}
ArrayScorer::new(scored_docs) as &Scorer
}
///|
/// Exact zero-slop phrase query within one field.
pub struct PhraseQuery {
field_id : FieldId
texts : ReadOnlyArray[String]
positions : ReadOnlyArray[Int]
}
///|
pub fn PhraseQuery::new(
field_id : FieldId,
texts : Array[String],
) -> PhraseQuery {
let frozen : Array[String] = []
let positions : Array[Int] = []
for text in texts {
frozen.push(text)
positions.push(positions.length())
}
{
field_id,
texts: ReadOnlyArray::from_array(frozen),
positions: ReadOnlyArray::from_array(positions),
}
}
///|
/// Creates a phrase with explicit relative token positions, preserving gaps
/// introduced during query-time analysis.
pub fn PhraseQuery::with_positions(
field_id : FieldId,
texts : Array[String],
positions : Array[Int],
) -> PhraseQuery {
guard texts.length() == positions.length() else {
abort("phrase texts and positions must have equal length")
}
let frozen_texts : Array[String] = []
let frozen_positions : Array[Int] = []
let mut previous_position = -1
for index in 0..= previous_position else {
abort("phrase positions must be non-decreasing")
}
frozen_texts.push(texts[index])
frozen_positions.push(positions[index])
previous_position = positions[index]
}
{
field_id,
texts: ReadOnlyArray::from_array(frozen_texts),
positions: ReadOnlyArray::from_array(frozen_positions),
}
}
///|
priv struct PhraseWeight {
terms : ReadOnlyArray[Term]
positions : ReadOnlyArray[Int]
bm25_scorers : ReadOnlyArray[Bm25Scorer]
}
///|
pub impl Query for PhraseQuery with fn weight(self, statistics) {
let terms : Array[Term] = []
let bm25_scorers : Array[Bm25Scorer] = []
for text in self.texts {
let term = Term::new(self.field_id, text)
terms.push(term)
bm25_scorers.push(Bm25Scorer::from_statistics(statistics, term))
}
PhraseWeight::{
terms: ReadOnlyArray::from_array(terms),
positions: self.positions,
bm25_scorers: ReadOnlyArray::from_array(bm25_scorers),
}
as &Weight
}
///|
fn posting_for_doc(
postings : ReadOnlyArray[Posting],
doc_id : DocId,
) -> Posting? {
match postings.search_by(posting => posting.doc_id == doc_id) {
Some(index) => Some(postings[index])
None => None
}
}
///|
fn contains_position(positions : ReadOnlyArray[Int], expected : Int) -> Bool {
positions.search_by(position => position == expected) is Some(_)
}
///|
fn phrase_matches(
postings : Array[Posting],
query_positions : ReadOnlyArray[Int],
) -> Bool {
if postings.length() == 0 {
return false
}
let base_position = query_positions[0]
for start_position in postings[0].positions {
let mut matched = true
for term_offset in 1.. matched_postings.push(posting)
None => {
all_terms_match = false
break
}
}
}
if all_terms_match && phrase_matches(matched_postings, self.positions) {
let mut score = 0.0
for term_index in 0..