2023-04-05 00:02:16 +08:00
|
|
|
use roaring::RoaringBitmap;
|
|
|
|
|
|
|
|
use super::logger::SearchLogger;
|
|
|
|
use super::ranking_rules::{BoxRankingRule, RankingRuleQueryTrait};
|
|
|
|
use super::SearchContext;
|
2023-06-15 23:37:16 +08:00
|
|
|
use crate::score_details::{ScoreDetails, ScoringStrategy};
|
2023-04-05 00:02:16 +08:00
|
|
|
use crate::search::new::distinct::{apply_distinct_rule, distinct_single_docid, DistinctOutput};
|
2024-03-05 18:21:46 +08:00
|
|
|
use crate::{Result, TimeBudget};
|
2023-04-05 00:02:16 +08:00
|
|
|
|
2023-04-07 17:09:01 +08:00
|
|
|
pub struct BucketSortOutput {
|
|
|
|
pub docids: Vec<u32>,
|
2023-06-15 23:37:16 +08:00
|
|
|
pub scores: Vec<Vec<ScoreDetails>>,
|
2023-04-07 17:09:01 +08:00
|
|
|
pub all_candidates: RoaringBitmap,
|
2024-03-05 18:21:46 +08:00
|
|
|
|
|
|
|
pub degraded: bool,
|
2023-04-07 17:09:01 +08:00
|
|
|
}
|
|
|
|
|
2023-06-15 23:37:16 +08:00
|
|
|
// TODO: would probably be good to regroup some of these inside of a struct?
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
2024-03-05 18:05:20 +08:00
|
|
|
#[tracing::instrument(level = "trace", skip_all, target = "search::bucket_sort")]
|
2023-04-05 00:02:16 +08:00
|
|
|
pub fn bucket_sort<'ctx, Q: RankingRuleQueryTrait>(
|
|
|
|
ctx: &mut SearchContext<'ctx>,
|
|
|
|
mut ranking_rules: Vec<BoxRankingRule<'ctx, Q>>,
|
|
|
|
query: &Q,
|
|
|
|
universe: &RoaringBitmap,
|
|
|
|
from: usize,
|
|
|
|
length: usize,
|
2023-06-15 23:37:16 +08:00
|
|
|
scoring_strategy: ScoringStrategy,
|
2023-04-05 00:02:16 +08:00
|
|
|
logger: &mut dyn SearchLogger<Q>,
|
2024-03-05 18:21:46 +08:00
|
|
|
time_budget: TimeBudget,
|
2024-04-12 01:04:06 +08:00
|
|
|
ranking_score_threshold: Option<f64>,
|
2023-04-07 17:09:01 +08:00
|
|
|
) -> Result<BucketSortOutput> {
|
2023-04-05 00:02:16 +08:00
|
|
|
logger.initial_query(query);
|
|
|
|
logger.ranking_rules(&ranking_rules);
|
|
|
|
logger.initial_universe(universe);
|
|
|
|
|
|
|
|
let distinct_fid = if let Some(field) = ctx.index.distinct_field(ctx.txn)? {
|
|
|
|
ctx.index.fields_ids_map(ctx.txn)?.id(field)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
|
|
|
|
|
|
|
if universe.len() < from as u64 {
|
2023-06-15 23:37:16 +08:00
|
|
|
return Ok(BucketSortOutput {
|
|
|
|
docids: vec![],
|
|
|
|
scores: vec![],
|
|
|
|
all_candidates: universe.clone(),
|
2024-03-05 18:21:46 +08:00
|
|
|
degraded: false,
|
2023-06-15 23:37:16 +08:00
|
|
|
});
|
2023-04-05 00:02:16 +08:00
|
|
|
}
|
|
|
|
if ranking_rules.is_empty() {
|
|
|
|
if let Some(distinct_fid) = distinct_fid {
|
|
|
|
let mut excluded = RoaringBitmap::new();
|
|
|
|
let mut results = vec![];
|
|
|
|
for docid in universe.iter() {
|
2023-10-19 19:18:45 +08:00
|
|
|
if results.len() >= from + length {
|
2023-04-05 00:02:16 +08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
if excluded.contains(docid) {
|
|
|
|
continue;
|
|
|
|
}
|
2023-10-09 18:46:50 +08:00
|
|
|
|
2023-04-05 00:02:16 +08:00
|
|
|
distinct_single_docid(ctx.index, ctx.txn, distinct_fid, docid, &mut excluded)?;
|
|
|
|
results.push(docid);
|
|
|
|
}
|
2023-10-09 18:46:50 +08:00
|
|
|
|
2023-04-07 17:09:01 +08:00
|
|
|
let mut all_candidates = universe - excluded;
|
|
|
|
all_candidates.extend(results.iter().copied());
|
2023-10-19 21:58:25 +08:00
|
|
|
// drain the results of the skipped elements
|
|
|
|
// this **must** be done **after** writing the entire results in `all_candidates` to ensure
|
|
|
|
// e.g. estimatedTotalHits is correct.
|
2023-10-19 19:18:45 +08:00
|
|
|
if results.len() >= from {
|
|
|
|
results.drain(..from);
|
|
|
|
} else {
|
|
|
|
results.clear();
|
|
|
|
}
|
2023-10-09 18:46:50 +08:00
|
|
|
|
2023-06-15 23:37:16 +08:00
|
|
|
return Ok(BucketSortOutput {
|
|
|
|
scores: vec![Default::default(); results.len()],
|
|
|
|
docids: results,
|
|
|
|
all_candidates,
|
2024-03-05 18:21:46 +08:00
|
|
|
degraded: false,
|
2023-06-15 23:37:16 +08:00
|
|
|
});
|
2023-04-05 00:02:16 +08:00
|
|
|
} else {
|
2023-06-15 23:37:16 +08:00
|
|
|
let docids: Vec<u32> = universe.iter().skip(from).take(length).collect();
|
|
|
|
return Ok(BucketSortOutput {
|
|
|
|
scores: vec![Default::default(); docids.len()],
|
|
|
|
docids,
|
|
|
|
all_candidates: universe.clone(),
|
2024-03-05 18:21:46 +08:00
|
|
|
degraded: false,
|
2023-06-15 23:37:16 +08:00
|
|
|
});
|
2023-04-05 00:02:16 +08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
let ranking_rules_len = ranking_rules.len();
|
|
|
|
|
|
|
|
logger.start_iteration_ranking_rule(0, ranking_rules[0].as_ref(), query, universe);
|
2023-06-15 23:37:16 +08:00
|
|
|
|
2023-04-05 00:02:16 +08:00
|
|
|
ranking_rules[0].start_iteration(ctx, logger, universe, query)?;
|
|
|
|
|
2023-06-15 23:37:16 +08:00
|
|
|
let mut ranking_rule_scores: Vec<ScoreDetails> = vec![];
|
|
|
|
|
2023-04-05 00:02:16 +08:00
|
|
|
let mut ranking_rule_universes: Vec<RoaringBitmap> =
|
|
|
|
vec![RoaringBitmap::default(); ranking_rules_len];
|
2024-05-14 23:20:57 +08:00
|
|
|
ranking_rule_universes[0].clone_from(universe);
|
2023-04-05 00:02:16 +08:00
|
|
|
let mut cur_ranking_rule_index = 0;
|
|
|
|
|
|
|
|
/// Finish iterating over the current ranking rule, yielding
|
|
|
|
/// control to the parent (or finishing the search if not possible).
|
2023-04-07 17:09:01 +08:00
|
|
|
/// Update the universes accordingly and inform the logger.
|
2023-04-05 00:02:16 +08:00
|
|
|
macro_rules! back {
|
|
|
|
() => {
|
2023-08-23 22:40:39 +08:00
|
|
|
// FIXME: temporarily disabled assert: see <https://github.com/meilisearch/meilisearch/pull/4013>
|
2023-08-23 22:11:44 +08:00
|
|
|
// assert!(
|
|
|
|
// ranking_rule_universes[cur_ranking_rule_index].is_empty(),
|
|
|
|
// "The ranking rule {} did not sort its bucket exhaustively",
|
|
|
|
// ranking_rules[cur_ranking_rule_index].id()
|
|
|
|
// );
|
2023-04-05 00:02:16 +08:00
|
|
|
logger.end_iteration_ranking_rule(
|
|
|
|
cur_ranking_rule_index,
|
|
|
|
ranking_rules[cur_ranking_rule_index].as_ref(),
|
|
|
|
&ranking_rule_universes[cur_ranking_rule_index],
|
|
|
|
);
|
|
|
|
ranking_rule_universes[cur_ranking_rule_index].clear();
|
|
|
|
ranking_rules[cur_ranking_rule_index].end_iteration(ctx, logger);
|
|
|
|
if cur_ranking_rule_index == 0 {
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
cur_ranking_rule_index -= 1;
|
|
|
|
}
|
2023-06-15 23:37:16 +08:00
|
|
|
if ranking_rule_scores.len() > cur_ranking_rule_index {
|
|
|
|
ranking_rule_scores.pop();
|
|
|
|
}
|
2023-04-05 00:02:16 +08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-04-24 18:11:25 +08:00
|
|
|
let mut all_candidates = universe.clone();
|
2023-04-07 17:09:01 +08:00
|
|
|
let mut valid_docids = vec![];
|
2023-06-15 23:37:16 +08:00
|
|
|
let mut valid_scores = vec![];
|
2023-04-05 00:02:16 +08:00
|
|
|
let mut cur_offset = 0usize;
|
|
|
|
|
|
|
|
macro_rules! maybe_add_to_results {
|
|
|
|
($candidates:expr) => {
|
2023-04-07 17:09:01 +08:00
|
|
|
maybe_add_to_results(
|
|
|
|
ctx,
|
|
|
|
from,
|
|
|
|
length,
|
2024-04-12 01:04:06 +08:00
|
|
|
ranking_score_threshold,
|
2023-04-07 17:09:01 +08:00
|
|
|
logger,
|
|
|
|
&mut valid_docids,
|
2023-06-15 23:37:16 +08:00
|
|
|
&mut valid_scores,
|
2023-04-07 17:09:01 +08:00
|
|
|
&mut all_candidates,
|
|
|
|
&mut ranking_rule_universes,
|
|
|
|
&mut ranking_rules,
|
|
|
|
cur_ranking_rule_index,
|
|
|
|
&mut cur_offset,
|
|
|
|
distinct_fid,
|
2023-06-15 23:37:16 +08:00
|
|
|
&ranking_rule_scores,
|
2023-04-07 17:09:01 +08:00
|
|
|
$candidates,
|
|
|
|
)?;
|
2023-04-05 00:02:16 +08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-04-07 17:09:01 +08:00
|
|
|
while valid_docids.len() < length {
|
2024-03-05 18:21:46 +08:00
|
|
|
if time_budget.exceeded() {
|
2024-03-15 00:34:46 +08:00
|
|
|
loop {
|
|
|
|
let bucket = std::mem::take(&mut ranking_rule_universes[cur_ranking_rule_index]);
|
|
|
|
ranking_rule_scores.push(ScoreDetails::Skipped);
|
2024-04-12 01:04:06 +08:00
|
|
|
|
2024-03-15 00:34:46 +08:00
|
|
|
maybe_add_to_results!(bucket);
|
2024-04-12 01:04:06 +08:00
|
|
|
|
2024-03-15 00:34:46 +08:00
|
|
|
ranking_rule_scores.pop();
|
|
|
|
|
|
|
|
if cur_ranking_rule_index == 0 {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
back!();
|
|
|
|
}
|
2024-03-05 18:21:46 +08:00
|
|
|
|
|
|
|
return Ok(BucketSortOutput {
|
2024-03-15 00:34:46 +08:00
|
|
|
scores: valid_scores,
|
2024-03-05 18:21:46 +08:00
|
|
|
docids: valid_docids,
|
|
|
|
all_candidates,
|
|
|
|
degraded: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-06-15 23:37:16 +08:00
|
|
|
// The universe for this bucket is zero, so we don't need to sort
|
|
|
|
// anything, just go back to the parent ranking rule.
|
|
|
|
if ranking_rule_universes[cur_ranking_rule_index].is_empty()
|
|
|
|
|| (scoring_strategy == ScoringStrategy::Skip
|
|
|
|
&& ranking_rule_universes[cur_ranking_rule_index].len() == 1)
|
|
|
|
{
|
2023-04-07 17:09:01 +08:00
|
|
|
let bucket = std::mem::take(&mut ranking_rule_universes[cur_ranking_rule_index]);
|
|
|
|
maybe_add_to_results!(bucket);
|
2023-04-05 00:02:16 +08:00
|
|
|
back!();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2024-02-06 17:49:23 +08:00
|
|
|
let span = tracing::trace_span!(target: "search::bucket_sort", "next_bucket", id = ranking_rules[cur_ranking_rule_index].id());
|
|
|
|
let entered = span.enter();
|
|
|
|
|
2023-07-03 16:20:28 +08:00
|
|
|
let Some(next_bucket) = ranking_rules[cur_ranking_rule_index].next_bucket(
|
|
|
|
ctx,
|
|
|
|
logger,
|
|
|
|
&ranking_rule_universes[cur_ranking_rule_index],
|
|
|
|
)?
|
|
|
|
else {
|
2023-04-05 00:02:16 +08:00
|
|
|
back!();
|
|
|
|
continue;
|
|
|
|
};
|
2024-02-06 17:49:23 +08:00
|
|
|
drop(entered);
|
2023-04-05 00:02:16 +08:00
|
|
|
|
2023-06-15 23:37:16 +08:00
|
|
|
ranking_rule_scores.push(next_bucket.score);
|
|
|
|
|
2023-04-05 00:02:16 +08:00
|
|
|
logger.next_bucket_ranking_rule(
|
|
|
|
cur_ranking_rule_index,
|
|
|
|
ranking_rules[cur_ranking_rule_index].as_ref(),
|
|
|
|
&ranking_rule_universes[cur_ranking_rule_index],
|
|
|
|
&next_bucket.candidates,
|
|
|
|
);
|
|
|
|
|
|
|
|
debug_assert!(
|
|
|
|
ranking_rule_universes[cur_ranking_rule_index].is_superset(&next_bucket.candidates)
|
|
|
|
);
|
2024-04-12 01:04:06 +08:00
|
|
|
|
|
|
|
if let Some(ranking_score_threshold) = ranking_score_threshold {
|
|
|
|
let current_score = ScoreDetails::global_score(ranking_rule_scores.iter());
|
|
|
|
if current_score < ranking_score_threshold {
|
|
|
|
all_candidates -=
|
|
|
|
next_bucket.candidates | &ranking_rule_universes[cur_ranking_rule_index];
|
|
|
|
back!();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-05 00:02:16 +08:00
|
|
|
ranking_rule_universes[cur_ranking_rule_index] -= &next_bucket.candidates;
|
|
|
|
|
|
|
|
if cur_ranking_rule_index == ranking_rules_len - 1
|
2023-06-15 23:37:16 +08:00
|
|
|
|| (scoring_strategy == ScoringStrategy::Skip && next_bucket.candidates.len() <= 1)
|
2023-04-05 00:02:16 +08:00
|
|
|
|| cur_offset + (next_bucket.candidates.len() as usize) < from
|
|
|
|
{
|
2023-04-07 17:09:01 +08:00
|
|
|
maybe_add_to_results!(next_bucket.candidates);
|
2023-06-15 23:37:16 +08:00
|
|
|
ranking_rule_scores.pop();
|
2023-04-05 00:02:16 +08:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
cur_ranking_rule_index += 1;
|
2024-05-14 23:20:57 +08:00
|
|
|
ranking_rule_universes[cur_ranking_rule_index].clone_from(&next_bucket.candidates);
|
2023-04-05 00:02:16 +08:00
|
|
|
logger.start_iteration_ranking_rule(
|
|
|
|
cur_ranking_rule_index,
|
|
|
|
ranking_rules[cur_ranking_rule_index].as_ref(),
|
|
|
|
&next_bucket.query,
|
|
|
|
&ranking_rule_universes[cur_ranking_rule_index],
|
|
|
|
);
|
|
|
|
ranking_rules[cur_ranking_rule_index].start_iteration(
|
|
|
|
ctx,
|
|
|
|
logger,
|
|
|
|
&next_bucket.candidates,
|
|
|
|
&next_bucket.query,
|
|
|
|
)?;
|
|
|
|
}
|
|
|
|
|
2024-03-05 18:21:46 +08:00
|
|
|
Ok(BucketSortOutput {
|
|
|
|
docids: valid_docids,
|
|
|
|
scores: valid_scores,
|
|
|
|
all_candidates,
|
|
|
|
degraded: false,
|
|
|
|
})
|
2023-04-07 17:09:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Add the candidates to the results. Take `distinct`, `from`, `length`, and `cur_offset`
|
|
|
|
/// into account and inform the logger.
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
fn maybe_add_to_results<'ctx, Q: RankingRuleQueryTrait>(
|
|
|
|
ctx: &mut SearchContext<'ctx>,
|
|
|
|
from: usize,
|
|
|
|
length: usize,
|
2024-04-12 01:04:06 +08:00
|
|
|
ranking_score_threshold: Option<f64>,
|
2023-04-07 17:09:01 +08:00
|
|
|
logger: &mut dyn SearchLogger<Q>,
|
|
|
|
|
|
|
|
valid_docids: &mut Vec<u32>,
|
2023-06-15 23:37:16 +08:00
|
|
|
valid_scores: &mut Vec<Vec<ScoreDetails>>,
|
2023-04-07 17:09:01 +08:00
|
|
|
all_candidates: &mut RoaringBitmap,
|
|
|
|
|
|
|
|
ranking_rule_universes: &mut [RoaringBitmap],
|
|
|
|
ranking_rules: &mut [BoxRankingRule<'ctx, Q>],
|
2023-06-15 23:37:16 +08:00
|
|
|
|
2023-04-07 17:09:01 +08:00
|
|
|
cur_ranking_rule_index: usize,
|
|
|
|
|
|
|
|
cur_offset: &mut usize,
|
2023-06-15 23:37:16 +08:00
|
|
|
|
2023-04-07 17:09:01 +08:00
|
|
|
distinct_fid: Option<u16>,
|
2023-06-15 23:37:16 +08:00
|
|
|
ranking_rule_scores: &[ScoreDetails],
|
2023-04-07 17:09:01 +08:00
|
|
|
candidates: RoaringBitmap,
|
|
|
|
) -> Result<()> {
|
2024-04-12 01:04:06 +08:00
|
|
|
// remove candidates from the universe without adding them to result if their score is below the threshold
|
|
|
|
if let Some(ranking_score_threshold) = ranking_score_threshold {
|
|
|
|
let score = ScoreDetails::global_score(ranking_rule_scores.iter());
|
|
|
|
if score < ranking_score_threshold {
|
|
|
|
*all_candidates -= candidates | &ranking_rule_universes[cur_ranking_rule_index];
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-07 17:09:01 +08:00
|
|
|
// First apply the distinct rule on the candidates, reducing the universes if necessary
|
|
|
|
let candidates = if let Some(distinct_fid) = distinct_fid {
|
|
|
|
let DistinctOutput { remaining, excluded } =
|
|
|
|
apply_distinct_rule(ctx, distinct_fid, &candidates)?;
|
|
|
|
for universe in ranking_rule_universes.iter_mut() {
|
|
|
|
*universe -= &excluded;
|
2023-04-24 18:11:25 +08:00
|
|
|
*all_candidates -= &excluded;
|
2023-04-07 17:09:01 +08:00
|
|
|
}
|
|
|
|
remaining
|
|
|
|
} else {
|
|
|
|
candidates.clone()
|
|
|
|
};
|
|
|
|
*all_candidates |= &candidates;
|
2023-04-24 18:11:25 +08:00
|
|
|
|
2023-04-07 17:09:01 +08:00
|
|
|
// if the candidates are empty, there is nothing to do;
|
|
|
|
if candidates.is_empty() {
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
|
|
|
// if we still haven't reached the first document to return
|
|
|
|
if *cur_offset < from {
|
|
|
|
// and if no document from this bucket can be returned
|
|
|
|
if *cur_offset + (candidates.len() as usize) < from {
|
|
|
|
// then just skip the bucket
|
|
|
|
logger.skip_bucket_ranking_rule(
|
|
|
|
cur_ranking_rule_index,
|
|
|
|
ranking_rules[cur_ranking_rule_index].as_ref(),
|
|
|
|
&candidates,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
// otherwise, skip some of the documents and add some of the rest, in order of ids
|
2023-04-24 18:11:25 +08:00
|
|
|
let candidates_vec = candidates.iter().collect::<Vec<_>>();
|
|
|
|
let (skipped_candidates, candidates) = candidates_vec.split_at(from - *cur_offset);
|
2023-04-07 17:09:01 +08:00
|
|
|
|
|
|
|
logger.skip_bucket_ranking_rule(
|
|
|
|
cur_ranking_rule_index,
|
|
|
|
ranking_rules[cur_ranking_rule_index].as_ref(),
|
|
|
|
&skipped_candidates.iter().collect(),
|
|
|
|
);
|
|
|
|
let candidates =
|
|
|
|
candidates.iter().take(length - valid_docids.len()).copied().collect::<Vec<_>>();
|
|
|
|
logger.add_to_results(&candidates);
|
2023-06-15 23:37:16 +08:00
|
|
|
valid_docids.extend_from_slice(&candidates);
|
|
|
|
valid_scores
|
|
|
|
.extend(std::iter::repeat(ranking_rule_scores.to_owned()).take(candidates.len()));
|
2023-04-07 17:09:01 +08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// if we have passed the offset already, add some of the documents (up to the limit)
|
|
|
|
let candidates = candidates.iter().take(length - valid_docids.len()).collect::<Vec<u32>>();
|
|
|
|
logger.add_to_results(&candidates);
|
2023-06-15 23:37:16 +08:00
|
|
|
valid_docids.extend_from_slice(&candidates);
|
|
|
|
valid_scores
|
|
|
|
.extend(std::iter::repeat(ranking_rule_scores.to_owned()).take(candidates.len()));
|
2023-04-07 17:09:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
*cur_offset += candidates.len() as usize;
|
|
|
|
Ok(())
|
2023-04-05 00:02:16 +08:00
|
|
|
}
|