2023-04-05 00:02:46 +08:00
|
|
|
mod bucket_sort;
|
2023-03-06 03:07:37 +08:00
|
|
|
mod db_cache;
|
2023-03-09 22:20:29 +08:00
|
|
|
mod distinct;
|
2023-04-13 19:45:34 +08:00
|
|
|
mod geo_sort;
|
2023-03-06 03:07:37 +08:00
|
|
|
mod graph_based_ranking_rule;
|
2023-03-07 02:21:55 +08:00
|
|
|
mod interner;
|
2023-03-31 15:19:18 +08:00
|
|
|
mod limits;
|
2023-03-06 03:07:37 +08:00
|
|
|
mod logger;
|
2023-04-06 19:58:56 +08:00
|
|
|
pub mod matches;
|
2023-03-06 03:07:37 +08:00
|
|
|
mod query_graph;
|
|
|
|
mod query_term;
|
|
|
|
mod ranking_rule_graph;
|
|
|
|
mod ranking_rules;
|
|
|
|
mod resolve_query_graph;
|
2023-03-07 21:42:58 +08:00
|
|
|
mod small_bitmap;
|
2023-04-04 23:12:07 +08:00
|
|
|
|
|
|
|
mod exact_attribute;
|
2023-03-06 03:07:37 +08:00
|
|
|
mod sort;
|
2023-02-21 16:41:58 +08:00
|
|
|
|
2023-04-04 21:38:30 +08:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests;
|
|
|
|
|
2023-03-30 19:12:51 +08:00
|
|
|
use std::collections::HashSet;
|
2023-03-08 16:55:53 +08:00
|
|
|
|
2023-04-13 19:45:34 +08:00
|
|
|
use bucket_sort::{bucket_sort, BucketSortOutput};
|
2023-03-23 16:39:16 +08:00
|
|
|
use charabia::TokenizerBuilder;
|
2023-03-06 15:35:01 +08:00
|
|
|
use db_cache::DatabaseCache;
|
2023-04-13 19:45:34 +08:00
|
|
|
use exact_attribute::ExactAttribute;
|
|
|
|
use graph_based_ranking_rule::{Exactness, Fid, Position, Proximity, Typo};
|
2023-03-06 15:35:01 +08:00
|
|
|
use heed::RoTxn;
|
2023-06-14 20:20:05 +08:00
|
|
|
use hnsw::Searcher;
|
2023-04-13 19:45:34 +08:00
|
|
|
use interner::{DedupInterner, Interner};
|
2023-04-11 17:56:31 +08:00
|
|
|
pub use logger::visual::VisualSearchLogger;
|
2023-03-19 22:15:58 +08:00
|
|
|
pub use logger::{DefaultSearchLogger, SearchLogger};
|
2023-03-30 19:12:51 +08:00
|
|
|
use query_graph::{QueryGraph, QueryNode};
|
2023-05-03 00:53:01 +08:00
|
|
|
use query_term::{located_query_terms_from_tokens, LocatedQueryTerm, Phrase, QueryTerm};
|
2023-04-13 19:45:34 +08:00
|
|
|
use ranking_rules::{
|
|
|
|
BoxRankingRule, PlaceholderQuery, RankingRule, RankingRuleOutput, RankingRuleQueryTrait,
|
|
|
|
};
|
|
|
|
use resolve_query_graph::{compute_query_graph_docids, PhraseDocIdsCache};
|
2023-03-07 21:42:58 +08:00
|
|
|
use roaring::RoaringBitmap;
|
2023-04-13 19:45:34 +08:00
|
|
|
use sort::Sort;
|
2023-06-14 20:20:05 +08:00
|
|
|
use space::Neighbor;
|
2023-03-08 16:55:53 +08:00
|
|
|
|
2023-04-13 19:45:34 +08:00
|
|
|
use self::geo_sort::GeoSort;
|
|
|
|
pub use self::geo_sort::Strategy as GeoSortStrategy;
|
2023-05-08 17:52:43 +08:00
|
|
|
use self::graph_based_ranking_rule::Words;
|
2023-04-13 19:45:34 +08:00
|
|
|
use self::interner::Interned;
|
2023-06-15 23:37:16 +08:00
|
|
|
use crate::score_details::{ScoreDetails, ScoringStrategy};
|
2023-04-03 16:09:27 +08:00
|
|
|
use crate::search::new::distinct::apply_distinct_rule;
|
2023-06-14 20:20:05 +08:00
|
|
|
use crate::{
|
|
|
|
AscDesc, DocumentId, Filter, Index, Member, Result, TermsMatchingStrategy, UserError, BEU32,
|
|
|
|
};
|
2023-04-12 00:27:41 +08:00
|
|
|
|
2023-03-16 18:52:51 +08:00
|
|
|
/// A structure used throughout the execution of a search query.
|
2023-03-13 21:03:48 +08:00
|
|
|
pub struct SearchContext<'ctx> {
|
|
|
|
pub index: &'ctx Index,
|
|
|
|
pub txn: &'ctx RoTxn<'ctx>,
|
|
|
|
pub db_cache: DatabaseCache<'ctx>,
|
2023-03-14 23:37:47 +08:00
|
|
|
pub word_interner: DedupInterner<String>,
|
|
|
|
pub phrase_interner: DedupInterner<Phrase>,
|
2023-03-30 15:52:47 +08:00
|
|
|
pub term_interner: Interner<QueryTerm>,
|
2023-03-30 17:10:38 +08:00
|
|
|
pub phrase_docids: PhraseDocIdsCache,
|
2023-03-07 02:21:55 +08:00
|
|
|
}
|
2023-04-06 21:02:23 +08:00
|
|
|
|
2023-03-13 21:03:48 +08:00
|
|
|
impl<'ctx> SearchContext<'ctx> {
|
|
|
|
pub fn new(index: &'ctx Index, txn: &'ctx RoTxn<'ctx>) -> Self {
|
2023-03-07 02:21:55 +08:00
|
|
|
Self {
|
|
|
|
index,
|
|
|
|
txn,
|
|
|
|
db_cache: <_>::default(),
|
|
|
|
word_interner: <_>::default(),
|
|
|
|
phrase_interner: <_>::default(),
|
2023-03-14 17:54:55 +08:00
|
|
|
term_interner: <_>::default(),
|
2023-03-30 17:10:38 +08:00
|
|
|
phrase_docids: <_>::default(),
|
2023-03-07 02:21:55 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-12 00:26:44 +08:00
|
|
|
#[derive(Clone, Copy, PartialEq, PartialOrd, Ord, Eq)]
|
|
|
|
pub enum Word {
|
|
|
|
Original(Interned<String>),
|
|
|
|
Derived(Interned<String>),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Word {
|
|
|
|
pub fn interned(&self) -> Interned<String> {
|
|
|
|
match self {
|
|
|
|
Word::Original(word) => *word,
|
|
|
|
Word::Derived(word) => *word,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-16 18:52:51 +08:00
|
|
|
/// Apply the [`TermsMatchingStrategy`] to the query graph and resolve it.
|
2023-03-23 16:15:57 +08:00
|
|
|
fn resolve_maximally_reduced_query_graph(
|
|
|
|
ctx: &mut SearchContext,
|
2023-03-06 15:35:01 +08:00
|
|
|
universe: &RoaringBitmap,
|
|
|
|
query_graph: &QueryGraph,
|
|
|
|
matching_strategy: TermsMatchingStrategy,
|
|
|
|
logger: &mut dyn SearchLogger<QueryGraph>,
|
|
|
|
) -> Result<RoaringBitmap> {
|
|
|
|
let mut graph = query_graph.clone();
|
2023-03-30 19:12:51 +08:00
|
|
|
|
|
|
|
let nodes_to_remove = match matching_strategy {
|
|
|
|
TermsMatchingStrategy::Last => query_graph
|
2023-03-30 20:54:08 +08:00
|
|
|
.removal_order_for_terms_matching_strategy_last(ctx)
|
2023-03-30 19:12:51 +08:00
|
|
|
.iter()
|
|
|
|
.flat_map(|x| x.iter())
|
|
|
|
.collect(),
|
2023-03-06 15:35:01 +08:00
|
|
|
TermsMatchingStrategy::All => vec![],
|
|
|
|
};
|
2023-03-30 20:49:25 +08:00
|
|
|
graph.remove_nodes_keep_edges(&nodes_to_remove);
|
2023-03-30 19:12:51 +08:00
|
|
|
|
2023-04-06 22:24:44 +08:00
|
|
|
logger.query_for_initial_universe(&graph);
|
2023-03-30 17:10:38 +08:00
|
|
|
let docids = compute_query_graph_docids(ctx, &graph, universe)?;
|
2023-03-06 15:35:01 +08:00
|
|
|
|
|
|
|
Ok(docids)
|
|
|
|
}
|
2023-03-16 18:52:51 +08:00
|
|
|
|
2023-04-24 22:57:12 +08:00
|
|
|
fn resolve_universe(
|
|
|
|
ctx: &mut SearchContext,
|
|
|
|
initial_universe: &RoaringBitmap,
|
|
|
|
query_graph: &QueryGraph,
|
|
|
|
matching_strategy: TermsMatchingStrategy,
|
|
|
|
logger: &mut dyn SearchLogger<QueryGraph>,
|
|
|
|
) -> Result<RoaringBitmap> {
|
2023-04-25 23:52:42 +08:00
|
|
|
resolve_maximally_reduced_query_graph(
|
|
|
|
ctx,
|
|
|
|
initial_universe,
|
|
|
|
query_graph,
|
|
|
|
matching_strategy,
|
|
|
|
logger,
|
|
|
|
)
|
2023-04-24 22:57:12 +08:00
|
|
|
}
|
|
|
|
|
2023-03-16 18:52:51 +08:00
|
|
|
/// Return the list of initialised ranking rules to be used for a placeholder search.
|
2023-03-13 21:03:48 +08:00
|
|
|
fn get_ranking_rules_for_placeholder_search<'ctx>(
|
|
|
|
ctx: &SearchContext<'ctx>,
|
2023-03-28 18:40:52 +08:00
|
|
|
sort_criteria: &Option<Vec<AscDesc>>,
|
2023-04-13 19:45:34 +08:00
|
|
|
geo_strategy: geo_sort::Strategy,
|
2023-03-28 18:39:42 +08:00
|
|
|
) -> Result<Vec<BoxRankingRule<'ctx, PlaceholderQuery>>> {
|
2023-03-28 22:41:03 +08:00
|
|
|
let mut sort = false;
|
2023-04-13 19:45:34 +08:00
|
|
|
let mut sorted_fields = HashSet::new();
|
|
|
|
let mut geo_sorted = false;
|
2023-03-28 22:41:03 +08:00
|
|
|
let mut ranking_rules: Vec<BoxRankingRule<PlaceholderQuery>> = vec![];
|
2023-03-13 16:52:17 +08:00
|
|
|
let settings_ranking_rules = ctx.index.criteria(ctx.txn)?;
|
|
|
|
for rr in settings_ranking_rules {
|
|
|
|
match rr {
|
2023-03-29 15:11:06 +08:00
|
|
|
// These rules need a query to have an effect; ignore them in placeholder search
|
2023-03-13 16:52:17 +08:00
|
|
|
crate::Criterion::Words
|
|
|
|
| crate::Criterion::Typo
|
|
|
|
| crate::Criterion::Attribute
|
|
|
|
| crate::Criterion::Proximity
|
|
|
|
| crate::Criterion::Exactness => continue,
|
2023-03-28 22:41:03 +08:00
|
|
|
crate::Criterion::Sort => {
|
|
|
|
if sort {
|
|
|
|
continue;
|
|
|
|
}
|
2023-04-13 19:45:34 +08:00
|
|
|
resolve_sort_criteria(
|
|
|
|
sort_criteria,
|
|
|
|
ctx,
|
|
|
|
&mut ranking_rules,
|
|
|
|
&mut sorted_fields,
|
|
|
|
&mut geo_sorted,
|
|
|
|
geo_strategy,
|
|
|
|
)?;
|
2023-03-28 22:41:03 +08:00
|
|
|
sort = true;
|
|
|
|
}
|
2023-03-28 22:41:25 +08:00
|
|
|
crate::Criterion::Asc(field_name) => {
|
2023-04-13 19:45:34 +08:00
|
|
|
if sorted_fields.contains(&field_name) {
|
2023-03-28 22:41:25 +08:00
|
|
|
continue;
|
|
|
|
}
|
2023-04-13 19:45:34 +08:00
|
|
|
sorted_fields.insert(field_name.clone());
|
2023-03-28 22:41:25 +08:00
|
|
|
ranking_rules.push(Box::new(Sort::new(ctx.index, ctx.txn, field_name, true)?));
|
|
|
|
}
|
|
|
|
crate::Criterion::Desc(field_name) => {
|
2023-04-13 19:45:34 +08:00
|
|
|
if sorted_fields.contains(&field_name) {
|
2023-03-28 22:41:25 +08:00
|
|
|
continue;
|
|
|
|
}
|
2023-04-13 19:45:34 +08:00
|
|
|
sorted_fields.insert(field_name.clone());
|
2023-03-28 22:41:25 +08:00
|
|
|
ranking_rules.push(Box::new(Sort::new(ctx.index, ctx.txn, field_name, false)?));
|
|
|
|
}
|
2023-03-13 16:52:17 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(ranking_rules)
|
|
|
|
}
|
2023-03-16 18:52:51 +08:00
|
|
|
|
|
|
|
/// Return the list of initialised ranking rules to be used for a query graph search.
|
2023-03-13 21:03:48 +08:00
|
|
|
fn get_ranking_rules_for_query_graph_search<'ctx>(
|
|
|
|
ctx: &SearchContext<'ctx>,
|
2023-03-28 18:40:52 +08:00
|
|
|
sort_criteria: &Option<Vec<AscDesc>>,
|
2023-04-13 19:45:34 +08:00
|
|
|
geo_strategy: geo_sort::Strategy,
|
2023-03-13 16:52:17 +08:00
|
|
|
terms_matching_strategy: TermsMatchingStrategy,
|
2023-04-25 23:52:42 +08:00
|
|
|
) -> Result<Vec<BoxRankingRule<'ctx, QueryGraph>>> {
|
2023-03-13 16:52:17 +08:00
|
|
|
// query graph search
|
|
|
|
let mut words = false;
|
|
|
|
let mut typo = false;
|
|
|
|
let mut proximity = false;
|
2023-03-28 18:40:52 +08:00
|
|
|
let mut sort = false;
|
2023-04-12 18:01:50 +08:00
|
|
|
let mut attribute = false;
|
2023-04-04 23:12:07 +08:00
|
|
|
let mut exactness = false;
|
2023-04-13 19:45:34 +08:00
|
|
|
let mut sorted_fields = HashSet::new();
|
|
|
|
let mut geo_sorted = false;
|
2023-03-13 16:52:17 +08:00
|
|
|
|
2023-05-08 17:52:43 +08:00
|
|
|
// Don't add the `words` ranking rule if the term matching strategy is `All`
|
|
|
|
if matches!(terms_matching_strategy, TermsMatchingStrategy::All) {
|
|
|
|
words = true;
|
|
|
|
}
|
|
|
|
|
2023-03-28 18:39:42 +08:00
|
|
|
let mut ranking_rules: Vec<BoxRankingRule<QueryGraph>> = vec![];
|
2023-03-13 16:52:17 +08:00
|
|
|
let settings_ranking_rules = ctx.index.criteria(ctx.txn)?;
|
|
|
|
for rr in settings_ranking_rules {
|
2023-04-24 22:57:12 +08:00
|
|
|
// Add Words before any of: typo, proximity, attribute
|
2023-03-13 16:52:17 +08:00
|
|
|
match rr {
|
2023-04-25 23:52:42 +08:00
|
|
|
crate::Criterion::Typo
|
|
|
|
| crate::Criterion::Attribute
|
|
|
|
| crate::Criterion::Proximity
|
|
|
|
| crate::Criterion::Exactness => {
|
2023-03-13 16:52:17 +08:00
|
|
|
if !words {
|
|
|
|
ranking_rules.push(Box::new(Words::new(terms_matching_strategy)));
|
|
|
|
words = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
match rr {
|
|
|
|
crate::Criterion::Words => {
|
|
|
|
if words {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
ranking_rules.push(Box::new(Words::new(terms_matching_strategy)));
|
|
|
|
words = true;
|
|
|
|
}
|
|
|
|
crate::Criterion::Typo => {
|
|
|
|
if typo {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
typo = true;
|
2023-03-30 18:12:41 +08:00
|
|
|
ranking_rules.push(Box::new(Typo::new(None)));
|
2023-03-13 16:52:17 +08:00
|
|
|
}
|
|
|
|
crate::Criterion::Proximity => {
|
|
|
|
if proximity {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
proximity = true;
|
2023-03-30 18:12:41 +08:00
|
|
|
ranking_rules.push(Box::new(Proximity::new(None)));
|
2023-03-13 16:52:17 +08:00
|
|
|
}
|
|
|
|
crate::Criterion::Attribute => {
|
|
|
|
if attribute {
|
|
|
|
continue;
|
|
|
|
}
|
2023-04-12 18:01:50 +08:00
|
|
|
attribute = true;
|
2023-04-13 16:46:09 +08:00
|
|
|
ranking_rules.push(Box::new(Fid::new(None)));
|
|
|
|
ranking_rules.push(Box::new(Position::new(None)));
|
2023-03-13 16:52:17 +08:00
|
|
|
}
|
|
|
|
crate::Criterion::Sort => {
|
|
|
|
if sort {
|
|
|
|
continue;
|
|
|
|
}
|
2023-04-13 19:45:34 +08:00
|
|
|
resolve_sort_criteria(
|
|
|
|
sort_criteria,
|
|
|
|
ctx,
|
|
|
|
&mut ranking_rules,
|
|
|
|
&mut sorted_fields,
|
|
|
|
&mut geo_sorted,
|
|
|
|
geo_strategy,
|
|
|
|
)?;
|
2023-03-28 18:40:52 +08:00
|
|
|
sort = true;
|
2023-03-13 16:52:17 +08:00
|
|
|
}
|
|
|
|
crate::Criterion::Exactness => {
|
|
|
|
if exactness {
|
|
|
|
continue;
|
|
|
|
}
|
2023-04-04 23:12:07 +08:00
|
|
|
ranking_rules.push(Box::new(ExactAttribute::new()));
|
|
|
|
ranking_rules.push(Box::new(Exactness::new()));
|
|
|
|
exactness = true;
|
2023-03-13 16:52:17 +08:00
|
|
|
}
|
2023-03-28 22:41:25 +08:00
|
|
|
crate::Criterion::Asc(field_name) => {
|
2023-04-13 19:45:34 +08:00
|
|
|
if sorted_fields.contains(&field_name) {
|
2023-03-13 16:52:17 +08:00
|
|
|
continue;
|
|
|
|
}
|
2023-04-13 19:45:34 +08:00
|
|
|
sorted_fields.insert(field_name.clone());
|
2023-03-28 22:41:25 +08:00
|
|
|
ranking_rules.push(Box::new(Sort::new(ctx.index, ctx.txn, field_name, true)?));
|
2023-03-13 16:52:17 +08:00
|
|
|
}
|
2023-03-28 22:41:25 +08:00
|
|
|
crate::Criterion::Desc(field_name) => {
|
2023-04-13 19:45:34 +08:00
|
|
|
if sorted_fields.contains(&field_name) {
|
2023-03-13 16:52:17 +08:00
|
|
|
continue;
|
|
|
|
}
|
2023-04-13 19:45:34 +08:00
|
|
|
sorted_fields.insert(field_name.clone());
|
2023-03-28 22:41:25 +08:00
|
|
|
ranking_rules.push(Box::new(Sort::new(ctx.index, ctx.txn, field_name, false)?));
|
2023-03-13 16:52:17 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-04-25 23:52:42 +08:00
|
|
|
Ok(ranking_rules)
|
2023-03-13 16:52:17 +08:00
|
|
|
}
|
2023-03-06 15:35:01 +08:00
|
|
|
|
2023-03-28 22:41:03 +08:00
|
|
|
fn resolve_sort_criteria<'ctx, Query: RankingRuleQueryTrait>(
|
|
|
|
sort_criteria: &Option<Vec<AscDesc>>,
|
|
|
|
ctx: &SearchContext<'ctx>,
|
|
|
|
ranking_rules: &mut Vec<BoxRankingRule<'ctx, Query>>,
|
2023-04-13 19:45:34 +08:00
|
|
|
sorted_fields: &mut HashSet<String>,
|
|
|
|
geo_sorted: &mut bool,
|
|
|
|
geo_strategy: geo_sort::Strategy,
|
2023-03-28 22:41:03 +08:00
|
|
|
) -> Result<()> {
|
|
|
|
let sort_criteria = sort_criteria.clone().unwrap_or_default();
|
|
|
|
ranking_rules.reserve(sort_criteria.len());
|
|
|
|
for criterion in sort_criteria {
|
2023-04-13 19:45:34 +08:00
|
|
|
match criterion {
|
2023-03-28 22:41:03 +08:00
|
|
|
AscDesc::Asc(Member::Field(field_name)) => {
|
2023-04-13 19:45:34 +08:00
|
|
|
if sorted_fields.contains(&field_name) {
|
2023-03-28 22:41:03 +08:00
|
|
|
continue;
|
|
|
|
}
|
2023-04-13 19:45:34 +08:00
|
|
|
sorted_fields.insert(field_name.clone());
|
|
|
|
ranking_rules.push(Box::new(Sort::new(ctx.index, ctx.txn, field_name, true)?));
|
2023-03-28 22:41:03 +08:00
|
|
|
}
|
|
|
|
AscDesc::Desc(Member::Field(field_name)) => {
|
2023-04-13 19:45:34 +08:00
|
|
|
if sorted_fields.contains(&field_name) {
|
2023-03-28 22:41:03 +08:00
|
|
|
continue;
|
|
|
|
}
|
2023-04-13 19:45:34 +08:00
|
|
|
sorted_fields.insert(field_name.clone());
|
|
|
|
ranking_rules.push(Box::new(Sort::new(ctx.index, ctx.txn, field_name, false)?));
|
2023-03-28 22:41:03 +08:00
|
|
|
}
|
2023-04-13 19:45:34 +08:00
|
|
|
AscDesc::Asc(Member::Geo(point)) => {
|
|
|
|
if *geo_sorted {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let geo_faceted_docids = ctx.index.geo_faceted_documents_ids(ctx.txn)?;
|
|
|
|
ranking_rules.push(Box::new(GeoSort::new(
|
|
|
|
geo_strategy,
|
|
|
|
geo_faceted_docids,
|
|
|
|
point,
|
|
|
|
true,
|
|
|
|
)?));
|
|
|
|
}
|
|
|
|
AscDesc::Desc(Member::Geo(point)) => {
|
|
|
|
if *geo_sorted {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let geo_faceted_docids = ctx.index.geo_faceted_documents_ids(ctx.txn)?;
|
|
|
|
ranking_rules.push(Box::new(GeoSort::new(
|
|
|
|
geo_strategy,
|
|
|
|
geo_faceted_docids,
|
|
|
|
point,
|
|
|
|
false,
|
|
|
|
)?));
|
2023-03-28 22:41:03 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2023-03-06 15:35:01 +08:00
|
|
|
#[allow(clippy::too_many_arguments)]
|
2023-03-23 16:15:57 +08:00
|
|
|
pub fn execute_search(
|
|
|
|
ctx: &mut SearchContext,
|
2023-03-23 16:35:53 +08:00
|
|
|
query: &Option<String>,
|
2023-06-09 00:47:06 +08:00
|
|
|
vector: &Option<Vec<f32>>,
|
2023-03-13 16:52:17 +08:00
|
|
|
terms_matching_strategy: TermsMatchingStrategy,
|
2023-06-07 00:26:33 +08:00
|
|
|
scoring_strategy: ScoringStrategy,
|
2023-03-30 22:10:10 +08:00
|
|
|
exhaustive_number_hits: bool,
|
2023-03-23 16:35:53 +08:00
|
|
|
filters: &Option<Filter>,
|
2023-03-28 18:40:52 +08:00
|
|
|
sort_criteria: &Option<Vec<AscDesc>>,
|
2023-04-13 19:45:34 +08:00
|
|
|
geo_strategy: geo_sort::Strategy,
|
2023-03-06 15:35:01 +08:00
|
|
|
from: usize,
|
|
|
|
length: usize,
|
2023-03-23 16:35:53 +08:00
|
|
|
words_limit: Option<usize>,
|
2023-03-13 16:52:17 +08:00
|
|
|
placeholder_search_logger: &mut dyn SearchLogger<PlaceholderQuery>,
|
|
|
|
query_graph_logger: &mut dyn SearchLogger<QueryGraph>,
|
2023-04-06 21:02:23 +08:00
|
|
|
) -> Result<PartialSearchResult> {
|
2023-03-18 22:04:34 +08:00
|
|
|
let mut universe = if let Some(filters) = filters {
|
2023-03-07 02:21:55 +08:00
|
|
|
filters.evaluate(ctx.txn, ctx.index)?
|
2023-03-06 15:35:01 +08:00
|
|
|
} else {
|
2023-03-07 02:21:55 +08:00
|
|
|
ctx.index.documents_ids(ctx.txn)?
|
2023-03-06 15:35:01 +08:00
|
|
|
};
|
|
|
|
|
2023-04-26 22:28:17 +08:00
|
|
|
check_sort_criteria(ctx, sort_criteria.as_ref())?;
|
|
|
|
|
2023-04-06 19:58:56 +08:00
|
|
|
let mut located_query_terms = None;
|
2023-04-27 19:30:09 +08:00
|
|
|
|
|
|
|
let query_terms = if let Some(query) = query {
|
2023-03-23 16:35:53 +08:00
|
|
|
// We make sure that the analyzer is aware of the stop words
|
|
|
|
// this ensures that the query builder is able to properly remove them.
|
|
|
|
let mut tokbuilder = TokenizerBuilder::new();
|
|
|
|
let stop_words = ctx.index.stop_words(ctx.txn)?;
|
|
|
|
if let Some(ref stop_words) = stop_words {
|
|
|
|
tokbuilder.stop_words(stop_words);
|
|
|
|
}
|
|
|
|
|
|
|
|
let script_lang_map = ctx.index.script_language(ctx.txn)?;
|
|
|
|
if !script_lang_map.is_empty() {
|
|
|
|
tokbuilder.allow_list(&script_lang_map);
|
|
|
|
}
|
|
|
|
|
|
|
|
let tokenizer = tokbuilder.build();
|
2023-03-23 16:39:16 +08:00
|
|
|
let tokens = tokenizer.tokenize(query);
|
2023-03-23 16:35:53 +08:00
|
|
|
|
2023-05-03 00:53:01 +08:00
|
|
|
let query_terms = located_query_terms_from_tokens(ctx, tokens, words_limit)?;
|
2023-04-27 19:30:09 +08:00
|
|
|
if query_terms.is_empty() {
|
|
|
|
// Do a placeholder search instead
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
Some(query_terms)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
|
|
|
let bucket_sort_output = if let Some(query_terms) = query_terms {
|
2023-05-03 19:39:19 +08:00
|
|
|
let (graph, new_located_query_terms) = QueryGraph::from_query(ctx, &query_terms)?;
|
|
|
|
located_query_terms = Some(new_located_query_terms);
|
2023-03-23 16:35:53 +08:00
|
|
|
|
2023-04-29 17:40:00 +08:00
|
|
|
let ranking_rules = get_ranking_rules_for_query_graph_search(
|
|
|
|
ctx,
|
|
|
|
sort_criteria,
|
|
|
|
geo_strategy,
|
|
|
|
terms_matching_strategy,
|
|
|
|
)?;
|
2023-04-24 22:57:12 +08:00
|
|
|
|
2023-04-25 23:52:42 +08:00
|
|
|
universe =
|
|
|
|
resolve_universe(ctx, &universe, &graph, terms_matching_strategy, query_graph_logger)?;
|
2023-03-13 16:52:17 +08:00
|
|
|
|
2023-06-15 23:37:16 +08:00
|
|
|
bucket_sort(
|
|
|
|
ctx,
|
|
|
|
ranking_rules,
|
|
|
|
&graph,
|
|
|
|
&universe,
|
|
|
|
from,
|
|
|
|
length,
|
2023-06-07 00:26:33 +08:00
|
|
|
scoring_strategy,
|
2023-06-15 23:37:16 +08:00
|
|
|
query_graph_logger,
|
|
|
|
)?
|
2023-03-13 16:52:17 +08:00
|
|
|
} else {
|
2023-04-13 19:45:34 +08:00
|
|
|
let ranking_rules =
|
|
|
|
get_ranking_rules_for_placeholder_search(ctx, sort_criteria, geo_strategy)?;
|
2023-03-13 16:52:17 +08:00
|
|
|
bucket_sort(
|
|
|
|
ctx,
|
|
|
|
ranking_rules,
|
|
|
|
&PlaceholderQuery,
|
|
|
|
&universe,
|
|
|
|
from,
|
|
|
|
length,
|
2023-06-07 00:26:33 +08:00
|
|
|
scoring_strategy,
|
2023-03-13 16:52:17 +08:00
|
|
|
placeholder_search_logger,
|
2023-03-18 22:04:34 +08:00
|
|
|
)?
|
|
|
|
};
|
|
|
|
|
2023-06-15 23:37:16 +08:00
|
|
|
let BucketSortOutput { docids, scores, mut all_candidates } = bucket_sort_output;
|
|
|
|
|
|
|
|
let fields_ids_map = ctx.index.fields_ids_map(ctx.txn)?;
|
2023-04-07 17:09:01 +08:00
|
|
|
|
2023-06-09 00:47:06 +08:00
|
|
|
let docids = match vector {
|
|
|
|
Some(vector) => {
|
|
|
|
// return the nearest documents that are also part of the candidates.
|
2023-06-14 20:20:05 +08:00
|
|
|
let mut searcher = Searcher::new();
|
|
|
|
let hnsw = ctx.index.vector_hnsw(ctx.txn)?.unwrap_or_default();
|
|
|
|
let ef = hnsw.len().min(100);
|
|
|
|
let mut dest = vec![Neighbor { index: 0, distance: 0 }; ef];
|
2023-06-14 21:59:10 +08:00
|
|
|
let neighbors = hnsw.nearest(vector, ef, &mut searcher, &mut dest[..]);
|
2023-06-14 20:20:05 +08:00
|
|
|
|
|
|
|
let mut docids = Vec::new();
|
2023-06-14 21:45:00 +08:00
|
|
|
for Neighbor { index, distance: _ } in neighbors.iter() {
|
2023-06-14 20:20:05 +08:00
|
|
|
let index = BEU32::new(*index as u32);
|
|
|
|
let docid = ctx.index.vector_id_docid.get(ctx.txn, &index)?.unwrap().get();
|
|
|
|
if universe.contains(docid) {
|
|
|
|
docids.push(docid);
|
2023-06-14 21:45:00 +08:00
|
|
|
if docids.len() == (from + length) {
|
2023-06-14 20:20:05 +08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-14 21:45:00 +08:00
|
|
|
docids.into_iter().skip(from).take(length).collect()
|
2023-06-09 00:47:06 +08:00
|
|
|
}
|
|
|
|
// return the search docids if the vector field is not specified
|
|
|
|
None => docids,
|
|
|
|
};
|
|
|
|
|
2023-03-30 22:10:10 +08:00
|
|
|
// The candidates is the universe unless the exhaustive number of hits
|
|
|
|
// is requested and a distinct attribute is set.
|
|
|
|
if exhaustive_number_hits {
|
|
|
|
if let Some(f) = ctx.index.distinct_field(ctx.txn)? {
|
2023-06-15 23:37:16 +08:00
|
|
|
if let Some(distinct_fid) = fields_ids_map.id(f) {
|
2023-04-07 17:09:01 +08:00
|
|
|
all_candidates = apply_distinct_rule(ctx, distinct_fid, &all_candidates)?.remaining;
|
2023-03-30 22:10:10 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-11 18:22:34 +08:00
|
|
|
Ok(PartialSearchResult {
|
2023-04-07 17:09:01 +08:00
|
|
|
candidates: all_candidates,
|
2023-06-07 00:26:33 +08:00
|
|
|
document_scores: scores,
|
2023-04-07 17:09:01 +08:00
|
|
|
documents_ids: docids,
|
2023-04-11 21:41:44 +08:00
|
|
|
located_query_terms,
|
2023-03-18 22:04:34 +08:00
|
|
|
})
|
|
|
|
}
|
2023-03-29 21:21:54 +08:00
|
|
|
|
|
|
|
fn check_sort_criteria(ctx: &SearchContext, sort_criteria: Option<&Vec<AscDesc>>) -> Result<()> {
|
|
|
|
let sort_criteria = if let Some(sort_criteria) = sort_criteria {
|
|
|
|
sort_criteria
|
|
|
|
} else {
|
|
|
|
return Ok(());
|
|
|
|
};
|
|
|
|
|
|
|
|
if sort_criteria.is_empty() {
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
|
|
|
// We check that the sort ranking rule exists and throw an
|
|
|
|
// error if we try to use it and that it doesn't.
|
|
|
|
let sort_ranking_rule_missing = !ctx.index.criteria(ctx.txn)?.contains(&crate::Criterion::Sort);
|
|
|
|
if sort_ranking_rule_missing {
|
|
|
|
return Err(UserError::SortRankingRuleMissing.into());
|
|
|
|
}
|
|
|
|
|
|
|
|
// We check that we are allowed to use the sort criteria, we check
|
|
|
|
// that they are declared in the sortable fields.
|
|
|
|
let sortable_fields = ctx.index.sortable_fields(ctx.txn)?;
|
|
|
|
for asc_desc in sort_criteria {
|
|
|
|
match asc_desc.member() {
|
|
|
|
Member::Field(ref field) if !crate::is_faceted(field, &sortable_fields) => {
|
|
|
|
return Err(UserError::InvalidSortableAttribute {
|
|
|
|
field: field.to_string(),
|
|
|
|
valid_fields: sortable_fields.into_iter().collect(),
|
|
|
|
})?
|
|
|
|
}
|
|
|
|
Member::Geo(_) if !sortable_fields.contains("_geo") => {
|
|
|
|
return Err(UserError::InvalidSortableAttribute {
|
|
|
|
field: "_geo".to_string(),
|
|
|
|
valid_fields: sortable_fields.into_iter().collect(),
|
|
|
|
})?
|
|
|
|
}
|
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2023-04-06 21:02:23 +08:00
|
|
|
|
|
|
|
pub struct PartialSearchResult {
|
|
|
|
pub located_query_terms: Option<Vec<LocatedQueryTerm>>,
|
|
|
|
pub candidates: RoaringBitmap,
|
|
|
|
pub documents_ids: Vec<DocumentId>,
|
2023-06-07 00:26:33 +08:00
|
|
|
pub document_scores: Vec<Vec<ScoreDetails>>,
|
2023-04-06 21:02:23 +08:00
|
|
|
}
|