mirror of
https://github.com/meilisearch/meilisearch.git
synced 2025-07-21 13:51:05 +00:00
Compare commits
36 Commits
latest
...
chat-route
Author | SHA1 | Date | |
---|---|---|---|
6c1739218c | |||
72d4998dce | |||
fde11573da | |||
41220f786b | |||
4d59fdb65d | |||
3e51c0a4c1 | |||
91c6ab8392 | |||
beff6adeb1 | |||
18eab165a7 | |||
5c6b63df65 | |||
7266aed770 | |||
bae6c98aa3 | |||
42c95cf3c4 | |||
4f919db344 | |||
295840d07a | |||
c0c3bddda8 | |||
10b5fcd4ba | |||
8113d4a52e | |||
5964289284 | |||
6b81854d48 | |||
9e5b466426 | |||
b43ffd8fac | |||
43da2bcb8c | |||
5e3b126d73 | |||
6c034754ca | |||
6329cf7ed6 | |||
e0c8c11a94 | |||
6e8b371111 | |||
da7d651f4b | |||
24050f06e4 | |||
af482d8ee9 | |||
7d62307739 | |||
3a71df7b5a | |||
ac39a436d9 | |||
e5c963a170 | |||
9baf2ce1a6 |
560
Cargo.lock
generated
560
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -31,7 +31,7 @@ anyhow = "1.0.95"
|
|||||||
bytes = "1.9.0"
|
bytes = "1.9.0"
|
||||||
convert_case = "0.6.0"
|
convert_case = "0.6.0"
|
||||||
flate2 = "1.0.35"
|
flate2 = "1.0.35"
|
||||||
reqwest = { version = "0.12.12", features = ["blocking", "rustls-tls"], default-features = false }
|
reqwest = { version = "0.12.15", features = ["blocking", "rustls-tls"], default-features = false }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["milli/all-tokenizations"]
|
default = ["milli/all-tokenizations"]
|
||||||
|
@ -398,6 +398,7 @@ impl<T> From<v5::Settings<T>> for v6::Settings<v6::Unchecked> {
|
|||||||
search_cutoff_ms: v6::Setting::NotSet,
|
search_cutoff_ms: v6::Setting::NotSet,
|
||||||
facet_search: v6::Setting::NotSet,
|
facet_search: v6::Setting::NotSet,
|
||||||
prefix_search: v6::Setting::NotSet,
|
prefix_search: v6::Setting::NotSet,
|
||||||
|
chat: v6::Setting::NotSet,
|
||||||
_kind: std::marker::PhantomData,
|
_kind: std::marker::PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ mod lru;
|
|||||||
mod processing;
|
mod processing;
|
||||||
mod queue;
|
mod queue;
|
||||||
mod scheduler;
|
mod scheduler;
|
||||||
|
mod settings;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_utils;
|
mod test_utils;
|
||||||
pub mod upgrade;
|
pub mod upgrade;
|
||||||
@ -53,8 +54,8 @@ use flate2::Compression;
|
|||||||
use meilisearch_types::batches::Batch;
|
use meilisearch_types::batches::Batch;
|
||||||
use meilisearch_types::features::{InstanceTogglableFeatures, Network, RuntimeTogglableFeatures};
|
use meilisearch_types::features::{InstanceTogglableFeatures, Network, RuntimeTogglableFeatures};
|
||||||
use meilisearch_types::heed::byteorder::BE;
|
use meilisearch_types::heed::byteorder::BE;
|
||||||
use meilisearch_types::heed::types::I128;
|
use meilisearch_types::heed::types::{SerdeJson, Str, I128};
|
||||||
use meilisearch_types::heed::{self, Env, RoTxn, WithoutTls};
|
use meilisearch_types::heed::{self, Database, Env, RoTxn, Unspecified, WithoutTls};
|
||||||
use meilisearch_types::milli::index::IndexEmbeddingConfig;
|
use meilisearch_types::milli::index::IndexEmbeddingConfig;
|
||||||
use meilisearch_types::milli::update::IndexerConfig;
|
use meilisearch_types::milli::update::IndexerConfig;
|
||||||
use meilisearch_types::milli::vector::{Embedder, EmbedderOptions, EmbeddingConfigs};
|
use meilisearch_types::milli::vector::{Embedder, EmbedderOptions, EmbeddingConfigs};
|
||||||
@ -142,6 +143,8 @@ pub struct IndexScheduler {
|
|||||||
/// The list of tasks currently processing
|
/// The list of tasks currently processing
|
||||||
pub(crate) processing_tasks: Arc<RwLock<ProcessingTasks>>,
|
pub(crate) processing_tasks: Arc<RwLock<ProcessingTasks>>,
|
||||||
|
|
||||||
|
/// The main database that also has the chat settings.
|
||||||
|
pub main: Database<Str, Unspecified>,
|
||||||
/// A database containing only the version of the index-scheduler
|
/// A database containing only the version of the index-scheduler
|
||||||
pub version: versioning::Versioning,
|
pub version: versioning::Versioning,
|
||||||
/// The queue containing both the tasks and the batches.
|
/// The queue containing both the tasks and the batches.
|
||||||
@ -196,7 +199,7 @@ impl IndexScheduler {
|
|||||||
version: self.version.clone(),
|
version: self.version.clone(),
|
||||||
queue: self.queue.private_clone(),
|
queue: self.queue.private_clone(),
|
||||||
scheduler: self.scheduler.private_clone(),
|
scheduler: self.scheduler.private_clone(),
|
||||||
|
main: self.main.clone(),
|
||||||
index_mapper: self.index_mapper.clone(),
|
index_mapper: self.index_mapper.clone(),
|
||||||
cleanup_enabled: self.cleanup_enabled,
|
cleanup_enabled: self.cleanup_enabled,
|
||||||
webhook_url: self.webhook_url.clone(),
|
webhook_url: self.webhook_url.clone(),
|
||||||
@ -267,6 +270,7 @@ impl IndexScheduler {
|
|||||||
let features = features::FeatureData::new(&env, &mut wtxn, options.instance_features)?;
|
let features = features::FeatureData::new(&env, &mut wtxn, options.instance_features)?;
|
||||||
let queue = Queue::new(&env, &mut wtxn, &options)?;
|
let queue = Queue::new(&env, &mut wtxn, &options)?;
|
||||||
let index_mapper = IndexMapper::new(&env, &mut wtxn, &options, budget)?;
|
let index_mapper = IndexMapper::new(&env, &mut wtxn, &options, budget)?;
|
||||||
|
let chat_settings = env.create_database(&mut wtxn, Some("chat-settings"))?;
|
||||||
wtxn.commit()?;
|
wtxn.commit()?;
|
||||||
|
|
||||||
// allow unreachable_code to get rids of the warning in the case of a test build.
|
// allow unreachable_code to get rids of the warning in the case of a test build.
|
||||||
@ -290,6 +294,7 @@ impl IndexScheduler {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
run_loop_iteration: Arc::new(RwLock::new(0)),
|
run_loop_iteration: Arc::new(RwLock::new(0)),
|
||||||
features,
|
features,
|
||||||
|
chat_settings,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.run();
|
this.run();
|
||||||
@ -857,6 +862,18 @@ impl IndexScheduler {
|
|||||||
.collect();
|
.collect();
|
||||||
res.map(EmbeddingConfigs::new)
|
res.map(EmbeddingConfigs::new)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn chat_settings(&self) -> Result<Option<serde_json::Value>> {
|
||||||
|
let rtxn = self.env.read_txn().map_err(Error::HeedTransaction)?;
|
||||||
|
self.chat_settings.get(&rtxn, "main").map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn put_chat_settings(&self, settings: &serde_json::Value) -> Result<()> {
|
||||||
|
let mut wtxn = self.env.write_txn().map_err(Error::HeedTransaction)?;
|
||||||
|
self.chat_settings.put(&mut wtxn, "main", settings)?;
|
||||||
|
wtxn.commit().map_err(Error::HeedTransaction)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The outcome of calling the [`IndexScheduler::tick`] function.
|
/// The outcome of calling the [`IndexScheduler::tick`] function.
|
||||||
|
432
crates/index-scheduler/src/settings/chat.rs
Normal file
432
crates/index-scheduler/src/settings/chat.rs
Normal file
@ -0,0 +1,432 @@
|
|||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
use std::convert::Infallible;
|
||||||
|
use std::fmt;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
|
use std::ops::{ControlFlow, Deref};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use deserr::{DeserializeError, Deserr, ErrorKind, MergeWithError, ValuePointerRef};
|
||||||
|
use fst::IntoStreamer;
|
||||||
|
use milli::disabled_typos_terms::DisabledTyposTerms;
|
||||||
|
use milli::index::{IndexEmbeddingConfig, PrefixSearch};
|
||||||
|
use milli::proximity::ProximityPrecision;
|
||||||
|
use milli::update::Setting;
|
||||||
|
use milli::{FilterableAttributesRule, Index};
|
||||||
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use crate::deserr::DeserrJsonError;
|
||||||
|
use crate::error::deserr_codes::*;
|
||||||
|
use crate::heed::RoTxn;
|
||||||
|
use crate::IndexScheduler;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)]
|
||||||
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
|
#[deserr(deny_unknown_fields, rename_all = camelCase, where_predicate = __Deserr_E: deserr::MergeWithError<DeserrJsonError<InvalidSettingsTypoTolerance>>)]
|
||||||
|
pub struct PromptsSettings {
|
||||||
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
|
#[deserr(default)]
|
||||||
|
#[schema(value_type = Option<String>)]
|
||||||
|
pub system: Setting<String>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsTypoTolerance>)]
|
||||||
|
#[schema(value_type = Option<MinWordSizeTyposSetting>, example = json!({ "oneTypo": 5, "twoTypo": 9 }))]
|
||||||
|
pub search_description: Setting<String>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
|
#[deserr(default)]
|
||||||
|
#[schema(value_type = Option<String>)]
|
||||||
|
pub search_q_param: Setting<String>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
|
#[deserr(default)]
|
||||||
|
#[schema(value_type = Option<String>)]
|
||||||
|
pub pre_query: Setting<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)]
|
||||||
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
|
pub enum ChatSource {
|
||||||
|
#[default]
|
||||||
|
OpenAi,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holds all the settings for an index. `T` can either be `Checked` if they represents settings
|
||||||
|
/// whose validity is guaranteed, or `Unchecked` if they need to be validated. In the later case, a
|
||||||
|
/// call to `check` will return a `Settings<Checked>` from a `Settings<Unchecked>`.
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)]
|
||||||
|
#[serde(
|
||||||
|
deny_unknown_fields,
|
||||||
|
rename_all = "camelCase",
|
||||||
|
bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'static>")
|
||||||
|
)]
|
||||||
|
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||||
|
#[schema(rename_all = "camelCase")]
|
||||||
|
pub struct ChatSettings<T> {
|
||||||
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsDisplayedAttributes>)]
|
||||||
|
#[schema(value_type = Option<String>)]
|
||||||
|
pub source: Setting<ChatSource>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsSearchableAttributes>)]
|
||||||
|
#[schema(value_type = Option<String>)]
|
||||||
|
pub base_api: Setting<String>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsSearchableAttributes>)]
|
||||||
|
#[schema(value_type = Option<String>)]
|
||||||
|
pub api_key: Setting<String>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsFilterableAttributes>)]
|
||||||
|
#[schema(value_type = Option<PromptsSettings>)]
|
||||||
|
pub prompts: Setting<PromptsSettings>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
#[deserr(skip)]
|
||||||
|
pub _kind: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ChatSettings<T> {
|
||||||
|
pub fn hide_secrets(&mut self) {
|
||||||
|
match &mut self.api_key {
|
||||||
|
Setting::Set(key) => Self::hide_secrets(key),
|
||||||
|
Setting::Reset => todo!(),
|
||||||
|
Setting::NotSet => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hide_secret(secret: &mut String) {
|
||||||
|
match secret.len() {
|
||||||
|
x if x < 10 => {
|
||||||
|
secret.replace_range(.., "XXX...");
|
||||||
|
}
|
||||||
|
x if x < 20 => {
|
||||||
|
secret.replace_range(2.., "XXXX...");
|
||||||
|
}
|
||||||
|
x if x < 30 => {
|
||||||
|
secret.replace_range(3.., "XXXXX...");
|
||||||
|
}
|
||||||
|
_x => {
|
||||||
|
secret.replace_range(5.., "XXXXXX...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChatSettings<Checked> {
|
||||||
|
pub fn cleared() -> ChatSettings<Checked> {
|
||||||
|
ChatSettings {
|
||||||
|
source: Setting::Reset,
|
||||||
|
base_api: Setting::Reset,
|
||||||
|
api_key: Setting::Reset,
|
||||||
|
prompts: Setting::Reset,
|
||||||
|
_kind: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_unchecked(self) -> ChatSettings<Unchecked> {
|
||||||
|
let Self { source, base_api, api_key, prompts, _kind } = self;
|
||||||
|
ChatSettings { source, base_api, api_key, prompts, _kind: PhantomData }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChatSettings<Unchecked> {
|
||||||
|
pub fn check(self) -> ChatSettings<Checked> {
|
||||||
|
ChatSettings {
|
||||||
|
source: self.source,
|
||||||
|
base_api: self.base_api,
|
||||||
|
api_key: self.api_key,
|
||||||
|
prompts: self.prompts,
|
||||||
|
_kind: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(self) -> Result<Self, milli::Error> {
|
||||||
|
self.validate_prompt_settings()?;
|
||||||
|
self.validate_global_settings()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_global_settings(mut self) -> Result<Self, milli::Error> {
|
||||||
|
// Check that the ApiBase is a valid URL
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_prompt_settings(mut self) -> Result<Self, milli::Error> {
|
||||||
|
// TODO
|
||||||
|
// let Setting::Set(mut configs) = self.embedders else { return Ok(self) };
|
||||||
|
// for (name, config) in configs.iter_mut() {
|
||||||
|
// let config_to_check = std::mem::take(config);
|
||||||
|
// let checked_config =
|
||||||
|
// milli::update::validate_embedding_settings(config_to_check.inner, name)?;
|
||||||
|
// *config = SettingEmbeddingSettings { inner: checked_config };
|
||||||
|
// }
|
||||||
|
// self.embedders = Setting::Set(configs);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge(&mut self, other: &Self) {
|
||||||
|
// For most settings only the latest version is kept
|
||||||
|
*self = Self {
|
||||||
|
source: other.source.or(self.source),
|
||||||
|
base_api: other.base_api.or(self.base_api),
|
||||||
|
api_key: other.api_key.or(self.api_key),
|
||||||
|
prompts: match (self.prompts, other.prompts) {
|
||||||
|
(Setting::NotSet, set) | (set, Setting::NotSet) => set,
|
||||||
|
(Setting::Set(_) | Setting::Reset, Setting::Reset) => Setting::Reset,
|
||||||
|
(Setting::Reset, Setting::Set(set)) => Setting::Set(set),
|
||||||
|
// If both are set we must merge the prompts settings
|
||||||
|
(Setting::Set(this), Setting::Set(other)) => Setting::Set(PromptsSettings {
|
||||||
|
system: other.system.or(system),
|
||||||
|
search_description: other.search_description.or(search_description),
|
||||||
|
search_q_param: other.search_q_param.or(search_q_param),
|
||||||
|
pre_query: other.pre_query.or(pre_query),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
_kind: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_settings_to_builder(
|
||||||
|
settings: &ChatSettings<Checked>,
|
||||||
|
// TODO we must not store this into milli but in the index scheduler
|
||||||
|
builder: &mut milli::update::Settings,
|
||||||
|
) {
|
||||||
|
let ChatSettings { source, base_api, api_key, prompts, _kind } = settings;
|
||||||
|
|
||||||
|
match source.deref() {
|
||||||
|
Setting::Set(ref names) => builder.set_searchable_fields(names.clone()),
|
||||||
|
Setting::Reset => builder.reset_searchable_fields(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match displayed_attributes.deref() {
|
||||||
|
Setting::Set(ref names) => builder.set_displayed_fields(names.clone()),
|
||||||
|
Setting::Reset => builder.reset_displayed_fields(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match filterable_attributes {
|
||||||
|
Setting::Set(ref facets) => {
|
||||||
|
builder.set_filterable_fields(facets.clone().into_iter().collect())
|
||||||
|
}
|
||||||
|
Setting::Reset => builder.reset_filterable_fields(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match sortable_attributes {
|
||||||
|
Setting::Set(ref fields) => builder.set_sortable_fields(fields.iter().cloned().collect()),
|
||||||
|
Setting::Reset => builder.reset_sortable_fields(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match ranking_rules {
|
||||||
|
Setting::Set(ref criteria) => {
|
||||||
|
builder.set_criteria(criteria.iter().map(|c| c.clone().into()).collect())
|
||||||
|
}
|
||||||
|
Setting::Reset => builder.reset_criteria(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match stop_words {
|
||||||
|
Setting::Set(ref stop_words) => builder.set_stop_words(stop_words.clone()),
|
||||||
|
Setting::Reset => builder.reset_stop_words(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match non_separator_tokens {
|
||||||
|
Setting::Set(ref non_separator_tokens) => {
|
||||||
|
builder.set_non_separator_tokens(non_separator_tokens.clone())
|
||||||
|
}
|
||||||
|
Setting::Reset => builder.reset_non_separator_tokens(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match separator_tokens {
|
||||||
|
Setting::Set(ref separator_tokens) => {
|
||||||
|
builder.set_separator_tokens(separator_tokens.clone())
|
||||||
|
}
|
||||||
|
Setting::Reset => builder.reset_separator_tokens(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match dictionary {
|
||||||
|
Setting::Set(ref dictionary) => builder.set_dictionary(dictionary.clone()),
|
||||||
|
Setting::Reset => builder.reset_dictionary(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match synonyms {
|
||||||
|
Setting::Set(ref synonyms) => builder.set_synonyms(synonyms.clone().into_iter().collect()),
|
||||||
|
Setting::Reset => builder.reset_synonyms(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match distinct_attribute {
|
||||||
|
Setting::Set(ref attr) => builder.set_distinct_field(attr.clone()),
|
||||||
|
Setting::Reset => builder.reset_distinct_field(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match proximity_precision {
|
||||||
|
Setting::Set(ref precision) => builder.set_proximity_precision((*precision).into()),
|
||||||
|
Setting::Reset => builder.reset_proximity_precision(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match localized_attributes_rules {
|
||||||
|
Setting::Set(ref rules) => builder
|
||||||
|
.set_localized_attributes_rules(rules.iter().cloned().map(|r| r.into()).collect()),
|
||||||
|
Setting::Reset => builder.reset_localized_attributes_rules(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match typo_tolerance {
|
||||||
|
Setting::Set(ref value) => {
|
||||||
|
match value.enabled {
|
||||||
|
Setting::Set(val) => builder.set_autorize_typos(val),
|
||||||
|
Setting::Reset => builder.reset_authorize_typos(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match value.min_word_size_for_typos {
|
||||||
|
Setting::Set(ref setting) => {
|
||||||
|
match setting.one_typo {
|
||||||
|
Setting::Set(val) => builder.set_min_word_len_one_typo(val),
|
||||||
|
Setting::Reset => builder.reset_min_word_len_one_typo(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
match setting.two_typos {
|
||||||
|
Setting::Set(val) => builder.set_min_word_len_two_typos(val),
|
||||||
|
Setting::Reset => builder.reset_min_word_len_two_typos(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Setting::Reset => {
|
||||||
|
builder.reset_min_word_len_one_typo();
|
||||||
|
builder.reset_min_word_len_two_typos();
|
||||||
|
}
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match value.disable_on_words {
|
||||||
|
Setting::Set(ref words) => {
|
||||||
|
builder.set_exact_words(words.clone());
|
||||||
|
}
|
||||||
|
Setting::Reset => builder.reset_exact_words(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match value.disable_on_attributes {
|
||||||
|
Setting::Set(ref words) => {
|
||||||
|
builder.set_exact_attributes(words.iter().cloned().collect())
|
||||||
|
}
|
||||||
|
Setting::Reset => builder.reset_exact_attributes(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match value.disable_on_numbers {
|
||||||
|
Setting::Set(val) => builder.set_disable_on_numbers(val),
|
||||||
|
Setting::Reset => builder.reset_disable_on_numbers(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Setting::Reset => {
|
||||||
|
// all typo settings need to be reset here.
|
||||||
|
builder.reset_authorize_typos();
|
||||||
|
builder.reset_min_word_len_one_typo();
|
||||||
|
builder.reset_min_word_len_two_typos();
|
||||||
|
builder.reset_exact_words();
|
||||||
|
builder.reset_exact_attributes();
|
||||||
|
}
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match faceting {
|
||||||
|
Setting::Set(FacetingSettings { max_values_per_facet, sort_facet_values_by }) => {
|
||||||
|
match max_values_per_facet {
|
||||||
|
Setting::Set(val) => builder.set_max_values_per_facet(*val),
|
||||||
|
Setting::Reset => builder.reset_max_values_per_facet(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
match sort_facet_values_by {
|
||||||
|
Setting::Set(val) => builder.set_sort_facet_values_by(
|
||||||
|
val.iter().map(|(name, order)| (name.clone(), (*order).into())).collect(),
|
||||||
|
),
|
||||||
|
Setting::Reset => builder.reset_sort_facet_values_by(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Setting::Reset => {
|
||||||
|
builder.reset_max_values_per_facet();
|
||||||
|
builder.reset_sort_facet_values_by();
|
||||||
|
}
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match pagination {
|
||||||
|
Setting::Set(ref value) => match value.max_total_hits {
|
||||||
|
Setting::Set(val) => builder.set_pagination_max_total_hits(val),
|
||||||
|
Setting::Reset => builder.reset_pagination_max_total_hits(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
},
|
||||||
|
Setting::Reset => builder.reset_pagination_max_total_hits(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match embedders {
|
||||||
|
Setting::Set(value) => builder.set_embedder_settings(
|
||||||
|
value.iter().map(|(k, v)| (k.clone(), v.inner.clone())).collect(),
|
||||||
|
),
|
||||||
|
Setting::Reset => builder.reset_embedder_settings(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match search_cutoff_ms {
|
||||||
|
Setting::Set(cutoff) => builder.set_search_cutoff(*cutoff),
|
||||||
|
Setting::Reset => builder.reset_search_cutoff(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match prefix_search {
|
||||||
|
Setting::Set(prefix_search) => {
|
||||||
|
builder.set_prefix_search(PrefixSearch::from(*prefix_search))
|
||||||
|
}
|
||||||
|
Setting::Reset => builder.reset_prefix_search(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match facet_search {
|
||||||
|
Setting::Set(facet_search) => builder.set_facet_search(*facet_search),
|
||||||
|
Setting::Reset => builder.reset_facet_search(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match chat {
|
||||||
|
Setting::Set(chat) => builder.set_chat(chat.clone()),
|
||||||
|
Setting::Reset => builder.reset_chat(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum SecretPolicy {
|
||||||
|
RevealSecrets,
|
||||||
|
HideSecrets,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn settings(
|
||||||
|
index_scheduler: &IndexScheduler,
|
||||||
|
rtxn: &RoTxn,
|
||||||
|
secret_policy: SecretPolicy,
|
||||||
|
) -> Result<Settings<Checked>, milli::Error> {
|
||||||
|
let mut settings = index_scheduler.chat_settings(rtxn)?;
|
||||||
|
if let SecretPolicy::HideSecrets = secret_policy {
|
||||||
|
settings.hide_secrets()
|
||||||
|
}
|
||||||
|
Ok(settings)
|
||||||
|
}
|
3
crates/index-scheduler/src/settings/mod.rs
Normal file
3
crates/index-scheduler/src/settings/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mod chat;
|
||||||
|
|
||||||
|
pub use chat::ChatSettings;
|
@ -351,6 +351,7 @@ pub struct IndexSearchRules {
|
|||||||
fn generate_default_keys(store: &HeedAuthStore) -> Result<()> {
|
fn generate_default_keys(store: &HeedAuthStore) -> Result<()> {
|
||||||
store.put_api_key(Key::default_admin())?;
|
store.put_api_key(Key::default_admin())?;
|
||||||
store.put_api_key(Key::default_search())?;
|
store.put_api_key(Key::default_search())?;
|
||||||
|
store.put_api_key(Key::default_chat())?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -387,7 +387,8 @@ VectorEmbeddingError , InvalidRequest , BAD_REQUEST ;
|
|||||||
NotFoundSimilarId , InvalidRequest , BAD_REQUEST ;
|
NotFoundSimilarId , InvalidRequest , BAD_REQUEST ;
|
||||||
InvalidDocumentEditionContext , InvalidRequest , BAD_REQUEST ;
|
InvalidDocumentEditionContext , InvalidRequest , BAD_REQUEST ;
|
||||||
InvalidDocumentEditionFunctionFilter , InvalidRequest , BAD_REQUEST ;
|
InvalidDocumentEditionFunctionFilter , InvalidRequest , BAD_REQUEST ;
|
||||||
EditDocumentsByFunctionError , InvalidRequest , BAD_REQUEST
|
EditDocumentsByFunctionError , InvalidRequest , BAD_REQUEST ;
|
||||||
|
InvalidSettingsIndexChat , InvalidRequest , BAD_REQUEST
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ErrorCode for JoinError {
|
impl ErrorCode for JoinError {
|
||||||
|
@ -158,6 +158,21 @@ impl Key {
|
|||||||
updated_at: now,
|
updated_at: now,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_chat() -> Self {
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
let uid = Uuid::new_v4();
|
||||||
|
Self {
|
||||||
|
name: Some("Default Chat API Key".to_string()),
|
||||||
|
description: Some("Use it to chat and search from the frontend".to_string()),
|
||||||
|
uid,
|
||||||
|
actions: vec![Action::Chat, Action::Search],
|
||||||
|
indexes: vec![IndexUidPattern::all()],
|
||||||
|
expires_at: None,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_expiration_date(
|
fn parse_expiration_date(
|
||||||
@ -308,6 +323,18 @@ pub enum Action {
|
|||||||
#[serde(rename = "network.update")]
|
#[serde(rename = "network.update")]
|
||||||
#[deserr(rename = "network.update")]
|
#[deserr(rename = "network.update")]
|
||||||
NetworkUpdate,
|
NetworkUpdate,
|
||||||
|
#[serde(rename = "chat.get")]
|
||||||
|
#[deserr(rename = "chat.get")]
|
||||||
|
Chat,
|
||||||
|
#[serde(rename = "chatSettings.*")]
|
||||||
|
#[deserr(rename = "chatSettings.*")]
|
||||||
|
ChatSettingsAll,
|
||||||
|
#[serde(rename = "chatSettings.get")]
|
||||||
|
#[deserr(rename = "chatSettings.get")]
|
||||||
|
ChatSettingsGet,
|
||||||
|
#[serde(rename = "chatSettings.update")]
|
||||||
|
#[deserr(rename = "chatSettings.update")]
|
||||||
|
ChatSettingsUpdate,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Action {
|
impl Action {
|
||||||
@ -333,6 +360,9 @@ impl Action {
|
|||||||
SETTINGS_ALL => Some(Self::SettingsAll),
|
SETTINGS_ALL => Some(Self::SettingsAll),
|
||||||
SETTINGS_GET => Some(Self::SettingsGet),
|
SETTINGS_GET => Some(Self::SettingsGet),
|
||||||
SETTINGS_UPDATE => Some(Self::SettingsUpdate),
|
SETTINGS_UPDATE => Some(Self::SettingsUpdate),
|
||||||
|
CHAT_SETTINGS_ALL => Some(Self::ChatSettingsAll),
|
||||||
|
CHAT_SETTINGS_GET => Some(Self::ChatSettingsGet),
|
||||||
|
CHAT_SETTINGS_UPDATE => Some(Self::ChatSettingsUpdate),
|
||||||
STATS_ALL => Some(Self::StatsAll),
|
STATS_ALL => Some(Self::StatsAll),
|
||||||
STATS_GET => Some(Self::StatsGet),
|
STATS_GET => Some(Self::StatsGet),
|
||||||
METRICS_ALL => Some(Self::MetricsAll),
|
METRICS_ALL => Some(Self::MetricsAll),
|
||||||
@ -349,6 +379,7 @@ impl Action {
|
|||||||
EXPERIMENTAL_FEATURES_UPDATE => Some(Self::ExperimentalFeaturesUpdate),
|
EXPERIMENTAL_FEATURES_UPDATE => Some(Self::ExperimentalFeaturesUpdate),
|
||||||
NETWORK_GET => Some(Self::NetworkGet),
|
NETWORK_GET => Some(Self::NetworkGet),
|
||||||
NETWORK_UPDATE => Some(Self::NetworkUpdate),
|
NETWORK_UPDATE => Some(Self::NetworkUpdate),
|
||||||
|
CHAT => Some(Self::Chat),
|
||||||
_otherwise => None,
|
_otherwise => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -397,4 +428,9 @@ pub mod actions {
|
|||||||
|
|
||||||
pub const NETWORK_GET: u8 = NetworkGet.repr();
|
pub const NETWORK_GET: u8 = NetworkGet.repr();
|
||||||
pub const NETWORK_UPDATE: u8 = NetworkUpdate.repr();
|
pub const NETWORK_UPDATE: u8 = NetworkUpdate.repr();
|
||||||
|
|
||||||
|
pub const CHAT: u8 = Chat.repr();
|
||||||
|
pub const CHAT_SETTINGS_ALL: u8 = ChatSettingsAll.repr();
|
||||||
|
pub const CHAT_SETTINGS_GET: u8 = ChatSettingsGet.repr();
|
||||||
|
pub const CHAT_SETTINGS_UPDATE: u8 = ChatSettingsUpdate.repr();
|
||||||
}
|
}
|
||||||
|
@ -11,11 +11,13 @@ use fst::IntoStreamer;
|
|||||||
use milli::disabled_typos_terms::DisabledTyposTerms;
|
use milli::disabled_typos_terms::DisabledTyposTerms;
|
||||||
use milli::index::{IndexEmbeddingConfig, PrefixSearch};
|
use milli::index::{IndexEmbeddingConfig, PrefixSearch};
|
||||||
use milli::proximity::ProximityPrecision;
|
use milli::proximity::ProximityPrecision;
|
||||||
|
pub use milli::update::ChatSettings;
|
||||||
use milli::update::Setting;
|
use milli::update::Setting;
|
||||||
use milli::{Criterion, CriterionError, FilterableAttributesRule, Index, DEFAULT_VALUES_PER_FACET};
|
use milli::{Criterion, CriterionError, FilterableAttributesRule, Index, DEFAULT_VALUES_PER_FACET};
|
||||||
use serde::{Deserialize, Serialize, Serializer};
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use super::{Checked, Unchecked};
|
||||||
use crate::deserr::DeserrJsonError;
|
use crate::deserr::DeserrJsonError;
|
||||||
use crate::error::deserr_codes::*;
|
use crate::error::deserr_codes::*;
|
||||||
use crate::facet_values_sort::FacetValuesSort;
|
use crate::facet_values_sort::FacetValuesSort;
|
||||||
@ -199,72 +201,86 @@ pub struct Settings<T> {
|
|||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsDisplayedAttributes>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsDisplayedAttributes>)]
|
||||||
#[schema(value_type = Option<Vec<String>>, example = json!(["id", "title", "description", "url"]))]
|
#[schema(value_type = Option<Vec<String>>, example = json!(["id", "title", "description", "url"]))]
|
||||||
pub displayed_attributes: WildcardSetting,
|
pub displayed_attributes: WildcardSetting,
|
||||||
|
|
||||||
/// Fields in which to search for matching query words sorted by order of importance.
|
/// Fields in which to search for matching query words sorted by order of importance.
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsSearchableAttributes>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsSearchableAttributes>)]
|
||||||
#[schema(value_type = Option<Vec<String>>, example = json!(["title", "description"]))]
|
#[schema(value_type = Option<Vec<String>>, example = json!(["title", "description"]))]
|
||||||
pub searchable_attributes: WildcardSetting,
|
pub searchable_attributes: WildcardSetting,
|
||||||
|
|
||||||
/// Attributes to use for faceting and filtering. See [Filtering and Faceted Search](https://www.meilisearch.com/docs/learn/filtering_and_sorting/search_with_facet_filters).
|
/// Attributes to use for faceting and filtering. See [Filtering and Faceted Search](https://www.meilisearch.com/docs/learn/filtering_and_sorting/search_with_facet_filters).
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsFilterableAttributes>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsFilterableAttributes>)]
|
||||||
#[schema(value_type = Option<Vec<FilterableAttributesRule>>, example = json!(["release_date", "genre"]))]
|
#[schema(value_type = Option<Vec<FilterableAttributesRule>>, example = json!(["release_date", "genre"]))]
|
||||||
pub filterable_attributes: Setting<Vec<FilterableAttributesRule>>,
|
pub filterable_attributes: Setting<Vec<FilterableAttributesRule>>,
|
||||||
|
|
||||||
/// Attributes to use when sorting search results.
|
/// Attributes to use when sorting search results.
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsSortableAttributes>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsSortableAttributes>)]
|
||||||
#[schema(value_type = Option<Vec<String>>, example = json!(["release_date"]))]
|
#[schema(value_type = Option<Vec<String>>, example = json!(["release_date"]))]
|
||||||
pub sortable_attributes: Setting<BTreeSet<String>>,
|
pub sortable_attributes: Setting<BTreeSet<String>>,
|
||||||
|
|
||||||
/// List of ranking rules sorted by order of importance. The order is customizable.
|
/// List of ranking rules sorted by order of importance. The order is customizable.
|
||||||
/// [A list of ordered built-in ranking rules](https://www.meilisearch.com/docs/learn/relevancy/relevancy).
|
/// [A list of ordered built-in ranking rules](https://www.meilisearch.com/docs/learn/relevancy/relevancy).
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsRankingRules>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsRankingRules>)]
|
||||||
#[schema(value_type = Option<Vec<String>>, example = json!([RankingRuleView::Words, RankingRuleView::Typo, RankingRuleView::Proximity, RankingRuleView::Attribute, RankingRuleView::Exactness]))]
|
#[schema(value_type = Option<Vec<String>>, example = json!([RankingRuleView::Words, RankingRuleView::Typo, RankingRuleView::Proximity, RankingRuleView::Attribute, RankingRuleView::Exactness]))]
|
||||||
pub ranking_rules: Setting<Vec<RankingRuleView>>,
|
pub ranking_rules: Setting<Vec<RankingRuleView>>,
|
||||||
|
|
||||||
/// List of words ignored when present in search queries.
|
/// List of words ignored when present in search queries.
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsStopWords>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsStopWords>)]
|
||||||
#[schema(value_type = Option<Vec<String>>, example = json!(["the", "a", "them", "their"]))]
|
#[schema(value_type = Option<Vec<String>>, example = json!(["the", "a", "them", "their"]))]
|
||||||
pub stop_words: Setting<BTreeSet<String>>,
|
pub stop_words: Setting<BTreeSet<String>>,
|
||||||
|
|
||||||
/// List of characters not delimiting where one term begins and ends.
|
/// List of characters not delimiting where one term begins and ends.
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsNonSeparatorTokens>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsNonSeparatorTokens>)]
|
||||||
#[schema(value_type = Option<Vec<String>>, example = json!([" ", "\n"]))]
|
#[schema(value_type = Option<Vec<String>>, example = json!([" ", "\n"]))]
|
||||||
pub non_separator_tokens: Setting<BTreeSet<String>>,
|
pub non_separator_tokens: Setting<BTreeSet<String>>,
|
||||||
|
|
||||||
/// List of characters delimiting where one term begins and ends.
|
/// List of characters delimiting where one term begins and ends.
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsSeparatorTokens>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsSeparatorTokens>)]
|
||||||
#[schema(value_type = Option<Vec<String>>, example = json!(["S"]))]
|
#[schema(value_type = Option<Vec<String>>, example = json!(["S"]))]
|
||||||
pub separator_tokens: Setting<BTreeSet<String>>,
|
pub separator_tokens: Setting<BTreeSet<String>>,
|
||||||
|
|
||||||
/// List of strings Meilisearch should parse as a single term.
|
/// List of strings Meilisearch should parse as a single term.
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsDictionary>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsDictionary>)]
|
||||||
#[schema(value_type = Option<Vec<String>>, example = json!(["iPhone pro"]))]
|
#[schema(value_type = Option<Vec<String>>, example = json!(["iPhone pro"]))]
|
||||||
pub dictionary: Setting<BTreeSet<String>>,
|
pub dictionary: Setting<BTreeSet<String>>,
|
||||||
|
|
||||||
/// List of associated words treated similarly. A word associated to an array of word as synonyms.
|
/// List of associated words treated similarly. A word associated to an array of word as synonyms.
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsSynonyms>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsSynonyms>)]
|
||||||
#[schema(value_type = Option<BTreeMap<String, Vec<String>>>, example = json!({ "he": ["she", "they", "them"], "phone": ["iPhone", "android"]}))]
|
#[schema(value_type = Option<BTreeMap<String, Vec<String>>>, example = json!({ "he": ["she", "they", "them"], "phone": ["iPhone", "android"]}))]
|
||||||
pub synonyms: Setting<BTreeMap<String, Vec<String>>>,
|
pub synonyms: Setting<BTreeMap<String, Vec<String>>>,
|
||||||
|
|
||||||
/// Search returns documents with distinct (different) values of the given field.
|
/// Search returns documents with distinct (different) values of the given field.
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsDistinctAttribute>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsDistinctAttribute>)]
|
||||||
#[schema(value_type = Option<String>, example = json!("sku"))]
|
#[schema(value_type = Option<String>, example = json!("sku"))]
|
||||||
pub distinct_attribute: Setting<String>,
|
pub distinct_attribute: Setting<String>,
|
||||||
|
|
||||||
/// Precision level when calculating the proximity ranking rule.
|
/// Precision level when calculating the proximity ranking rule.
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsProximityPrecision>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsProximityPrecision>)]
|
||||||
#[schema(value_type = Option<String>, example = json!(ProximityPrecisionView::ByAttribute))]
|
#[schema(value_type = Option<String>, example = json!(ProximityPrecisionView::ByAttribute))]
|
||||||
pub proximity_precision: Setting<ProximityPrecisionView>,
|
pub proximity_precision: Setting<ProximityPrecisionView>,
|
||||||
|
|
||||||
/// Customize typo tolerance feature.
|
/// Customize typo tolerance feature.
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsTypoTolerance>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsTypoTolerance>)]
|
||||||
#[schema(value_type = Option<TypoSettings>, example = json!({ "enabled": true, "disableOnAttributes": ["title"]}))]
|
#[schema(value_type = Option<TypoSettings>, example = json!({ "enabled": true, "disableOnAttributes": ["title"]}))]
|
||||||
pub typo_tolerance: Setting<TypoSettings>,
|
pub typo_tolerance: Setting<TypoSettings>,
|
||||||
|
|
||||||
/// Faceting settings.
|
/// Faceting settings.
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsFaceting>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsFaceting>)]
|
||||||
#[schema(value_type = Option<FacetingSettings>, example = json!({ "maxValuesPerFacet": 10, "sortFacetValuesBy": { "genre": FacetValuesSort::Count }}))]
|
#[schema(value_type = Option<FacetingSettings>, example = json!({ "maxValuesPerFacet": 10, "sortFacetValuesBy": { "genre": FacetValuesSort::Count }}))]
|
||||||
pub faceting: Setting<FacetingSettings>,
|
pub faceting: Setting<FacetingSettings>,
|
||||||
|
|
||||||
/// Pagination settings.
|
/// Pagination settings.
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsPagination>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsPagination>)]
|
||||||
@ -276,24 +292,34 @@ pub struct Settings<T> {
|
|||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsEmbedders>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsEmbedders>)]
|
||||||
#[schema(value_type = Option<BTreeMap<String, SettingEmbeddingSettings>>)]
|
#[schema(value_type = Option<BTreeMap<String, SettingEmbeddingSettings>>)]
|
||||||
pub embedders: Setting<BTreeMap<String, SettingEmbeddingSettings>>,
|
pub embedders: Setting<BTreeMap<String, SettingEmbeddingSettings>>,
|
||||||
|
|
||||||
/// Maximum duration of a search query.
|
/// Maximum duration of a search query.
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsSearchCutoffMs>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsSearchCutoffMs>)]
|
||||||
#[schema(value_type = Option<u64>, example = json!(50))]
|
#[schema(value_type = Option<u64>, example = json!(50))]
|
||||||
pub search_cutoff_ms: Setting<u64>,
|
pub search_cutoff_ms: Setting<u64>,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsLocalizedAttributes>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsLocalizedAttributes>)]
|
||||||
#[schema(value_type = Option<Vec<LocalizedAttributesRuleView>>, example = json!(50))]
|
#[schema(value_type = Option<Vec<LocalizedAttributesRuleView>>, example = json!(50))]
|
||||||
pub localized_attributes: Setting<Vec<LocalizedAttributesRuleView>>,
|
pub localized_attributes: Setting<Vec<LocalizedAttributesRuleView>>,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsFacetSearch>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsFacetSearch>)]
|
||||||
#[schema(value_type = Option<bool>, example = json!(true))]
|
#[schema(value_type = Option<bool>, example = json!(true))]
|
||||||
pub facet_search: Setting<bool>,
|
pub facet_search: Setting<bool>,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsPrefixSearch>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsPrefixSearch>)]
|
||||||
#[schema(value_type = Option<PrefixSearchSettings>, example = json!("Hemlo"))]
|
#[schema(value_type = Option<PrefixSearchSettings>, example = json!("Hemlo"))]
|
||||||
pub prefix_search: Setting<PrefixSearchSettings>,
|
pub prefix_search: Setting<PrefixSearchSettings>,
|
||||||
|
|
||||||
|
/// Customize the chat prompting.
|
||||||
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsIndexChat>)]
|
||||||
|
#[schema(value_type = Option<ChatSettings>)]
|
||||||
|
pub chat: Setting<ChatSettings>,
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
#[deserr(skip)]
|
#[deserr(skip)]
|
||||||
pub _kind: PhantomData<T>,
|
pub _kind: PhantomData<T>,
|
||||||
@ -359,6 +385,7 @@ impl Settings<Checked> {
|
|||||||
localized_attributes: Setting::Reset,
|
localized_attributes: Setting::Reset,
|
||||||
facet_search: Setting::Reset,
|
facet_search: Setting::Reset,
|
||||||
prefix_search: Setting::Reset,
|
prefix_search: Setting::Reset,
|
||||||
|
chat: Setting::Reset,
|
||||||
_kind: PhantomData,
|
_kind: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -385,6 +412,7 @@ impl Settings<Checked> {
|
|||||||
localized_attributes: localized_attributes_rules,
|
localized_attributes: localized_attributes_rules,
|
||||||
facet_search,
|
facet_search,
|
||||||
prefix_search,
|
prefix_search,
|
||||||
|
chat,
|
||||||
_kind,
|
_kind,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
@ -409,6 +437,7 @@ impl Settings<Checked> {
|
|||||||
localized_attributes: localized_attributes_rules,
|
localized_attributes: localized_attributes_rules,
|
||||||
facet_search,
|
facet_search,
|
||||||
prefix_search,
|
prefix_search,
|
||||||
|
chat,
|
||||||
_kind: PhantomData,
|
_kind: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -459,6 +488,7 @@ impl Settings<Unchecked> {
|
|||||||
localized_attributes: self.localized_attributes,
|
localized_attributes: self.localized_attributes,
|
||||||
facet_search: self.facet_search,
|
facet_search: self.facet_search,
|
||||||
prefix_search: self.prefix_search,
|
prefix_search: self.prefix_search,
|
||||||
|
chat: self.chat,
|
||||||
_kind: PhantomData,
|
_kind: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -533,8 +563,9 @@ impl Settings<Unchecked> {
|
|||||||
Setting::Set(this)
|
Setting::Set(this)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
prefix_search: other.prefix_search.or(self.prefix_search),
|
|
||||||
facet_search: other.facet_search.or(self.facet_search),
|
facet_search: other.facet_search.or(self.facet_search),
|
||||||
|
prefix_search: other.prefix_search.or(self.prefix_search),
|
||||||
|
chat: other.chat.clone().or(self.chat.clone()),
|
||||||
_kind: PhantomData,
|
_kind: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -573,6 +604,7 @@ pub fn apply_settings_to_builder(
|
|||||||
localized_attributes: localized_attributes_rules,
|
localized_attributes: localized_attributes_rules,
|
||||||
facet_search,
|
facet_search,
|
||||||
prefix_search,
|
prefix_search,
|
||||||
|
chat,
|
||||||
_kind,
|
_kind,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
@ -783,6 +815,12 @@ pub fn apply_settings_to_builder(
|
|||||||
Setting::Reset => builder.reset_facet_search(),
|
Setting::Reset => builder.reset_facet_search(),
|
||||||
Setting::NotSet => (),
|
Setting::NotSet => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match chat {
|
||||||
|
Setting::Set(chat) => builder.set_chat(chat.clone()),
|
||||||
|
Setting::Reset => builder.reset_chat(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum SecretPolicy {
|
pub enum SecretPolicy {
|
||||||
@ -880,14 +918,11 @@ pub fn settings(
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let embedders = Setting::Set(embedders);
|
let embedders = Setting::Set(embedders);
|
||||||
|
|
||||||
let search_cutoff_ms = index.search_cutoff(rtxn)?;
|
let search_cutoff_ms = index.search_cutoff(rtxn)?;
|
||||||
|
|
||||||
let localized_attributes_rules = index.localized_attributes_rules(rtxn)?;
|
let localized_attributes_rules = index.localized_attributes_rules(rtxn)?;
|
||||||
|
|
||||||
let prefix_search = index.prefix_search(rtxn)?.map(PrefixSearchSettings::from);
|
let prefix_search = index.prefix_search(rtxn)?.map(PrefixSearchSettings::from);
|
||||||
|
|
||||||
let facet_search = index.facet_search(rtxn)?;
|
let facet_search = index.facet_search(rtxn)?;
|
||||||
|
let chat = index.chat_config(rtxn).map(ChatSettings::from)?;
|
||||||
|
|
||||||
let mut settings = Settings {
|
let mut settings = Settings {
|
||||||
displayed_attributes: match displayed_attributes {
|
displayed_attributes: match displayed_attributes {
|
||||||
@ -925,8 +960,9 @@ pub fn settings(
|
|||||||
Some(rules) => Setting::Set(rules.into_iter().map(|r| r.into()).collect()),
|
Some(rules) => Setting::Set(rules.into_iter().map(|r| r.into()).collect()),
|
||||||
None => Setting::Reset,
|
None => Setting::Reset,
|
||||||
},
|
},
|
||||||
prefix_search: Setting::Set(prefix_search.unwrap_or_default()),
|
|
||||||
facet_search: Setting::Set(facet_search),
|
facet_search: Setting::Set(facet_search),
|
||||||
|
prefix_search: Setting::Set(prefix_search.unwrap_or_default()),
|
||||||
|
chat: Setting::Set(chat),
|
||||||
_kind: PhantomData,
|
_kind: PhantomData,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1154,6 +1190,7 @@ pub(crate) mod test {
|
|||||||
search_cutoff_ms: Setting::NotSet,
|
search_cutoff_ms: Setting::NotSet,
|
||||||
facet_search: Setting::NotSet,
|
facet_search: Setting::NotSet,
|
||||||
prefix_search: Setting::NotSet,
|
prefix_search: Setting::NotSet,
|
||||||
|
chat: Setting::NotSet,
|
||||||
_kind: PhantomData::<Unchecked>,
|
_kind: PhantomData::<Unchecked>,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1185,6 +1222,8 @@ pub(crate) mod test {
|
|||||||
search_cutoff_ms: Setting::NotSet,
|
search_cutoff_ms: Setting::NotSet,
|
||||||
facet_search: Setting::NotSet,
|
facet_search: Setting::NotSet,
|
||||||
prefix_search: Setting::NotSet,
|
prefix_search: Setting::NotSet,
|
||||||
|
chat: Setting::NotSet,
|
||||||
|
|
||||||
_kind: PhantomData::<Unchecked>,
|
_kind: PhantomData::<Unchecked>,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ async-trait = "0.1.85"
|
|||||||
bstr = "1.11.3"
|
bstr = "1.11.3"
|
||||||
byte-unit = { version = "5.1.6", features = ["serde"] }
|
byte-unit = { version = "5.1.6", features = ["serde"] }
|
||||||
bytes = "1.9.0"
|
bytes = "1.9.0"
|
||||||
|
bumpalo = "3.16.0"
|
||||||
clap = { version = "4.5.24", features = ["derive", "env"] }
|
clap = { version = "4.5.24", features = ["derive", "env"] }
|
||||||
crossbeam-channel = "0.5.15"
|
crossbeam-channel = "0.5.15"
|
||||||
deserr = { version = "0.6.3", features = ["actix-web"] }
|
deserr = { version = "0.6.3", features = ["actix-web"] }
|
||||||
@ -48,6 +49,7 @@ is-terminal = "0.4.13"
|
|||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
jsonwebtoken = "9.3.0"
|
jsonwebtoken = "9.3.0"
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
|
liquid = "0.26.9"
|
||||||
meilisearch-auth = { path = "../meilisearch-auth" }
|
meilisearch-auth = { path = "../meilisearch-auth" }
|
||||||
meilisearch-types = { path = "../meilisearch-types" }
|
meilisearch-types = { path = "../meilisearch-types" }
|
||||||
mimalloc = { version = "0.1.43", default-features = false }
|
mimalloc = { version = "0.1.43", default-features = false }
|
||||||
@ -111,6 +113,8 @@ utoipa = { version = "5.3.1", features = [
|
|||||||
"openapi_extensions",
|
"openapi_extensions",
|
||||||
] }
|
] }
|
||||||
utoipa-scalar = { version = "0.3.0", optional = true, features = ["actix-web"] }
|
utoipa-scalar = { version = "0.3.0", optional = true, features = ["actix-web"] }
|
||||||
|
async-openai = { git = "https://github.com/meilisearch/async-openai", branch = "optional-type-function" }
|
||||||
|
actix-web-lab = { version = "0.24.1", default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.10.0"
|
actix-rt = "2.10.0"
|
||||||
|
@ -4,6 +4,7 @@ use std::marker::PhantomData;
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
|
use actix_web::http::header::AUTHORIZATION;
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use actix_web::FromRequest;
|
use actix_web::FromRequest;
|
||||||
pub use error::AuthenticationError;
|
pub use error::AuthenticationError;
|
||||||
@ -94,36 +95,44 @@ impl<P: Policy + 'static, D: 'static + Clone> FromRequest for GuardedData<P, D>
|
|||||||
_payload: &mut actix_web::dev::Payload,
|
_payload: &mut actix_web::dev::Payload,
|
||||||
) -> Self::Future {
|
) -> Self::Future {
|
||||||
match req.app_data::<Data<AuthController>>().cloned() {
|
match req.app_data::<Data<AuthController>>().cloned() {
|
||||||
Some(auth) => match req
|
Some(auth) => match extract_token_from_request(req) {
|
||||||
.headers()
|
Ok(Some(token)) => {
|
||||||
.get("Authorization")
|
|
||||||
.map(|type_token| type_token.to_str().unwrap_or_default().splitn(2, ' '))
|
|
||||||
{
|
|
||||||
Some(mut type_token) => match type_token.next() {
|
|
||||||
Some("Bearer") => {
|
|
||||||
// TODO: find a less hardcoded way?
|
// TODO: find a less hardcoded way?
|
||||||
let index = req.match_info().get("index_uid");
|
let index = req.match_info().get("index_uid");
|
||||||
match type_token.next() {
|
Box::pin(Self::auth_bearer(
|
||||||
Some(token) => Box::pin(Self::auth_bearer(
|
|
||||||
auth,
|
auth,
|
||||||
token.to_string(),
|
token.to_string(),
|
||||||
index.map(String::from),
|
index.map(String::from),
|
||||||
req.app_data::<D>().cloned(),
|
req.app_data::<D>().cloned(),
|
||||||
)),
|
))
|
||||||
None => Box::pin(err(AuthenticationError::InvalidToken.into())),
|
|
||||||
}
|
}
|
||||||
}
|
Ok(None) => Box::pin(Self::auth_token(auth, req.app_data::<D>().cloned())),
|
||||||
_otherwise => {
|
Err(e) => Box::pin(err(e.into())),
|
||||||
Box::pin(err(AuthenticationError::MissingAuthorizationHeader.into()))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => Box::pin(Self::auth_token(auth, req.app_data::<D>().cloned())),
|
|
||||||
},
|
},
|
||||||
None => Box::pin(err(AuthenticationError::IrretrievableState.into())),
|
None => Box::pin(err(AuthenticationError::IrretrievableState.into())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn extract_token_from_request(
|
||||||
|
req: &actix_web::HttpRequest,
|
||||||
|
) -> Result<Option<&str>, AuthenticationError> {
|
||||||
|
match req
|
||||||
|
.headers()
|
||||||
|
.get(AUTHORIZATION)
|
||||||
|
.map(|type_token| type_token.to_str().unwrap_or_default().splitn(2, ' '))
|
||||||
|
{
|
||||||
|
Some(mut type_token) => match type_token.next() {
|
||||||
|
Some("Bearer") => match type_token.next() {
|
||||||
|
Some(token) => Ok(Some(token)),
|
||||||
|
None => Err(AuthenticationError::InvalidToken),
|
||||||
|
},
|
||||||
|
_otherwise => Err(AuthenticationError::MissingAuthorizationHeader),
|
||||||
|
},
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait Policy {
|
pub trait Policy {
|
||||||
fn authenticate(
|
fn authenticate(
|
||||||
auth: Data<AuthController>,
|
auth: Data<AuthController>,
|
||||||
@ -299,8 +308,8 @@ pub mod policies {
|
|||||||
auth: &AuthController,
|
auth: &AuthController,
|
||||||
token: &str,
|
token: &str,
|
||||||
) -> Result<TenantTokenOutcome, AuthError> {
|
) -> Result<TenantTokenOutcome, AuthError> {
|
||||||
// Only search action can be accessed by a tenant token.
|
// Only search and chat actions can be accessed by a tenant token.
|
||||||
if A != actions::SEARCH {
|
if A != actions::SEARCH && A != actions::CHAT {
|
||||||
return Ok(TenantTokenOutcome::NotATenantToken);
|
return Ok(TenantTokenOutcome::NotATenantToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
560
crates/meilisearch/src/routes/chat.rs
Normal file
560
crates/meilisearch/src/routes/chat.rs
Normal file
@ -0,0 +1,560 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::mem;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use actix_web::web::{self, Data};
|
||||||
|
use actix_web::{Either, HttpRequest, HttpResponse, Responder};
|
||||||
|
use actix_web_lab::sse::{self, Event, Sse};
|
||||||
|
use async_openai::config::OpenAIConfig;
|
||||||
|
use async_openai::types::{
|
||||||
|
ChatCompletionMessageToolCall, ChatCompletionMessageToolCallChunk,
|
||||||
|
ChatCompletionRequestAssistantMessageArgs, ChatCompletionRequestMessage,
|
||||||
|
ChatCompletionRequestSystemMessage, ChatCompletionRequestSystemMessageContent,
|
||||||
|
ChatCompletionRequestToolMessage, ChatCompletionRequestToolMessageContent,
|
||||||
|
ChatCompletionStreamResponseDelta, ChatCompletionToolArgs, ChatCompletionToolType,
|
||||||
|
CreateChatCompletionRequest, FinishReason, FunctionCall, FunctionCallStream,
|
||||||
|
FunctionObjectArgs,
|
||||||
|
};
|
||||||
|
use async_openai::Client;
|
||||||
|
use bumpalo::Bump;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use index_scheduler::IndexScheduler;
|
||||||
|
use meilisearch_auth::AuthController;
|
||||||
|
use meilisearch_types::error::ResponseError;
|
||||||
|
use meilisearch_types::heed::RoTxn;
|
||||||
|
use meilisearch_types::keys::actions;
|
||||||
|
use meilisearch_types::milli::index::ChatConfig;
|
||||||
|
use meilisearch_types::milli::prompt::{Prompt, PromptData};
|
||||||
|
use meilisearch_types::milli::update::new::document::DocumentFromDb;
|
||||||
|
use meilisearch_types::milli::{
|
||||||
|
DocumentId, FieldIdMapWithMetadata, GlobalFieldsIdsMap, MetadataBuilder, TimeBudget,
|
||||||
|
};
|
||||||
|
use meilisearch_types::Index;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
use tokio::runtime::Handle;
|
||||||
|
use tokio::sync::mpsc::error::SendError;
|
||||||
|
|
||||||
|
use super::settings::chat::{ChatPrompts, GlobalChatSettings};
|
||||||
|
use crate::error::MeilisearchHttpError;
|
||||||
|
use crate::extractors::authentication::policies::ActionPolicy;
|
||||||
|
use crate::extractors::authentication::{extract_token_from_request, GuardedData, Policy as _};
|
||||||
|
use crate::metrics::MEILISEARCH_DEGRADED_SEARCH_REQUESTS;
|
||||||
|
use crate::routes::indexes::search::search_kind;
|
||||||
|
use crate::search::{
|
||||||
|
add_search_rules, prepare_search, search_from_kind, HybridQuery, MatchingStrategy, SearchQuery,
|
||||||
|
SemanticRatio,
|
||||||
|
};
|
||||||
|
use crate::search_queue::SearchQueue;
|
||||||
|
|
||||||
|
const EMBEDDER_NAME: &str = "openai";
|
||||||
|
const SEARCH_IN_INDEX_FUNCTION_NAME: &str = "_meiliSearchInIndex";
|
||||||
|
|
||||||
|
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(web::resource("/completions").route(web::post().to(chat)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a chat completion
|
||||||
|
async fn chat(
|
||||||
|
index_scheduler: GuardedData<ActionPolicy<{ actions::CHAT }>, Data<IndexScheduler>>,
|
||||||
|
auth_ctrl: web::Data<AuthController>,
|
||||||
|
req: HttpRequest,
|
||||||
|
search_queue: web::Data<SearchQueue>,
|
||||||
|
web::Json(chat_completion): web::Json<CreateChatCompletionRequest>,
|
||||||
|
) -> impl Responder {
|
||||||
|
// To enable later on, when the feature will be experimental
|
||||||
|
// index_scheduler.features().check_chat("Using the /chat route")?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
chat_completion.n.unwrap_or(1),
|
||||||
|
1,
|
||||||
|
"Meilisearch /chat only support one completion at a time (n = 1, n = null)"
|
||||||
|
);
|
||||||
|
|
||||||
|
if chat_completion.stream.unwrap_or(false) {
|
||||||
|
Either::Right(
|
||||||
|
streamed_chat(index_scheduler, auth_ctrl, req, search_queue, chat_completion).await,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Either::Left(
|
||||||
|
non_streamed_chat(index_scheduler, auth_ctrl, req, search_queue, chat_completion).await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setup search tool in chat completion request
|
||||||
|
fn setup_search_tool(
|
||||||
|
index_scheduler: &Data<IndexScheduler>,
|
||||||
|
filters: &meilisearch_auth::AuthFilter,
|
||||||
|
chat_completion: &mut CreateChatCompletionRequest,
|
||||||
|
prompts: &ChatPrompts,
|
||||||
|
) -> Result<(), ResponseError> {
|
||||||
|
let tools = chat_completion.tools.get_or_insert_default();
|
||||||
|
if tools.iter().find(|t| t.function.name == SEARCH_IN_INDEX_FUNCTION_NAME).is_some() {
|
||||||
|
panic!("{SEARCH_IN_INDEX_FUNCTION_NAME} function already set");
|
||||||
|
}
|
||||||
|
|
||||||
|
let index_uids: Vec<_> = index_scheduler
|
||||||
|
.index_names()?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|index_uid| filters.is_index_authorized(&index_uid))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let tool = ChatCompletionToolArgs::default()
|
||||||
|
.r#type(ChatCompletionToolType::Function)
|
||||||
|
.function(
|
||||||
|
FunctionObjectArgs::default()
|
||||||
|
.name(SEARCH_IN_INDEX_FUNCTION_NAME)
|
||||||
|
.description(&prompts.search_description)
|
||||||
|
.parameters(json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"index_uid": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": index_uids,
|
||||||
|
"description": prompts.search_index_uid_param,
|
||||||
|
},
|
||||||
|
"q": {
|
||||||
|
// Unfortunately, Mistral does not support an array of types, here.
|
||||||
|
// "type": ["string", "null"],
|
||||||
|
"type": "string",
|
||||||
|
"description": prompts.search_q_param,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["index_uid", "q"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
}))
|
||||||
|
.strict(true)
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
tools.push(tool);
|
||||||
|
chat_completion.messages.insert(
|
||||||
|
0,
|
||||||
|
ChatCompletionRequestMessage::System(ChatCompletionRequestSystemMessage {
|
||||||
|
content: ChatCompletionRequestSystemMessageContent::Text(prompts.system.clone()),
|
||||||
|
name: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process search request and return formatted results
|
||||||
|
async fn process_search_request(
|
||||||
|
index_scheduler: &GuardedData<ActionPolicy<{ actions::CHAT }>, Data<IndexScheduler>>,
|
||||||
|
auth_ctrl: web::Data<AuthController>,
|
||||||
|
search_queue: &web::Data<SearchQueue>,
|
||||||
|
auth_token: &str,
|
||||||
|
index_uid: String,
|
||||||
|
q: Option<String>,
|
||||||
|
) -> Result<(Index, String), ResponseError> {
|
||||||
|
let mut query = SearchQuery {
|
||||||
|
q,
|
||||||
|
hybrid: Some(HybridQuery {
|
||||||
|
semantic_ratio: SemanticRatio::default(),
|
||||||
|
embedder: EMBEDDER_NAME.to_string(),
|
||||||
|
}),
|
||||||
|
limit: 20,
|
||||||
|
matching_strategy: MatchingStrategy::Frequency,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let auth_filter = ActionPolicy::<{ actions::SEARCH }>::authenticate(
|
||||||
|
auth_ctrl,
|
||||||
|
auth_token,
|
||||||
|
Some(index_uid.as_str()),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Tenant token search_rules.
|
||||||
|
if let Some(search_rules) = auth_filter.get_index_search_rules(&index_uid) {
|
||||||
|
add_search_rules(&mut query.filter, search_rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TBD
|
||||||
|
// let mut aggregate = SearchAggregator::<SearchPOST>::from_query(&query);
|
||||||
|
|
||||||
|
let index = index_scheduler.index(&index_uid)?;
|
||||||
|
let search_kind =
|
||||||
|
search_kind(&query, index_scheduler.get_ref(), index_uid.to_string(), &index)?;
|
||||||
|
|
||||||
|
let permit = search_queue.try_get_search_permit().await?;
|
||||||
|
let features = index_scheduler.features();
|
||||||
|
let index_cloned = index.clone();
|
||||||
|
let search_result = tokio::task::spawn_blocking(move || -> Result<_, ResponseError> {
|
||||||
|
let rtxn = index_cloned.read_txn()?;
|
||||||
|
let time_budget = match index_cloned
|
||||||
|
.search_cutoff(&rtxn)
|
||||||
|
.map_err(|e| MeilisearchHttpError::from_milli(e, Some(index_uid.clone())))?
|
||||||
|
{
|
||||||
|
Some(cutoff) => TimeBudget::new(Duration::from_millis(cutoff)),
|
||||||
|
None => TimeBudget::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (search, _is_finite_pagination, _max_total_hits, _offset) =
|
||||||
|
prepare_search(&index_cloned, &rtxn, &query, &search_kind, time_budget, features)?;
|
||||||
|
|
||||||
|
search_from_kind(index_uid, search_kind, search)
|
||||||
|
.map(|(search_results, _)| search_results)
|
||||||
|
.map_err(ResponseError::from)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
permit.drop().await;
|
||||||
|
|
||||||
|
let search_result = search_result?;
|
||||||
|
if let Ok(ref search_result) = search_result {
|
||||||
|
// aggregate.succeed(search_result);
|
||||||
|
if search_result.degraded {
|
||||||
|
MEILISEARCH_DEGRADED_SEARCH_REQUESTS.inc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// analytics.publish(aggregate, &req);
|
||||||
|
|
||||||
|
let search_result = search_result?;
|
||||||
|
let rtxn = index.read_txn()?;
|
||||||
|
let render_alloc = Bump::new();
|
||||||
|
let formatted = format_documents(&rtxn, &index, &render_alloc, search_result.documents_ids)?;
|
||||||
|
let text = formatted.join("\n");
|
||||||
|
drop(rtxn);
|
||||||
|
|
||||||
|
Ok((index, text))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn non_streamed_chat(
|
||||||
|
index_scheduler: GuardedData<ActionPolicy<{ actions::CHAT }>, Data<IndexScheduler>>,
|
||||||
|
auth_ctrl: web::Data<AuthController>,
|
||||||
|
req: HttpRequest,
|
||||||
|
search_queue: web::Data<SearchQueue>,
|
||||||
|
mut chat_completion: CreateChatCompletionRequest,
|
||||||
|
) -> Result<HttpResponse, ResponseError> {
|
||||||
|
let filters = index_scheduler.filters();
|
||||||
|
|
||||||
|
let chat_settings = match index_scheduler.chat_settings().unwrap() {
|
||||||
|
Some(value) => serde_json::from_value(value).unwrap(),
|
||||||
|
None => GlobalChatSettings::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut config = OpenAIConfig::default();
|
||||||
|
if let Some(api_key) = chat_settings.api_key.as_ref() {
|
||||||
|
config = config.with_api_key(api_key);
|
||||||
|
}
|
||||||
|
if let Some(base_api) = chat_settings.base_api.as_ref() {
|
||||||
|
config = config.with_api_base(base_api);
|
||||||
|
}
|
||||||
|
let client = Client::with_config(config);
|
||||||
|
|
||||||
|
let auth_token = extract_token_from_request(&req)?.unwrap();
|
||||||
|
setup_search_tool(&index_scheduler, filters, &mut chat_completion, &chat_settings.prompts)?;
|
||||||
|
|
||||||
|
let mut response;
|
||||||
|
loop {
|
||||||
|
response = client.chat().create(chat_completion.clone()).await.unwrap();
|
||||||
|
|
||||||
|
let choice = &mut response.choices[0];
|
||||||
|
match choice.finish_reason {
|
||||||
|
Some(FinishReason::ToolCalls) => {
|
||||||
|
let tool_calls = mem::take(&mut choice.message.tool_calls).unwrap_or_default();
|
||||||
|
|
||||||
|
let (meili_calls, other_calls): (Vec<_>, Vec<_>) = tool_calls
|
||||||
|
.into_iter()
|
||||||
|
.partition(|call| call.function.name == SEARCH_IN_INDEX_FUNCTION_NAME);
|
||||||
|
|
||||||
|
chat_completion.messages.push(
|
||||||
|
ChatCompletionRequestAssistantMessageArgs::default()
|
||||||
|
.tool_calls(meili_calls.clone())
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for call in meili_calls {
|
||||||
|
let result = match serde_json::from_str(&call.function.arguments) {
|
||||||
|
Ok(SearchInIndexParameters { index_uid, q }) => process_search_request(
|
||||||
|
&index_scheduler,
|
||||||
|
auth_ctrl.clone(),
|
||||||
|
&search_queue,
|
||||||
|
&auth_token,
|
||||||
|
index_uid,
|
||||||
|
q,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string()),
|
||||||
|
Err(err) => Err(err.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = match result {
|
||||||
|
Ok((_, text)) => text,
|
||||||
|
Err(err) => err,
|
||||||
|
};
|
||||||
|
|
||||||
|
chat_completion.messages.push(ChatCompletionRequestMessage::Tool(
|
||||||
|
ChatCompletionRequestToolMessage {
|
||||||
|
tool_call_id: call.id.clone(),
|
||||||
|
content: ChatCompletionRequestToolMessageContent::Text(format!(
|
||||||
|
"{}\n\n{text}",
|
||||||
|
chat_settings.prompts.pre_query
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let the client call other tools by themselves
|
||||||
|
if !other_calls.is_empty() {
|
||||||
|
response.choices[0].message.tool_calls = Some(other_calls);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn streamed_chat(
|
||||||
|
index_scheduler: GuardedData<ActionPolicy<{ actions::CHAT }>, Data<IndexScheduler>>,
|
||||||
|
auth_ctrl: web::Data<AuthController>,
|
||||||
|
req: HttpRequest,
|
||||||
|
search_queue: web::Data<SearchQueue>,
|
||||||
|
mut chat_completion: CreateChatCompletionRequest,
|
||||||
|
) -> Result<impl Responder, ResponseError> {
|
||||||
|
let filters = index_scheduler.filters();
|
||||||
|
|
||||||
|
let chat_settings = match index_scheduler.chat_settings().unwrap() {
|
||||||
|
Some(value) => serde_json::from_value(value).unwrap(),
|
||||||
|
None => GlobalChatSettings::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut config = OpenAIConfig::default();
|
||||||
|
if let Some(api_key) = chat_settings.api_key.as_ref() {
|
||||||
|
config = config.with_api_key(api_key);
|
||||||
|
}
|
||||||
|
if let Some(base_api) = chat_settings.base_api.as_ref() {
|
||||||
|
config = config.with_api_base(base_api);
|
||||||
|
}
|
||||||
|
|
||||||
|
let auth_token = extract_token_from_request(&req)?.unwrap().to_string();
|
||||||
|
setup_search_tool(&index_scheduler, filters, &mut chat_completion, &chat_settings.prompts)?;
|
||||||
|
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::channel(10);
|
||||||
|
let _join_handle = Handle::current().spawn(async move {
|
||||||
|
let client = Client::with_config(config.clone());
|
||||||
|
let mut global_tool_calls = HashMap::<u32, Call>::new();
|
||||||
|
let mut finish_reason = None;
|
||||||
|
|
||||||
|
// Limit the number of internal calls to satisfy the search requests of the LLM
|
||||||
|
'main: for _ in 0..20 {
|
||||||
|
let mut response = client.chat().create_stream(chat_completion.clone()).await.unwrap();
|
||||||
|
while let Some(result) = response.next().await {
|
||||||
|
match result {
|
||||||
|
Ok(resp) => {
|
||||||
|
let choice = &resp.choices[0];
|
||||||
|
finish_reason = choice.finish_reason;
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
|
let ChatCompletionStreamResponseDelta {
|
||||||
|
content,
|
||||||
|
// Using deprecated field but keeping for compatibility
|
||||||
|
function_call: _,
|
||||||
|
ref tool_calls,
|
||||||
|
role: _,
|
||||||
|
refusal: _,
|
||||||
|
} = &choice.delta;
|
||||||
|
|
||||||
|
if content.is_some() {
|
||||||
|
if let Err(SendError(_)) = tx.send(Event::Data(sse::Data::new_json(&resp).unwrap())).await {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match tool_calls {
|
||||||
|
Some(tool_calls) => {
|
||||||
|
for chunk in tool_calls {
|
||||||
|
let ChatCompletionMessageToolCallChunk {
|
||||||
|
index,
|
||||||
|
id,
|
||||||
|
r#type: _,
|
||||||
|
function,
|
||||||
|
} = chunk;
|
||||||
|
let FunctionCallStream { name, arguments } =
|
||||||
|
function.as_ref().unwrap();
|
||||||
|
global_tool_calls
|
||||||
|
.entry(*index)
|
||||||
|
.and_modify(|call| call.append(arguments.as_ref().unwrap()))
|
||||||
|
.or_insert_with(|| Call {
|
||||||
|
id: id.as_ref().unwrap().clone(),
|
||||||
|
function_name: name.as_ref().unwrap().clone(),
|
||||||
|
arguments: arguments.as_ref().unwrap().clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None if !global_tool_calls.is_empty() => {
|
||||||
|
let (meili_calls, _other_calls): (Vec<_>, Vec<_>) =
|
||||||
|
mem::take(&mut global_tool_calls)
|
||||||
|
.into_values()
|
||||||
|
.map(|call| ChatCompletionMessageToolCall {
|
||||||
|
id: call.id,
|
||||||
|
r#type: Some(ChatCompletionToolType::Function),
|
||||||
|
function: FunctionCall {
|
||||||
|
name: call.function_name,
|
||||||
|
arguments: call.arguments,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.partition(|call| call.function.name == SEARCH_IN_INDEX_FUNCTION_NAME);
|
||||||
|
|
||||||
|
chat_completion.messages.push(
|
||||||
|
ChatCompletionRequestAssistantMessageArgs::default()
|
||||||
|
.tool_calls(meili_calls.clone())
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for call in meili_calls {
|
||||||
|
if let Err(SendError(_)) = tx.send(Event::Data(
|
||||||
|
sse::Data::new_json(json!({
|
||||||
|
"object": "chat.completion.tool.call",
|
||||||
|
"tool": call,
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
))
|
||||||
|
.await {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = match serde_json::from_str(&call.function.arguments) {
|
||||||
|
Ok(SearchInIndexParameters { index_uid, q }) => process_search_request(
|
||||||
|
&index_scheduler,
|
||||||
|
auth_ctrl.clone(),
|
||||||
|
&search_queue,
|
||||||
|
&auth_token,
|
||||||
|
index_uid,
|
||||||
|
q,
|
||||||
|
).await.map_err(|e| e.to_string()),
|
||||||
|
Err(err) => Err(err.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_error = result.is_err();
|
||||||
|
let text = match result {
|
||||||
|
Ok((_, text)) => text,
|
||||||
|
Err(err) => err,
|
||||||
|
};
|
||||||
|
|
||||||
|
let tool = ChatCompletionRequestToolMessage {
|
||||||
|
tool_call_id: call.id.clone(),
|
||||||
|
content: ChatCompletionRequestToolMessageContent::Text(
|
||||||
|
format!("{}\n\n{text}", chat_settings.prompts.pre_query),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(SendError(_)) = tx.send(Event::Data(
|
||||||
|
sse::Data::new_json(json!({
|
||||||
|
"object": if is_error {
|
||||||
|
"chat.completion.tool.error"
|
||||||
|
} else {
|
||||||
|
"chat.completion.tool.output"
|
||||||
|
},
|
||||||
|
"tool": ChatCompletionRequestToolMessage {
|
||||||
|
tool_call_id: call.id,
|
||||||
|
content: ChatCompletionRequestToolMessageContent::Text(
|
||||||
|
text,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
))
|
||||||
|
.await {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chat_completion.messages.push(ChatCompletionRequestMessage::Tool(tool));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("{err:?}");
|
||||||
|
if let Err(SendError(_)) = tx.send(Event::Data(sse::Data::new_json(&json!({
|
||||||
|
"object": "chat.completion.error",
|
||||||
|
"tool": err.to_string(),
|
||||||
|
})).unwrap())).await {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
break 'main;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We must stop if the finish reason is not something we can solve with Meilisearch
|
||||||
|
if finish_reason.map_or(true, |fr| fr != FinishReason::ToolCalls) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = tx.send(Event::Data(sse::Data::new("[DONE]")));
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Sse::from_infallible_receiver(rx).with_retry_duration(Duration::from_secs(10)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The structure used to aggregate the function calls to make.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Call {
|
||||||
|
id: String,
|
||||||
|
function_name: String,
|
||||||
|
arguments: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Call {
|
||||||
|
fn append(&mut self, arguments: &str) {
|
||||||
|
self.arguments.push_str(arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct SearchInIndexParameters {
|
||||||
|
/// The index uid to search in.
|
||||||
|
index_uid: String,
|
||||||
|
/// The query parameter to use.
|
||||||
|
q: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_documents<'t, 'doc>(
|
||||||
|
rtxn: &RoTxn<'t>,
|
||||||
|
index: &Index,
|
||||||
|
doc_alloc: &'doc Bump,
|
||||||
|
internal_docids: Vec<DocumentId>,
|
||||||
|
) -> Result<Vec<&'doc str>, ResponseError> {
|
||||||
|
let ChatConfig { prompt: PromptData { template, max_bytes }, .. } = index.chat_config(rtxn)?;
|
||||||
|
|
||||||
|
let prompt = Prompt::new(template, max_bytes).unwrap();
|
||||||
|
let fid_map = index.fields_ids_map(rtxn)?;
|
||||||
|
let metadata_builder = MetadataBuilder::from_index(index, rtxn)?;
|
||||||
|
let fid_map_with_meta = FieldIdMapWithMetadata::new(fid_map.clone(), metadata_builder);
|
||||||
|
let global = RwLock::new(fid_map_with_meta);
|
||||||
|
let gfid_map = RefCell::new(GlobalFieldsIdsMap::new(&global));
|
||||||
|
|
||||||
|
let external_ids: Vec<String> = index
|
||||||
|
.external_id_of(rtxn, internal_docids.iter().copied())?
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
|
let mut renders = Vec::new();
|
||||||
|
for (docid, external_docid) in internal_docids.into_iter().zip(external_ids) {
|
||||||
|
let document = match DocumentFromDb::new(docid, rtxn, index, &fid_map)? {
|
||||||
|
Some(doc) => doc,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = prompt.render_document(&external_docid, document, &gfid_map, doc_alloc).unwrap();
|
||||||
|
renders.push(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(renders)
|
||||||
|
}
|
@ -6,7 +6,7 @@ use meilisearch_types::deserr::DeserrJsonError;
|
|||||||
use meilisearch_types::error::ResponseError;
|
use meilisearch_types::error::ResponseError;
|
||||||
use meilisearch_types::index_uid::IndexUid;
|
use meilisearch_types::index_uid::IndexUid;
|
||||||
use meilisearch_types::settings::{
|
use meilisearch_types::settings::{
|
||||||
settings, SecretPolicy, SettingEmbeddingSettings, Settings, Unchecked,
|
settings, ChatSettings, SecretPolicy, SettingEmbeddingSettings, Settings, Unchecked,
|
||||||
};
|
};
|
||||||
use meilisearch_types::tasks::KindWithContent;
|
use meilisearch_types::tasks::KindWithContent;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
@ -508,6 +508,17 @@ make_setting_routes!(
|
|||||||
camelcase_attr: "prefixSearch",
|
camelcase_attr: "prefixSearch",
|
||||||
analytics: PrefixSearchAnalytics
|
analytics: PrefixSearchAnalytics
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
route: "/chat",
|
||||||
|
update_verb: put,
|
||||||
|
value_type: ChatSettings,
|
||||||
|
err_type: meilisearch_types::deserr::DeserrJsonError<
|
||||||
|
meilisearch_types::error::deserr_codes::InvalidSettingsIndexChat,
|
||||||
|
>,
|
||||||
|
attr: chat,
|
||||||
|
camelcase_attr: "chat",
|
||||||
|
analytics: ChatAnalytics
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@ -597,6 +608,7 @@ pub async fn update_all(
|
|||||||
),
|
),
|
||||||
facet_search: FacetSearchAnalytics::new(new_settings.facet_search.as_ref().set()),
|
facet_search: FacetSearchAnalytics::new(new_settings.facet_search.as_ref().set()),
|
||||||
prefix_search: PrefixSearchAnalytics::new(new_settings.prefix_search.as_ref().set()),
|
prefix_search: PrefixSearchAnalytics::new(new_settings.prefix_search.as_ref().set()),
|
||||||
|
chat: ChatAnalytics::new(new_settings.chat.as_ref().set()),
|
||||||
},
|
},
|
||||||
&req,
|
&req,
|
||||||
);
|
);
|
||||||
|
@ -10,8 +10,8 @@ use meilisearch_types::locales::{Locale, LocalizedAttributesRuleView};
|
|||||||
use meilisearch_types::milli::update::Setting;
|
use meilisearch_types::milli::update::Setting;
|
||||||
use meilisearch_types::milli::FilterableAttributesRule;
|
use meilisearch_types::milli::FilterableAttributesRule;
|
||||||
use meilisearch_types::settings::{
|
use meilisearch_types::settings::{
|
||||||
FacetingSettings, PaginationSettings, PrefixSearchSettings, ProximityPrecisionView,
|
ChatSettings, FacetingSettings, PaginationSettings, PrefixSearchSettings,
|
||||||
RankingRuleView, SettingEmbeddingSettings, TypoSettings,
|
ProximityPrecisionView, RankingRuleView, SettingEmbeddingSettings, TypoSettings,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
@ -39,6 +39,7 @@ pub struct SettingsAnalytics {
|
|||||||
pub non_separator_tokens: NonSeparatorTokensAnalytics,
|
pub non_separator_tokens: NonSeparatorTokensAnalytics,
|
||||||
pub facet_search: FacetSearchAnalytics,
|
pub facet_search: FacetSearchAnalytics,
|
||||||
pub prefix_search: PrefixSearchAnalytics,
|
pub prefix_search: PrefixSearchAnalytics,
|
||||||
|
pub chat: ChatAnalytics,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Aggregate for SettingsAnalytics {
|
impl Aggregate for SettingsAnalytics {
|
||||||
@ -198,6 +199,7 @@ impl Aggregate for SettingsAnalytics {
|
|||||||
set: new.prefix_search.set | self.prefix_search.set,
|
set: new.prefix_search.set | self.prefix_search.set,
|
||||||
value: new.prefix_search.value.or(self.prefix_search.value),
|
value: new.prefix_search.value.or(self.prefix_search.value),
|
||||||
},
|
},
|
||||||
|
chat: ChatAnalytics { set: new.chat.set | self.chat.set },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -674,3 +676,18 @@ impl PrefixSearchAnalytics {
|
|||||||
SettingsAnalytics { prefix_search: self, ..Default::default() }
|
SettingsAnalytics { prefix_search: self, ..Default::default() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Default)]
|
||||||
|
pub struct ChatAnalytics {
|
||||||
|
pub set: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChatAnalytics {
|
||||||
|
pub fn new(settings: Option<&ChatSettings>) -> Self {
|
||||||
|
Self { set: settings.is_some() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_settings(self) -> SettingsAnalytics {
|
||||||
|
SettingsAnalytics { chat: self, ..Default::default() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -52,6 +52,7 @@ const PAGINATION_DEFAULT_LIMIT_FN: fn() -> usize = || 20;
|
|||||||
|
|
||||||
mod api_key;
|
mod api_key;
|
||||||
pub mod batches;
|
pub mod batches;
|
||||||
|
pub mod chat;
|
||||||
mod dump;
|
mod dump;
|
||||||
pub mod features;
|
pub mod features;
|
||||||
pub mod indexes;
|
pub mod indexes;
|
||||||
@ -61,6 +62,7 @@ mod multi_search;
|
|||||||
mod multi_search_analytics;
|
mod multi_search_analytics;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
mod open_api_utils;
|
mod open_api_utils;
|
||||||
|
pub mod settings;
|
||||||
mod snapshot;
|
mod snapshot;
|
||||||
mod swap_indexes;
|
mod swap_indexes;
|
||||||
pub mod tasks;
|
pub mod tasks;
|
||||||
@ -113,7 +115,9 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||||||
.service(web::scope("/swap-indexes").configure(swap_indexes::configure))
|
.service(web::scope("/swap-indexes").configure(swap_indexes::configure))
|
||||||
.service(web::scope("/metrics").configure(metrics::configure))
|
.service(web::scope("/metrics").configure(metrics::configure))
|
||||||
.service(web::scope("/experimental-features").configure(features::configure))
|
.service(web::scope("/experimental-features").configure(features::configure))
|
||||||
.service(web::scope("/network").configure(network::configure));
|
.service(web::scope("/network").configure(network::configure))
|
||||||
|
.service(web::scope("/chat").configure(chat::configure))
|
||||||
|
.service(web::scope("/settings/chat").configure(settings::chat::configure));
|
||||||
|
|
||||||
#[cfg(feature = "swagger")]
|
#[cfg(feature = "swagger")]
|
||||||
{
|
{
|
||||||
|
107
crates/meilisearch/src/routes/settings/chat.rs
Normal file
107
crates/meilisearch/src/routes/settings/chat.rs
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
use actix_web::web::{self, Data};
|
||||||
|
use actix_web::HttpResponse;
|
||||||
|
use index_scheduler::IndexScheduler;
|
||||||
|
use meilisearch_types::error::ResponseError;
|
||||||
|
use meilisearch_types::keys::actions;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::extractors::authentication::policies::ActionPolicy;
|
||||||
|
use crate::extractors::authentication::GuardedData;
|
||||||
|
use crate::extractors::sequential_extractor::SeqHandler;
|
||||||
|
|
||||||
|
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(
|
||||||
|
web::resource("")
|
||||||
|
.route(web::get().to(get_settings))
|
||||||
|
.route(web::patch().to(SeqHandler(patch_settings))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_settings(
|
||||||
|
index_scheduler: GuardedData<
|
||||||
|
ActionPolicy<{ actions::CHAT_SETTINGS_GET }>,
|
||||||
|
Data<IndexScheduler>,
|
||||||
|
>,
|
||||||
|
) -> Result<HttpResponse, ResponseError> {
|
||||||
|
let settings = match index_scheduler.chat_settings()? {
|
||||||
|
Some(value) => serde_json::from_value(value).unwrap(),
|
||||||
|
None => GlobalChatSettings::default(),
|
||||||
|
};
|
||||||
|
Ok(HttpResponse::Ok().json(settings))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn patch_settings(
|
||||||
|
index_scheduler: GuardedData<
|
||||||
|
ActionPolicy<{ actions::CHAT_SETTINGS_UPDATE }>,
|
||||||
|
Data<IndexScheduler>,
|
||||||
|
>,
|
||||||
|
web::Json(chat_settings): web::Json<GlobalChatSettings>,
|
||||||
|
) -> Result<HttpResponse, ResponseError> {
|
||||||
|
let chat_settings = serde_json::to_value(chat_settings).unwrap();
|
||||||
|
index_scheduler.put_chat_settings(&chat_settings)?;
|
||||||
|
Ok(HttpResponse::Ok().finish())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
|
pub struct GlobalChatSettings {
|
||||||
|
pub source: String,
|
||||||
|
pub base_api: Option<String>,
|
||||||
|
pub api_key: Option<String>,
|
||||||
|
pub prompts: ChatPrompts,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
|
pub struct ChatPrompts {
|
||||||
|
pub system: String,
|
||||||
|
pub search_description: String,
|
||||||
|
pub search_q_param: String,
|
||||||
|
pub search_index_uid_param: String,
|
||||||
|
pub pre_query: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
|
pub struct ChatIndexSettings {
|
||||||
|
pub description: String,
|
||||||
|
pub document_template: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_SYSTEM_MESSAGE: &str = "You are a highly capable research assistant with access to powerful search tools. IMPORTANT INSTRUCTIONS:\
|
||||||
|
1. When answering questions, you MUST make multiple tool calls (at least 2-3) to gather comprehensive information.\
|
||||||
|
2. Use different search queries for each tool call - vary keywords, rephrase questions, and explore different semantic angles to ensure broad coverage.\
|
||||||
|
3. Always explicitly announce BEFORE making each tool call by saying: \"I'll search for [specific information] now.\"\
|
||||||
|
4. Combine information from ALL tool calls to provide complete, nuanced answers rather than relying on a single source.\
|
||||||
|
5. For complex topics, break down your research into multiple targeted queries rather than using a single generic search.";
|
||||||
|
|
||||||
|
/// The default description of the searchInIndex tool provided to OpenAI.
|
||||||
|
const DEFAULT_SEARCH_IN_INDEX_TOOL_DESCRIPTION: &str =
|
||||||
|
"Search the database for relevant JSON documents using an optional query.";
|
||||||
|
/// The default description of the searchInIndex `q` parameter tool provided to OpenAI.
|
||||||
|
const DEFAULT_SEARCH_IN_INDEX_Q_PARAMETER_TOOL_DESCRIPTION: &str =
|
||||||
|
"The search query string used to find relevant documents in the index. \
|
||||||
|
This should contain keywords or phrases that best represent what the user is looking for. \
|
||||||
|
More specific queries will yield more precise results.";
|
||||||
|
/// The default description of the searchInIndex `index` parameter tool provided to OpenAI.
|
||||||
|
const DEFAULT_SEARCH_IN_INDEX_INDEX_PARAMETER_TOOL_DESCRIPTION: &str =
|
||||||
|
"The name of the index to search within. An index is a collection of documents organized for search. \
|
||||||
|
Selecting the right index ensures the most relevant results for the user query";
|
||||||
|
|
||||||
|
impl Default for GlobalChatSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
GlobalChatSettings {
|
||||||
|
source: "openAi".to_string(),
|
||||||
|
base_api: None,
|
||||||
|
api_key: None,
|
||||||
|
prompts: ChatPrompts {
|
||||||
|
system: DEFAULT_SYSTEM_MESSAGE.to_string(),
|
||||||
|
search_description: DEFAULT_SEARCH_IN_INDEX_TOOL_DESCRIPTION.to_string(),
|
||||||
|
search_q_param: DEFAULT_SEARCH_IN_INDEX_Q_PARAMETER_TOOL_DESCRIPTION.to_string(),
|
||||||
|
search_index_uid_param: DEFAULT_SEARCH_IN_INDEX_INDEX_PARAMETER_TOOL_DESCRIPTION
|
||||||
|
.to_string(),
|
||||||
|
pre_query: "".to_string(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
crates/meilisearch/src/routes/settings/mod.rs
Normal file
1
crates/meilisearch/src/routes/settings/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod chat;
|
@ -882,7 +882,7 @@ pub fn add_search_rules(filter: &mut Option<Value>, rules: IndexSearchRules) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_search<'t>(
|
pub fn prepare_search<'t>(
|
||||||
index: &'t Index,
|
index: &'t Index,
|
||||||
rtxn: &'t RoTxn,
|
rtxn: &'t RoTxn,
|
||||||
query: &'t SearchQuery,
|
query: &'t SearchQuery,
|
||||||
|
@ -820,6 +820,22 @@ async fn list_api_keys() {
|
|||||||
"createdAt": "[ignored]",
|
"createdAt": "[ignored]",
|
||||||
"updatedAt": "[ignored]"
|
"updatedAt": "[ignored]"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Default Chat API Key",
|
||||||
|
"description": "Use it to chat and search from the frontend",
|
||||||
|
"key": "[ignored]",
|
||||||
|
"uid": "[ignored]",
|
||||||
|
"actions": [
|
||||||
|
"search",
|
||||||
|
"chat.get"
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"expiresAt": null,
|
||||||
|
"createdAt": "[ignored]",
|
||||||
|
"updatedAt": "[ignored]"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Default Search API Key",
|
"name": "Default Search API Key",
|
||||||
"description": "Use it to search from the frontend",
|
"description": "Use it to search from the frontend",
|
||||||
|
@ -32,13 +32,13 @@ impl ExternalDocumentsIds {
|
|||||||
&self,
|
&self,
|
||||||
rtxn: &RoTxn<'_>,
|
rtxn: &RoTxn<'_>,
|
||||||
external_id: A,
|
external_id: A,
|
||||||
) -> heed::Result<Option<u32>> {
|
) -> heed::Result<Option<DocumentId>> {
|
||||||
self.0.get(rtxn, external_id.as_ref())
|
self.0.get(rtxn, external_id.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An helper function to debug this type, returns an `HashMap` of both,
|
/// An helper function to debug this type, returns an `HashMap` of both,
|
||||||
/// soft and hard fst maps, combined.
|
/// soft and hard fst maps, combined.
|
||||||
pub fn to_hash_map(&self, rtxn: &RoTxn<'_>) -> heed::Result<HashMap<String, u32>> {
|
pub fn to_hash_map(&self, rtxn: &RoTxn<'_>) -> heed::Result<HashMap<String, DocumentId>> {
|
||||||
let mut map = HashMap::default();
|
let mut map = HashMap::default();
|
||||||
for result in self.0.iter(rtxn)? {
|
for result in self.0.iter(rtxn)? {
|
||||||
let (external, internal) = result?;
|
let (external, internal) = result?;
|
||||||
|
@ -7,6 +7,7 @@ use crate::FieldId;
|
|||||||
mod global;
|
mod global;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
pub use global::GlobalFieldsIdsMap;
|
pub use global::GlobalFieldsIdsMap;
|
||||||
|
pub use metadata::{FieldIdMapWithMetadata, MetadataBuilder};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct FieldsIdsMap {
|
pub struct FieldsIdsMap {
|
||||||
|
@ -23,6 +23,7 @@ use crate::heed_codec::facet::{
|
|||||||
use crate::heed_codec::version::VersionCodec;
|
use crate::heed_codec::version::VersionCodec;
|
||||||
use crate::heed_codec::{BEU16StrCodec, FstSetCodec, StrBEU16Codec, StrRefCodec};
|
use crate::heed_codec::{BEU16StrCodec, FstSetCodec, StrBEU16Codec, StrRefCodec};
|
||||||
use crate::order_by_map::OrderByMap;
|
use crate::order_by_map::OrderByMap;
|
||||||
|
use crate::prompt::PromptData;
|
||||||
use crate::proximity::ProximityPrecision;
|
use crate::proximity::ProximityPrecision;
|
||||||
use crate::vector::{ArroyStats, ArroyWrapper, Embedding, EmbeddingConfig};
|
use crate::vector::{ArroyStats, ArroyWrapper, Embedding, EmbeddingConfig};
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -79,6 +80,7 @@ pub mod main_key {
|
|||||||
pub const PREFIX_SEARCH: &str = "prefix_search";
|
pub const PREFIX_SEARCH: &str = "prefix_search";
|
||||||
pub const DOCUMENTS_STATS: &str = "documents_stats";
|
pub const DOCUMENTS_STATS: &str = "documents_stats";
|
||||||
pub const DISABLED_TYPOS_TERMS: &str = "disabled_typos_terms";
|
pub const DISABLED_TYPOS_TERMS: &str = "disabled_typos_terms";
|
||||||
|
pub const CHAT: &str = "chat";
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod db_name {
|
pub mod db_name {
|
||||||
@ -1691,6 +1693,25 @@ impl Index {
|
|||||||
self.main.remap_key_type::<Str>().delete(txn, main_key::FACET_SEARCH)
|
self.main.remap_key_type::<Str>().delete(txn, main_key::FACET_SEARCH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn chat_config(&self, txn: &RoTxn<'_>) -> heed::Result<ChatConfig> {
|
||||||
|
self.main
|
||||||
|
.remap_types::<Str, SerdeBincode<_>>()
|
||||||
|
.get(txn, main_key::CHAT)
|
||||||
|
.map(|o| o.unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn put_chat_config(
|
||||||
|
&self,
|
||||||
|
txn: &mut RwTxn<'_>,
|
||||||
|
val: &ChatConfig,
|
||||||
|
) -> heed::Result<()> {
|
||||||
|
self.main.remap_types::<Str, SerdeBincode<_>>().put(txn, main_key::CHAT, &val)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn delete_chat_config(&self, txn: &mut RwTxn<'_>) -> heed::Result<bool> {
|
||||||
|
self.main.remap_key_type::<Str>().delete(txn, main_key::CHAT)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn localized_attributes_rules(
|
pub fn localized_attributes_rules(
|
||||||
&self,
|
&self,
|
||||||
rtxn: &RoTxn<'_>,
|
rtxn: &RoTxn<'_>,
|
||||||
@ -1917,6 +1938,13 @@ pub struct IndexEmbeddingConfig {
|
|||||||
pub user_provided: RoaringBitmap,
|
pub user_provided: RoaringBitmap,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||||
|
pub struct ChatConfig {
|
||||||
|
pub description: String,
|
||||||
|
/// Contains the document template and max template length.
|
||||||
|
pub prompt: PromptData,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct PrefixSettings {
|
pub struct PrefixSettings {
|
||||||
pub prefix_count_threshold: usize,
|
pub prefix_count_threshold: usize,
|
||||||
|
@ -52,18 +52,19 @@ pub use search::new::{
|
|||||||
};
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
pub use thread_pool_no_abort::{PanicCatched, ThreadPoolNoAbort, ThreadPoolNoAbortBuilder};
|
pub use thread_pool_no_abort::{PanicCatched, ThreadPoolNoAbort, ThreadPoolNoAbortBuilder};
|
||||||
pub use {charabia as tokenizer, heed, rhai};
|
pub use {arroy, charabia as tokenizer, heed, rhai};
|
||||||
|
|
||||||
pub use self::asc_desc::{AscDesc, AscDescError, Member, SortError};
|
pub use self::asc_desc::{AscDesc, AscDescError, Member, SortError};
|
||||||
pub use self::attribute_patterns::AttributePatterns;
|
pub use self::attribute_patterns::{AttributePatterns, PatternMatch};
|
||||||
pub use self::attribute_patterns::PatternMatch;
|
|
||||||
pub use self::criterion::{default_criteria, Criterion, CriterionError};
|
pub use self::criterion::{default_criteria, Criterion, CriterionError};
|
||||||
pub use self::error::{
|
pub use self::error::{
|
||||||
Error, FieldIdMapMissingEntry, InternalError, SerializationError, UserError,
|
Error, FieldIdMapMissingEntry, InternalError, SerializationError, UserError,
|
||||||
};
|
};
|
||||||
pub use self::external_documents_ids::ExternalDocumentsIds;
|
pub use self::external_documents_ids::ExternalDocumentsIds;
|
||||||
pub use self::fieldids_weights_map::FieldidsWeightsMap;
|
pub use self::fieldids_weights_map::FieldidsWeightsMap;
|
||||||
pub use self::fields_ids_map::{FieldsIdsMap, GlobalFieldsIdsMap};
|
pub use self::fields_ids_map::{
|
||||||
|
FieldIdMapWithMetadata, FieldsIdsMap, GlobalFieldsIdsMap, MetadataBuilder,
|
||||||
|
};
|
||||||
pub use self::filterable_attributes_rules::{
|
pub use self::filterable_attributes_rules::{
|
||||||
FilterFeatures, FilterableAttributesFeatures, FilterableAttributesPatterns,
|
FilterFeatures, FilterableAttributesFeatures, FilterableAttributesPatterns,
|
||||||
FilterableAttributesRule,
|
FilterableAttributesRule,
|
||||||
@ -84,8 +85,6 @@ pub use self::search::{
|
|||||||
};
|
};
|
||||||
pub use self::update::ChannelCongestion;
|
pub use self::update::ChannelCongestion;
|
||||||
|
|
||||||
pub use arroy;
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, error::Error>;
|
pub type Result<T> = std::result::Result<T, error::Error>;
|
||||||
|
|
||||||
pub type Attribute = u32;
|
pub type Attribute = u32;
|
||||||
|
@ -105,10 +105,10 @@ impl Prompt {
|
|||||||
max_bytes,
|
max_bytes,
|
||||||
};
|
};
|
||||||
|
|
||||||
// render template with special object that's OK with `doc.*` and `fields.*`
|
// // render template with special object that's OK with `doc.*` and `fields.*`
|
||||||
this.template
|
// this.template
|
||||||
.render(&template_checker::TemplateChecker)
|
// .render(&template_checker::TemplateChecker)
|
||||||
.map_err(NewPromptError::invalid_fields_in_template)?;
|
// .map_err(NewPromptError::invalid_fields_in_template)?;
|
||||||
|
|
||||||
Ok(this)
|
Ok(this)
|
||||||
}
|
}
|
||||||
|
45
crates/milli/src/update/chat.rs
Normal file
45
crates/milli/src/update/chat.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
use deserr::Deserr;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use crate::index::ChatConfig;
|
||||||
|
use crate::prompt::{default_max_bytes, PromptData};
|
||||||
|
use crate::update::Setting;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)]
|
||||||
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
|
#[deserr(deny_unknown_fields, rename_all = camelCase)]
|
||||||
|
pub struct ChatSettings {
|
||||||
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
|
#[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.
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
|
#[deserr(default)]
|
||||||
|
#[schema(value_type = Option<String>)]
|
||||||
|
pub document_template: Setting<String>,
|
||||||
|
|
||||||
|
/// Rendered texts are truncated to this size. Defaults to 400.
|
||||||
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
|
#[deserr(default)]
|
||||||
|
#[schema(value_type = Option<usize>)]
|
||||||
|
pub document_template_max_bytes: Setting<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ChatConfig> for ChatSettings {
|
||||||
|
fn from(config: ChatConfig) -> Self {
|
||||||
|
let ChatConfig { description, prompt: PromptData { template, max_bytes } } = config;
|
||||||
|
ChatSettings {
|
||||||
|
description: Setting::Set(description),
|
||||||
|
document_template: Setting::Set(template),
|
||||||
|
document_template_max_bytes: Setting::Set(
|
||||||
|
max_bytes.unwrap_or(default_max_bytes()).get(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
pub use self::available_ids::AvailableIds;
|
pub use self::available_ids::AvailableIds;
|
||||||
|
pub use self::chat::ChatSettings;
|
||||||
pub use self::clear_documents::ClearDocuments;
|
pub use self::clear_documents::ClearDocuments;
|
||||||
pub use self::concurrent_available_ids::ConcurrentAvailableIds;
|
pub use self::concurrent_available_ids::ConcurrentAvailableIds;
|
||||||
pub use self::facet::bulk::FacetsUpdateBulk;
|
pub use self::facet::bulk::FacetsUpdateBulk;
|
||||||
@ -13,6 +14,7 @@ pub use self::words_prefix_integer_docids::WordPrefixIntegerDocids;
|
|||||||
pub use self::words_prefixes_fst::WordsPrefixesFst;
|
pub use self::words_prefixes_fst::WordsPrefixesFst;
|
||||||
|
|
||||||
mod available_ids;
|
mod available_ids;
|
||||||
|
mod chat;
|
||||||
mod clear_documents;
|
mod clear_documents;
|
||||||
mod concurrent_available_ids;
|
mod concurrent_available_ids;
|
||||||
pub(crate) mod del_add;
|
pub(crate) mod del_add;
|
||||||
|
@ -13,7 +13,7 @@ use time::OffsetDateTime;
|
|||||||
|
|
||||||
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::IndexerConfig;
|
use super::{ChatSettings, IndexerConfig};
|
||||||
use crate::attribute_patterns::PatternMatch;
|
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;
|
||||||
@ -22,11 +22,11 @@ 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::{
|
||||||
IndexEmbeddingConfig, PrefixSearch, DEFAULT_MIN_WORD_LEN_ONE_TYPO,
|
ChatConfig, IndexEmbeddingConfig, PrefixSearch, DEFAULT_MIN_WORD_LEN_ONE_TYPO,
|
||||||
DEFAULT_MIN_WORD_LEN_TWO_TYPOS,
|
DEFAULT_MIN_WORD_LEN_TWO_TYPOS,
|
||||||
};
|
};
|
||||||
use crate::order_by_map::OrderByMap;
|
use crate::order_by_map::OrderByMap;
|
||||||
use crate::prompt::default_max_bytes;
|
use crate::prompt::{default_max_bytes, PromptData};
|
||||||
use crate::proximity::ProximityPrecision;
|
use crate::proximity::ProximityPrecision;
|
||||||
use crate::update::index_documents::IndexDocumentsMethod;
|
use crate::update::index_documents::IndexDocumentsMethod;
|
||||||
use crate::update::{IndexDocuments, UpdateIndexingStep};
|
use crate::update::{IndexDocuments, UpdateIndexingStep};
|
||||||
@ -185,6 +185,7 @@ pub struct Settings<'a, 't, 'i> {
|
|||||||
localized_attributes_rules: Setting<Vec<LocalizedAttributesRule>>,
|
localized_attributes_rules: Setting<Vec<LocalizedAttributesRule>>,
|
||||||
prefix_search: Setting<PrefixSearch>,
|
prefix_search: Setting<PrefixSearch>,
|
||||||
facet_search: Setting<bool>,
|
facet_search: Setting<bool>,
|
||||||
|
chat: Setting<ChatSettings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 't, 'i> Settings<'a, 't, 'i> {
|
impl<'a, 't, 'i> Settings<'a, 't, 'i> {
|
||||||
@ -223,6 +224,7 @@ impl<'a, 't, 'i> Settings<'a, 't, 'i> {
|
|||||||
localized_attributes_rules: Setting::NotSet,
|
localized_attributes_rules: Setting::NotSet,
|
||||||
prefix_search: Setting::NotSet,
|
prefix_search: Setting::NotSet,
|
||||||
facet_search: Setting::NotSet,
|
facet_search: Setting::NotSet,
|
||||||
|
chat: Setting::NotSet,
|
||||||
indexer_config,
|
indexer_config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -453,6 +455,14 @@ impl<'a, 't, 'i> Settings<'a, 't, 'i> {
|
|||||||
self.facet_search = Setting::Reset;
|
self.facet_search = Setting::Reset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_chat(&mut self, value: ChatSettings) {
|
||||||
|
self.chat = Setting::Set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_chat(&mut self) {
|
||||||
|
self.chat = Setting::Reset;
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(
|
#[tracing::instrument(
|
||||||
level = "trace"
|
level = "trace"
|
||||||
skip(self, progress_callback, should_abort, settings_diff),
|
skip(self, progress_callback, should_abort, settings_diff),
|
||||||
@ -1239,6 +1249,45 @@ impl<'a, 't, 'i> Settings<'a, 't, 'i> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_chat_config(&mut self) -> heed::Result<bool> {
|
||||||
|
match &mut self.chat {
|
||||||
|
Setting::Set(ChatSettings {
|
||||||
|
description: new_description,
|
||||||
|
document_template: new_document_template,
|
||||||
|
document_template_max_bytes: new_document_template_max_bytes,
|
||||||
|
}) => {
|
||||||
|
let mut old = self.index.chat_config(self.wtxn)?;
|
||||||
|
let ChatConfig {
|
||||||
|
ref mut description,
|
||||||
|
prompt: PromptData { ref mut template, ref mut max_bytes },
|
||||||
|
} = old;
|
||||||
|
|
||||||
|
match new_description {
|
||||||
|
Setting::Set(d) => *description = d.clone(),
|
||||||
|
Setting::Reset => *description = Default::default(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match new_document_template {
|
||||||
|
Setting::Set(dt) => *template = dt.clone(),
|
||||||
|
Setting::Reset => *template = Default::default(),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match new_document_template_max_bytes {
|
||||||
|
Setting::Set(m) => *max_bytes = NonZeroUsize::new(*m),
|
||||||
|
Setting::Reset => *max_bytes = Some(default_max_bytes()),
|
||||||
|
Setting::NotSet => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.index.put_chat_config(self.wtxn, &old)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
Setting::Reset => self.index.delete_chat_config(self.wtxn),
|
||||||
|
Setting::NotSet => Ok(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn execute<FP, FA>(mut self, progress_callback: FP, should_abort: FA) -> Result<()>
|
pub fn execute<FP, FA>(mut self, progress_callback: FP, should_abort: FA) -> Result<()>
|
||||||
where
|
where
|
||||||
FP: Fn(UpdateIndexingStep) + Sync,
|
FP: Fn(UpdateIndexingStep) + Sync,
|
||||||
@ -1276,6 +1325,7 @@ impl<'a, 't, 'i> Settings<'a, 't, 'i> {
|
|||||||
self.update_facet_search()?;
|
self.update_facet_search()?;
|
||||||
self.update_localized_attributes_rules()?;
|
self.update_localized_attributes_rules()?;
|
||||||
self.update_disabled_typos_terms()?;
|
self.update_disabled_typos_terms()?;
|
||||||
|
self.update_chat_config()?;
|
||||||
|
|
||||||
let embedding_config_updates = self.update_embedding_configs()?;
|
let embedding_config_updates = self.update_embedding_configs()?;
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ pub struct EmbeddingSettings {
|
|||||||
///
|
///
|
||||||
/// - Defaults to `openAi`
|
/// - Defaults to `openAi`
|
||||||
pub source: Setting<EmbedderSource>,
|
pub source: Setting<EmbedderSource>,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default)]
|
#[deserr(default)]
|
||||||
#[schema(value_type = Option<String>)]
|
#[schema(value_type = Option<String>)]
|
||||||
@ -55,6 +56,7 @@ pub struct EmbeddingSettings {
|
|||||||
/// - For source `openAi`, defaults to `text-embedding-3-small`
|
/// - For source `openAi`, defaults to `text-embedding-3-small`
|
||||||
/// - For source `huggingFace`, defaults to `BAAI/bge-base-en-v1.5`
|
/// - For source `huggingFace`, defaults to `BAAI/bge-base-en-v1.5`
|
||||||
pub model: Setting<String>,
|
pub model: Setting<String>,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default)]
|
#[deserr(default)]
|
||||||
#[schema(value_type = Option<String>)]
|
#[schema(value_type = Option<String>)]
|
||||||
@ -75,6 +77,7 @@ pub struct EmbeddingSettings {
|
|||||||
/// - When `model` is set to default, defaults to `617ca489d9e86b49b8167676d8220688b99db36e`
|
/// - When `model` is set to default, defaults to `617ca489d9e86b49b8167676d8220688b99db36e`
|
||||||
/// - Otherwise, defaults to `null`
|
/// - Otherwise, defaults to `null`
|
||||||
pub revision: Setting<String>,
|
pub revision: Setting<String>,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default)]
|
#[deserr(default)]
|
||||||
#[schema(value_type = Option<OverridePooling>)]
|
#[schema(value_type = Option<OverridePooling>)]
|
||||||
@ -96,6 +99,7 @@ pub struct EmbeddingSettings {
|
|||||||
///
|
///
|
||||||
/// - Embedders created before this parameter was available default to `forceMean` to preserve the existing behavior.
|
/// - Embedders created before this parameter was available default to `forceMean` to preserve the existing behavior.
|
||||||
pub pooling: Setting<OverridePooling>,
|
pub pooling: Setting<OverridePooling>,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default)]
|
#[deserr(default)]
|
||||||
#[schema(value_type = Option<String>)]
|
#[schema(value_type = Option<String>)]
|
||||||
@ -118,6 +122,7 @@ pub struct EmbeddingSettings {
|
|||||||
///
|
///
|
||||||
/// - This setting is partially hidden when returned by the settings
|
/// - This setting is partially hidden when returned by the settings
|
||||||
pub api_key: Setting<String>,
|
pub api_key: Setting<String>,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default)]
|
#[deserr(default)]
|
||||||
#[schema(value_type = Option<String>)]
|
#[schema(value_type = Option<String>)]
|
||||||
@ -141,6 +146,7 @@ pub struct EmbeddingSettings {
|
|||||||
/// - For source `openAi`, the dimensions is the maximum allowed by the model.
|
/// - For source `openAi`, the dimensions is the maximum allowed by the model.
|
||||||
/// - For sources `ollama` and `rest`, the dimensions are inferred by embedding a sample text.
|
/// - For sources `ollama` and `rest`, the dimensions are inferred by embedding a sample text.
|
||||||
pub dimensions: Setting<usize>,
|
pub dimensions: Setting<usize>,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default)]
|
#[deserr(default)]
|
||||||
#[schema(value_type = Option<bool>)]
|
#[schema(value_type = Option<bool>)]
|
||||||
@ -167,6 +173,7 @@ pub struct EmbeddingSettings {
|
|||||||
/// first enabling it. If you are unsure of whether the performance-relevancy tradeoff is right for you,
|
/// first enabling it. If you are unsure of whether the performance-relevancy tradeoff is right for you,
|
||||||
/// we recommend to use this parameter on a test index first.
|
/// we recommend to use this parameter on a test index first.
|
||||||
pub binary_quantized: Setting<bool>,
|
pub binary_quantized: Setting<bool>,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default)]
|
#[deserr(default)]
|
||||||
#[schema(value_type = Option<bool>)]
|
#[schema(value_type = Option<bool>)]
|
||||||
@ -183,6 +190,7 @@ pub struct EmbeddingSettings {
|
|||||||
///
|
///
|
||||||
/// - ποΈ When modified, embeddings are regenerated for documents whose rendering through the template produces a different text.
|
/// - ποΈ When modified, embeddings are regenerated for documents whose rendering through the template produces a different text.
|
||||||
pub document_template: Setting<String>,
|
pub document_template: Setting<String>,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default)]
|
#[deserr(default)]
|
||||||
#[schema(value_type = Option<usize>)]
|
#[schema(value_type = Option<usize>)]
|
||||||
@ -201,6 +209,7 @@ pub struct EmbeddingSettings {
|
|||||||
///
|
///
|
||||||
/// - Defaults to 400
|
/// - Defaults to 400
|
||||||
pub document_template_max_bytes: Setting<usize>,
|
pub document_template_max_bytes: Setting<usize>,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default)]
|
#[deserr(default)]
|
||||||
#[schema(value_type = Option<String>)]
|
#[schema(value_type = Option<String>)]
|
||||||
@ -219,6 +228,7 @@ pub struct EmbeddingSettings {
|
|||||||
/// - π± When modified for source `openAi`, embeddings are never regenerated
|
/// - π± When modified for source `openAi`, embeddings are never regenerated
|
||||||
/// - ποΈ When modified for sources `ollama` and `rest`, embeddings are always regenerated
|
/// - ποΈ When modified for sources `ollama` and `rest`, embeddings are always regenerated
|
||||||
pub url: Setting<String>,
|
pub url: Setting<String>,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default)]
|
#[deserr(default)]
|
||||||
#[schema(value_type = Option<serde_json::Value>)]
|
#[schema(value_type = Option<serde_json::Value>)]
|
||||||
@ -236,6 +246,7 @@ pub struct EmbeddingSettings {
|
|||||||
///
|
///
|
||||||
/// - ποΈ Changing the value of this parameter always regenerates embeddings
|
/// - ποΈ Changing the value of this parameter always regenerates embeddings
|
||||||
pub request: Setting<serde_json::Value>,
|
pub request: Setting<serde_json::Value>,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default)]
|
#[deserr(default)]
|
||||||
#[schema(value_type = Option<serde_json::Value>)]
|
#[schema(value_type = Option<serde_json::Value>)]
|
||||||
@ -253,6 +264,7 @@ pub struct EmbeddingSettings {
|
|||||||
///
|
///
|
||||||
/// - ποΈ Changing the value of this parameter always regenerates embeddings
|
/// - ποΈ Changing the value of this parameter always regenerates embeddings
|
||||||
pub response: Setting<serde_json::Value>,
|
pub response: Setting<serde_json::Value>,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default)]
|
#[deserr(default)]
|
||||||
#[schema(value_type = Option<BTreeMap<String, String>>)]
|
#[schema(value_type = Option<BTreeMap<String, String>>)]
|
||||||
|
Reference in New Issue
Block a user