Skip to content

Commit 972da7f

Browse files
authored
Merge pull request #674 from JiaYingZhang/similar-support
Add support for similar docs query
2 parents c3a1135 + d8cc992 commit 972da7f

File tree

6 files changed

+468
-50
lines changed

6 files changed

+468
-50
lines changed

.github/workflows/tests.yml

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -85,34 +85,21 @@ jobs:
8585
config_file: .yamllint.yml
8686

8787
coverage:
88-
# Will not run if the actor is Dependabot (dependabot PRs)
89-
# Will not run if the event is a PR to bump-meilisearch-v* (so a pre-release PR)
9088
if: github.actor != 'dependabot[bot]' && !( github.event_name == 'pull_request' && startsWith(github.base_ref, 'bump-meilisearch-v') )
9189
runs-on: ubuntu-latest
9290
needs: integration_tests
9391
name: Code Coverage
9492
steps:
9593
- uses: actions/checkout@v4
96-
# Nightly Rust is used for cargo llvm-cov --doc below.
97-
- uses: dtolnay/rust-toolchain@nightly
98-
with:
99-
components: llvm-tools-preview
100-
- name: Install cargo-llvm-cov
101-
uses: taiki-e/install-action@v2
102-
with:
103-
tool: cargo-llvm-cov
10494
- name: Meilisearch (latest version) setup with Docker
10595
run: docker run -d -p 7700:7700 getmeili/meilisearch:latest meilisearch --no-analytics --master-key=masterKey
106-
- name: Collect coverage data
107-
# Generate separate reports for tests and doctests, and combine them.
108-
run: |
109-
set -euo pipefail
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'
112-
cargo llvm-cov report --doctests --codecov --output-path codecov.json
113-
- name: Upload coverage reports to Codecov
96+
- name: Install cargo-llvm-cov
97+
uses: taiki-e/install-action@cargo-llvm-cov
98+
- name: Generate code coverage
99+
run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info
100+
- name: Upload coverage to Codecov
114101
uses: codecov/codecov-action@v5
115102
with:
116103
token: ${{ secrets.CODECOV_TOKEN }}
117-
files: codecov.json
104+
files: lcov.info
118105
fail_ci_if_error: true

src/documents.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ pub struct DocumentsQuery<'a, Http: HttpClient> {
193193
/// Filters to apply.
194194
///
195195
/// Available since v1.2 of Meilisearch
196-
/// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/fine_tuning_results/filtering#filter-basics) to learn the syntax.
196+
/// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/filtering_and_sorting) to learn the syntax.
197197
#[serde(skip_serializing_if = "Option::is_none")]
198198
pub filter: Option<&'a str>,
199199
}

src/indexes.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::{
44
errors::{Error, MeilisearchCommunicationError, MeilisearchError, MEILISEARCH_VERSION_HINT},
55
request::*,
66
search::*,
7+
similar::*,
78
task_info::TaskInfo,
89
tasks::*,
910
DefaultHttpClient,
@@ -1673,6 +1674,47 @@ impl<Http: HttpClient> Index<Http> {
16731674
}
16741675
Ok(task)
16751676
}
1677+
1678+
/// Get similar documents in the index.
1679+
///
1680+
/// # Example
1681+
///
1682+
/// ```no_run
1683+
/// # use serde::{Serialize, Deserialize};
1684+
/// # use meilisearch_sdk::{client::*, indexes::*, similar::*};
1685+
/// #
1686+
/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
1687+
/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
1688+
/// #
1689+
/// # #[derive(Serialize, Deserialize, Debug)]
1690+
/// # struct Movie {
1691+
/// # name: String,
1692+
/// # description: String,
1693+
/// # }
1694+
/// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
1695+
/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
1696+
/// # let movies = client.index("similar_query");
1697+
/// #
1698+
/// let query = SimilarQuery::new(&movies, "1", "default");
1699+
/// let results = movies.similar_query::<Movie>(&query).await.unwrap();
1700+
///
1701+
/// assert!(results.hits.len() > 0);
1702+
/// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
1703+
/// # });
1704+
/// ```
1705+
pub async fn similar_query<T: 'static + DeserializeOwned + Send + Sync>(
1706+
&self,
1707+
body: &SimilarQuery<'_, Http>,
1708+
) -> Result<SimilarResults<T>, Error> {
1709+
self.client
1710+
.http_client
1711+
.request::<(), &SimilarQuery<Http>, SimilarResults<T>>(
1712+
&format!("{}/indexes/{}/similar", self.client.host, self.uid),
1713+
Method::Post { body, query: () },
1714+
200,
1715+
)
1716+
.await
1717+
}
16761718
}
16771719

