#[derive(Clone, Debug, PartialEq)]
pub enum QueryExpression {
Scoring(ScoringExpression),
Ranking(RankingExpression),
}
#[derive(Clone, Debug, PartialEq)]
pub enum RankingExpression {
Fusion {
sources: Vec<QueryExpression>,
method: FusionMethod,
rank_constant: f32,
rank_window_size: Option<usize>,
weights: Option<Vec<f32>>,
},
}
#[derive(Clone, Debug, PartialEq)]
pub enum FusionMethod {
ReciprocalRank,
Sum,
ArithmeticMean,
HarmonicMean,
GeometricMean,
}
impl QueryExpression {
pub fn as_scoring(&self) -> Option<&ScoringExpression> {
match self {
QueryExpression::Scoring(s) => Some(s),
QueryExpression::Ranking(_) => None,
}
}
pub fn scoring_expressions(&self) -> Vec<&ScoringExpression> {
match self {
QueryExpression::Scoring(s) => vec![s],
QueryExpression::Ranking(RankingExpression::Fusion { sources, .. }) => sources
.iter()
.flat_map(|s| s.scoring_expressions())
.collect(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct InnerHitsConfig {
pub name: Option<String>,
pub size: usize,
pub from: usize,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ScoringExpression {
Term { field: String, value: String },
Terms { field: String, values: Vec<String> },
Match {
field: String,
query: String,
analyzer: Option<String>,
},
MatchPhrase {
field: String,
query: String,
analyzer: Option<String>,
},
MatchBoolPrefix {
field: String,
query: String,
analyzer: Option<String>,
},
Bool {
must: Vec<ScoringExpression>,
should: Vec<ScoringExpression>,
must_not: Vec<ScoringExpression>,
filter: Vec<ScoringExpression>,
minimum_should_match: Option<u32>,
},
DisMax {
queries: Vec<ScoringExpression>,
tie_breaker: f32,
},
Exists { field: String },
Prefix { field: String, value: String },
ConstantScore {
query: Box<ScoringExpression>,
boost: f32,
},
Nested {
path: String,
query: Box<ScoringExpression>,
inner_hits: Option<InnerHitsConfig>,
},
GeoDistance {
field: String,
lat: f64,
lon: f64,
distance: String, },
GeoBoundingBox {
field: String,
top_left_lat: f64,
top_left_lon: f64,
bottom_right_lat: f64,
bottom_right_lon: f64,
},
GeoShape {
field: String,
shape: GeoShapeValue,
relation: SpatialRelation,
},
Range {
field: String,
gte: Option<f64>,
gt: Option<f64>,
lte: Option<f64>,
lt: Option<f64>,
},
Boost {
query: Box<ScoringExpression>,
boost: f32,
},
ScriptScore {
query: Box<ScoringExpression>,
script: String,
params: std::collections::HashMap<String, f64>,
},
FunctionScore {
query: Box<ScoringExpression>,
functions: Vec<ScoreFunction>,
score_mode: FunctionScoreMode,
boost_mode: FunctionBoostMode,
},
Boosting {
positive: Box<ScoringExpression>,
negative: Box<ScoringExpression>,
negative_boost: f32,
},
Fuzzy {
field: String,
value: String,
fuzziness: u32,
},
Regexp { field: String, value: String },
Wildcard { field: String, value: String },
MultiMatch {
fields: Vec<String>,
query: String,
analyzer: Option<String>,
tie_breaker: f32,
},
Span(SpanExpression),
Knn {
field: String,
query_vector: Vec<f32>,
k: usize,
num_candidates: usize,
threshold: Option<f32>,
},
MatchAll,
MatchNone,
}
#[derive(Clone, Debug, PartialEq)]
pub enum SpanExpression {
SpanTerm { field: String, value: String },
SpanNear {
field: String,
terms: Vec<String>,
slop: u32,
in_order: bool,
},
SpanNot {
include: Box<SpanExpression>,
exclude: Box<SpanExpression>,
},
SpanFirst {
query: Box<SpanExpression>,
end: u32,
},
}
#[derive(Clone, Debug, PartialEq)]
pub enum ScoreFunction {
Weight(f32),
FieldValueFactor {
field: String,
factor: f32,
modifier: FieldValueModifier,
missing: f64,
},
RandomScore { seed: u64 },
}
#[derive(Clone, Debug, PartialEq)]
pub enum FieldValueModifier {
None,
Log1p,
Log2p,
Ln1p,
Ln2p,
Sqrt,
Square,
Reciprocal,
}
#[derive(Clone, Debug, PartialEq)]
pub enum FunctionScoreMode {
Multiply,
Sum,
Avg,
First,
Max,
Min,
}
#[derive(Clone, Debug, PartialEq)]
pub enum FunctionBoostMode {
Multiply,
Replace,
Sum,
Avg,
Max,
Min,
}
#[derive(Clone, Debug, PartialEq)]
pub enum SpatialRelation {
Intersects,
Within,
Contains,
Disjoint,
Touches,
Crosses,
Overlaps,
Equals,
Covers,
CoveredBy,
ContainsProperly,
}
#[derive(Clone, Debug, PartialEq)]
pub struct GeoShapeValue {
pub json: serde_json::Value,
}
impl ScoringExpression {
pub fn bool_query(
must: Vec<ScoringExpression>,
should: Vec<ScoringExpression>,
must_not: Vec<ScoringExpression>,
filter: Vec<ScoringExpression>,
) -> Self {
Self::Bool {
must,
should,
must_not,
filter,
minimum_should_match: None,
}
}
}
impl super::Query for ScoringExpression {
fn bind(
&self,
searcher: &crate::search::searcher::Searcher,
score_mode: crate::core::ScoreMode,
) -> crate::core::Result<Box<dyn super::BoundQuery>> {
match self {
ScoringExpression::Term { field, value } => crate::query::term::TermQuery {
field: field.clone(),
value: value.clone(),
}
.bind(searcher, score_mode),
ScoringExpression::Terms { field, values } => {
let should: Vec<ScoringExpression> = values
.iter()
.map(|v| ScoringExpression::Term {
field: field.clone(),
value: v.clone(),
})
.collect();
ScoringExpression::Bool {
must: vec![],
should,
must_not: vec![],
filter: vec![],
minimum_should_match: None,
}
.bind(searcher, score_mode)
}
ScoringExpression::Match {
field,
query,
analyzer,
} => crate::query::match_query::MatchQuery {
field: field.clone(),
query_text: query.clone(),
analyzer: analyzer.clone(),
}
.bind(searcher, score_mode),
ScoringExpression::MatchPhrase {
field,
query,
analyzer,
} => crate::query::phrase::MatchPhraseQuery {
field: field.clone(),
query_text: query.clone(),
analyzer: analyzer.clone(),
}
.bind(searcher, score_mode),
ScoringExpression::MatchBoolPrefix {
field,
query,
analyzer,
} => {
let analyzer_name = searcher.resolve_search_analyzer(field, analyzer.as_deref());
let analyzers = searcher.analyzers();
let analyzer_impl = analyzers.get(analyzer_name);
let analyzed = analyzer_impl.analyze(query);
let tokens: Vec<String> = analyzed.into_iter().map(|t| t.text).collect();
if tokens.is_empty() {
crate::query::convert::MatchNoneQuery.bind(searcher, score_mode)
} else {
let mut should: Vec<ScoringExpression> = Vec::new();
for (i, token) in tokens.iter().enumerate() {
if i == tokens.len() - 1 {
should.push(ScoringExpression::Prefix {
field: field.clone(),
value: token.clone(),
});
} else {
should.push(ScoringExpression::Term {
field: field.clone(),
value: token.clone(),
});
}
}
ScoringExpression::Bool {
must: vec![],
should,
must_not: vec![],
filter: vec![],
minimum_should_match: None,
}
.bind(searcher, score_mode)
}
}
ScoringExpression::MultiMatch {
fields,
query,
analyzer,
tie_breaker,
} => {
let queries: Vec<ScoringExpression> = fields
.iter()
.map(|f| ScoringExpression::Match {
field: f.clone(),
query: query.clone(),
analyzer: analyzer.clone(),
})
.collect();
ScoringExpression::DisMax {
queries,
tie_breaker: *tie_breaker,
}
.bind(searcher, score_mode)
}
ScoringExpression::Bool {
must,
should,
must_not,
filter,
minimum_should_match,
} => crate::query::boolean::BoolQuery {
must: must
.iter()
.map(|q| -> Box<dyn super::Query> { Box::new(q.clone()) })
.collect(),
should: should
.iter()
.map(|q| -> Box<dyn super::Query> { Box::new(q.clone()) })
.collect(),
must_not: must_not
.iter()
.map(|q| -> Box<dyn super::Query> { Box::new(q.clone()) })
.collect(),
filter: filter
.iter()
.map(|q| -> Box<dyn super::Query> { Box::new(q.clone()) })
.collect(),
minimum_should_match: *minimum_should_match,
}
.bind(searcher, score_mode),
ScoringExpression::DisMax {
queries,
tie_breaker,
} => crate::query::dis_max::DisMaxQuery {
queries: queries
.iter()
.map(|q| -> Box<dyn super::Query> { Box::new(q.clone()) })
.collect(),
tie_breaker: *tie_breaker,
}
.bind(searcher, score_mode),
ScoringExpression::ConstantScore { query, boost } => {
crate::query::constant_score::ConstantScoreQuery {
inner: Box::new(query.as_ref().clone()),
boost: *boost,
}
.bind(searcher, score_mode)
}
ScoringExpression::Boost { query, boost } => crate::query::boost::BoostQuery {
inner: Box::new(query.as_ref().clone()),
boost: *boost,
}
.bind(searcher, score_mode),
ScoringExpression::Boosting {
positive,
negative,
negative_boost,
} => crate::query::boosting::BoostingQuery {
positive: Box::new(positive.as_ref().clone()),
negative: Box::new(negative.as_ref().clone()),
negative_boost: *negative_boost,
}
.bind(searcher, score_mode),
ScoringExpression::ScriptScore {
query,
script,
params,
} => crate::query::script_score::ScriptScoreQuery {
query: Box::new(query.as_ref().clone()),
script: script.clone(),
params: params.clone(),
}
.bind(searcher, score_mode),
ScoringExpression::FunctionScore {
query,
functions,
score_mode: fn_score_mode,
boost_mode,
} => crate::query::function_score::FunctionScoreQuery {
query: Box::new(query.as_ref().clone()),
functions: functions.clone(),
score_mode: fn_score_mode.clone(),
boost_mode: boost_mode.clone(),
}
.bind(searcher, score_mode),
ScoringExpression::Nested {
path,
query,
inner_hits,
} => crate::query::nested::NestedQuery {
path: path.clone(),
inner: Box::new(query.as_ref().clone()),
inner_hits: inner_hits.clone(),
}
.bind(searcher, score_mode),
ScoringExpression::Exists { field } => crate::query::exists::ExistsQuery {
field: field.clone(),
}
.bind(searcher, score_mode),
ScoringExpression::Prefix { field, value } => crate::query::prefix::PrefixQuery {
field: field.clone(),
value: value.clone(),
}
.bind(searcher, score_mode),
ScoringExpression::Range {
field,
gte,
gt,
lte,
lt,
} => crate::query::range::RangeQuery {
field: field.clone(),
gte: *gte,
gt: *gt,
lte: *lte,
lt: *lt,
}
.bind(searcher, score_mode),
ScoringExpression::Fuzzy {
field,
value,
fuzziness,
} => crate::query::fuzzy::FuzzyQuery {
field: field.clone(),
value: value.clone(),
fuzziness: *fuzziness,
}
.bind(searcher, score_mode),
ScoringExpression::Wildcard { field, value } => crate::query::wildcard::WildcardQuery {
field: field.clone(),
pattern: value.clone(),
}
.bind(searcher, score_mode),
ScoringExpression::Regexp { field, value } => crate::query::regexp::RegexpQuery {
field: field.clone(),
pattern: value.clone(),
}
.bind(searcher, score_mode),
ScoringExpression::GeoDistance {
field,
lat,
lon,
distance,
} => {
let distance_km = crate::query::parser::parse_distance_km(distance);
crate::spatial::query::GeoDistanceQuery {
field: field.clone(),
center: crate::spatial::geo::GeoPoint::new(*lat, *lon),
distance_km,
}
.bind(searcher, score_mode)
}
ScoringExpression::GeoBoundingBox {
field,
top_left_lat,
top_left_lon,
bottom_right_lat,
bottom_right_lon,
} => crate::spatial::query::GeoBoundingBoxQuery {
field: field.clone(),
top_left: crate::spatial::geo::GeoPoint::new(*top_left_lat, *top_left_lon),
bottom_right: crate::spatial::geo::GeoPoint::new(
*bottom_right_lat,
*bottom_right_lon,
),
}
.bind(searcher, score_mode),
ScoringExpression::GeoShape {
field,
shape,
relation,
} => {
let query_geom = crate::spatial::shape::parse_geojson(&shape.json)
.unwrap_or(::geo::Geometry::Point(::geo::Point::new(0.0, 0.0)));
let query_bbox = crate::spatial::shape::compute_bbox(&query_geom)
.unwrap_or((0.0, 0.0, 0.0, 0.0));
crate::spatial::query::GeoShapeQuery {
field: field.clone(),
query_shape: query_geom,
query_bbox,
relation: relation.clone(),
}
.bind(searcher, score_mode)
}
ScoringExpression::Span(span_ast) => {
Ok(crate::query::convert::ast_to_span_query(span_ast)
.bind_span(searcher, score_mode)?)
}
ScoringExpression::Knn {
field,
query_vector,
k,
num_candidates,
threshold,
} => crate::vector::query::KnnQuery {
field: field.clone(),
query_vector: query_vector.clone(),
k: *k,
num_candidates: *num_candidates,
threshold: *threshold,
}
.bind(searcher, score_mode),
ScoringExpression::MatchAll => {
crate::query::convert::MatchAllQuery.bind(searcher, score_mode)
}
ScoringExpression::MatchNone => {
crate::query::convert::MatchNoneQuery.bind(searcher, score_mode)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn term_query() {
let q = ScoringExpression::Term {
field: "status".into(),
value: "active".into(),
};
assert_eq!(
q,
ScoringExpression::Term {
field: "status".into(),
value: "active".into()
}
);
}
#[test]
fn bool_query_construction() {
let q = ScoringExpression::bool_query(
vec![ScoringExpression::Term {
field: "f".into(),
value: "v".into(),
}],
vec![],
vec![],
vec![],
);
if let ScoringExpression::Bool { must, .. } = &q {
assert_eq!(must.len(), 1);
} else {
panic!("expected Bool");
}
}
#[test]
fn nested_bool() {
let inner = ScoringExpression::bool_query(
vec![ScoringExpression::MatchAll],
vec![],
vec![],
vec![],
);
let outer = ScoringExpression::bool_query(vec![inner], vec![], vec![], vec![]);
if let ScoringExpression::Bool { must, .. } = &outer {
assert!(matches!(&must[0], ScoringExpression::Bool { .. }));
}
}
#[test]
fn constant_score_wraps() {
let q = ScoringExpression::ConstantScore {
query: Box::new(ScoringExpression::Term {
field: "f".into(),
value: "v".into(),
}),
boost: 1.5,
};
if let ScoringExpression::ConstantScore { boost, .. } = q {
assert_eq!(boost, 1.5);
}
}
#[test]
fn match_query() {
let q = ScoringExpression::Match {
field: "body".into(),
query: "search engine".into(),
analyzer: None,
};
if let ScoringExpression::Match { field, query, .. } = &q {
assert_eq!(field, "body");
assert_eq!(query, "search engine");
}
}
#[test]
fn debug_format() {
let q = ScoringExpression::MatchAll;
let s = format!("{q:?}");
assert!(s.contains("MatchAll"));
}
#[test]
fn clone_works() {
let q = ScoringExpression::Term {
field: "f".into(),
value: "v".into(),
};
let q2 = q.clone();
assert_eq!(q, q2);
}
#[test]
fn terms_query() {
let q = ScoringExpression::Terms {
field: "tags".into(),
values: vec!["a".into(), "b".into(), "c".into()],
};
if let ScoringExpression::Terms { values, .. } = &q {
assert_eq!(values.len(), 3);
}
}
}