Skip to content

Commit 5a84fb3

Browse files
committed
feat: helper method to parse federated search results; added support for union search
1 parent 579d4b2 commit 5a84fb3

File tree

10 files changed

+818
-148
lines changed

10 files changed

+818
-148
lines changed

typesense/src/client/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,7 @@ impl Client {
733733
/// # ..Default::default()
734734
/// # };
735735
/// # let common_params = models::MultiSearchParameters::default();
736-
/// let results = client.multi_search().perform(search_requests, common_params).await.unwrap();
736+
/// let results = client.multi_search().perform(&search_requests, &common_params).await.unwrap();
737737
/// # Ok(())
738738
/// # }
739739
/// ```

typesense/src/client/multi_search.rs

Lines changed: 353 additions & 87 deletions
Large diffs are not rendered by default.

typesense/src/error.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,56 @@ where
5252
#[error("Failed to deserialize the API response into the target struct: {0}")]
5353
Deserialization(#[from] serde_json::Error),
5454
}
55+
56+
/// Represents the possible errors that can occur when parsing a `multi_search` response.
57+
///
58+
/// This error enum is returned by the `MultiSearchResultExt::parse_at` method when it
59+
/// fails to convert a raw search result into a strongly-typed `SearchResult<T>`.
60+
#[derive(Debug, Error)]
61+
pub enum MultiSearchParseError {
62+
/// Indicates that the requested index was outside the bounds of the results vector.
63+
///
64+
/// For a `multi_search` request with `n` search queries, the valid indices for the
65+
/// results are `0` through `n-1`. This error occurs if the provided index is `n` or greater.
66+
///
67+
/// # Fields
68+
/// * `0` - The invalid index that was requested.
69+
#[error("Search result index {0} is out of bounds.")]
70+
IndexOutOfBounds(usize),
71+
72+
/// Indicates that the Typesense server returned an error for the specific search query at this index.
73+
///
74+
// It's possible for a `multi_search` request to succeed overall, but for one or more
75+
// individual searches within it to fail (e.g., due to a typo in a collection name).
76+
///
77+
/// # Fields
78+
/// * `index` - The index of the search query that failed.
79+
/// * `message` - The error message returned by the Typesense API for this specific search.
80+
#[error("The search at index {index} failed with an API error: {message}")]
81+
ApiError {
82+
/// The index of the search query that failed.
83+
index: usize,
84+
/// The error message returned by the Typesense API for this specific search.
85+
message: String,
86+
},
87+
88+
/// Indicates a failure to deserialize a document's JSON into the target struct `T`.
89+
///
90+
/// This typically happens when the fields in the document stored in Typesense do not
91+
/// match the fields defined in the target Rust struct `T`. Check for mismatches in
92+
/// field names or data types.
93+
///
94+
/// # Fields
95+
/// * `index` - The index of the search query where the deserialization error occurred.
96+
/// * `source` - The underlying `serde_json::Error` that provides detailed information
97+
/// about the deserialization failure.
98+
#[error("Failed to deserialize a document at index {index}: {source}")]
99+
Deserialization {
100+
/// The index of the search query where the deserialization error occurred.
101+
index: usize,
102+
/// The underlying `serde_json::Error` that provides detailed information
103+
/// about the deserialization failure.
104+
#[source]
105+
source: serde_json::Error,
106+
},
107+
}

typesense/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,14 @@ mod error;
4949
pub mod collection_schema;
5050
pub mod document;
5151
pub mod field;
52+
pub mod prelude;
5253
// pub mod keys;
5354
pub mod models;
5455

5556
pub use client::{Client, MultiNodeConfiguration};
56-
pub use error::{ApiError, Error};
57+
pub use error::*;
58+
pub use models::*;
59+
pub use prelude::*;
5760

5861
#[cfg(feature = "typesense_derive")]
5962
#[doc(hidden)]

typesense/src/models/mod.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
//! # Typesense generic models
2+
mod multi_search;
23
mod scoped_key_parameters;
34
mod search_result;
4-
5+
pub use multi_search::*;
56
pub use scoped_key_parameters::*;
67
pub use search_result::*;
78

@@ -19,18 +20,17 @@ pub use typesense_codegen::models::{
1920
FacetCountsStats, Field, FieldEmbed, FieldEmbedModelConfig, HealthStatus,
2021
ImportDocumentsParameters, IndexAction, ListStemmingDictionaries200Response,
2122
MultiSearchCollectionParameters, MultiSearchParameters, MultiSearchResult,
22-
MultiSearchResultItem, MultiSearchSearchesParameter, NlSearchModelBase,
23-
NlSearchModelCreateSchema, NlSearchModelDeleteSchema, NlSearchModelSchema, PresetDeleteSchema,
24-
PresetSchema, PresetUpsertSchema, PresetUpsertSchemaValue, PresetsRetrieveSchema,
25-
SchemaChangeStatus, SearchGroupedHit, SearchHighlight, SearchOverride,
26-
SearchOverrideDeleteResponse, SearchOverrideExclude, SearchOverrideInclude, SearchOverrideRule,
27-
SearchOverrideSchema, SearchOverridesResponse, SearchParameters, SearchResultConversation,
28-
SearchResultHitTextMatchInfo, SearchResultRequestParams, SearchResultRequestParamsVoiceQuery,
29-
SearchSynonym, SearchSynonymDeleteResponse, SearchSynonymSchema, SearchSynonymsResponse,
30-
SnapshotParameters, StemmingDictionary, StemmingDictionaryWordsInner,
31-
StopwordsSetRetrieveSchema, StopwordsSetSchema, StopwordsSetUpsertSchema,
32-
StopwordsSetsRetrieveAllSchema, SuccessStatus, UpdateDocuments200Response,
33-
UpdateDocumentsParameters, VoiceQueryModelCollectionConfig,
23+
MultiSearchResultItem, NlSearchModelBase, NlSearchModelCreateSchema, NlSearchModelDeleteSchema,
24+
NlSearchModelSchema, PresetDeleteSchema, PresetSchema, PresetUpsertSchema,
25+
PresetUpsertSchemaValue, PresetsRetrieveSchema, SchemaChangeStatus, SearchGroupedHit,
26+
SearchHighlight, SearchOverride, SearchOverrideDeleteResponse, SearchOverrideExclude,
27+
SearchOverrideInclude, SearchOverrideRule, SearchOverrideSchema, SearchOverridesResponse,
28+
SearchParameters, SearchResultConversation, SearchResultHitTextMatchInfo,
29+
SearchResultRequestParams, SearchResultRequestParamsVoiceQuery, SearchSynonym,
30+
SearchSynonymDeleteResponse, SearchSynonymSchema, SearchSynonymsResponse, SnapshotParameters,
31+
StemmingDictionary, StemmingDictionaryWordsInner, StopwordsSetRetrieveSchema,
32+
StopwordsSetSchema, StopwordsSetUpsertSchema, StopwordsSetsRetrieveAllSchema, SuccessStatus,
33+
UpdateDocuments200Response, UpdateDocumentsParameters, VoiceQueryModelCollectionConfig,
3434
};
3535
// Only re-export the sub modules that have enums inside them.
3636
pub use typesense_codegen::models::{

typesense/src/models/multi_search.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use crate::models;
2+
use serde::{Deserialize, Serialize};
3+
4+
/// Represents the body of a multi-search request.
5+
///
6+
/// This struct acts as a container for a list of individual search queries that will be
7+
/// sent to the Typesense multi-search endpoint. Each search query is defined in a
8+
/// `MultiSearchCollectionParameters` struct within the `searches` vector.
9+
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
10+
pub struct MultiSearchSearchesParameter {
11+
/// A vector of individual search queries to be executed. The order of the search results returned by Typesense will match the order of these queries.
12+
#[serde(rename = "searches")]
13+
pub searches: Vec<models::MultiSearchCollectionParameters>,
14+
}
15+
16+
impl MultiSearchSearchesParameter {
17+
/// Creates a new `MultiSearchSearchesParameter` instance.
18+
pub fn new(
19+
searches: Vec<models::MultiSearchCollectionParameters>,
20+
) -> MultiSearchSearchesParameter {
21+
MultiSearchSearchesParameter { searches }
22+
}
23+
}

typesense/src/models/search_result.rs

Lines changed: 91 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Contains the generic `SearchResult` and `SearchResultHit` structs
22
33
use serde::{de::DeserializeOwned, Deserialize, Serialize};
4+
use serde_json::Value;
45
use typesense_codegen::models as raw_models;
56

67
/// Represents a single search result hit, with the document deserialized into a strongly-typed struct `T`.
@@ -105,24 +106,28 @@ where
105106
) -> Result<Self, serde_json::Error> {
106107
let typed_hits = match raw_result.hits {
107108
Some(raw_hits) => {
108-
let mut hits = Vec::with_capacity(raw_hits.len());
109-
for raw_hit in raw_hits {
110-
let document: Option<T> = match raw_hit.document {
111-
Some(doc_value) => Some(serde_json::from_value(doc_value)?),
112-
None => None,
113-
};
114-
115-
hits.push(SearchResultHit {
116-
document,
117-
highlights: raw_hit.highlights,
118-
highlight: raw_hit.highlight,
119-
text_match: raw_hit.text_match,
120-
text_match_info: raw_hit.text_match_info,
121-
geo_distance_meters: raw_hit.geo_distance_meters,
122-
vector_distance: raw_hit.vector_distance,
123-
});
124-
}
125-
Some(hits)
109+
let hits_result: Result<Vec<SearchResultHit<T>>, _> = raw_hits
110+
.into_iter()
111+
.map(|raw_hit| {
112+
// Map each raw hit to a Result<SearchResultHit<T>, _>
113+
let document: Result<Option<T>, _> = raw_hit
114+
.document
115+
.map(|doc_value| serde_json::from_value(doc_value))
116+
.transpose();
117+
118+
Ok(SearchResultHit {
119+
document: document?,
120+
highlights: raw_hit.highlights,
121+
highlight: raw_hit.highlight,
122+
text_match: raw_hit.text_match,
123+
text_match_info: raw_hit.text_match_info,
124+
geo_distance_meters: raw_hit.geo_distance_meters,
125+
vector_distance: raw_hit.vector_distance,
126+
})
127+
})
128+
.collect();
129+
130+
Some(hits_result?)
126131
}
127132
None => None,
128133
};
@@ -142,3 +147,71 @@ where
142147
})
143148
}
144149
}
150+
151+
// This impl block specifically targets `SearchResult<serde_json::Value>`.
152+
// The methods inside will only be available on a search result of that exact type.
153+
impl SearchResult<Value> {
154+
/// Attempts to convert a `SearchResult<serde_json::Value>` into a `SearchResult<T>`.
155+
///
156+
/// This method is useful after a `perform_union` call where you know all resulting
157+
/// documents share the same schema and can be deserialized into a single concrete type `T`.
158+
///
159+
/// It iterates through each hit and tries to deserialize its `document` field. If any
160+
/// document fails to deserialize into type `T`, the entire conversion fails.
161+
///
162+
/// # Type Parameters
163+
///
164+
/// * `T` - The concrete, `DeserializeOwned` type you want to convert the documents into.
165+
///
166+
/// # Errors
167+
///
168+
/// Returns a `serde_json::Error` if any document in the hit list cannot be successfully
169+
/// deserialized into `T`.
170+
pub fn try_into_typed<T: DeserializeOwned>(self) -> Result<SearchResult<T>, serde_json::Error> {
171+
// This logic is very similar to `from_raw`, but it converts between generic types
172+
// instead of from a raw model.
173+
let typed_hits = match self.hits {
174+
Some(value_hits) => {
175+
let hits_result: Result<Vec<SearchResultHit<T>>, _> = value_hits
176+
.into_iter()
177+
.map(|value_hit| {
178+
// `value_hit` here is `SearchResultHit<serde_json::Value>`
179+
let document: Option<T> = match value_hit.document {
180+
Some(doc_value) => Some(serde_json::from_value(doc_value)?),
181+
None => None,
182+
};
183+
184+
// Construct the new, strongly-typed hit.
185+
Ok(SearchResultHit {
186+
document,
187+
highlights: value_hit.highlights,
188+
highlight: value_hit.highlight,
189+
text_match: value_hit.text_match,
190+
text_match_info: value_hit.text_match_info,
191+
geo_distance_meters: value_hit.geo_distance_meters,
192+
vector_distance: value_hit.vector_distance,
193+
})
194+
})
195+
.collect();
196+
197+
Some(hits_result?)
198+
}
199+
None => None,
200+
};
201+
202+
// Construct the final, strongly-typed search result, carrying over all metadata.
203+
Ok(SearchResult {
204+
hits: typed_hits,
205+
found: self.found,
206+
found_docs: self.found_docs,
207+
out_of: self.out_of,
208+
page: self.page,
209+
search_time_ms: self.search_time_ms,
210+
facet_counts: self.facet_counts,
211+
grouped_hits: self.grouped_hits,
212+
search_cutoff: self.search_cutoff,
213+
request_params: self.request_params,
214+
conversation: self.conversation,
215+
})
216+
}
217+
}

typesense/src/prelude.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//! The Typesense prelude.
2+
//!
3+
//! This module re-exports the most commonly used traits and types from the library,
4+
//! making them easy to import with a single `use` statement.
5+
6+
use serde::de::DeserializeOwned;
7+
8+
use crate::{MultiSearchParseError, SearchResult};
9+
10+
/// An extension trait for `typesense_codegen::models::MultiSearchResult` to provide typed parsing.
11+
pub trait MultiSearchResultExt {
12+
/// Parses the result at a specific index from a multi-search response into a strongly-typed `SearchResult<T>`.
13+
///
14+
/// # Arguments
15+
/// * `index` - The zero-based index of the search result to parse.
16+
///
17+
/// # Type Parameters
18+
/// * `T` - The concrete document type to deserialize the hits into.
19+
fn parse_at<T: DeserializeOwned>(
20+
&self,
21+
index: usize,
22+
) -> Result<SearchResult<T>, MultiSearchParseError>;
23+
}

0 commit comments

Comments
 (0)