///|
/// 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 advance_to(Self, DocId) -> 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
}
///|
priv struct TermScorer {
cursor : PostingCursor
segment : Segment
bm25 : Bm25Scorer
}
///|
impl Scorer for TermScorer with fn advance(self) {
self.cursor.advance()
}
///|
impl Scorer for TermScorer with fn advance_to(self, target) {
self.cursor.advance_to(target)
}
///|
impl Scorer for TermScorer with fn doc(self) {
self.cursor.doc()
}
///|
impl Scorer for TermScorer with fn score(self) {
match self.cursor.posting() {
Some(posting) => self.bm25.score(posting, self.segment)
None => 0.0
}
}
///|
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) {
TermScorer::{
cursor: segment.posting_cursor(self.term),
segment,
bm25: self.bm25,
}
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
}
///|
priv struct BoostScorer {
child : &Scorer
boost : Double
}
///|
impl Scorer for BoostScorer with fn advance(self) {
self.child.advance()
}
///|
impl Scorer for BoostScorer with fn advance_to(self, target) {
self.child.advance_to(target)
}
///|
impl Scorer for BoostScorer with fn doc(self) {
self.child.doc()
}
///|
impl Scorer for BoostScorer with fn score(self) {
self.child.score() * self.boost
}
///|
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) {
BoostScorer::{ child: self.weight.scorer(segment), boost: self.boost }
as &Scorer
}
///|
/// Boolean occurrence semantics modeled after Lucene and Tantivy.
pub(all) enum Occur {
Must
Filter
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]
}
///|
priv struct BooleanScorer {
required : ReadOnlyArray[&Scorer]
optional : ReadOnlyArray[&Scorer]
prohibited : ReadOnlyArray[&Scorer]
mut current_doc : DocId
mut current_score : Double
}
///|
priv struct FilterScorer {
child : &Scorer
}
///|
impl Scorer for FilterScorer with fn advance(self) {
self.child.advance()
}
///|
impl Scorer for FilterScorer with fn advance_to(self, target) {
self.child.advance_to(target)
}
///|
impl Scorer for FilterScorer with fn doc(self) {
self.child.doc()
}
///|
impl Scorer for FilterScorer with fn score(_self) {
0.0
}
///|
fn BooleanScorer::is_prohibited(
self : BooleanScorer,
candidate : DocId,
) -> Bool {
for scorer in self.prohibited {
if scorer.advance_to(candidate) && scorer.doc() == candidate {
return true
}
}
false
}
///|
fn BooleanScorer::seek_required(
self : BooleanScorer,
initial_target : DocId,
) -> Bool {
let mut candidate = initial_target
while true {
let mut aligned = false
while !aligned {
aligned = true
for scorer in self.required {
if !scorer.advance_to(candidate) {
self.current_doc = DocId::new(-1)
return false
}
if scorer.doc().value > candidate.value {
candidate = scorer.doc()
aligned = false
}
}
}
if self.is_prohibited(candidate) {
candidate = DocId::new(candidate.value + 1)
continue
}
let mut score = 0.0
for scorer in self.required {
score += scorer.score()
}
for scorer in self.optional {
if scorer.advance_to(candidate) && scorer.doc() == candidate {
score += scorer.score()
}
}
self.current_doc = candidate
self.current_score = score
return true
} nobreak {
false
}
}
///|
fn BooleanScorer::seek_optional(
self : BooleanScorer,
initial_target : DocId,
) -> Bool {
let mut target = initial_target
while true {
let mut found = false
let mut candidate = DocId::new(-1)
for scorer in self.optional {
if scorer.advance_to(target) &&
(!found || scorer.doc().value < candidate.value) {
candidate = scorer.doc()
found = true
}
}
if !found {
self.current_doc = DocId::new(-1)
return false
}
if self.is_prohibited(candidate) {
target = DocId::new(candidate.value + 1)
continue
}
let mut score = 0.0
for scorer in self.optional {
if scorer.doc() == candidate {
score += scorer.score()
}
}
self.current_doc = candidate
self.current_score = score
return true
} nobreak {
false
}
}
///|
impl Scorer for BooleanScorer with fn advance(self) {
let target = if self.current_doc.value < 0 {
DocId::new(0)
} else {
DocId::new(self.current_doc.value + 1)
}
Scorer::advance_to(self, target)
}
///|
impl Scorer for BooleanScorer with fn advance_to(self, target) {
if self.current_doc.value >= target.value {
return true
}
if self.required.length() > 0 {
self.seek_required(target)
} else if self.optional.length() > 0 {
self.seek_optional(target)
} else {
self.current_doc = DocId::new(-1)
false
}
}
///|
impl Scorer for BooleanScorer with fn doc(self) {
self.current_doc
}
///|
impl Scorer for BooleanScorer with fn score(self) {
self.current_score
}
///|
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 required : Array[&Scorer] = []
let optional : Array[&Scorer] = []
let prohibited : Array[&Scorer] = []
for clause in self.clauses {
let child = clause.weight.scorer(segment)
match clause.occur {
Occur::Must => required.push(child)
Occur::Filter => required.push(FilterScorer::{ child, } as &Scorer)
Occur::Should => optional.push(child)
Occur::MustNot => prohibited.push(child)
}
}
BooleanScorer::{
required: ReadOnlyArray::from_array(required),
optional: ReadOnlyArray::from_array(optional),
prohibited: ReadOnlyArray::from_array(prohibited),
current_doc: DocId::new(-1),
current_score: 0.0,
}
as &Scorer
}
///|
/// Exact zero-slop phrase query within one field.
pub struct PhraseQuery {
field_id : FieldId
texts : ReadOnlyArray[String]
positions : ReadOnlyArray[Int]
slop : 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),
slop: 0,
}
}
///|
/// 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),
slop: 0,
}
}
///|
/// Allows the ordered phrase to consume up to `slop` positional moves. A
/// value of zero preserves exact phrase semantics.
pub fn PhraseQuery::with_slop(self : PhraseQuery, slop : Int) -> PhraseQuery {
guard slop >= 0 else { abort("phrase slop must be non-negative") }
{
field_id: self.field_id,
texts: self.texts,
positions: self.positions,
slop,
}
}
///|
pub fn PhraseQuery::slop(self : PhraseQuery) -> Int {
self.slop
}
///|
priv struct PhraseWeight {
terms : ReadOnlyArray[Term]
positions : ReadOnlyArray[Int]
slop : 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,
slop: self.slop,
bm25_scorers: ReadOnlyArray::from_array(bm25_scorers),
}
as &Weight
}
///|
priv struct PhraseScorer {
cursors : ReadOnlyArray[PostingCursor]
positions : ReadOnlyArray[Int]
slop : Int
bm25_scorers : ReadOnlyArray[Bm25Scorer]
segment : Segment
mut current_doc : DocId
mut current_score : Double
}
///|
impl Scorer for PhraseScorer with fn advance(self) {
let target = if self.current_doc.value < 0 {
DocId::new(0)
} else {
DocId::new(self.current_doc.value + 1)
}
Scorer::advance_to(self, target)
}
///|
impl Scorer for PhraseScorer with fn advance_to(self, target) {
if self.current_doc.value >= target.value {
return true
}
let mut candidate = target
while true {
let mut aligned = false
while !aligned {
aligned = true
for cursor in self.cursors {
if !cursor.advance_to(candidate) {
self.current_doc = DocId::new(-1)
return false
}
if cursor.doc().value > candidate.value {
candidate = cursor.doc()
aligned = false
}
}
}
let matched_postings : Array[Posting] = []
for cursor in self.cursors {
match cursor.posting() {
Some(posting) => matched_postings.push(posting)
None => abort("aligned phrase cursor has no posting")
}
}
if phrase_matches(matched_postings, self.positions, self.slop) {
let mut score = 0.0
for term_index in 0.. Bool {
if postings.length() == 0 {
return false
}
for start_position in postings[0].positions {
let previous_positions : Array[Int] = [start_position]
let previous_costs : Array[Int] = [0]
let mut matched_terms = 1
for term_offset in 1.. 0 && actual_delta == 0) {
continue
}
let move_cost = (actual_delta - expected_delta).abs()
let total_cost = previous_costs[state_index] + move_cost
if total_cost <= slop {
match next_positions.search_by(position => position == candidate) {
Some(existing) =>
if total_cost < next_costs[existing] {
next_costs[existing] = total_cost
}
None => {
next_positions.push(candidate)
next_costs.push(total_cost)
}
}
}
}
}
if next_positions.length() == 0 {
break
}
previous_positions.clear()
previous_costs.clear()
for position in next_positions {
previous_positions.push(position)
}
for cost in next_costs {
previous_costs.push(cost)
}
matched_terms += 1
}
if matched_terms == postings.length() {
return true
}
}
false
}
///|
impl Weight for PhraseWeight with fn scorer(self, segment) {
if self.terms.length() == 0 {
return EmptyScorer::{ } as &Scorer
}
let cursors : Array[PostingCursor] = []
for term in self.terms {
cursors.push(segment.posting_cursor(term))
}
PhraseScorer::{
cursors: ReadOnlyArray::from_array(cursors),
positions: self.positions,
slop: self.slop,
bm25_scorers: self.bm25_scorers,
segment,
current_doc: DocId::new(-1),
current_score: 0.0,
}
as &Scorer
}