Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .code-samples.meilisearch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1941,6 +1941,20 @@ search_parameter_reference_retrieve_vectors_1: |-
.execute()
.await
.unwrap();
search_parameter_reference_media_1: |-
let results = index
.search()
.with_hybrid("EMBEDDER_NAME", 0.5)
.with_media(json!({
"FIELD_A": "VALUE_A",
"FIELD_B": {
"FIELD_C": "VALUE_B",
"FIELD_D": "VALUE_C"
}
}))
.execute()
.await
.unwrap();
update_embedders_1: |-
let embedders = HashMap::from([(
String::from("default"),
Expand Down
12 changes: 12 additions & 0 deletions src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ pub struct ExperimentalFeaturesResult {
pub contains_filter: bool,
pub network: bool,
pub edit_documents_by_function: bool,
#[serde(default)]
pub multimodal: bool,
}

/// Struct representing the experimental features request.
Expand Down Expand Up @@ -45,6 +47,8 @@ pub struct ExperimentalFeatures<'a, Http: HttpClient> {
pub network: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub edit_documents_by_function: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub multimodal: Option<bool>,
}

impl<'a, Http: HttpClient> ExperimentalFeatures<'a, Http> {
Expand All @@ -57,6 +61,7 @@ impl<'a, Http: HttpClient> ExperimentalFeatures<'a, Http> {
network: None,
contains_filter: None,
edit_documents_by_function: None,
multimodal: None,
}
}

Expand Down Expand Up @@ -140,6 +145,11 @@ impl<'a, Http: HttpClient> ExperimentalFeatures<'a, Http> {
self.network = Some(network);
self
}

pub fn set_multimodal(&mut self, multimodal: bool) -> &mut Self {
self.multimodal = Some(multimodal);
self
}
}

#[cfg(test)]
Expand All @@ -155,6 +165,7 @@ mod tests {
features.set_contains_filter(true);
features.set_network(true);
features.set_edit_documents_by_function(true);
features.set_multimodal(true);
let _ = features.update().await.unwrap();

let res = features.get().await.unwrap();
Expand All @@ -163,5 +174,6 @@ mod tests {
assert!(res.contains_filter);
assert!(res.network);
assert!(res.edit_documents_by_function);
assert!(res.multimodal);
}
}
39 changes: 39 additions & 0 deletions src/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,10 @@ pub struct SearchQuery<'a, Http: HttpClient> {
#[serde(skip_serializing_if = "Option::is_none")]
pub retrieve_vectors: Option<bool>,

/// Provides multimodal data for search queries.
#[serde(skip_serializing_if = "Option::is_none")]
pub media: Option<Value>,

#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) federation_options: Option<QueryFederationOptions>,
}
Expand Down Expand Up @@ -449,6 +453,7 @@ impl<'a, Http: HttpClient> SearchQuery<'a, Http> {
hybrid: None,
vector: None,
retrieve_vectors: None,
media: None,
distinct: None,
ranking_score_threshold: None,
locales: None,
Expand Down Expand Up @@ -695,6 +700,12 @@ impl<'a, Http: HttpClient> SearchQuery<'a, Http> {
self
}

/// Attach media fragments to the search query.
pub fn with_media<'b>(&'b mut self, media: Value) -> &'b mut SearchQuery<'a, Http> {
self.media = Some(media);
self
}

pub fn with_distinct<'b>(&'b mut self, distinct: &'a str) -> &'b mut SearchQuery<'a, Http> {
self.distinct = Some(distinct);
self
Expand Down Expand Up @@ -1096,6 +1107,34 @@ pub(crate) mod tests {
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};

#[test]
fn search_query_serializes_media_parameter() {
let client = Client::new("http://localhost:7700", Some("masterKey")).unwrap();
let index = client.index("media_query");
let mut query = SearchQuery::new(&index);

query.with_query("example").with_media(json!({
"FIELD_A": "VALUE_A",
"FIELD_B": {
"FIELD_C": "VALUE_B",
"FIELD_D": "VALUE_C"
}
}));

let serialized = serde_json::to_value(&query.build()).unwrap();

assert_eq!(
serialized.get("media"),
Some(&json!({
"FIELD_A": "VALUE_A",
"FIELD_B": {
"FIELD_C": "VALUE_B",
"FIELD_D": "VALUE_C"
}
}))
);
}

#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Nested {
child: String,
Expand Down
101 changes: 101 additions & 0 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,20 @@ pub struct Embedder {
/// Configures embedder to vectorize search queries (composite embedders only)
#[serde(skip_serializing_if = "Option::is_none")]
pub search_embedder: Option<Box<Embedder>>,

/// Configures multimodal embedding generation at indexing time.
#[serde(skip_serializing_if = "Option::is_none")]
pub indexing_fragments: Option<HashMap<String, EmbedderFragment>>,

/// Configures incoming media fragments for multimodal search queries.
#[serde(skip_serializing_if = "Option::is_none")]
pub search_fragments: Option<HashMap<String, EmbedderFragment>>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct EmbedderFragment {
pub value: serde_json::Value,
}

#[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)]
Expand Down Expand Up @@ -2798,6 +2812,7 @@ mod tests {

use crate::client::*;
use meilisearch_test_macro::meilisearch_test;
use serde_json::json;

#[meilisearch_test]
async fn test_set_faceting_settings(client: Client, index: Index) {
Expand Down Expand Up @@ -3139,6 +3154,92 @@ mod tests {
assert_eq!(embedders, res);
}

#[test]
fn embedder_with_fragments_serializes() {
let embedder = Embedder {
source: EmbedderSource::Rest,
url: Some(String::from("https://example.com/embeddings")),
indexing_fragments: Some(HashMap::from([(
String::from("default"),
EmbedderFragment {
value: json!({
"content": [
{ "type": "text", "text": "{{ doc.description }}" }
]
}),
},
)])),
search_fragments: Some(HashMap::from([(
String::from("default"),
EmbedderFragment {
value: json!({
"content": [
{ "type": "text", "text": "{{ query.q }}" }
]
}),
},
)])),
request: Some(json!({
"input": [
"{{fragment}}",
"{{..}}"
],
"model": "example-model"
})),
response: Some(json!({
"data": [
{
"embedding": "{{embedding}}"
},
"{{..}}"
]
})),
..Default::default()
};

let serialized = serde_json::to_value(&embedder).unwrap();

assert_eq!(
serialized
.get("indexingFragments")
.and_then(|value| value.get("default"))
.and_then(|value| value.get("value"))
.and_then(|value| value.get("content"))
.and_then(|value| value.get(0))
.and_then(|value| value.get("text")),
Some(&json!("{{ doc.description }}"))
);

assert_eq!(
serialized
.get("searchFragments")
.and_then(|value| value.get("default"))
.and_then(|value| value.get("value"))
.and_then(|value| value.get("content"))
.and_then(|value| value.get(0))
.and_then(|value| value.get("text")),
Some(&json!("{{ query.q }}"))
);

assert_eq!(
serialized.get("request"),
Some(&json!({
"input": ["{{fragment}}", "{{..}}"],
"model": "example-model"
}))
);

assert_eq!(
serialized.get("response"),
Some(&json!({
"data": [
{ "embedding": "{{embedding}}" },
"{{..}}"
]
}))
);
}

#[meilisearch_test]
async fn test_reset_proximity_precision(index: Index) {
let expected = "byWord".to_string();
Expand Down