///|
/// Minimal field-aware query-time analysis. Query syntax parsing remains a
/// separate concern; this type converts raw text into existing Query objects.
pub struct QueryParser {
schema : Schema
tokenizers : TokenizerManager
mut conjunction_by_default : Bool
}
///|
pub fn QueryParser::new(
schema : Schema,
tokenizers : TokenizerManager,
) -> QueryParser {
{ schema, tokenizers: tokenizers.snapshot(), conjunction_by_default: false }
}
///|
pub fn QueryParser::set_conjunction_by_default(
self : QueryParser,
conjunction : Bool,
) -> Unit {
self.conjunction_by_default = conjunction
}
///|
pub fn QueryParser::analyze(
self : QueryParser,
field_id : FieldId,
text : String,
) -> Array[Token] raise AnalysisError {
guard self.schema.is_indexed(field_id) else {
raise AnalysisError::FieldNotIndexed(field_id)
}
let tokenizer_name = match self.schema.tokenizer_name(field_id) {
Some(name) => name
None => raise AnalysisError::FieldNotIndexed(field_id)
}
self.tokenizers.analyze(tokenizer_name, text)
}
///|
/// Analyzes raw text and creates one TermQuery or a BooleanQuery. Multiple
/// terms are disjunctive by default, matching Tantivy's default parser mode.
pub fn QueryParser::parse_query(
self : QueryParser,
field_id : FieldId,
text : String,
) -> &Query raise AnalysisError {
let tokens = self.analyze(field_id, text)
guard tokens.length() > 0 else { raise AnalysisError::EmptyQuery }
let paths = @analysis.TokenGraph::from_tokens(tokens).finite_strings()
if self.conjunction_by_default && paths.length() > 1 {
let alternatives : Array[BooleanClause] = []
for path in paths {
let path_clauses : Array[BooleanClause] = []
for token in path.tokens() {
path_clauses.push(
BooleanClause::new(
Occur::Must,
TermQuery::new(Term::new(field_id, token.text)),
),
)
}
let path_query : &Query = if path_clauses.length() == 1 {
path_clauses[0].query
} else {
BooleanQuery::new(path_clauses) as &Query
}
alternatives.push(BooleanClause::new(Occur::Should, path_query))
}
return BooleanQuery::new(alternatives) as &Query
}
let terms : Array[String] = []
for token in tokens {
if terms.search_by(text => text == token.text) is None {
terms.push(token.text)
}
}
if terms.length() == 1 {
return TermQuery::new(Term::new(field_id, terms[0])) as &Query
}
let occur = if self.conjunction_by_default {
Occur::Must
} else {
Occur::Should
}
let clauses : Array[BooleanClause] = []
for term_text in terms {
clauses.push(
BooleanClause::new(occur, TermQuery::new(Term::new(field_id, term_text))),
)
}
BooleanQuery::new(clauses) as &Query
}
///|
/// Analyzes raw text and preserves analyzer positions in a PhraseQuery.
pub fn QueryParser::parse_phrase(
self : QueryParser,
field_id : FieldId,
text : String,
) -> &Query raise AnalysisError {
let tokens = self.analyze(field_id, text)
guard tokens.length() > 0 else { raise AnalysisError::EmptyQuery }
let paths = @analysis.TokenGraph::from_tokens(tokens).finite_strings()
let alternatives : Array[BooleanClause] = []
for path in paths {
let texts : Array[String] = []
let positions : Array[Int] = []
for token in path.tokens() {
texts.push(token.text)
positions.push(token.position)
}
alternatives.push(
BooleanClause::new(
Occur::Should,
PhraseQuery::with_positions(field_id, texts, positions),
),
)
}
if alternatives.length() == 1 {
alternatives[0].query
} else {
BooleanQuery::new(alternatives) as &Query
}
}