Compare commits

...

6 Commits

Author SHA1 Message Date
Mubelotix
997e8c475d Merge branch 'main' into investigate-issue-5772 2025-08-05 14:27:14 +02:00
Mubelotix
edbbe210ad Fix warnings 2025-07-28 12:56:07 +02:00
Mubelotix
1916787bdd Format 2025-07-28 12:53:04 +02:00
Mubelotix
d4865b31b6 Update test 2025-07-28 12:52:48 +02:00
Mubelotix
705b195941 Make updating settings override previous value 2025-07-28 12:37:44 +02:00
Mubelotix
1207785a07 Add test 2025-07-23 11:27:43 +02:00
6 changed files with 158 additions and 153 deletions

View File

@@ -174,6 +174,11 @@ impl<'a> Index<'a, Owned> {
self._update_settings(settings).await self._update_settings(settings).await
} }
pub async fn update_settings_chat(&self, settings: Value) -> (Value, StatusCode) {
let url = format!("/indexes/{}/settings/chat", urlencode(self.uid.as_ref()));
self.service.put_encoded(url, settings, self.encoder).await
}
pub async fn update_settings_displayed_attributes( pub async fn update_settings_displayed_attributes(
&self, &self,
settings: Value, settings: Value,

View File

@@ -0,0 +1,64 @@
use crate::common::Server;
use crate::json;
use meili_snap::{json_string, snapshot};
#[actix_rt::test]
async fn set_reset_chat_issue_5772() {
let server = Server::new().await;
let index = server.unique_index();
let (_, code) = server
.set_features(json!({
"chatCompletions": true,
}))
.await;
snapshot!(code, @r#"200 OK"#);
let (task1, _code) = index.update_settings_chat(json!({
"description": "test!",
"documentTemplate": "{% for field in fields %}{% if field.is_searchable and field.value != nil %}{{ field.name }}: {{ field.value }}\n{% endif %}{% endfor %}",
"documentTemplateMaxBytes": 400,
"searchParameters": {
"limit": 15,
"sort": [],
"attributesToSearchOn": []
}
})).await;
server.wait_task(task1.uid()).await.succeeded();
let (response, _) = index.settings().await;
snapshot!(json_string!(response["chat"]), @r#"
{
"description": "test!",
"documentTemplate": "{% for field in fields %}{% if field.is_searchable and field.value != nil %}{{ field.name }}: {{ field.value }}\n{% endif %}{% endfor %}",
"documentTemplateMaxBytes": 400,
"searchParameters": {
"limit": 15,
"sort": [],
"attributesToSearchOn": []
}
}
"#);
let (task2, _status_code) = index.update_settings_chat(json!({
"description": "test!",
"documentTemplate": "{% for field in fields %}{% if field.is_searchable and field.value != nil %}{{ field.name }}: {{ field.value }}\n{% endif %}{% endfor %}",
"documentTemplateMaxBytes": 400,
"searchParameters": {
"limit": 16
}
})).await;
server.wait_task(task2.uid()).await.succeeded();
let (response, _) = index.settings().await;
snapshot!(json_string!(response["chat"]), @r#"
{
"description": "test!",
"documentTemplate": "{% for field in fields %}{% if field.is_searchable and field.value != nil %}{{ field.name }}: {{ field.value }}\n{% endif %}{% endfor %}",
"documentTemplateMaxBytes": 400,
"searchParameters": {
"limit": 16
}
}
"#);
}

View File

@@ -190,8 +190,7 @@ test_setting_routes!(
default_value: { default_value: {
"description": "", "description": "",
"documentTemplate": "{% for field in fields %}{% if field.is_searchable and field.value != nil %}{{ field.name }}: {{ field.value }}\n{% endif %}{% endfor %}", "documentTemplate": "{% for field in fields %}{% if field.is_searchable and field.value != nil %}{{ field.name }}: {{ field.value }}\n{% endif %}{% endfor %}",
"documentTemplateMaxBytes": 400, "documentTemplateMaxBytes": 400
"searchParameters": {}
} }
}, },
); );

View File

@@ -1,3 +1,4 @@
mod chat;
mod distinct; mod distinct;
mod errors; mod errors;
mod get_settings; mod get_settings;

View File

@@ -1,44 +1,38 @@
use std::error::Error; use std::error::Error;
use std::fmt; use std::fmt;
use std::num::NonZeroUsize;
use deserr::errors::JsonError; use deserr::errors::JsonError;
use deserr::Deserr; use deserr::Deserr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use utoipa::ToSchema; use utoipa::ToSchema;
use crate::index::{self, ChatConfig, MatchingStrategy, RankingScoreThreshold, SearchParameters}; use crate::index::{ChatConfig, MatchingStrategy, RankingScoreThreshold, SearchParameters};
use crate::prompt::{default_max_bytes, PromptData}; use crate::prompt::PromptData;
use crate::update::Setting;
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Deserr, ToSchema)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Deserr, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")] #[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(error = JsonError, deny_unknown_fields, rename_all = camelCase)] #[deserr(error = JsonError, deny_unknown_fields, rename_all = camelCase)]
pub struct ChatSettings { pub struct ChatSettings {
#[serde(default, skip_serializing_if = "Setting::is_not_set")] pub description: String,
#[deserr(default)]
#[schema(value_type = Option<String>)]
pub description: Setting<String>,
/// A liquid template used to render documents to a text that can be embedded. /// A liquid template used to render documents to a text that can be embedded.
/// ///
/// Meillisearch interpolates the template for each document and sends the resulting text to the embedder. /// Meillisearch interpolates the template for each document and sends the resulting text to the embedder.
/// The embedder then generates document vectors based on this text. /// The embedder then generates document vectors based on this text.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] pub document_template: String,
#[deserr(default)]
#[schema(value_type = Option<String>)]
pub document_template: Setting<String>,
/// Rendered texts are truncated to this size. Defaults to 400. /// Rendered texts are truncated to this size. Defaults to 400.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[deserr(default)] #[deserr(default)]
#[schema(value_type = Option<usize>)] #[schema(value_type = Option<usize>)]
pub document_template_max_bytes: Setting<usize>, pub document_template_max_bytes: Option<NonZeroUsize>,
/// The search parameters to use for the LLM. /// The search parameters to use for the LLM.
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[deserr(default)] #[deserr(default)]
#[schema(value_type = Option<ChatSearchParams>)] #[schema(value_type = Option<ChatSearchParams>)]
pub search_parameters: Setting<ChatSearchParams>, pub search_parameters: Option<ChatSearchParams>,
} }
impl From<ChatConfig> for ChatSettings { impl From<ChatConfig> for ChatSettings {
@@ -49,12 +43,10 @@ impl From<ChatConfig> for ChatSettings {
search_parameters, search_parameters,
} = config; } = config;
ChatSettings { ChatSettings {
description: Setting::Set(description), description,
document_template: Setting::Set(template), document_template: template,
document_template_max_bytes: Setting::Set( document_template_max_bytes: max_bytes,
max_bytes.unwrap_or(default_max_bytes()).get(), search_parameters: {
),
search_parameters: Setting::Set({
let SearchParameters { let SearchParameters {
hybrid, hybrid,
limit, limit,
@@ -65,62 +57,104 @@ impl From<ChatConfig> for ChatSettings {
ranking_score_threshold, ranking_score_threshold,
} = search_parameters; } = search_parameters;
let hybrid = hybrid.map(|index::HybridQuery { semantic_ratio, embedder }| { if hybrid.is_none()
HybridQuery { semantic_ratio: SemanticRatio(semantic_ratio), embedder } && limit.is_none()
}); && sort.is_none()
&& distinct.is_none()
ChatSearchParams { && matching_strategy.is_none()
hybrid: Setting::some_or_not_set(hybrid), && attributes_to_search_on.is_none()
limit: Setting::some_or_not_set(limit), && ranking_score_threshold.is_none()
sort: Setting::some_or_not_set(sort), {
distinct: Setting::some_or_not_set(distinct), None
matching_strategy: Setting::some_or_not_set(matching_strategy), } else {
attributes_to_search_on: Setting::some_or_not_set(attributes_to_search_on), Some(ChatSearchParams {
ranking_score_threshold: Setting::some_or_not_set(ranking_score_threshold), hybrid: hybrid.map(|h| HybridQuery {
semantic_ratio: SemanticRatio(h.semantic_ratio),
embedder: h.embedder,
}),
limit,
sort,
distinct,
matching_strategy,
attributes_to_search_on,
ranking_score_threshold,
})
} }
}), },
} }
} }
} }
impl From<ChatSettings> for ChatConfig {
fn from(settings: ChatSettings) -> Self {
let ChatSettings {
description,
document_template,
document_template_max_bytes,
search_parameters,
} = settings;
let prompt =
PromptData { template: document_template, max_bytes: document_template_max_bytes };
let search_parameters = match search_parameters {
Some(params) => SearchParameters {
hybrid: params.hybrid.map(|h| crate::index::HybridQuery {
semantic_ratio: h.semantic_ratio.0,
embedder: h.embedder,
}),
limit: params.limit,
sort: params.sort,
distinct: params.distinct,
matching_strategy: params.matching_strategy,
attributes_to_search_on: params.attributes_to_search_on,
ranking_score_threshold: params.ranking_score_threshold,
},
None => SearchParameters::default(),
};
ChatConfig { description, prompt, search_parameters }
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Deserr, ToSchema)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Deserr, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")] #[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(error = JsonError, deny_unknown_fields, rename_all = camelCase)] #[deserr(error = JsonError, deny_unknown_fields, rename_all = camelCase)]
pub struct ChatSearchParams { pub struct ChatSearchParams {
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[deserr(default)] #[deserr(default)]
#[schema(value_type = Option<HybridQuery>)] #[schema(value_type = Option<HybridQuery>)]
pub hybrid: Setting<HybridQuery>, pub hybrid: Option<HybridQuery>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[deserr(default)] #[deserr(default)]
#[schema(value_type = Option<usize>)] #[schema(value_type = Option<usize>)]
pub limit: Setting<usize>, pub limit: Option<usize>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[deserr(default)] #[deserr(default)]
#[schema(value_type = Option<Vec<String>>)] #[schema(value_type = Option<Vec<String>>)]
pub sort: Setting<Vec<String>>, pub sort: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[deserr(default)] #[deserr(default)]
#[schema(value_type = Option<String>)] #[schema(value_type = Option<String>)]
pub distinct: Setting<String>, pub distinct: Option<String>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[deserr(default)] #[deserr(default)]
#[schema(value_type = Option<MatchingStrategy>)] #[schema(value_type = Option<MatchingStrategy>)]
pub matching_strategy: Setting<MatchingStrategy>, pub matching_strategy: Option<MatchingStrategy>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[deserr(default)] #[deserr(default)]
#[schema(value_type = Option<Vec<String>>)] #[schema(value_type = Option<Vec<String>>)]
pub attributes_to_search_on: Setting<Vec<String>>, pub attributes_to_search_on: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[deserr(default)] #[deserr(default)]
#[schema(value_type = Option<RankingScoreThreshold>)] #[schema(value_type = Option<RankingScoreThreshold>)]
pub ranking_score_threshold: Setting<RankingScoreThreshold>, pub ranking_score_threshold: Option<RankingScoreThreshold>,
} }
#[derive(Debug, Clone, Default, Deserr, ToSchema, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Deserr, ToSchema, PartialEq, Serialize, Deserialize)]

View File

@@ -10,7 +10,6 @@ use itertools::{merge_join_by, EitherOrBoth, Itertools};
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use time::OffsetDateTime; use time::OffsetDateTime;
use super::chat::ChatSearchParams;
use super::del_add::{DelAdd, DelAddOperation}; use super::del_add::{DelAdd, DelAddOperation};
use super::index_documents::{IndexDocumentsConfig, Transform}; use super::index_documents::{IndexDocumentsConfig, Transform};
use super::{ChatSettings, IndexerConfig}; use super::{ChatSettings, IndexerConfig};
@@ -18,16 +17,13 @@ use crate::attribute_patterns::PatternMatch;
use crate::constants::RESERVED_GEO_FIELD_NAME; use crate::constants::RESERVED_GEO_FIELD_NAME;
use crate::criterion::Criterion; use crate::criterion::Criterion;
use crate::disabled_typos_terms::DisabledTyposTerms; use crate::disabled_typos_terms::DisabledTyposTerms;
use crate::error::UserError::{self, InvalidChatSettingsDocumentTemplateMaxBytes}; use crate::error::UserError;
use crate::fields_ids_map::metadata::{FieldIdMapWithMetadata, MetadataBuilder}; use crate::fields_ids_map::metadata::{FieldIdMapWithMetadata, MetadataBuilder};
use crate::filterable_attributes_rules::match_faceted_field; use crate::filterable_attributes_rules::match_faceted_field;
use crate::index::{ use crate::index::{PrefixSearch, DEFAULT_MIN_WORD_LEN_ONE_TYPO, DEFAULT_MIN_WORD_LEN_TWO_TYPOS};
ChatConfig, PrefixSearch, SearchParameters, DEFAULT_MIN_WORD_LEN_ONE_TYPO,
DEFAULT_MIN_WORD_LEN_TWO_TYPOS,
};
use crate::order_by_map::OrderByMap; use crate::order_by_map::OrderByMap;
use crate::progress::{EmbedderStats, Progress}; use crate::progress::{EmbedderStats, Progress};
use crate::prompt::{default_max_bytes, default_template_text, PromptData}; use crate::prompt::default_max_bytes;
use crate::proximity::ProximityPrecision; use crate::proximity::ProximityPrecision;
use crate::update::index_documents::IndexDocumentsMethod; use crate::update::index_documents::IndexDocumentsMethod;
use crate::update::new::indexer::reindex; use crate::update::new::indexer::reindex;
@@ -1312,102 +1308,8 @@ impl<'a, 't, 'i> Settings<'a, 't, 'i> {
fn update_chat_config(&mut self) -> Result<bool> { fn update_chat_config(&mut self) -> Result<bool> {
match &mut self.chat { match &mut self.chat {
Setting::Set(ChatSettings { Setting::Set(settings) => {
description: new_description, self.index.put_chat_config(self.wtxn, &settings.clone().into())?;
document_template: new_document_template,
document_template_max_bytes: new_document_template_max_bytes,
search_parameters: new_search_parameters,
}) => {
let ChatConfig { description, prompt, search_parameters } =
self.index.chat_config(self.wtxn)?;
let description = match new_description {
Setting::Set(new) => new.clone(),
Setting::Reset => Default::default(),
Setting::NotSet => description,
};
let prompt = PromptData {
template: match new_document_template {
Setting::Set(new) => new.clone(),
Setting::Reset => default_template_text().to_string(),
Setting::NotSet => prompt.template.clone(),
},
max_bytes: match new_document_template_max_bytes {
Setting::Set(m) => Some(
NonZeroUsize::new(*m)
.ok_or(InvalidChatSettingsDocumentTemplateMaxBytes)?,
),
Setting::Reset => Some(default_max_bytes()),
Setting::NotSet => prompt.max_bytes,
},
};
let search_parameters = match new_search_parameters {
Setting::Set(sp) => {
let ChatSearchParams {
hybrid,
limit,
sort,
distinct,
matching_strategy,
attributes_to_search_on,
ranking_score_threshold,
} = sp;
SearchParameters {
hybrid: match hybrid {
Setting::Set(hybrid) => Some(crate::index::HybridQuery {
semantic_ratio: *hybrid.semantic_ratio,
embedder: hybrid.embedder.clone(),
}),
Setting::Reset => None,
Setting::NotSet => search_parameters.hybrid.clone(),
},
limit: match limit {
Setting::Set(limit) => Some(*limit),
Setting::Reset => None,
Setting::NotSet => search_parameters.limit,
},
sort: match sort {
Setting::Set(sort) => Some(sort.clone()),
Setting::Reset => None,
Setting::NotSet => search_parameters.sort.clone(),
},
distinct: match distinct {
Setting::Set(distinct) => Some(distinct.clone()),
Setting::Reset => None,
Setting::NotSet => search_parameters.distinct.clone(),
},
matching_strategy: match matching_strategy {
Setting::Set(matching_strategy) => Some(*matching_strategy),
Setting::Reset => None,
Setting::NotSet => search_parameters.matching_strategy,
},
attributes_to_search_on: match attributes_to_search_on {
Setting::Set(attributes_to_search_on) => {
Some(attributes_to_search_on.clone())
}
Setting::Reset => None,
Setting::NotSet => {
search_parameters.attributes_to_search_on.clone()
}
},
ranking_score_threshold: match ranking_score_threshold {
Setting::Set(rst) => Some(*rst),
Setting::Reset => None,
Setting::NotSet => search_parameters.ranking_score_threshold,
},
}
}
Setting::Reset => Default::default(),
Setting::NotSet => search_parameters,
};
self.index.put_chat_config(
self.wtxn,
&ChatConfig { description, prompt, search_parameters },
)?;
Ok(true) Ok(true)
} }