16781720
impl<Http: HttpClient> AsRef<str> for Index<Http> {

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,8 @@ pub mod request;
249249
pub mod search;
250250
/// Module containing [`Settings`](settings::Settings).
251251
pub mod settings;
252+
/// Module related to [similar queries](https://www.meilisearch.com/docs/learn/ai_powered_search/retrieve_related_search_results#return-similar-documents).
253+
pub mod similar;
252254
/// Module containing the [snapshots](snapshots::create_snapshot)-feature.
253255
pub mod snapshots;
254256
/// Module representing the [`TaskInfo`](task_info::TaskInfo)s.

src/search.rs

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -123,17 +123,6 @@ pub struct SearchResults<T> {
123123
pub index_uid: Option<String>,
124124
}
125125

126-
fn serialize_with_wildcard<S: Serializer, T: Serialize>(
127-
data: &Option<Selectors<T>>,
128-
s: S,
129-
) -> Result<S::Ok, S::Error> {
130-
match data {
131-
Some(Selectors::All) => ["*"].serialize(s),
132-
Some(Selectors::Some(data)) => data.serialize(s),
133-
None => s.serialize_none(),
134-
}
135-
}
136-
137126
fn serialize_attributes_to_crop_with_wildcard<S: Serializer>(
138127
data: &Option<Selectors<&[AttributeToCrop]>>,
139128
s: S,
@@ -169,6 +158,15 @@ pub enum Selectors<T> {
169158
All,
170159
}
171160

161+
impl<T: Serialize> Serialize for Selectors<T> {
162+
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
163+
match self {
164+
Selectors::Some(data) => data.serialize(s),
165+
Selectors::All => ["*"].serialize(s),
166+
}
167+
}
168+
}
169+
172170
/// Configures Meilisearch to return search results based on a query’s meaning and context
173171
#[derive(Debug, Serialize, Clone)]
174172
#[serde(rename_all = "camelCase")]
@@ -282,7 +280,7 @@ pub struct SearchQuery<'a, Http: HttpClient> {
282280
pub hits_per_page: Option<usize>,
283281
/// Filter applied to documents.
284282
///
285-
/// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/advanced/filtering) to learn the syntax.
283+
/// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/filtering_and_sorting) to learn the syntax.
286284
#[serde(skip_serializing_if = "Option::is_none")]
287285
pub filter: Option<Filter<'a>>,
288286
/// Facets for which to retrieve the matching count.
@@ -291,7 +289,6 @@ pub struct SearchQuery<'a, Http: HttpClient> {
291289
///
292290
/// **Default: all attributes found in the documents.**
293291
#[serde(skip_serializing_if = "Option::is_none")]
294-
#[serde(serialize_with = "serialize_with_wildcard")]
295292
pub facets: Option<Selectors<&'a [&'a str]>>,
296293
/// Attributes to sort.
297294
#[serde(skip_serializing_if = "Option::is_none")]
@@ -309,7 +306,6 @@ pub struct SearchQuery<'a, Http: HttpClient> {
309306
///
310307
/// **Default: all attributes found in the documents.**
311308
#[serde(skip_serializing_if = "Option::is_none")]
312-
#[serde(serialize_with = "serialize_with_wildcard")]
313309
pub attributes_to_retrieve: Option<Selectors<&'a [&'a str]>>,
314310
/// Attributes whose values have to be cropped.
315311
///
@@ -337,7 +333,6 @@ pub struct SearchQuery<'a, Http: HttpClient> {
337333
///
338334
/// Can be set to a [wildcard value](enum.Selectors.html#variant.All) that will select all existing attributes.
339335
#[serde(skip_serializing_if = "Option::is_none")]
340-
#[serde(serialize_with = "serialize_with_wildcard")]
341336
pub attributes_to_highlight: Option<Selectors<&'a [&'a str]>>,
342337
/// Tag in front of a highlighted term.
343338
///
@@ -971,7 +966,7 @@ pub struct FacetSearchQuery<'a, Http: HttpClient = DefaultHttpClient> {
971966
pub search_query: Option<&'a str>,
972967
/// Filter applied to documents.
973968
///
974-
/// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/advanced/filtering) to learn the syntax.
969+
/// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/filtering_and_sorting) to learn the syntax.
975970
#[serde(skip_serializing_if = "Option::is_none")]
976971
pub filter: Option<Filter<'a>>,
977972
/// Defines the strategy on how to handle search queries containing multiple words.
@@ -1078,7 +1073,7 @@ pub struct FacetSearchResponse {
10781073
}
10791074

