diff --git a/meilisearch-types/src/error.rs b/meilisearch-types/src/error.rs index 1509847b7..ae6e42c17 100644 --- a/meilisearch-types/src/error.rs +++ b/meilisearch-types/src/error.rs @@ -240,6 +240,7 @@ InvalidSearchOffset , InvalidRequest , BAD_REQUEST ; InvalidSearchPage , InvalidRequest , BAD_REQUEST ; InvalidSearchQ , InvalidRequest , BAD_REQUEST ; InvalidSearchShowMatchesPosition , 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..46c1d90d9 100644 --- a/meilisearch/src/routes/indexes/search.rs +++ b/meilisearch/src/routes/indexes/search.rs @@ -56,6 +56,8 @@ pub struct SearchQueryGet { sort: Option, #[deserr(default, error = DeserrQueryParamError)] show_matches_position: 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 +93,7 @@ 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_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..68180446b 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; use meilisearch_types::settings::DEFAULT_PAGINATION_MAX_TOTAL_HITS; use meilisearch_types::{milli, Document}; use milli::tokenizer::TokenizerBuilder; @@ -54,6 +55,8 @@ 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_details: bool, #[deserr(default, error = DeserrJsonError)] pub filter: Option, #[deserr(default, error = DeserrJsonError)] @@ -103,6 +106,8 @@ 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_details: bool, #[deserr(default, error = DeserrJsonError, default)] pub show_matches_position: bool, #[deserr(default, error = DeserrJsonError)] @@ -134,6 +139,7 @@ impl SearchQueryWithIndex { attributes_to_crop, crop_length, attributes_to_highlight, + show_ranking_score_details, show_matches_position, filter, sort, @@ -155,6 +161,7 @@ impl SearchQueryWithIndex { attributes_to_crop, crop_length, attributes_to_highlight, + show_ranking_score_details, show_matches_position, filter, sort, @@ -194,7 +201,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 +209,10 @@ pub struct SearchHit { pub formatted: Document, #[serde(rename = "_matchesPosition", skip_serializing_if = "Option::is_none")] pub matches_position: Option, + #[serde(rename = "_rankingScoreDetails", skip_serializing_if = "Option::is_none")] + pub ranking_score_details: Option>, + #[serde(rename = "_rankingScore")] + pub ranking_score: f64, } #[derive(Serialize, Debug, Clone, PartialEq)] @@ -320,7 +331,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 +404,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 +428,17 @@ pub fn perform_search( insert_geo_distance(sort, &mut document); } - let hit = SearchHit { document, formatted, matches_position }; + let ranking_score = 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/src/index.rs b/milli/src/index.rs index 0d74e0732..80b2aff37 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..1b2d69aeb 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; use crate::{ execute_search, AscDesc, DefaultSearchLogger, DocumentId, Index, Result, SearchContext, }; @@ -93,7 +94,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,7 +103,7 @@ 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, @@ -124,7 +125,7 @@ impl<'a> Search<'a> { None => MatchingWords::default(), }; - Ok(SearchResult { matching_words, candidates, documents_ids }) + Ok(SearchResult { matching_words, candidates, document_scores, documents_ids }) } } @@ -160,8 +161,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/mod.rs b/milli/src/search/new/mod.rs index 95566427c..011ef0cc3 100644 --- a/milli/src/search/new/mod.rs +++ b/milli/src/search/new/mod.rs @@ -443,6 +443,7 @@ pub fn execute_search( Ok(PartialSearchResult { candidates: all_candidates, + document_scores: scores, documents_ids: docids, located_query_terms, }) @@ -494,4 +495,5 @@ pub struct PartialSearchResult { pub located_query_terms: Option>, pub candidates: RoaringBitmap, pub documents_ids: Vec, + pub document_scores: Vec>, }