diff --git a/meilisearch-types/src/error.rs b/meilisearch-types/src/error.rs index 1509847b7..3e08498de 100644 --- a/meilisearch-types/src/error.rs +++ b/meilisearch-types/src/error.rs @@ -240,6 +240,8 @@ InvalidSearchOffset , InvalidRequest , BAD_REQUEST ; InvalidSearchPage , InvalidRequest , BAD_REQUEST ; InvalidSearchQ , InvalidRequest , BAD_REQUEST ; InvalidSearchShowMatchesPosition , InvalidRequest , BAD_REQUEST ; +InvalidSearchShowRankingScore , InvalidRequest , BAD_REQUEST ; +InvalidSearchShowRankingScoreDetails , InvalidRequest , BAD_REQUEST ; InvalidSearchSort , InvalidRequest , BAD_REQUEST ; InvalidSettingsDisplayedAttributes , InvalidRequest , BAD_REQUEST ; InvalidSettingsDistinctAttribute , InvalidRequest , BAD_REQUEST ; diff --git a/meilisearch/src/routes/indexes/search.rs b/meilisearch/src/routes/indexes/search.rs index f9242f320..cb70147cd 100644 --- a/meilisearch/src/routes/indexes/search.rs +++ b/meilisearch/src/routes/indexes/search.rs @@ -56,6 +56,10 @@ pub struct SearchQueryGet { sort: Option, #[deserr(default, error = DeserrQueryParamError)] show_matches_position: Param, + #[deserr(default, error = DeserrQueryParamError)] + show_ranking_score: Param, + #[deserr(default, error = DeserrQueryParamError)] + show_ranking_score_details: Param, #[deserr(default, error = DeserrQueryParamError)] facets: Option>, #[deserr( default = DEFAULT_HIGHLIGHT_PRE_TAG(), error = DeserrQueryParamError)] @@ -91,6 +95,8 @@ impl From for SearchQuery { filter, sort: other.sort.map(|attr| fix_sort_query_parameters(&attr)), show_matches_position: other.show_matches_position.0, + show_ranking_score: other.show_ranking_score.0, + show_ranking_score_details: other.show_ranking_score_details.0, facets: other.facets.map(|o| o.into_iter().collect()), highlight_pre_tag: other.highlight_pre_tag, highlight_post_tag: other.highlight_post_tag, diff --git a/meilisearch/src/search.rs b/meilisearch/src/search.rs index 581f4b653..b2858ead3 100644 --- a/meilisearch/src/search.rs +++ b/meilisearch/src/search.rs @@ -9,6 +9,7 @@ use meilisearch_auth::IndexSearchRules; use meilisearch_types::deserr::DeserrJsonError; use meilisearch_types::error::deserr_codes::*; use meilisearch_types::index_uid::IndexUid; +use meilisearch_types::milli::score_details::{ScoreDetails, ScoringStrategy}; use meilisearch_types::settings::DEFAULT_PAGINATION_MAX_TOTAL_HITS; use meilisearch_types::{milli, Document}; use milli::tokenizer::TokenizerBuilder; @@ -54,6 +55,10 @@ pub struct SearchQuery { pub attributes_to_highlight: Option>, #[deserr(default, error = DeserrJsonError, default)] pub show_matches_position: bool, + #[deserr(default, error = DeserrJsonError, default)] + pub show_ranking_score: bool, + #[deserr(default, error = DeserrJsonError, default)] + pub show_ranking_score_details: bool, #[deserr(default, error = DeserrJsonError)] pub filter: Option, #[deserr(default, error = DeserrJsonError)] @@ -103,6 +108,10 @@ pub struct SearchQueryWithIndex { pub crop_length: usize, #[deserr(default, error = DeserrJsonError)] pub attributes_to_highlight: Option>, + #[deserr(default, error = DeserrJsonError, default)] + pub show_ranking_score: bool, + #[deserr(default, error = DeserrJsonError, default)] + pub show_ranking_score_details: bool, #[deserr(default, error = DeserrJsonError, default)] pub show_matches_position: bool, #[deserr(default, error = DeserrJsonError)] @@ -134,6 +143,8 @@ impl SearchQueryWithIndex { attributes_to_crop, crop_length, attributes_to_highlight, + show_ranking_score, + show_ranking_score_details, show_matches_position, filter, sort, @@ -155,6 +166,8 @@ impl SearchQueryWithIndex { attributes_to_crop, crop_length, attributes_to_highlight, + show_ranking_score, + show_ranking_score_details, show_matches_position, filter, sort, @@ -194,7 +207,7 @@ impl From for TermsMatchingStrategy { } } -#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, PartialEq)] pub struct SearchHit { #[serde(flatten)] pub document: Document, @@ -202,6 +215,10 @@ pub struct SearchHit { pub formatted: Document, #[serde(rename = "_matchesPosition", skip_serializing_if = "Option::is_none")] pub matches_position: Option, + #[serde(rename = "_rankingScore", skip_serializing_if = "Option::is_none")] + pub ranking_score: Option, + #[serde(rename = "_rankingScoreDetails", skip_serializing_if = "Option::is_none")] + pub ranking_score_details: Option>, } #[derive(Serialize, Debug, Clone, PartialEq)] @@ -283,6 +300,11 @@ pub fn perform_search( .unwrap_or(DEFAULT_PAGINATION_MAX_TOTAL_HITS); search.exhaustive_number_hits(is_finite_pagination); + search.scoring_strategy(if query.show_ranking_score || query.show_ranking_score_details { + ScoringStrategy::Detailed + } else { + ScoringStrategy::Skip + }); // compute the offset on the limit depending on the pagination mode. let (offset, limit) = if is_finite_pagination { @@ -320,7 +342,8 @@ pub fn perform_search( search.sort_criteria(sort); } - let milli::SearchResult { documents_ids, matching_words, candidates, .. } = search.execute()?; + let milli::SearchResult { documents_ids, matching_words, candidates, document_scores, .. } = + search.execute()?; let fields_ids_map = index.fields_ids_map(&rtxn).unwrap(); @@ -392,7 +415,7 @@ pub fn perform_search( let documents_iter = index.documents(&rtxn, documents_ids)?; - for (_id, obkv) in documents_iter { + for ((_id, obkv), score) in documents_iter.into_iter().zip(document_scores.into_iter()) { // First generate a document with all the displayed fields let displayed_document = make_document(&displayed_ids, &fields_ids_map, obkv)?; @@ -416,7 +439,18 @@ pub fn perform_search( insert_geo_distance(sort, &mut document); } - let hit = SearchHit { document, formatted, matches_position }; + let ranking_score = + query.show_ranking_score.then(|| ScoreDetails::global_score(score.iter())); + let ranking_score_details = + query.show_ranking_score_details.then(|| ScoreDetails::to_json_map(score.iter())); + + let hit = SearchHit { + document, + formatted, + matches_position, + ranking_score_details, + ranking_score, + }; documents.push(hit); } diff --git a/milli/examples/search.rs b/milli/examples/search.rs index 8898e5dac..87c9a004d 100644 --- a/milli/examples/search.rs +++ b/milli/examples/search.rs @@ -53,6 +53,7 @@ fn main() -> Result<(), Box> { &mut ctx, &(!query.trim().is_empty()).then(|| query.trim().to_owned()), TermsMatchingStrategy::Last, + milli::score_details::ScoringStrategy::Skip, false, &None, &None, diff --git a/milli/src/index.rs b/milli/src/index.rs index 1ccef13dd..fad3f665c 100644 --- a/milli/src/index.rs +++ b/milli/src/index.rs @@ -2488,8 +2488,12 @@ pub(crate) mod tests { let rtxn = index.read_txn().unwrap(); let search = Search::new(&rtxn, &index); - let SearchResult { matching_words: _, candidates: _, mut documents_ids } = - search.execute().unwrap(); + let SearchResult { + matching_words: _, + candidates: _, + document_scores: _, + mut documents_ids, + } = search.execute().unwrap(); let primary_key_id = index.fields_ids_map(&rtxn).unwrap().id("primary_key").unwrap(); documents_ids.sort_unstable(); let docs = index.documents(&rtxn, documents_ids).unwrap(); diff --git a/milli/src/search/mod.rs b/milli/src/search/mod.rs index dcef30920..3c972d9b0 100644 --- a/milli/src/search/mod.rs +++ b/milli/src/search/mod.rs @@ -7,6 +7,7 @@ use roaring::bitmap::RoaringBitmap; pub use self::facet::{FacetDistribution, Filter, DEFAULT_VALUES_PER_FACET}; pub use self::new::matches::{FormatOptions, MatchBounds, Matcher, MatcherBuilder, MatchingWords}; use self::new::PartialSearchResult; +use crate::score_details::{ScoreDetails, ScoringStrategy}; use crate::{ execute_search, AscDesc, DefaultSearchLogger, DocumentId, Index, Result, SearchContext, }; @@ -29,6 +30,7 @@ pub struct Search<'a> { sort_criteria: Option>, geo_strategy: new::GeoSortStrategy, terms_matching_strategy: TermsMatchingStrategy, + scoring_strategy: ScoringStrategy, words_limit: usize, exhaustive_number_hits: bool, rtxn: &'a heed::RoTxn<'a>, @@ -45,6 +47,7 @@ impl<'a> Search<'a> { sort_criteria: None, geo_strategy: new::GeoSortStrategy::default(), terms_matching_strategy: TermsMatchingStrategy::default(), + scoring_strategy: Default::default(), exhaustive_number_hits: false, words_limit: 10, rtxn, @@ -77,6 +80,11 @@ impl<'a> Search<'a> { self } + pub fn scoring_strategy(&mut self, value: ScoringStrategy) -> &mut Search<'a> { + self.scoring_strategy = value; + self + } + pub fn words_limit(&mut self, value: usize) -> &mut Search<'a> { self.words_limit = value; self @@ -93,7 +101,7 @@ impl<'a> Search<'a> { self } - /// Force the search to exhastivelly compute the number of candidates, + /// Forces the search to exhaustively compute the number of candidates, /// this will increase the search time but allows finite pagination. pub fn exhaustive_number_hits(&mut self, exhaustive_number_hits: bool) -> &mut Search<'a> { self.exhaustive_number_hits = exhaustive_number_hits; @@ -102,11 +110,12 @@ impl<'a> Search<'a> { pub fn execute(&self) -> Result { let mut ctx = SearchContext::new(self.index, self.rtxn); - let PartialSearchResult { located_query_terms, candidates, documents_ids } = + let PartialSearchResult { located_query_terms, candidates, documents_ids, document_scores } = execute_search( &mut ctx, &self.query, self.terms_matching_strategy, + self.scoring_strategy, self.exhaustive_number_hits, &self.filter, &self.sort_criteria, @@ -124,7 +133,7 @@ impl<'a> Search<'a> { None => MatchingWords::default(), }; - Ok(SearchResult { matching_words, candidates, documents_ids }) + Ok(SearchResult { matching_words, candidates, document_scores, documents_ids }) } } @@ -138,6 +147,7 @@ impl fmt::Debug for Search<'_> { sort_criteria, geo_strategy: _, terms_matching_strategy, + scoring_strategy, words_limit, exhaustive_number_hits, rtxn: _, @@ -150,6 +160,7 @@ impl fmt::Debug for Search<'_> { .field("limit", limit) .field("sort_criteria", sort_criteria) .field("terms_matching_strategy", terms_matching_strategy) + .field("scoring_strategy", scoring_strategy) .field("exhaustive_number_hits", exhaustive_number_hits) .field("words_limit", words_limit) .finish() @@ -160,8 +171,8 @@ impl fmt::Debug for Search<'_> { pub struct SearchResult { pub matching_words: MatchingWords, pub candidates: RoaringBitmap, - // TODO those documents ids should be associated with their criteria scores. pub documents_ids: Vec, + pub document_scores: Vec>, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/milli/src/search/new/matches/mod.rs b/milli/src/search/new/matches/mod.rs index 677687b32..f33d595e5 100644 --- a/milli/src/search/new/matches/mod.rs +++ b/milli/src/search/new/matches/mod.rs @@ -510,6 +510,7 @@ mod tests { &mut ctx, &Some(query.to_string()), crate::TermsMatchingStrategy::default(), + crate::score_details::ScoringStrategy::Skip, false, &None, &None, diff --git a/milli/src/search/new/mod.rs b/milli/src/search/new/mod.rs index 9609384d5..8df764f29 100644 --- a/milli/src/search/new/mod.rs +++ b/milli/src/search/new/mod.rs @@ -351,6 +351,7 @@ pub fn execute_search( ctx: &mut SearchContext, query: &Option, terms_matching_strategy: TermsMatchingStrategy, + scoring_strategy: ScoringStrategy, exhaustive_number_hits: bool, filters: &Option, sort_criteria: &Option>, @@ -419,7 +420,7 @@ pub fn execute_search( &universe, from, length, - ScoringStrategy::Skip, + scoring_strategy, query_graph_logger, )? } else { @@ -432,7 +433,7 @@ pub fn execute_search( &universe, from, length, - ScoringStrategy::Skip, + scoring_strategy, placeholder_search_logger, )? }; @@ -453,6 +454,7 @@ pub fn execute_search( Ok(PartialSearchResult { candidates: all_candidates, + document_scores: scores, documents_ids: docids, located_query_terms, }) @@ -504,4 +506,5 @@ pub struct PartialSearchResult { pub located_query_terms: Option>, pub candidates: RoaringBitmap, pub documents_ids: Vec, + pub document_scores: Vec>, }