use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; use deserr::actix_web::AwebJson; use index_scheduler::IndexScheduler; use meilisearch_types::deserr::DeserrJsonError; use meilisearch_types::error::deserr_codes::*; use meilisearch_types::error::ResponseError; use meilisearch_types::index_uid::IndexUid; use serde_json::Value; use tracing::debug; use crate::analytics::{Analytics, FacetSearchAggregator}; use crate::extractors::authentication::policies::*; use crate::extractors::authentication::GuardedData; use crate::search::{ add_search_rules, perform_facet_search, HybridQuery, MatchingStrategy, SearchQuery, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_OFFSET, }; pub fn configure(cfg: &mut web::ServiceConfig) { cfg.service(web::resource("").route(web::post().to(search))); } /// # Important /// /// Intentionally don't use `deny_unknown_fields` to ignore search parameters sent by user #[derive(Debug, Clone, Default, PartialEq, deserr::Deserr)] #[deserr(error = DeserrJsonError, rename_all = camelCase)] pub struct FacetSearchQuery { #[deserr(default, error = DeserrJsonError)] pub facet_query: Option, #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_facet_search_facet_name)] pub facet_name: String, #[deserr(default, error = DeserrJsonError)] pub q: Option, #[deserr(default, error = DeserrJsonError)] pub vector: Option>, #[deserr(default, error = DeserrJsonError)] pub hybrid: Option, #[deserr(default, error = DeserrJsonError)] pub filter: Option, #[deserr(default, error = DeserrJsonError, default)] pub matching_strategy: MatchingStrategy, #[deserr(default, error = DeserrJsonError, default)] pub attributes_to_search_on: Option>, } pub async fn search( index_scheduler: GuardedData, Data>, index_uid: web::Path, params: AwebJson, req: HttpRequest, analytics: web::Data, ) -> Result { let index_uid = IndexUid::try_from(index_uid.into_inner())?; let query = params.into_inner(); debug!("facet search called with params: {:?}", query); let mut aggregate = FacetSearchAggregator::from_query(&query, &req); let facet_query = query.facet_query.clone(); let facet_name = query.facet_name.clone(); let mut search_query = SearchQuery::from(query); // Tenant token search_rules. if let Some(search_rules) = index_scheduler.filters().get_index_search_rules(&index_uid) { add_search_rules(&mut search_query, search_rules); } let index = index_scheduler.index(&index_uid)?; let features = index_scheduler.features(); let search_result = tokio::task::spawn_blocking(move || { perform_facet_search(&index, search_query, facet_query, facet_name, features) }) .await?; if let Ok(ref search_result) = search_result { aggregate.succeed(search_result); } analytics.post_facet_search(aggregate); let search_result = search_result?; debug!("returns: {:?}", search_result); Ok(HttpResponse::Ok().json(search_result)) } impl From for SearchQuery { fn from(value: FacetSearchQuery) -> Self { let FacetSearchQuery { facet_query: _, facet_name: _, q, vector, filter, matching_strategy, attributes_to_search_on, hybrid, } = value; SearchQuery { q, offset: DEFAULT_SEARCH_OFFSET(), limit: DEFAULT_SEARCH_LIMIT(), page: None, hits_per_page: None, attributes_to_retrieve: None, attributes_to_crop: None, crop_length: DEFAULT_CROP_LENGTH(), attributes_to_highlight: None, show_matches_position: false, show_ranking_score: false, show_ranking_score_details: false, filter, sort: None, facets: None, highlight_pre_tag: DEFAULT_HIGHLIGHT_PRE_TAG(), highlight_post_tag: DEFAULT_HIGHLIGHT_POST_TAG(), crop_marker: DEFAULT_CROP_MARKER(), matching_strategy, vector, attributes_to_search_on, hybrid, } } }