Skip to content

Commit c3a1135

Browse files
authored
Merge pull request #681 from meilisearch/federation
Implement federated multi search
2 parents 7d0d3ee + 1101fa0 commit c3a1135

File tree

6 files changed

+427
-102
lines changed

6 files changed

+427
-102
lines changed

.github/workflows/tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ jobs:
107107
# Generate separate reports for tests and doctests, and combine them.
108108
run: |
109109
set -euo pipefail
110-
cargo llvm-cov --no-report --all-features --workspace
111-
cargo llvm-cov --no-report --doc --all-features --workspace
110+
cargo llvm-cov --no-report --all-features --workspace --exclude 'meilisearch-test-macro'
111+
cargo llvm-cov --no-report --doc --all-features --workspace --exclude 'meilisearch-test-macro'
112112
cargo llvm-cov report --doctests --codecov --output-path codecov.json
113113
- name: Upload coverage reports to Codecov
114114
uses: codecov/codecov-action@v5

meilisearch-test-macro/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ There are a few rules, though:
6868
- `String`: It returns the name of the test.
6969
- `Client`: It creates a client like that: `Client::new("http://localhost:7700", "masterKey")`.
7070
- `Index`: It creates and deletes an index, as we've seen before.
71+
You can include multiple `Index` parameter to automatically create multiple indexes.
7172

7273
2. You only get what you asked for. That means if you don't ask for an index, no index will be created in meilisearch.
7374
So, if you are testing the creation of indexes, you can ask for a `Client` and a `String` and then create it yourself.

meilisearch-test-macro/src/lib.rs

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ pub fn meilisearch_test(params: TokenStream, input: TokenStream) -> TokenStream
7777
let use_name = params
7878
.iter()
7979
.any(|param| matches!(param, Param::String | Param::Index));
80-
let use_index = params.contains(&Param::Index);
8180

8281
// Now we are going to build the body of the outer function
8382
let mut outer_block: Vec<Stmt> = Vec::new();
@@ -106,59 +105,77 @@ pub fn meilisearch_test(params: TokenStream, input: TokenStream) -> TokenStream
106105
));
107106
}
108107