10801075
#[cfg(test)]
1081-
mod tests {
1076+
pub(crate) mod tests {
10821077
use crate::{
10831078
client::*,
10841079
key::{Action, KeyBuilder},
@@ -1091,19 +1086,19 @@ mod tests {
10911086
use serde_json::{json, Map, Value};
10921087

10931088
#[derive(Debug, Serialize, Deserialize, PartialEq)]
1094-
struct Nested {
1089+
pub struct Nested {
10951090
child: String,
10961091
}
10971092

10981093
#[derive(Debug, Serialize, Deserialize, PartialEq)]
1099-
struct Document {
1100-
id: usize,
1101-
value: String,
1102-
kind: String,
1103-
number: i32,
1104-
nested: Nested,
1094+
pub struct Document {
1095+
pub id: usize,
1096+
pub value: String,
1097+
pub kind: String,
1098+
pub number: i32,
1099+
pub nested: Nested,
11051100
#[serde(skip_serializing_if = "Option::is_none", default)]
1106-
_vectors: Option<Vectors>,
1101+
pub _vectors: Option<Vectors>,
11071102
}
11081103

11091104
#[derive(Debug, Serialize, Deserialize, PartialEq)]
@@ -1120,7 +1115,7 @@ mod tests {
11201115
}
11211116

11221117
#[derive(Debug, Serialize, Deserialize, PartialEq)]
1123-
struct Vectors(HashMap<String, Vector>);
1118+
pub struct Vectors(HashMap<String, Vector>);
11241119

11251120
impl<T: Into<Vec<f32>>> From<T> for Vectors {
11261121
fn from(value: T) -> Self {
@@ -1151,7 +1146,7 @@ mod tests {
11511146
vector
11521147
}
11531148

1154-
async fn setup_test_index(client: &Client, index: &Index) -> Result<(), Error> {
1149+
pub(crate) async fn setup_test_index(client: &Client, index: &Index) -> Result<(), Error> {
11551150
let t0 = index.add_documents(&[
11561151
Document { id: 0, kind: "text".into(), number: 0, value: S("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), nested: Nested { child: S("first") }, _vectors: Some(Vectors::from(vectorize(false, 0))) },
11571152
Document { id: 1, kind: "text".into(), number: 10, value: S("dolor sit amet, consectetur adipiscing elit"), nested: Nested { child: S("second") }, _vectors: Some(Vectors::from(vectorize(false, 1))) },
@@ -1225,7 +1220,7 @@ mod tests {
12251220
Ok(())
12261221
}
12271222

1228-
async fn setup_hybrid_searching(client: &Client, index: &Index) -> Result<(), Error> {
1223+
pub(crate) async fn setup_embedder(client: &Client, index: &Index) -> Result<(), Error> {
12291224
use crate::settings::Embedder;
12301225
let embedder_setting = Embedder {
12311226
source: EmbedderSource::UserProvided,
@@ -1998,7 +1993,7 @@ mod tests {
19981993

19991994
#[meilisearch_test]
20001995
async fn test_with_vectors(client: Client, index: Index) -> Result<(), Error> {
2001-
setup_hybrid_searching(&client, &index).await?;
1996+
setup_embedder(&client, &index).await?;
20021997
setup_test_index(&client, &index).await?;
20031998

20041999
let results: SearchResults<Document> = index
@@ -2024,7 +2019,7 @@ mod tests {
20242019

20252020
#[meilisearch_test]
20262021
async fn test_hybrid(client: Client, index: Index) -> Result<(), Error> {
2027-
setup_hybrid_searching(&client, &index).await?;
2022+
setup_embedder(&client, &index).await?;
20282023
setup_test_index(&client, &index).await?;
20292024

20302025
// Search for an Harry Potter but with lorem ipsum's id

0 commit comments

Comments
 (0)