diff --git a/meilisearch/tests/search/restrict_searchable.rs b/meilisearch/tests/search/restrict_searchable.rs index cfdff95ee..7bbdca38f 100644 --- a/meilisearch/tests/search/restrict_searchable.rs +++ b/meilisearch/tests/search/restrict_searchable.rs @@ -335,3 +335,35 @@ async fn exactness_ranking_rule_order() { }) .await; } + +#[actix_rt::test] +async fn search_on_exact_field() { + let server = Server::new().await; + let index = index_with_documents( + &server, + &json!([ + { + "title": "Captain Marvel", + "exact": "Captain Marivel", + "id": "1", + }, + { + "title": "Captain Marivel", + "exact": "Captain the Marvel", + "id": "2", + }]), + ) + .await; + + let (response, code) = + index.update_settings_typo_tolerance(json!({ "disableOnAttributes": ["exact"] })).await; + assert_eq!(202, code, "{:?}", response); + index.wait_task(1).await; + // Searching on an exact attribute should only return the document matching without typo. + index + .search(json!({"q": "Marvel", "attributesToSearchOn": ["exact"]}), |response, code| { + snapshot!(code, @"200 OK"); + snapshot!(response["hits"].as_array().unwrap().len(), @"1"); + }) + .await; +} diff --git a/milli/src/search/new/db_cache.rs b/milli/src/search/new/db_cache.rs index e0a2ba3cf..ce846009a 100644 --- a/milli/src/search/new/db_cache.rs +++ b/milli/src/search/new/db_cache.rs @@ -157,7 +157,8 @@ impl<'ctx> SearchContext<'ctx> { match &self.restricted_fids { Some(restricted_fids) => { let interned = self.word_interner.get(word).as_str(); - let keys: Vec<_> = restricted_fids.iter().map(|fid| (interned, *fid)).collect(); + let keys: Vec<_> = + restricted_fids.tolerant.iter().map(|fid| (interned, *fid)).collect(); DatabaseCache::get_value_from_keys::<_, _, CboRoaringBitmapCodec>( self.txn, @@ -182,13 +183,29 @@ impl<'ctx> SearchContext<'ctx> { &mut self, word: Interned, ) -> Result> { - DatabaseCache::get_value::<_, _, RoaringBitmapCodec>( - self.txn, - word, - self.word_interner.get(word).as_str(), - &mut self.db_cache.exact_word_docids, - self.index.exact_word_docids.remap_data_type::(), - ) + match &self.restricted_fids { + Some(restricted_fids) => { + let interned = self.word_interner.get(word).as_str(); + let keys: Vec<_> = + restricted_fids.exact.iter().map(|fid| (interned, *fid)).collect(); + + DatabaseCache::get_value_from_keys::<_, _, CboRoaringBitmapCodec>( + self.txn, + word, + &keys[..], + &mut self.db_cache.exact_word_docids, + self.index.word_fid_docids.remap_data_type::(), + merge_cbo_roaring_bitmaps, + ) + } + None => DatabaseCache::get_value::<_, _, RoaringBitmapCodec>( + self.txn, + word, + self.word_interner.get(word).as_str(), + &mut self.db_cache.exact_word_docids, + self.index.exact_word_docids.remap_data_type::(), + ), + } } pub fn word_prefix_docids(&mut self, prefix: Word) -> Result> { @@ -219,7 +236,8 @@ impl<'ctx> SearchContext<'ctx> { match &self.restricted_fids { Some(restricted_fids) => { let interned = self.word_interner.get(prefix).as_str(); - let keys: Vec<_> = restricted_fids.iter().map(|fid| (interned, *fid)).collect(); + let keys: Vec<_> = + restricted_fids.tolerant.iter().map(|fid| (interned, *fid)).collect(); DatabaseCache::get_value_from_keys::<_, _, CboRoaringBitmapCodec>( self.txn, @@ -244,13 +262,29 @@ impl<'ctx> SearchContext<'ctx> { &mut self, prefix: Interned, ) -> Result> { - DatabaseCache::get_value::<_, _, RoaringBitmapCodec>( - self.txn, - prefix, - self.word_interner.get(prefix).as_str(), - &mut self.db_cache.exact_word_prefix_docids, - self.index.exact_word_prefix_docids.remap_data_type::(), - ) + match &self.restricted_fids { + Some(restricted_fids) => { + let interned = self.word_interner.get(prefix).as_str(); + let keys: Vec<_> = + restricted_fids.exact.iter().map(|fid| (interned, *fid)).collect(); + + DatabaseCache::get_value_from_keys::<_, _, CboRoaringBitmapCodec>( + self.txn, + prefix, + &keys[..], + &mut self.db_cache.exact_word_prefix_docids, + self.index.word_prefix_fid_docids.remap_data_type::(), + merge_cbo_roaring_bitmaps, + ) + } + None => DatabaseCache::get_value::<_, _, RoaringBitmapCodec>( + self.txn, + prefix, + self.word_interner.get(prefix).as_str(), + &mut self.db_cache.exact_word_prefix_docids, + self.index.exact_word_prefix_docids.remap_data_type::(), + ), + } } pub fn get_db_word_pair_proximity_docids( diff --git a/milli/src/search/new/mod.rs b/milli/src/search/new/mod.rs index 6ceb78223..ba29dbd1f 100644 --- a/milli/src/search/new/mod.rs +++ b/milli/src/search/new/mod.rs @@ -51,7 +51,8 @@ use crate::error::FieldIdMapMissingEntry; use crate::score_details::{ScoreDetails, ScoringStrategy}; use crate::search::new::distinct::apply_distinct_rule; use crate::{ - AscDesc, DocumentId, Filter, Index, Member, Result, TermsMatchingStrategy, UserError, BEU32, + AscDesc, DocumentId, FieldId, Filter, Index, Member, Result, TermsMatchingStrategy, UserError, + BEU32, }; /// A structure used throughout the execution of a search query. @@ -63,7 +64,7 @@ pub struct SearchContext<'ctx> { pub phrase_interner: DedupInterner, pub term_interner: Interner, pub phrase_docids: PhraseDocIdsCache, - pub restricted_fids: Option>, + pub restricted_fids: Option, } impl<'ctx> SearchContext<'ctx> { @@ -83,8 +84,9 @@ impl<'ctx> SearchContext<'ctx> { pub fn searchable_attributes(&mut self, searchable_attributes: &'ctx [String]) -> Result<()> { let fids_map = self.index.fields_ids_map(self.txn)?; let searchable_names = self.index.searchable_fields(self.txn)?; + let exact_attributes_ids = self.index.exact_attributes_ids(self.txn)?; - let mut restricted_fids = Vec::new(); + let mut restricted_fids = RestrictedFids::default(); let mut contains_wildcard = false; for field_name in searchable_attributes { if field_name == "*" { @@ -123,7 +125,11 @@ impl<'ctx> SearchContext<'ctx> { } }; - restricted_fids.push(fid); + if exact_attributes_ids.contains(&fid) { + restricted_fids.exact.push(fid); + } else { + restricted_fids.tolerant.push(fid); + }; } self.restricted_fids = (!contains_wildcard).then_some(restricted_fids); @@ -147,6 +153,18 @@ impl Word { } } +#[derive(Debug, Clone, Default)] +pub struct RestrictedFids { + pub tolerant: Vec, + pub exact: Vec, +} + +impl RestrictedFids { + pub fn contains(&self, fid: &FieldId) -> bool { + self.tolerant.contains(fid) || self.exact.contains(fid) + } +} + /// Apply the [`TermsMatchingStrategy`] to the query graph and resolve it. fn resolve_maximally_reduced_query_graph( ctx: &mut SearchContext,