108+
let index_var = |idx: usize| Ident::new(&format!("index_{idx}"), Span::call_site());
109+
109110
// And finally if an index was asked, we delete it, and we (re)create it and wait until meilisearch confirm its creation.
110-
if use_index {
111-
outer_block.push(parse_quote!({
112-
let res = client
113-
.delete_index(&name)
114-
.await
115-
.expect("Network issue while sending the delete index task")
116-
.wait_for_completion(&client, None, None)
117-
.await
118-
.expect("Network issue while waiting for the index deletion");
119-
if res.is_failure() {
120-
let error = res.unwrap_failure();
121-
assert_eq!(
122-
error.error_code,
123-
crate::errors::ErrorCode::IndexNotFound,
124-
"{:?}",
125-
error
126-
);
127-
}
128-
}));
111+
for (i, param) in params.iter().enumerate() {
112+
if !matches!(param, Param::Index) {
113+
continue;
114+
}
129115

116+
let var_name = index_var(i);
130117
outer_block.push(parse_quote!(
131-
let index = client
132-
.create_index(&name, None)
133-
.await
134-
.expect("Network issue while sending the create index task")
135-
.wait_for_completion(&client, None, None)
136-
.await
137-
.expect("Network issue while waiting for the index creation")
138-
.try_make_index(&client)
139-
.expect("Could not create the index out of the create index task");
118+
let #var_name = {
119+
let index_uid = format!("{name}_{}", #i);
120+
let res = client
121+
.delete_index(&index_uid)
122+
.await
123+
.expect("Network issue while sending the delete index task")
124+
.wait_for_completion(&client, None, None)
125+
.await
126+
.expect("Network issue while waiting for the index deletion");
127+
128+
if res.is_failure() {
129+
let error = res.unwrap_failure();
130+
assert_eq!(
131+
error.error_code,
132+
crate::errors::ErrorCode::IndexNotFound,
133+
"{:?}",
134+
error
135+
);
136+
}
137+
138+
client
139+
.create_index(&index_uid, None)
140+
.await
141+
.expect("Network issue while sending the create index task")
142+
.wait_for_completion(&client, None, None)
143+
.await
144+
.expect("Network issue while waiting for the index creation")
145+
.try_make_index(&client)
146+
.expect("Could not create the index out of the create index task")
147+
};
140148
));
141149
}
142150

143151
// Create a list of params separated by comma with the name we defined previously.
144-
let params: Vec<Expr> = params
145-
.into_iter()
146-
.map(|param| match param {
152+
let args: Vec<Expr> = params
153+
.iter()
154+
.enumerate()
155+
.map(|(i, param)| match param {
147156
Param::Client => parse_quote!(client),
148-
Param::Index => parse_quote!(index),
157+
Param::Index => {
158+
let var = index_var(i);
159+
parse_quote!(#var)
160+
}
149161
Param::String => parse_quote!(name),
150162
})
151163
.collect();
152164

153165
// Now we can call the user code with our parameters :tada:
154166
outer_block.push(parse_quote!(
155-
let result = #inner_ident(#(#params.clone()),*).await;
167+
let result = #inner_ident(#(#args.clone()),*).await;
156168
));
157169

158170
// And right before the end, if an index was created and the tests successfully executed we delete it.
159-
if use_index {
171+
for (i, param) in params.iter().enumerate() {
172+
if !matches!(param, Param::Index) {
173+
continue;
174+
}
175+
176+
let var_name = index_var(i);
160177
outer_block.push(parse_quote!(
161-
index
178+
#var_name
162179
.delete()
163180
.await
164181
.expect("Network issue while sending the last delete index task");

src/client.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,21 @@ impl<Http: HttpClient> Client<Http> {
128128
.await
129129
}
130130

131+
pub async fn execute_federated_multi_search_query<
132+
T: 'static + DeserializeOwned + Send + Sync,
133+
>(
134+
&self,
135+
body: &FederatedMultiSearchQuery<'_, '_, Http>,
136+
) -> Result<FederatedMultiSearchResponse<T>, Error> {
137+
self.http_client
138+
.request::<(), &FederatedMultiSearchQuery<Http>, FederatedMultiSearchResponse<T>>(
139+
&format!("{}/multi-search", &self.host),
140+
Method::Post { body, query: () },
141+
200,
142+
)
143+
.await
144+
}
145+
131146
/// Make multiple search requests.
132147
///
133148
/// # Example
@@ -170,6 +185,22 @@ impl<Http: HttpClient> Client<Http> {
170185
/// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
171186
/// # });
172187
/// ```
188+
///
189+
/// # Federated Search
190+
///
191+
/// You can use [`MultiSearchQuery::with_federation`] to perform a [federated
192+
/// search][1] where results from different indexes are merged and returned as
193+
/// one list.
194+
///
195+
/// When executing a federated query, the type parameter `T` is less clear,
196+
/// as the documents in the different indexes potentially have different
197+
/// fields and you might have one Rust type per index. In most cases, you
198+
/// either want to create an enum with one variant per index and `#[serde
199+
/// (untagged)]` attribute, or if you need more control, just pass
200+
/// `serde_json::Map<String, serde_json::Value>` and then deserialize that
201+
/// into the appropriate target types later.
202+
///
203+
/// [1]: https://www.meilisearch.com/docs/learn/multi_search/multi_search_vs_federated_search#what-is-federated-search
173204
#[must_use]
174205
pub fn multi_search(&self) -> MultiSearchQuery<Http> {
175206
MultiSearchQuery::new(self)

src/features.rs

Lines changed: 10 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,10 @@ impl<'a, Http: HttpClient> ExperimentalFeatures<'a, Http> {
6969
/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
7070
/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
7171
/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
72-
/// tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
73-
/// let features = ExperimentalFeatures::new(&client);
74-
/// features.get().await.unwrap();
75-
/// });
72+
/// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
73+
/// let features = ExperimentalFeatures::new(&client);
74+
/// features.get().await.unwrap();
75+
/// # });
7676
/// ```
7777
pub async fn get(&self) -> Result<ExperimentalFeaturesResult, Error> {
7878
self.client
@@ -148,52 +148,20 @@ mod tests {
148148
use meilisearch_test_macro::meilisearch_test;
149149

150150
#[meilisearch_test]
151-
async fn test_experimental_features_set_metrics(client: Client) {
151+
async fn test_experimental_features(client: Client) {
152152
let mut features = ExperimentalFeatures::new(&client);
153153
features.set_metrics(true);
154-
let _ = features.update().await.unwrap();
155-
156-
let res = features.get().await.unwrap();
157-
assert!(res.metrics)
158-
}
159-
160-
#[meilisearch_test]
161-
async fn test_experimental_features_set_logs_route(client: Client) {
162-
let mut features = ExperimentalFeatures::new(&client);
163154
features.set_logs_route(true);
164-
let _ = features.update().await.unwrap();
165-
166-
let res = features.get().await.unwrap();
167-
assert!(res.logs_route)
168-
}
169-
170-
#[meilisearch_test]
171-
async fn test_experimental_features_set_contains_filter(client: Client) {
172-
let mut features = ExperimentalFeatures::new(&client);
173155
features.set_contains_filter(true);
174-
let _ = features.update().await.unwrap();
175-
176-
let res = features.get().await.unwrap();
177-
assert!(res.contains_filter)
178-
}
179-
180-
#[meilisearch_test]
181-
async fn test_experimental_features_set_network(client: Client) {
182-
let mut features = ExperimentalFeatures::new(&client);
183156
features.set_network(true);
184-
let _ = features.update().await.unwrap();
185-
186-
let res = features.get().await.unwrap();
187-
assert!(res.network)
188-
}
189-
190-
#[meilisearch_test]
191-
async fn test_experimental_features_set_edit_documents_by_function(client: Client) {
192-
let mut features = ExperimentalFeatures::new(&client);
193157
features.set_edit_documents_by_function(true);
194158
let _ = features.update().await.unwrap();
195159

196160
let res = features.get().await.unwrap();
197-
assert!(res.edit_documents_by_function)
161+
assert!(res.metrics);
162+
assert!(res.logs_route);
163+
assert!(res.contains_filter);
164+
assert!(res.network);
165+
assert!(res.edit_documents_by_function);
198166
}
199167
}

0 commit comments

Comments
 (0)