feat: add metadata field with queryUid to search responses

- Add SearchMetadata struct with queryUid field (UUID v7)
- Add metadata field to SearchResult for /search route
- Add metadata field to FederatedSearchResult for /multi-search route
- Update perform_search to generate queryUid and set metadata
- Update federated search to generate queryUid for each query
- Update multi-search non-federated path to include metadata
- Fix pattern matching in analytics and other code

The metadata field contains:
- For /search: single object with queryUid
- For /multi-search: array of objects, one per query
- For federated search: array of objects, one per query

All queryUid values are generated using Uuid::now_v7() for time-ordered uniqueness.
This commit is contained in:
ManyTheFish
2025-09-30 15:22:35 +02:00
parent b98e2cef81
commit 194ace6bd1
4 changed files with 30 additions and 0 deletions

View File

@@ -235,6 +235,7 @@ impl<Method: AggregateMethod> SearchAggregator<Method> {
degraded, degraded,
used_negative_operator, used_negative_operator,
request_uid: _, request_uid: _,
metadata: _,
} = result; } = result;
self.total_succeeded = self.total_succeeded.saturating_add(1); self.total_succeeded = self.total_succeeded.saturating_add(1);

View File

@@ -20,6 +20,7 @@ use tokio::task::JoinHandle;
use uuid::Uuid; use uuid::Uuid;
use super::super::ranking_rules::{self, RankingRules}; use super::super::ranking_rules::{self, RankingRules};
use super::super::SearchMetadata;
use super::super::{ use super::super::{
compute_facet_distribution_stats, prepare_search, AttributesFormat, ComputedFacets, HitMaker, compute_facet_distribution_stats, prepare_search, AttributesFormat, ComputedFacets, HitMaker,
HitsInfo, RetrieveVectors, SearchHit, SearchKind, SearchQuery, SearchQueryWithIndex, HitsInfo, RetrieveVectors, SearchHit, SearchKind, SearchQuery, SearchQueryWithIndex,
@@ -59,7 +60,10 @@ pub async fn perform_federated_search(
// 1. partition queries by host and index // 1. partition queries by host and index
let mut partitioned_queries = PartitionedQueries::new(); let mut partitioned_queries = PartitionedQueries::new();
let mut query_metadata = Vec::new();
for (query_index, federated_query) in queries.into_iter().enumerate() { for (query_index, federated_query) in queries.into_iter().enumerate() {
let query_uid = Uuid::now_v7();
query_metadata.push(SearchMetadata { query_uid });
partitioned_queries.partition(federated_query, query_index, &network, features)? partitioned_queries.partition(federated_query, query_index, &network, features)?
} }
@@ -173,6 +177,7 @@ pub async fn perform_federated_search(
facets_by_index, facets_by_index,
remote_errors: partitioned_queries.has_remote.then_some(remote_errors), remote_errors: partitioned_queries.has_remote.then_some(remote_errors),
request_uid: Some(request_uid), request_uid: Some(request_uid),
metadata: Some(query_metadata),
}) })
} }
@@ -442,6 +447,7 @@ fn merge_metadata(
degraded: degraded_for_host, degraded: degraded_for_host,
used_negative_operator: host_used_negative_operator, used_negative_operator: host_used_negative_operator,
remote_errors: _, remote_errors: _,
metadata: _,
request_uid: _, request_uid: _,
} in remote_results } in remote_results
{ {

View File

@@ -18,6 +18,8 @@ use serde::{Deserialize, Serialize};
use utoipa::ToSchema; use utoipa::ToSchema;
use uuid::Uuid; use uuid::Uuid;
use crate::search::SearchMetadata;
use super::super::{ComputedFacets, FacetStats, HitsInfo, SearchHit, SearchQueryWithIndex}; use super::super::{ComputedFacets, FacetStats, HitsInfo, SearchHit, SearchQueryWithIndex};
use crate::milli::vector::Embedding; use crate::milli::vector::Embedding;
@@ -134,6 +136,8 @@ pub struct FederatedSearchResult {
pub facets_by_index: FederatedFacets, pub facets_by_index: FederatedFacets,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub request_uid: Option<Uuid>, pub request_uid: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<Vec<SearchMetadata>>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub remote_errors: Option<BTreeMap<String, ResponseError>>, pub remote_errors: Option<BTreeMap<String, ResponseError>>,
@@ -160,6 +164,7 @@ impl fmt::Debug for FederatedSearchResult {
facets_by_index, facets_by_index,
remote_errors, remote_errors,
request_uid, request_uid,
metadata,
} = self; } = self;
let mut debug = f.debug_struct("SearchResult"); let mut debug = f.debug_struct("SearchResult");
@@ -195,6 +200,9 @@ impl fmt::Debug for FederatedSearchResult {
if let Some(request_uid) = request_uid { if let Some(request_uid) = request_uid {
debug.field("request_uid", &request_uid); debug.field("request_uid", &request_uid);
} }
if let Some(metadata) = metadata {
debug.field("metadata", &metadata);
}
debug.finish() debug.finish()
} }

View File

@@ -836,6 +836,13 @@ pub struct SearchHit {
pub ranking_score_details: Option<serde_json::Map<String, serde_json::Value>>, pub ranking_score_details: Option<serde_json::Map<String, serde_json::Value>>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)]
#[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")]
pub struct SearchMetadata {
pub query_uid: Uuid,
}
#[derive(Serialize, Clone, PartialEq, ToSchema)] #[derive(Serialize, Clone, PartialEq, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[schema(rename_all = "camelCase")] #[schema(rename_all = "camelCase")]
@@ -854,6 +861,8 @@ pub struct SearchResult {
pub facet_stats: Option<BTreeMap<String, FacetStats>>, pub facet_stats: Option<BTreeMap<String, FacetStats>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub request_uid: Option<Uuid>, pub request_uid: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<SearchMetadata>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub semantic_hit_count: Option<u32>, pub semantic_hit_count: Option<u32>,
@@ -876,6 +885,7 @@ impl fmt::Debug for SearchResult {
facet_distribution, facet_distribution,
facet_stats, facet_stats,
request_uid, request_uid,
metadata,
semantic_hit_count, semantic_hit_count,
degraded, degraded,
used_negative_operator, used_negative_operator,
@@ -908,6 +918,9 @@ impl fmt::Debug for SearchResult {
if let Some(request_uid) = request_uid { if let Some(request_uid) = request_uid {
debug.field("request_uid", &request_uid); debug.field("request_uid", &request_uid);
} }
if let Some(metadata) = metadata {
debug.field("metadata", &metadata);
}
debug.finish() debug.finish()
} }
@@ -1234,6 +1247,7 @@ pub fn perform_search(
.map(|ComputedFacets { distribution, stats }| (distribution, stats)) .map(|ComputedFacets { distribution, stats }| (distribution, stats))
.unzip(); .unzip();
let query_uid = Uuid::now_v7();
let result = SearchResult { let result = SearchResult {
hits: documents, hits: documents,
hits_info, hits_info,
@@ -1246,6 +1260,7 @@ pub fn perform_search(
used_negative_operator, used_negative_operator,
semantic_hit_count, semantic_hit_count,
request_uid: Some(request_uid), request_uid: Some(request_uid),
metadata: Some(SearchMetadata { query_uid }),
}; };
Ok(result) Ok(result)
} }