MeiliSearch/milli/src/search/criteria/typo.rs

499 lines
19 KiB
Rust
Raw Normal View History

2021-06-16 18:33:33 +02:00
use std::borrow::Cow;
use std::collections::HashMap;
use std::mem::take;
2021-02-19 15:20:07 +01:00
2021-02-24 15:37:37 +01:00
use log::debug;
use roaring::RoaringBitmap;
2021-02-19 15:20:07 +01:00
use super::{
2021-06-16 18:33:33 +02:00
query_docids, resolve_query_tree, Candidates, Context, Criterion, CriterionParameters,
CriterionResult,
};
use crate::search::criteria::{resolve_phrase, InitialCandidates};
2021-06-16 18:33:33 +02:00
use crate::search::query_tree::{maximum_typo, Operation, Query, QueryKind};
use crate::search::{word_derivations, WordDerivationsCache};
use crate::Result;
2021-02-19 15:20:07 +01:00
2021-05-05 20:46:56 +02:00
/// Maximum number of typo for a word of any length.
const MAX_TYPOS_PER_WORD: u8 = 2;
2021-02-19 15:20:07 +01:00
pub struct Typo<'t> {
ctx: &'t dyn Context<'t>,
2021-05-05 20:46:56 +02:00
/// (max_typos, query_tree, candidates)
state: Option<(u8, Operation, Candidates)>,
typos: u8,
initial_candidates: Option<InitialCandidates>,
2021-03-23 15:25:46 +01:00
parent: Box<dyn Criterion + 't>,
candidates_cache: HashMap<(Operation, u8), RoaringBitmap>,
2021-02-19 15:20:07 +01:00
}
impl<'t> Typo<'t> {
pub fn new(ctx: &'t dyn Context<'t>, parent: Box<dyn Criterion + 't>) -> Self {
Typo {
2021-02-17 15:27:35 +01:00
ctx,
2021-05-05 20:46:56 +02:00
state: None,
typos: 0,
initial_candidates: None,
2021-03-23 15:25:46 +01:00
parent,
candidates_cache: HashMap::new(),
}
2021-02-19 15:20:07 +01:00
}
}
impl<'t> Criterion for Typo<'t> {
#[logging_timer::time("Typo::{}")]
fn next(&mut self, params: &mut CriterionParameters) -> Result<Option<CriterionResult>> {
2021-02-19 15:20:07 +01:00
use Candidates::{Allowed, Forbidden};
// remove excluded candidates when next is called, instead of doing it in the loop.
2021-05-05 20:46:56 +02:00
match self.state.as_mut() {
Some((_, _, Allowed(candidates))) => *candidates -= params.excluded_candidates,
Some((_, _, Forbidden(candidates))) => *candidates |= params.excluded_candidates,
None => (),
}
2021-02-23 17:33:20 +01:00
loop {
2021-06-16 18:33:33 +02:00
debug!(
"Typo at iteration {} (max typos {:?}) ({:?})",
2021-05-05 20:46:56 +02:00
self.typos,
self.state.as_ref().map(|(mt, _, _)| mt),
self.state.as_ref().map(|(_, _, cd)| cd),
);
match self.state.as_mut() {
Some((max_typos, _, _)) if self.typos > *max_typos => {
self.state = None; // reset state
2021-06-16 18:33:33 +02:00
}
2021-05-05 20:46:56 +02:00
Some((_, _, Allowed(allowed_candidates))) if allowed_candidates.is_empty() => {
self.state = None; // reset state
2021-06-16 18:33:33 +02:00
}
2021-05-05 20:46:56 +02:00
Some((_, query_tree, candidates_authorization)) => {
let fst = self.ctx.words_fst();
2021-05-10 10:27:18 +02:00
let new_query_tree = match self.typos {
2021-06-16 18:33:33 +02:00
typos if typos < MAX_TYPOS_PER_WORD => alterate_query_tree(
fst,
2021-06-16 18:33:33 +02:00
query_tree.clone(),
self.typos,
params.wdcache,
)?,
2021-05-10 10:27:18 +02:00
MAX_TYPOS_PER_WORD => {
// When typos >= MAX_TYPOS_PER_WORD, no more alteration of the query tree is possible,
// we keep the altered query tree
2021-06-16 18:33:33 +02:00
*query_tree = alterate_query_tree(
fst,
2021-06-16 18:33:33 +02:00
query_tree.clone(),
self.typos,
params.wdcache,
)?;
2021-05-10 10:27:18 +02:00
// we compute the allowed candidates
2021-06-16 18:33:33 +02:00
let query_tree_allowed_candidates =
resolve_query_tree(self.ctx, query_tree, params.wdcache)?;
2021-05-10 10:27:18 +02:00
// we assign the allowed candidates to the candidates authorization.
*candidates_authorization = match take(candidates_authorization) {
2021-06-16 18:33:33 +02:00
Allowed(allowed_candidates) => {
Allowed(query_tree_allowed_candidates & allowed_candidates)
}
Forbidden(forbidden_candidates) => {
Allowed(query_tree_allowed_candidates - forbidden_candidates)
}
2021-05-10 10:27:18 +02:00
};
query_tree.clone()
2021-06-16 18:33:33 +02:00
}
2021-05-10 10:27:18 +02:00
_otherwise => query_tree.clone(),
2021-05-05 20:46:56 +02:00
};
2021-05-05 20:46:56 +02:00
let mut candidates = resolve_candidates(
self.ctx,
&new_query_tree,
self.typos,
&mut self.candidates_cache,
params.wdcache,
)?;
match candidates_authorization {
Allowed(allowed_candidates) => {
candidates &= &*allowed_candidates;
*allowed_candidates -= &candidates;
2021-06-16 18:33:33 +02:00
}
2021-05-05 20:46:56 +02:00
Forbidden(forbidden_candidates) => {
candidates -= &*forbidden_candidates;
*forbidden_candidates |= &candidates;
2021-06-16 18:33:33 +02:00
}
2021-02-23 17:33:20 +01:00
}
2021-05-05 20:46:56 +02:00
let initial_candidates = match self.initial_candidates.as_mut() {
Some(initial_candidates) => initial_candidates.take(),
None => InitialCandidates::Estimated(candidates.clone()),
2021-05-05 20:46:56 +02:00
};
self.typos += 1;
return Ok(Some(CriterionResult {
2021-05-05 20:46:56 +02:00
query_tree: Some(new_query_tree),
candidates: Some(candidates),
2021-05-10 12:33:37 +02:00
filtered_candidates: None,
initial_candidates: Some(initial_candidates),
}));
2021-06-16 18:33:33 +02:00
}
None => match self.parent.next(params)? {
Some(CriterionResult {
query_tree: Some(query_tree),
candidates,
filtered_candidates,
initial_candidates,
2021-06-16 18:33:33 +02:00
}) => {
self.initial_candidates =
match (self.initial_candidates.take(), initial_candidates) {
(Some(self_ic), Some(parent_ic)) => Some(self_ic | parent_ic),
(self_ic, parent_ic) => self_ic.or(parent_ic),
};
2021-06-16 18:33:33 +02:00
let candidates = match candidates.or(filtered_candidates) {
Some(candidates) => {
Candidates::Allowed(candidates - params.excluded_candidates)
}
None => Candidates::Forbidden(params.excluded_candidates.clone()),
};
2021-05-05 20:46:56 +02:00
2021-06-16 18:33:33 +02:00
let maximum_typos = maximum_typo(&query_tree) as u8;
self.state = Some((maximum_typos, query_tree, candidates));
self.typos = 0;
2021-02-19 15:20:07 +01:00
}
2021-06-16 18:33:33 +02:00
Some(CriterionResult {
query_tree: None,
candidates,
filtered_candidates,
initial_candidates,
2021-06-16 18:33:33 +02:00
}) => {
return Ok(Some(CriterionResult {
query_tree: None,
candidates,
filtered_candidates,
initial_candidates,
2021-06-16 18:33:33 +02:00
}));
}
None => return Ok(None),
2021-02-19 15:20:07 +01:00
},
}
}
}
}
/// Modify the query tree by replacing every tolerant query by an Or operation
/// containing all of the corresponding exact words in the words FST. Each tolerant
/// query will only be replaced by exact query with up to `number_typos` maximum typos.
fn alterate_query_tree(
words_fst: &fst::Set<Cow<[u8]>>,
mut query_tree: Operation,
number_typos: u8,
wdcache: &mut WordDerivationsCache,
2021-06-16 18:33:33 +02:00
) -> Result<Operation> {
2021-02-19 15:20:07 +01:00
fn recurse(
words_fst: &fst::Set<Cow<[u8]>>,
operation: &mut Operation,
number_typos: u8,
wdcache: &mut WordDerivationsCache,
2021-06-16 18:33:33 +02:00
) -> Result<()> {
use Operation::{And, Or, Phrase};
2021-02-19 15:20:07 +01:00
match operation {
And(ops) | Or(_, ops) => {
2021-03-09 15:18:30 +01:00
ops.iter_mut().try_for_each(|op| recurse(words_fst, op, number_typos, wdcache))
2021-06-16 18:33:33 +02:00
}
// Because Phrases don't allow typos, no alteration can be done.
Phrase(_words) => Ok(()),
2021-02-19 15:20:07 +01:00
Operation::Query(q) => {
if let QueryKind::Tolerant { typo, word } = &q.kind {
2021-02-18 16:31:10 +01:00
// if no typo is allowed we don't call word_derivations function,
// and directly create an Exact query
if number_typos == 0 {
*operation = Operation::Query(Query {
prefix: q.prefix,
kind: QueryKind::Exact { original_typo: 0, word: word.clone() },
});
} else {
let typo = *typo.min(&number_typos);
2021-03-09 15:18:30 +01:00
let words = word_derivations(word, q.prefix, typo, words_fst, wdcache)?;
2021-06-16 18:33:33 +02:00
let queries = words
.iter()
.map(|(word, typo)| {
Operation::Query(Query {
prefix: false,
kind: QueryKind::Exact {
original_typo: *typo,
word: word.to_string(),
},
})
})
2021-06-16 18:33:33 +02:00
.collect();
2021-02-19 15:20:07 +01:00
*operation = Operation::or(false, queries);
}
2021-02-19 15:20:07 +01:00
}
Ok(())
2021-06-16 18:33:33 +02:00
}
2021-02-19 15:20:07 +01:00
}
}
2021-03-09 15:18:30 +01:00
recurse(words_fst, &mut query_tree, number_typos, wdcache)?;
2021-02-19 15:20:07 +01:00
Ok(query_tree)
}
2021-02-17 15:27:35 +01:00
fn resolve_candidates<'t>(
ctx: &'t dyn Context,
2021-02-19 15:20:07 +01:00
query_tree: &Operation,
number_typos: u8,
2021-02-18 14:40:59 +01:00
cache: &mut HashMap<(Operation, u8), RoaringBitmap>,
wdcache: &mut WordDerivationsCache,
2021-06-16 18:33:33 +02:00
) -> Result<RoaringBitmap> {
2021-02-17 15:27:35 +01:00
fn resolve_operation<'t>(
ctx: &'t dyn Context,
2021-02-19 15:20:07 +01:00
query_tree: &Operation,
number_typos: u8,
2021-02-18 14:40:59 +01:00
cache: &mut HashMap<(Operation, u8), RoaringBitmap>,
wdcache: &mut WordDerivationsCache,
2021-06-16 18:33:33 +02:00
) -> Result<RoaringBitmap> {
use Operation::{And, Or, Phrase, Query};
2021-02-19 15:20:07 +01:00
match query_tree {
2021-06-16 18:33:33 +02:00
And(ops) => mdfs(ctx, ops, number_typos, cache, wdcache),
Phrase(words) => resolve_phrase(ctx, words),
2021-02-19 15:20:07 +01:00
Or(_, ops) => {
let mut candidates = RoaringBitmap::new();
for op in ops {
let docids = resolve_operation(ctx, op, number_typos, cache, wdcache)?;
candidates |= docids;
2021-02-19 15:20:07 +01:00
}
Ok(candidates)
2021-06-16 18:33:33 +02:00
}
Query(q) => {
if q.kind.typo() == number_typos {
Ok(query_docids(ctx, q, wdcache)?)
} else {
Ok(RoaringBitmap::new())
}
}
2021-02-19 15:20:07 +01:00
}
}
2021-02-17 15:27:35 +01:00
fn mdfs<'t>(
ctx: &'t dyn Context,
2021-02-19 15:20:07 +01:00
branches: &[Operation],
mana: u8,
2021-02-18 14:40:59 +01:00
cache: &mut HashMap<(Operation, u8), RoaringBitmap>,
wdcache: &mut WordDerivationsCache,
2021-06-16 18:33:33 +02:00
) -> Result<RoaringBitmap> {
2021-02-19 15:20:07 +01:00
match branches.split_first() {
2021-02-18 14:40:59 +01:00
Some((head, [])) => {
let cache_key = (head.clone(), mana);
if let Some(candidates) = cache.get(&cache_key) {
2021-02-18 14:40:59 +01:00
Ok(candidates.clone())
} else {
let candidates = resolve_operation(ctx, head, mana, cache, wdcache)?;
cache.insert(cache_key, candidates.clone());
2021-02-18 14:40:59 +01:00
Ok(candidates)
}
2021-06-16 18:33:33 +02:00
}
2021-02-19 15:20:07 +01:00
Some((head, tail)) => {
let mut candidates = RoaringBitmap::new();
for m in 0..=mana {
2021-02-18 14:40:59 +01:00
let mut head_candidates = {
let cache_key = (head.clone(), m);
if let Some(candidates) = cache.get(&cache_key) {
2021-02-18 14:40:59 +01:00
candidates.clone()
} else {
let candidates = resolve_operation(ctx, head, m, cache, wdcache)?;
cache.insert(cache_key, candidates.clone());
2021-02-18 14:40:59 +01:00
candidates
}
};
2021-02-19 15:20:07 +01:00
if !head_candidates.is_empty() {
let tail_candidates = mdfs(ctx, tail, mana - m, cache, wdcache)?;
head_candidates &= tail_candidates;
candidates |= head_candidates;
2021-02-19 15:20:07 +01:00
}
}
Ok(candidates)
2021-06-16 18:33:33 +02:00
}
2021-02-19 15:20:07 +01:00
None => Ok(RoaringBitmap::new()),
}
}
resolve_operation(ctx, query_tree, number_typos, cache, wdcache)
2021-02-19 15:20:07 +01:00
}
2021-02-24 10:25:22 +01:00
#[cfg(test)]
mod test {
2021-03-23 15:25:46 +01:00
use super::super::initial::Initial;
2021-02-24 10:25:22 +01:00
use super::super::test::TestContext;
2021-06-16 18:33:33 +02:00
use super::*;
use crate::search::NoopDistinct;
2021-02-24 10:25:22 +01:00
fn display_criteria(mut criteria: Typo, mut parameters: CriterionParameters) -> String {
let mut result = String::new();
while let Some(criterion) = criteria.next(&mut parameters).unwrap() {
result.push_str(&format!("{criterion:?}\n\n"));
}
result
}
2021-02-24 10:25:22 +01:00
#[test]
fn initial_placeholder_no_facets() {
let context = TestContext::default();
let query_tree = None;
let facet_candidates = None;
let criterion_parameters = CriterionParameters {
wdcache: &mut WordDerivationsCache::new(),
excluded_candidates: &RoaringBitmap::new(),
};
let parent =
Initial::<NoopDistinct>::new(&context, query_tree, facet_candidates, false, None);
let criteria = Typo::new(&context, Box::new(parent));
2022-08-04 11:34:10 +02:00
let result = display_criteria(criteria, criterion_parameters);
insta::assert_snapshot!(result, @r###"
CriterionResult { query_tree: None, candidates: None, filtered_candidates: None, initial_candidates: None }
2021-02-24 10:25:22 +01:00
"###);
2021-02-24 10:25:22 +01:00
}
#[test]
fn initial_query_tree_no_facets() {
let context = TestContext::default();
2021-06-16 18:33:33 +02:00
let query_tree = Operation::Or(
false,
vec![Operation::And(vec![
Operation::Query(Query {
prefix: false,
kind: QueryKind::exact("split".to_string()),
}),
Operation::Query(Query {
prefix: false,
kind: QueryKind::exact("this".to_string()),
}),
Operation::Query(Query {
prefix: false,
kind: QueryKind::tolerant(1, "world".to_string()),
}),
])],
);
2021-02-24 10:25:22 +01:00
let facet_candidates = None;
let criterion_parameters = CriterionParameters {
wdcache: &mut WordDerivationsCache::new(),
excluded_candidates: &RoaringBitmap::new(),
};
let parent =
Initial::<NoopDistinct>::new(&context, Some(query_tree), facet_candidates, false, None);
let criteria = Typo::new(&context, Box::new(parent));
let result = display_criteria(criteria, criterion_parameters);
insta::assert_snapshot!(result, @r###"
CriterionResult { query_tree: Some(OR
AND
Exact { word: "split" }
Exact { word: "this" }
Exact { word: "world" }
), candidates: Some(RoaringBitmap<[]>), filtered_candidates: None, initial_candidates: Some(Estimated(RoaringBitmap<[]>)) }
CriterionResult { query_tree: Some(OR
AND
Exact { word: "split" }
Exact { word: "this" }
OR
Exact { word: "word" }
Exact { word: "world" }
), candidates: Some(RoaringBitmap<[]>), filtered_candidates: None, initial_candidates: Some(Estimated(RoaringBitmap<[]>)) }
"###);
2021-02-24 10:25:22 +01:00
}
#[test]
fn initial_placeholder_with_facets() {
let context = TestContext::default();
let query_tree = None;
2021-02-25 16:14:38 +01:00
let facet_candidates = context.word_docids("earth").unwrap().unwrap();
2021-02-24 10:25:22 +01:00
let criterion_parameters = CriterionParameters {
wdcache: &mut WordDerivationsCache::new(),
excluded_candidates: &RoaringBitmap::new(),
};
let parent = Initial::<NoopDistinct>::new(
&context,
query_tree,
2023-01-17 18:01:26 +01:00
Some(facet_candidates),
false,
None,
);
let criteria = Typo::new(&context, Box::new(parent));
2021-02-24 10:25:22 +01:00
let result = display_criteria(criteria, criterion_parameters);
insta::assert_snapshot!(result, @r###"
CriterionResult { query_tree: None, candidates: None, filtered_candidates: Some(RoaringBitmap<8000 values between 986424 and 4294786076>), initial_candidates: None }
2021-02-24 10:25:22 +01:00
"###);
2021-02-24 10:25:22 +01:00
}
#[test]
fn initial_query_tree_with_facets() {
let context = TestContext::default();
2021-06-16 18:33:33 +02:00
let query_tree = Operation::Or(
false,
vec![Operation::And(vec![
Operation::Query(Query {
prefix: false,
kind: QueryKind::exact("split".to_string()),
}),
Operation::Query(Query {
prefix: false,
kind: QueryKind::exact("this".to_string()),
}),
Operation::Query(Query {
prefix: false,
kind: QueryKind::tolerant(1, "world".to_string()),
}),
])],
);
2021-02-24 10:25:22 +01:00
let facet_candidates = context.word_docids("earth").unwrap().unwrap();
let criterion_parameters = CriterionParameters {
wdcache: &mut WordDerivationsCache::new(),
excluded_candidates: &RoaringBitmap::new(),
};
let parent = Initial::<NoopDistinct>::new(
&context,
Some(query_tree),
2023-01-17 18:01:26 +01:00
Some(facet_candidates),
false,
None,
);
let criteria = Typo::new(&context, Box::new(parent));
let result = display_criteria(criteria, criterion_parameters);
insta::assert_snapshot!(result, @r###"
CriterionResult { query_tree: Some(OR
AND
Exact { word: "split" }
Exact { word: "this" }
Exact { word: "world" }
), candidates: Some(RoaringBitmap<[]>), filtered_candidates: None, initial_candidates: Some(Estimated(RoaringBitmap<[]>)) }
CriterionResult { query_tree: Some(OR
AND
Exact { word: "split" }
Exact { word: "this" }
OR
Exact { word: "word" }
Exact { word: "world" }
), candidates: Some(RoaringBitmap<[]>), filtered_candidates: None, initial_candidates: Some(Estimated(RoaringBitmap<[]>)) }
"###);
2021-02-24 10:25:22 +01:00
}
}