milli_core/vector/
settings.rs

1use std::collections::BTreeMap;
2use std::num::NonZeroUsize;
3
4use deserr::Deserr;
5use roaring::RoaringBitmap;
6use serde::{Deserialize, Serialize};
7use utoipa::ToSchema;
8
9use super::composite::SubEmbedderOptions;
10use super::hf::OverridePooling;
11use super::{ollama, openai, DistributionShift, EmbedderOptions};
12use crate::prompt::{default_max_bytes, PromptData};
13use crate::update::Setting;
14use crate::vector::EmbeddingConfig;
15use crate::UserError;
16
17#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)]
18#[serde(deny_unknown_fields, rename_all = "camelCase")]
19#[deserr(rename_all = camelCase, deny_unknown_fields)]
20pub struct EmbeddingSettings {
21    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
22    #[deserr(default)]
23    #[schema(value_type = Option<EmbedderSource>)]
24    /// The source used to provide the embeddings.
25    ///
26    /// Which embedder parameters are available and mandatory is determined by the value of this setting.
27    ///
28    /// # 🔄 Reindexing
29    ///
30    /// - 🏗️ Changing the value of this parameter always regenerates embeddings.
31    ///
32    /// # Defaults
33    ///
34    /// - Defaults to `openAi`
35    pub source: Setting<EmbedderSource>,
36    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
37    #[deserr(default)]
38    #[schema(value_type = Option<String>)]
39    /// The name of the model to use.
40    ///
41    /// # Mandatory
42    ///
43    /// - This parameter is mandatory for source `ollama`
44    ///
45    /// # Availability
46    ///
47    /// - This parameter is available for sources `openAi`, `huggingFace`, `ollama`
48    ///
49    /// # 🔄 Reindexing
50    ///
51    /// - 🏗️ Changing the value of this parameter always regenerates embeddings.
52    ///
53    /// # Defaults
54    ///
55    /// - For source `openAi`, defaults to `text-embedding-3-small`
56    /// - For source `huggingFace`, defaults to `BAAI/bge-base-en-v1.5`
57    pub model: Setting<String>,
58    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
59    #[deserr(default)]
60    #[schema(value_type = Option<String>)]
61    /// The revision (commit SHA1) of the model to use.
62    ///
63    /// If unspecified, Meilisearch picks the latest revision of the model.
64    ///
65    /// # Availability
66    ///
67    /// - This parameter is available for source `huggingFace`
68    ///
69    /// # 🔄 Reindexing
70    ///
71    /// - 🏗️ Changing the value of this parameter always regenerates embeddings
72    ///
73    /// # Defaults
74    ///
75    /// - When `model` is set to default, defaults to `617ca489d9e86b49b8167676d8220688b99db36e`
76    /// - Otherwise, defaults to `null`
77    pub revision: Setting<String>,
78    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
79    #[deserr(default)]
80    #[schema(value_type = Option<OverridePooling>)]
81    /// The pooling method to use.
82    ///
83    /// # Availability
84    ///
85    /// - This parameter is available for source `huggingFace`
86    ///
87    /// # 🔄 Reindexing
88    ///
89    /// - 🏗️ Changing the value of this parameter always regenerates embeddings
90    ///
91    /// # Defaults
92    ///
93    /// - Defaults to `useModel`
94    ///
95    /// # Compatibility Note
96    ///
97    /// - Embedders created before this parameter was available default to `forceMean` to preserve the existing behavior.
98    pub pooling: Setting<OverridePooling>,
99    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
100    #[deserr(default)]
101    #[schema(value_type = Option<String>)]
102    /// The API key to pass to the remote embedder while making requests.
103    ///
104    /// # Availability
105    ///
106    /// - This parameter is available for source `openAi`, `ollama`, `rest`
107    ///
108    /// # 🔄 Reindexing
109    ///
110    /// - 🌱 Changing the value of this parameter never regenerates embeddings
111    ///
112    /// # Defaults
113    ///
114    /// - For source `openAi`, the key is read from `OPENAI_API_KEY`, then `MEILI_OPENAI_API_KEY`.
115    /// - For other sources, no bearer token is sent if this parameter is not set.
116    ///
117    /// # Note
118    ///
119    /// - This setting is partially hidden when returned by the settings
120    pub api_key: Setting<String>,
121    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
122    #[deserr(default)]
123    #[schema(value_type = Option<String>)]
124    /// The expected dimensions of the embeddings produced by this embedder.
125    ///
126    /// # Mandatory
127    ///
128    /// - This parameter is mandatory for source `userProvided`
129    ///
130    /// # Availability
131    ///
132    /// - This parameter is available for source `openAi`, `ollama`, `rest`, `userProvided`
133    ///
134    /// # 🔄 Reindexing
135    ///
136    /// - 🏗️ When the source is `openAi`, changing the value of this parameter always regenerates embeddings
137    /// - 🌱 For other sources, changing the value of this parameter never regenerates embeddings
138    ///
139    /// # Defaults
140    ///
141    /// - For source `openAi`, the dimensions is the maximum allowed by the model.
142    /// - For sources `ollama` and `rest`, the dimensions are inferred by embedding a sample text.
143    pub dimensions: Setting<usize>,
144    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
145    #[deserr(default)]
146    #[schema(value_type = Option<bool>)]
147    /// Whether to binary quantize the embeddings of this embedder.
148    ///
149    /// Binary quantized embeddings are smaller than regular embeddings, which improves
150    /// disk usage and retrieval speed, at the cost of relevancy.
151    ///
152    /// # Availability
153    ///
154    /// - This parameter is available for all embedders
155    ///
156    /// # 🔄 Reindexing
157    ///
158    /// - 🏗️ When set to `true`, embeddings are not regenerated, but they are binary quantized, which takes time.
159    ///
160    /// # Defaults
161    ///
162    /// - Defaults to `false`
163    ///
164    /// # Note
165    ///
166    /// As binary quantization is a destructive operation, it is not possible to disable again this setting after
167    /// first enabling it. If you are unsure of whether the performance-relevancy tradeoff is right for you,
168    /// we recommend to use this parameter on a test index first.
169    pub binary_quantized: Setting<bool>,
170    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
171    #[deserr(default)]
172    #[schema(value_type = Option<bool>)]
173    /// A liquid template used to render documents to a text that can be embedded.
174    ///
175    /// Meillisearch interpolates the template for each document and sends the resulting text to the embedder.
176    /// The embedder then generates document vectors based on this text.
177    ///
178    /// # Availability
179    ///
180    /// - This parameter is available for source `openAi`, `huggingFace`, `ollama` and `rest
181    ///
182    /// # 🔄 Reindexing
183    ///
184    /// - 🏗️ When modified, embeddings are regenerated for documents whose rendering through the template produces a different text.
185    pub document_template: Setting<String>,
186    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
187    #[deserr(default)]
188    #[schema(value_type = Option<usize>)]
189    /// Rendered texts are truncated to this size.
190    ///
191    /// # Availability
192    ///
193    /// - This parameter is available for source `openAi`, `huggingFace`, `ollama` and `rest`
194    ///
195    /// # 🔄 Reindexing
196    ///
197    /// - 🏗️ When increased, embeddings are regenerated for documents whose rendering through the template produces a different text.
198    /// - 🌱 When decreased, embeddings are never regenerated
199    ///
200    /// # Default
201    ///
202    /// - Defaults to 400
203    pub document_template_max_bytes: Setting<usize>,
204    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
205    #[deserr(default)]
206    #[schema(value_type = Option<String>)]
207    /// URL to reach the remote embedder.
208    ///
209    /// # Mandatory
210    ///
211    /// - This parameter is mandatory for source `rest`
212    ///
213    /// # Availability
214    ///
215    /// - This parameter is available for source `openAi`, `ollama` and `rest`
216    ///
217    /// # 🔄 Reindexing
218    ///
219    /// - 🌱 When modified for source `openAi`, embeddings are never regenerated
220    /// - 🏗️ When modified for sources `ollama` and `rest`, embeddings are always regenerated
221    pub url: Setting<String>,
222    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
223    #[deserr(default)]
224    #[schema(value_type = Option<serde_json::Value>)]
225    /// Template request to send to the remote embedder.
226    ///
227    /// # Mandatory
228    ///
229    /// - This parameter is mandatory for source `rest`
230    ///
231    /// # Availability
232    ///
233    /// - This parameter is available for source `rest`
234    ///
235    /// # 🔄 Reindexing
236    ///
237    /// - 🏗️ Changing the value of this parameter always regenerates embeddings
238    pub request: Setting<serde_json::Value>,
239    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
240    #[deserr(default)]
241    #[schema(value_type = Option<serde_json::Value>)]
242    /// Template response indicating how to find the embeddings in the response from the remote embedder.
243    ///
244    /// # Mandatory
245    ///
246    /// - This parameter is mandatory for source `rest`
247    ///
248    /// # Availability
249    ///
250    /// - This parameter is available for source `rest`
251    ///
252    /// # 🔄 Reindexing
253    ///
254    /// - 🏗️ Changing the value of this parameter always regenerates embeddings
255    pub response: Setting<serde_json::Value>,
256    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
257    #[deserr(default)]
258    #[schema(value_type = Option<BTreeMap<String, String>>)]
259    /// Additional headers to send to the remote embedder.
260    ///
261    /// # Availability
262    ///
263    /// - This parameter is available for source `rest`
264    ///
265    /// # 🔄 Reindexing
266    ///
267    /// - 🌱 Changing the value of this parameter never regenerates embeddings
268    pub headers: Setting<BTreeMap<String, String>>,
269
270    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
271    #[deserr(default)]
272    #[schema(value_type = Option<SubEmbeddingSettings>)]
273    pub search_embedder: Setting<SubEmbeddingSettings>,
274
275    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
276    #[deserr(default)]
277    #[schema(value_type = Option<SubEmbeddingSettings>)]
278    pub indexing_embedder: Setting<SubEmbeddingSettings>,
279
280    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
281    #[deserr(default)]
282    #[schema(value_type = Option<DistributionShift>)]
283    /// Affine transformation applied to the semantic score to make it more comparable to the ranking score.
284    ///
285    /// # Availability
286    ///
287    /// - This parameter is available for all embedders
288    ///
289    /// # 🔄 Reindexing
290    ///
291    /// - 🌱 Changing the value of this parameter never regenerates embeddings
292    pub distribution: Setting<DistributionShift>,
293}
294
295#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)]
296#[serde(deny_unknown_fields, rename_all = "camelCase")]
297#[deserr(rename_all = camelCase, deny_unknown_fields)]
298pub struct SubEmbeddingSettings {
299    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
300    #[deserr(default)]
301    #[schema(value_type = Option<EmbedderSource>)]
302    /// The source used to provide the embeddings.
303    ///
304    /// Which embedder parameters are available and mandatory is determined by the value of this setting.
305    ///
306    /// # 🔄 Reindexing
307    ///
308    /// - 🏗️ Changing the value of this parameter always regenerates embeddings.
309    ///
310    /// # Defaults
311    ///
312    /// - Defaults to `openAi`
313    pub source: Setting<EmbedderSource>,
314    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
315    #[deserr(default)]
316    #[schema(value_type = Option<String>)]
317    /// The name of the model to use.
318    ///
319    /// # Mandatory
320    ///
321    /// - This parameter is mandatory for source `ollama`
322    ///
323    /// # Availability
324    ///
325    /// - This parameter is available for sources `openAi`, `huggingFace`, `ollama`
326    ///
327    /// # 🔄 Reindexing
328    ///
329    /// - 🏗️ Changing the value of this parameter always regenerates embeddings.
330    ///
331    /// # Defaults
332    ///
333    /// - For source `openAi`, defaults to `text-embedding-3-small`
334    /// - For source `huggingFace`, defaults to `BAAI/bge-base-en-v1.5`
335    pub model: Setting<String>,
336    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
337    #[deserr(default)]
338    #[schema(value_type = Option<String>)]
339    /// The revision (commit SHA1) of the model to use.
340    ///
341    /// If unspecified, Meilisearch picks the latest revision of the model.
342    ///
343    /// # Availability
344    ///
345    /// - This parameter is available for source `huggingFace`
346    ///
347    /// # 🔄 Reindexing
348    ///
349    /// - 🏗️ Changing the value of this parameter always regenerates embeddings
350    ///
351    /// # Defaults
352    ///
353    /// - When `model` is set to default, defaults to `617ca489d9e86b49b8167676d8220688b99db36e`
354    /// - Otherwise, defaults to `null`
355    pub revision: Setting<String>,
356    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
357    #[deserr(default)]
358    #[schema(value_type = Option<OverridePooling>)]
359    /// The pooling method to use.
360    ///
361    /// # Availability
362    ///
363    /// - This parameter is available for source `huggingFace`
364    ///
365    /// # 🔄 Reindexing
366    ///
367    /// - 🏗️ Changing the value of this parameter always regenerates embeddings
368    ///
369    /// # Defaults
370    ///
371    /// - Defaults to `useModel`
372    ///
373    /// # Compatibility Note
374    ///
375    /// - Embedders created before this parameter was available default to `forceMean` to preserve the existing behavior.
376    pub pooling: Setting<OverridePooling>,
377    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
378    #[deserr(default)]
379    #[schema(value_type = Option<String>)]
380    /// The API key to pass to the remote embedder while making requests.
381    ///
382    /// # Availability
383    ///
384    /// - This parameter is available for source `openAi`, `ollama`, `rest`
385    ///
386    /// # 🔄 Reindexing
387    ///
388    /// - 🌱 Changing the value of this parameter never regenerates embeddings
389    ///
390    /// # Defaults
391    ///
392    /// - For source `openAi`, the key is read from `OPENAI_API_KEY`, then `MEILI_OPENAI_API_KEY`.
393    /// - For other sources, no bearer token is sent if this parameter is not set.
394    ///
395    /// # Note
396    ///
397    /// - This setting is partially hidden when returned by the settings
398    pub api_key: Setting<String>,
399    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
400    #[deserr(default)]
401    #[schema(value_type = Option<String>)]
402    /// The expected dimensions of the embeddings produced by this embedder.
403    ///
404    /// # Mandatory
405    ///
406    /// - This parameter is mandatory for source `userProvided`
407    ///
408    /// # Availability
409    ///
410    /// - This parameter is available for source `openAi`, `ollama`, `rest`, `userProvided`
411    ///
412    /// # 🔄 Reindexing
413    ///
414    /// - 🏗️ When the source is `openAi`, changing the value of this parameter always regenerates embeddings
415    /// - 🌱 For other sources, changing the value of this parameter never regenerates embeddings
416    ///
417    /// # Defaults
418    ///
419    /// - For source `openAi`, the dimensions is the maximum allowed by the model.
420    /// - For sources `ollama` and `rest`, the dimensions are inferred by embedding a sample text.
421    pub dimensions: Setting<usize>,
422    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
423    #[deserr(default)]
424    #[schema(value_type = Option<bool>)]
425    /// A liquid template used to render documents to a text that can be embedded.
426    ///
427    /// Meillisearch interpolates the template for each document and sends the resulting text to the embedder.
428    /// The embedder then generates document vectors based on this text.
429    ///
430    /// # Availability
431    ///
432    /// - This parameter is available for source `openAi`, `huggingFace`, `ollama` and `rest
433    ///
434    /// # 🔄 Reindexing
435    ///
436    /// - 🏗️ When modified, embeddings are regenerated for documents whose rendering through the template produces a different text.
437    pub document_template: Setting<String>,
438    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
439    #[deserr(default)]
440    #[schema(value_type = Option<usize>)]
441    /// Rendered texts are truncated to this size.
442    ///
443    /// # Availability
444    ///
445    /// - This parameter is available for source `openAi`, `huggingFace`, `ollama` and `rest`
446    ///
447    /// # 🔄 Reindexing
448    ///
449    /// - 🏗️ When increased, embeddings are regenerated for documents whose rendering through the template produces a different text.
450    /// - 🌱 When decreased, embeddings are never regenerated
451    ///
452    /// # Default
453    ///
454    /// - Defaults to 400
455    pub document_template_max_bytes: Setting<usize>,
456    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
457    #[deserr(default)]
458    #[schema(value_type = Option<String>)]
459    /// URL to reach the remote embedder.
460    ///
461    /// # Mandatory
462    ///
463    /// - This parameter is mandatory for source `rest`
464    ///
465    /// # Availability
466    ///
467    /// - This parameter is available for source `openAi`, `ollama` and `rest`
468    ///
469    /// # 🔄 Reindexing
470    ///
471    /// - 🌱 When modified for source `openAi`, embeddings are never regenerated
472    /// - 🏗️ When modified for sources `ollama` and `rest`, embeddings are always regenerated
473    pub url: Setting<String>,
474    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
475    #[deserr(default)]
476    #[schema(value_type = Option<serde_json::Value>)]
477    /// Template request to send to the remote embedder.
478    ///
479    /// # Mandatory
480    ///
481    /// - This parameter is mandatory for source `rest`
482    ///
483    /// # Availability
484    ///
485    /// - This parameter is available for source `rest`
486    ///
487    /// # 🔄 Reindexing
488    ///
489    /// - 🏗️ Changing the value of this parameter always regenerates embeddings
490    pub request: Setting<serde_json::Value>,
491    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
492    #[deserr(default)]
493    #[schema(value_type = Option<serde_json::Value>)]
494    /// Template response indicating how to find the embeddings in the response from the remote embedder.
495    ///
496    /// # Mandatory
497    ///
498    /// - This parameter is mandatory for source `rest`
499    ///
500    /// # Availability
501    ///
502    /// - This parameter is available for source `rest`
503    ///
504    /// # 🔄 Reindexing
505    ///
506    /// - 🏗️ Changing the value of this parameter always regenerates embeddings
507    pub response: Setting<serde_json::Value>,
508    #[serde(default, skip_serializing_if = "Setting::is_not_set")]
509    #[deserr(default)]
510    #[schema(value_type = Option<BTreeMap<String, String>>)]
511    /// Additional headers to send to the remote embedder.
512    ///
513    /// # Availability
514    ///
515    /// - This parameter is available for source `rest`
516    ///
517    /// # 🔄 Reindexing
518    ///
519    /// - 🌱 Changing the value of this parameter never regenerates embeddings
520    pub headers: Setting<BTreeMap<String, String>>,
521
522    // The following fields are provided for the sake of improving error handling
523    // They should always be set to `NotSet`, otherwise an error will be returned
524    #[serde(default, skip_serializing)]
525    #[deserr(default)]
526    #[schema(ignore)]
527    pub distribution: Setting<DistributionShift>,
528
529    #[serde(default, skip_serializing)]
530    #[deserr(default)]
531    #[schema(ignore)]
532    pub binary_quantized: Setting<bool>,
533
534    #[serde(default, skip_serializing)]
535    #[deserr(default)]
536    #[schema(ignore)]
537    pub search_embedder: Setting<serde_json::Value>,
538
539    #[serde(default, skip_serializing)]
540    #[deserr(default)]
541    #[schema(ignore)]
542    pub indexing_embedder: Setting<serde_json::Value>,
543}
544
545/// Indicates what action should take place during a reindexing operation for an embedder
546#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
547pub enum ReindexAction {
548    /// An indexing operation should take place for this embedder, keeping existing vectors
549    /// and checking whether the document template changed or not
550    RegeneratePrompts,
551    /// An indexing operation should take place for all documents for this embedder, removing existing vectors
552    /// (except userProvided ones)
553    FullReindex,
554}
555
556pub enum SettingsDiff {
557    Remove,
558    Reindex { action: ReindexAction, updated_settings: EmbeddingSettings, quantize: bool },
559    UpdateWithoutReindex { updated_settings: EmbeddingSettings, quantize: bool },
560}
561
562#[derive(Default, Debug)]
563pub struct EmbedderAction {
564    pub was_quantized: bool,
565    pub is_being_quantized: bool,
566    pub write_back: Option<WriteBackToDocuments>,
567    pub reindex: Option<ReindexAction>,
568}
569
570impl EmbedderAction {
571    pub fn is_being_quantized(&self) -> bool {
572        self.is_being_quantized
573    }
574
575    pub fn write_back(&self) -> Option<&WriteBackToDocuments> {
576        self.write_back.as_ref()
577    }
578
579    pub fn reindex(&self) -> Option<&ReindexAction> {
580        self.reindex.as_ref()
581    }
582
583    pub fn with_is_being_quantized(mut self, quantize: bool) -> Self {
584        self.is_being_quantized = quantize;
585        self
586    }
587
588    pub fn with_write_back(write_back: WriteBackToDocuments, was_quantized: bool) -> Self {
589        Self {
590            was_quantized,
591            is_being_quantized: false,
592            write_back: Some(write_back),
593            reindex: None,
594        }
595    }
596
597    pub fn with_reindex(reindex: ReindexAction, was_quantized: bool) -> Self {
598        Self { was_quantized, is_being_quantized: false, write_back: None, reindex: Some(reindex) }
599    }
600}
601
602#[derive(Debug)]
603pub struct WriteBackToDocuments {
604    pub embedder_id: u8,
605    pub user_provided: RoaringBitmap,
606}
607
608impl SettingsDiff {
609    pub fn from_settings(
610        embedder_name: &str,
611        old: EmbeddingSettings,
612        new: Setting<EmbeddingSettings>,
613    ) -> Result<Self, UserError> {
614        let ret = match new {
615            Setting::Set(new) => {
616                let EmbeddingSettings {
617                    mut source,
618                    mut model,
619                    mut revision,
620                    mut pooling,
621                    mut api_key,
622                    mut dimensions,
623                    mut document_template,
624                    mut url,
625                    mut request,
626                    mut response,
627                    mut search_embedder,
628                    mut indexing_embedder,
629                    mut distribution,
630                    mut headers,
631                    mut document_template_max_bytes,
632                    binary_quantized: mut binary_quantize,
633                } = old;
634
635                let EmbeddingSettings {
636                    source: new_source,
637                    model: new_model,
638                    revision: new_revision,
639                    pooling: new_pooling,
640                    api_key: new_api_key,
641                    dimensions: new_dimensions,
642                    document_template: new_document_template,
643                    url: new_url,
644                    request: new_request,
645                    response: new_response,
646                    search_embedder: new_search_embedder,
647                    indexing_embedder: new_indexing_embedder,
648                    distribution: new_distribution,
649                    headers: new_headers,
650                    document_template_max_bytes: new_document_template_max_bytes,
651                    binary_quantized: new_binary_quantize,
652                } = new;
653
654                if matches!(binary_quantize, Setting::Set(true))
655                    && matches!(new_binary_quantize, Setting::Set(false))
656                {
657                    return Err(UserError::InvalidDisableBinaryQuantization {
658                        embedder_name: embedder_name.to_string(),
659                    });
660                }
661
662                let mut reindex_action = None;
663
664                Self::apply_and_diff(
665                    &mut reindex_action,
666                    &mut source,
667                    &mut model,
668                    &mut revision,
669                    &mut pooling,
670                    &mut api_key,
671                    &mut dimensions,
672                    &mut document_template,
673                    &mut document_template_max_bytes,
674                    &mut url,
675                    &mut request,
676                    &mut response,
677                    &mut headers,
678                    new_source,
679                    new_model,
680                    new_revision,
681                    new_pooling,
682                    new_api_key,
683                    new_dimensions,
684                    new_document_template,
685                    new_document_template_max_bytes,
686                    new_url,
687                    new_request,
688                    new_response,
689                    new_headers,
690                );
691
692                let binary_quantize_changed = binary_quantize.apply(new_binary_quantize);
693
694                // changes to the *search* embedder never triggers any reindexing
695                search_embedder.apply(new_search_embedder);
696                indexing_embedder = Self::from_sub_settings(
697                    indexing_embedder,
698                    new_indexing_embedder,
699                    &mut reindex_action,
700                )?;
701
702                distribution.apply(new_distribution);
703
704                let updated_settings = EmbeddingSettings {
705                    source,
706                    model,
707                    revision,
708                    pooling,
709                    api_key,
710                    dimensions,
711                    document_template,
712                    url,
713                    request,
714                    response,
715                    search_embedder,
716                    indexing_embedder,
717                    distribution,
718                    headers,
719                    document_template_max_bytes,
720                    binary_quantized: binary_quantize,
721                };
722
723                match reindex_action {
724                    Some(action) => Self::Reindex {
725                        action,
726                        updated_settings,
727                        quantize: binary_quantize_changed,
728                    },
729                    None => Self::UpdateWithoutReindex {
730                        updated_settings,
731                        quantize: binary_quantize_changed,
732                    },
733                }
734            }
735            Setting::Reset => Self::Remove,
736            Setting::NotSet => {
737                Self::UpdateWithoutReindex { updated_settings: old, quantize: false }
738            }
739        };
740        Ok(ret)
741    }
742
743    fn from_sub_settings(
744        sub_embedder: Setting<SubEmbeddingSettings>,
745        new_sub_embedder: Setting<SubEmbeddingSettings>,
746        reindex_action: &mut Option<ReindexAction>,
747    ) -> Result<Setting<SubEmbeddingSettings>, UserError> {
748        let ret = match new_sub_embedder {
749            Setting::Set(new_sub_embedder) => {
750                let Setting::Set(SubEmbeddingSettings {
751                    mut source,
752                    mut model,
753                    mut revision,
754                    mut pooling,
755                    mut api_key,
756                    mut dimensions,
757                    mut document_template,
758                    mut document_template_max_bytes,
759                    mut url,
760                    mut request,
761                    mut response,
762                    mut headers,
763                    // phony settings
764                    mut distribution,
765                    mut binary_quantized,
766                    mut search_embedder,
767                    mut indexing_embedder,
768                }) = sub_embedder
769                else {
770                    // return the new_indexing_embedder if the indexing_embedder was not set
771                    // this should happen only when changing the source, so the decision to reindex is already taken.
772                    return Ok(Setting::Set(new_sub_embedder));
773                };
774
775                let SubEmbeddingSettings {
776                    source: new_source,
777                    model: new_model,
778                    revision: new_revision,
779                    pooling: new_pooling,
780                    api_key: new_api_key,
781                    dimensions: new_dimensions,
782                    document_template: new_document_template,
783                    document_template_max_bytes: new_document_template_max_bytes,
784                    url: new_url,
785                    request: new_request,
786                    response: new_response,
787                    headers: new_headers,
788                    distribution: new_distribution,
789                    binary_quantized: new_binary_quantized,
790                    search_embedder: new_search_embedder,
791                    indexing_embedder: new_indexing_embedder,
792                } = new_sub_embedder;
793
794                Self::apply_and_diff(
795                    reindex_action,
796                    &mut source,
797                    &mut model,
798                    &mut revision,
799                    &mut pooling,
800                    &mut api_key,
801                    &mut dimensions,
802                    &mut document_template,
803                    &mut document_template_max_bytes,
804                    &mut url,
805                    &mut request,
806                    &mut response,
807                    &mut headers,
808                    new_source,
809                    new_model,
810                    new_revision,
811                    new_pooling,
812                    new_api_key,
813                    new_dimensions,
814                    new_document_template,
815                    new_document_template_max_bytes,
816                    new_url,
817                    new_request,
818                    new_response,
819                    new_headers,
820                );
821
822                // update phony settings, it is always an error to have them set.
823                distribution.apply(new_distribution);
824                binary_quantized.apply(new_binary_quantized);
825                search_embedder.apply(new_search_embedder);
826                indexing_embedder.apply(new_indexing_embedder);
827
828                let updated_settings = SubEmbeddingSettings {
829                    source,
830                    model,
831                    revision,
832                    pooling,
833                    api_key,
834                    dimensions,
835                    document_template,
836                    url,
837                    request,
838                    response,
839                    headers,
840                    document_template_max_bytes,
841                    distribution,
842                    binary_quantized,
843                    search_embedder,
844                    indexing_embedder,
845                };
846                Setting::Set(updated_settings)
847            }
848            // handled during validation of the settings
849            Setting::Reset | Setting::NotSet => sub_embedder,
850        };
851        Ok(ret)
852    }
853
854    #[allow(clippy::too_many_arguments)]
855    fn apply_and_diff(
856        reindex_action: &mut Option<ReindexAction>,
857        source: &mut Setting<EmbedderSource>,
858        model: &mut Setting<String>,
859        revision: &mut Setting<String>,
860        pooling: &mut Setting<OverridePooling>,
861        api_key: &mut Setting<String>,
862        dimensions: &mut Setting<usize>,
863        document_template: &mut Setting<String>,
864        document_template_max_bytes: &mut Setting<usize>,
865        url: &mut Setting<String>,
866        request: &mut Setting<serde_json::Value>,
867        response: &mut Setting<serde_json::Value>,
868        headers: &mut Setting<BTreeMap<String, String>>,
869        new_source: Setting<EmbedderSource>,
870        new_model: Setting<String>,
871        new_revision: Setting<String>,
872        new_pooling: Setting<OverridePooling>,
873        new_api_key: Setting<String>,
874        new_dimensions: Setting<usize>,
875        new_document_template: Setting<String>,
876        new_document_template_max_bytes: Setting<usize>,
877        new_url: Setting<String>,
878        new_request: Setting<serde_json::Value>,
879        new_response: Setting<serde_json::Value>,
880        new_headers: Setting<BTreeMap<String, String>>,
881    ) {
882        // **Warning**: do not use short-circuiting || here, we want all these operations applied
883        if source.apply(new_source) {
884            ReindexAction::push_action(reindex_action, ReindexAction::FullReindex);
885            // when the source changes, we need to reapply the default settings for the new source
886            apply_default_for_source(
887                &*source,
888                model,
889                revision,
890                pooling,
891                dimensions,
892                url,
893                request,
894                response,
895                document_template,
896                document_template_max_bytes,
897                headers,
898                // send dummy values, the source cannot recursively be composite
899                &mut Setting::NotSet,
900                &mut Setting::NotSet,
901            )
902        }
903        if model.apply(new_model) {
904            ReindexAction::push_action(reindex_action, ReindexAction::FullReindex);
905        }
906        if revision.apply(new_revision) {
907            ReindexAction::push_action(reindex_action, ReindexAction::FullReindex);
908        }
909        if pooling.apply(new_pooling) {
910            ReindexAction::push_action(reindex_action, ReindexAction::FullReindex);
911        }
912        if dimensions.apply(new_dimensions) {
913            match *source {
914                // regenerate on dimensions change in OpenAI since truncation is supported
915                Setting::Set(EmbedderSource::OpenAi) | Setting::Reset => {
916                    ReindexAction::push_action(reindex_action, ReindexAction::FullReindex);
917                }
918                // for all other embedders, the parameter is a hint that should not be able to change the result
919                // and so won't cause a reindex by itself.
920                _ => {}
921            }
922        }
923        if url.apply(new_url) {
924            match *source {
925                // do not regenerate on an url change in OpenAI
926                Setting::Set(EmbedderSource::OpenAi) | Setting::Reset => {}
927                _ => {
928                    ReindexAction::push_action(reindex_action, ReindexAction::FullReindex);
929                }
930            }
931        }
932        if request.apply(new_request) {
933            ReindexAction::push_action(reindex_action, ReindexAction::FullReindex);
934        }
935        if response.apply(new_response) {
936            ReindexAction::push_action(reindex_action, ReindexAction::FullReindex);
937        }
938        if document_template.apply(new_document_template) {
939            ReindexAction::push_action(reindex_action, ReindexAction::RegeneratePrompts);
940        }
941
942        if document_template_max_bytes.apply(new_document_template_max_bytes) {
943            let previous_document_template_max_bytes =
944                document_template_max_bytes.set().unwrap_or(default_max_bytes().get());
945            let new_document_template_max_bytes =
946                new_document_template_max_bytes.set().unwrap_or(default_max_bytes().get());
947
948            // only reindex if the size increased. Reasoning:
949            // - size decrease is a performance optimization, so we don't reindex and we keep the more accurate vectors
950            // - size increase is an accuracy optimization, so we want to reindex
951            if new_document_template_max_bytes > previous_document_template_max_bytes {
952                ReindexAction::push_action(reindex_action, ReindexAction::RegeneratePrompts)
953            }
954        }
955
956        api_key.apply(new_api_key);
957        headers.apply(new_headers);
958    }
959}
960
961impl ReindexAction {
962    fn push_action(this: &mut Option<Self>, other: Self) {
963        *this = match (*this, other) {
964            (_, ReindexAction::FullReindex) => Some(ReindexAction::FullReindex),
965            (Some(ReindexAction::FullReindex), _) => Some(ReindexAction::FullReindex),
966            (_, ReindexAction::RegeneratePrompts) => Some(ReindexAction::RegeneratePrompts),
967        }
968    }
969}
970
971#[allow(clippy::too_many_arguments)] // private function
972fn apply_default_for_source(
973    source: &Setting<EmbedderSource>,
974    model: &mut Setting<String>,
975    revision: &mut Setting<String>,
976    pooling: &mut Setting<OverridePooling>,
977    dimensions: &mut Setting<usize>,
978    url: &mut Setting<String>,
979    request: &mut Setting<serde_json::Value>,
980    response: &mut Setting<serde_json::Value>,
981    document_template: &mut Setting<String>,
982    document_template_max_bytes: &mut Setting<usize>,
983    headers: &mut Setting<BTreeMap<String, String>>,
984    search_embedder: &mut Setting<SubEmbeddingSettings>,
985    indexing_embedder: &mut Setting<SubEmbeddingSettings>,
986) {
987    match source {
988        Setting::Set(EmbedderSource::HuggingFace) => {
989            *model = Setting::Reset;
990            *revision = Setting::Reset;
991            *pooling = Setting::Reset;
992            *dimensions = Setting::NotSet;
993            *url = Setting::NotSet;
994            *request = Setting::NotSet;
995            *response = Setting::NotSet;
996            *headers = Setting::NotSet;
997            *search_embedder = Setting::NotSet;
998            *indexing_embedder = Setting::NotSet;
999        }
1000        Setting::Set(EmbedderSource::Ollama) => {
1001            *model = Setting::Reset;
1002            *revision = Setting::NotSet;
1003            *pooling = Setting::NotSet;
1004            *dimensions = Setting::Reset;
1005            *url = Setting::NotSet;
1006            *request = Setting::NotSet;
1007            *response = Setting::NotSet;
1008            *headers = Setting::NotSet;
1009            *search_embedder = Setting::NotSet;
1010            *indexing_embedder = Setting::NotSet;
1011        }
1012        Setting::Set(EmbedderSource::OpenAi) | Setting::Reset => {
1013            *model = Setting::Reset;
1014            *revision = Setting::NotSet;
1015            *pooling = Setting::NotSet;
1016            *dimensions = Setting::NotSet;
1017            *url = Setting::Reset;
1018            *request = Setting::NotSet;
1019            *response = Setting::NotSet;
1020            *headers = Setting::NotSet;
1021            *search_embedder = Setting::NotSet;
1022            *indexing_embedder = Setting::NotSet;
1023        }
1024        Setting::Set(EmbedderSource::Rest) => {
1025            *model = Setting::NotSet;
1026            *revision = Setting::NotSet;
1027            *pooling = Setting::NotSet;
1028            *dimensions = Setting::Reset;
1029            *url = Setting::Reset;
1030            *request = Setting::Reset;
1031            *response = Setting::Reset;
1032            *headers = Setting::Reset;
1033            *search_embedder = Setting::NotSet;
1034            *indexing_embedder = Setting::NotSet;
1035        }
1036        Setting::Set(EmbedderSource::UserProvided) => {
1037            *model = Setting::NotSet;
1038            *revision = Setting::NotSet;
1039            *pooling = Setting::NotSet;
1040            *dimensions = Setting::Reset;
1041            *url = Setting::NotSet;
1042            *request = Setting::NotSet;
1043            *response = Setting::NotSet;
1044            *document_template = Setting::NotSet;
1045            *document_template_max_bytes = Setting::NotSet;
1046            *headers = Setting::NotSet;
1047            *search_embedder = Setting::NotSet;
1048            *indexing_embedder = Setting::NotSet;
1049        }
1050        Setting::Set(EmbedderSource::Composite) => {
1051            *model = Setting::NotSet;
1052            *revision = Setting::NotSet;
1053            *pooling = Setting::NotSet;
1054            *dimensions = Setting::NotSet;
1055            *url = Setting::NotSet;
1056            *request = Setting::NotSet;
1057            *response = Setting::NotSet;
1058            *document_template = Setting::NotSet;
1059            *document_template_max_bytes = Setting::NotSet;
1060            *headers = Setting::NotSet;
1061            *search_embedder = Setting::Reset;
1062            *indexing_embedder = Setting::Reset;
1063        }
1064        Setting::NotSet => {}
1065    }
1066}
1067
1068pub(crate) enum FieldStatus {
1069    Mandatory,
1070    Allowed,
1071    Disallowed,
1072}
1073
1074#[derive(Debug, Clone, Copy)]
1075pub enum NestingContext {
1076    NotNested,
1077    Search,
1078    Indexing,
1079}
1080
1081impl NestingContext {
1082    pub fn embedder_name_with_context(&self, embedder_name: &str) -> String {
1083        match self {
1084            NestingContext::NotNested => embedder_name.to_string(),
1085            NestingContext::Search => format!("{embedder_name}.searchEmbedder"),
1086            NestingContext::Indexing => format!("{embedder_name}.indexingEmbedder",),
1087        }
1088    }
1089
1090    pub fn in_context(&self) -> &'static str {
1091        match self {
1092            NestingContext::NotNested => "",
1093            NestingContext::Search => " for the search embedder",
1094            NestingContext::Indexing => " for the indexing embedder",
1095        }
1096    }
1097
1098    pub fn nesting_embedders(&self) -> &'static str {
1099        match self {
1100            NestingContext::NotNested => "",
1101            NestingContext::Search => {
1102                "\n  - note: nesting embedders in `searchEmbedder` is not allowed"
1103            }
1104            NestingContext::Indexing => {
1105                "\n  - note: nesting embedders in `indexingEmbedder` is not allowed"
1106            }
1107        }
1108    }
1109}
1110
1111#[derive(Debug, Clone, Copy, enum_iterator::Sequence)]
1112pub enum MetaEmbeddingSetting {
1113    Source,
1114    Model,
1115    Revision,
1116    Pooling,
1117    ApiKey,
1118    Dimensions,
1119    DocumentTemplate,
1120    DocumentTemplateMaxBytes,
1121    Url,
1122    Request,
1123    Response,
1124    Headers,
1125    SearchEmbedder,
1126    IndexingEmbedder,
1127    Distribution,
1128    BinaryQuantized,
1129}
1130
1131impl MetaEmbeddingSetting {
1132    pub(crate) fn name(&self) -> &'static str {
1133        use MetaEmbeddingSetting::*;
1134        match self {
1135            Source => "source",
1136            Model => "model",
1137            Revision => "revision",
1138            Pooling => "pooling",
1139            ApiKey => "apiKey",
1140            Dimensions => "dimensions",
1141            DocumentTemplate => "documentTemplate",
1142            DocumentTemplateMaxBytes => "documentTemplateMaxBytes",
1143            Url => "url",
1144            Request => "request",
1145            Response => "response",
1146            Headers => "headers",
1147            SearchEmbedder => "searchEmbedder",
1148            IndexingEmbedder => "indexingEmbedder",
1149            Distribution => "distribution",
1150            BinaryQuantized => "binaryQuantized",
1151        }
1152    }
1153}
1154
1155impl EmbeddingSettings {
1156    #[allow(clippy::too_many_arguments)]
1157    pub(crate) fn check_settings(
1158        embedder_name: &str,
1159        source: EmbedderSource,
1160        context: NestingContext,
1161        model: &Setting<String>,
1162        revision: &Setting<String>,
1163        pooling: &Setting<OverridePooling>,
1164        dimensions: &Setting<usize>,
1165        api_key: &Setting<String>,
1166        url: &Setting<String>,
1167        request: &Setting<serde_json::Value>,
1168        response: &Setting<serde_json::Value>,
1169        document_template: &Setting<String>,
1170        document_template_max_bytes: &Setting<usize>,
1171        headers: &Setting<BTreeMap<String, String>>,
1172        search_embedder: &Setting<SubEmbeddingSettings>,
1173        indexing_embedder: &Setting<SubEmbeddingSettings>,
1174        binary_quantized: &Setting<bool>,
1175        distribution: &Setting<DistributionShift>,
1176    ) -> Result<(), UserError> {
1177        Self::check_setting(embedder_name, source, MetaEmbeddingSetting::Model, context, model)?;
1178        Self::check_setting(
1179            embedder_name,
1180            source,
1181            MetaEmbeddingSetting::Revision,
1182            context,
1183            revision,
1184        )?;
1185        Self::check_setting(
1186            embedder_name,
1187            source,
1188            MetaEmbeddingSetting::Pooling,
1189            context,
1190            pooling,
1191        )?;
1192        Self::check_setting(
1193            embedder_name,
1194            source,
1195            MetaEmbeddingSetting::Dimensions,
1196            context,
1197            dimensions,
1198        )?;
1199        Self::check_setting(embedder_name, source, MetaEmbeddingSetting::ApiKey, context, api_key)?;
1200        Self::check_setting(embedder_name, source, MetaEmbeddingSetting::Url, context, url)?;
1201        Self::check_setting(
1202            embedder_name,
1203            source,
1204            MetaEmbeddingSetting::Request,
1205            context,
1206            request,
1207        )?;
1208        Self::check_setting(
1209            embedder_name,
1210            source,
1211            MetaEmbeddingSetting::Response,
1212            context,
1213            response,
1214        )?;
1215        Self::check_setting(
1216            embedder_name,
1217            source,
1218            MetaEmbeddingSetting::DocumentTemplate,
1219            context,
1220            document_template,
1221        )?;
1222        Self::check_setting(
1223            embedder_name,
1224            source,
1225            MetaEmbeddingSetting::DocumentTemplateMaxBytes,
1226            context,
1227            document_template_max_bytes,
1228        )?;
1229        Self::check_setting(
1230            embedder_name,
1231            source,
1232            MetaEmbeddingSetting::Headers,
1233            context,
1234            headers,
1235        )?;
1236        Self::check_setting(
1237            embedder_name,
1238            source,
1239            MetaEmbeddingSetting::SearchEmbedder,
1240            context,
1241            search_embedder,
1242        )?;
1243        Self::check_setting(
1244            embedder_name,
1245            source,
1246            MetaEmbeddingSetting::IndexingEmbedder,
1247            context,
1248            indexing_embedder,
1249        )?;
1250        Self::check_setting(
1251            embedder_name,
1252            source,
1253            MetaEmbeddingSetting::BinaryQuantized,
1254            context,
1255            binary_quantized,
1256        )?;
1257        Self::check_setting(
1258            embedder_name,
1259            source,
1260            MetaEmbeddingSetting::Distribution,
1261            context,
1262            distribution,
1263        )
1264    }
1265
1266    pub(crate) fn allowed_sources_for_field(
1267        field: MetaEmbeddingSetting,
1268        context: NestingContext,
1269    ) -> Vec<EmbedderSource> {
1270        enum_iterator::all()
1271            .filter(|source| {
1272                !matches!(Self::field_status(*source, field, context), FieldStatus::Disallowed)
1273            })
1274            .collect()
1275    }
1276
1277    pub(crate) fn allowed_fields_for_source(
1278        source: EmbedderSource,
1279        context: NestingContext,
1280    ) -> Vec<&'static str> {
1281        enum_iterator::all()
1282            .filter(|field| {
1283                !matches!(Self::field_status(source, *field, context), FieldStatus::Disallowed)
1284            })
1285            .map(|field| field.name())
1286            .collect()
1287    }
1288
1289    fn check_setting<T>(
1290        embedder_name: &str,
1291        source: EmbedderSource,
1292        field: MetaEmbeddingSetting,
1293        context: NestingContext,
1294        setting: &Setting<T>,
1295    ) -> Result<(), UserError> {
1296        match (Self::field_status(source, field, context), setting) {
1297            (FieldStatus::Mandatory, Setting::Set(_))
1298            | (FieldStatus::Allowed, _)
1299            | (FieldStatus::Disallowed, Setting::NotSet) => Ok(()),
1300            (FieldStatus::Disallowed, _) => Err(UserError::InvalidFieldForSource {
1301                embedder_name: context.embedder_name_with_context(embedder_name),
1302                source_: source,
1303                context,
1304                field,
1305            }),
1306            (FieldStatus::Mandatory, _) => Err(UserError::MissingFieldForSource {
1307                field: field.name(),
1308                source_: source,
1309                embedder_name: embedder_name.to_owned(),
1310            }),
1311        }
1312    }
1313
1314    pub(crate) fn field_status(
1315        source: EmbedderSource,
1316        field: MetaEmbeddingSetting,
1317        context: NestingContext,
1318    ) -> FieldStatus {
1319        use EmbedderSource::*;
1320        use MetaEmbeddingSetting::*;
1321        use NestingContext::*;
1322        match (source, field, context) {
1323            (_, Distribution | BinaryQuantized, NotNested) => FieldStatus::Allowed,
1324            (_, Distribution | BinaryQuantized, _) => FieldStatus::Disallowed,
1325            (_, DocumentTemplate | DocumentTemplateMaxBytes, Search) => FieldStatus::Disallowed,
1326            (
1327                OpenAi,
1328                Source
1329                | Model
1330                | ApiKey
1331                | DocumentTemplate
1332                | DocumentTemplateMaxBytes
1333                | Dimensions
1334                | Url,
1335                _,
1336            ) => FieldStatus::Allowed,
1337            (
1338                OpenAi,
1339                Revision | Pooling | Request | Response | Headers | SearchEmbedder
1340                | IndexingEmbedder,
1341                _,
1342            ) => FieldStatus::Disallowed,
1343            (
1344                HuggingFace,
1345                Source | Model | Revision | Pooling | DocumentTemplate | DocumentTemplateMaxBytes,
1346                _,
1347            ) => FieldStatus::Allowed,
1348            (
1349                HuggingFace,
1350                ApiKey | Dimensions | Url | Request | Response | Headers | SearchEmbedder
1351                | IndexingEmbedder,
1352                _,
1353            ) => FieldStatus::Disallowed,
1354            (Ollama, Model, _) => FieldStatus::Mandatory,
1355            (
1356                Ollama,
1357                Source | DocumentTemplate | DocumentTemplateMaxBytes | Url | ApiKey | Dimensions,
1358                _,
1359            ) => FieldStatus::Allowed,
1360            (
1361                Ollama,
1362                Revision | Pooling | Request | Response | Headers | SearchEmbedder
1363                | IndexingEmbedder,
1364                _,
1365            ) => FieldStatus::Disallowed,
1366            (UserProvided, Dimensions, _) => FieldStatus::Mandatory,
1367            (UserProvided, Source, _) => FieldStatus::Allowed,
1368            (
1369                UserProvided,
1370                Model
1371                | Revision
1372                | Pooling
1373                | ApiKey
1374                | DocumentTemplate
1375                | DocumentTemplateMaxBytes
1376                | Url
1377                | Request
1378                | Response
1379                | Headers
1380                | SearchEmbedder
1381                | IndexingEmbedder,
1382                _,
1383            ) => FieldStatus::Disallowed,
1384            (Rest, Url | Request | Response, _) => FieldStatus::Mandatory,
1385            (
1386                Rest,
1387                Source
1388                | ApiKey
1389                | Dimensions
1390                | DocumentTemplate
1391                | DocumentTemplateMaxBytes
1392                | Headers,
1393                _,
1394            ) => FieldStatus::Allowed,
1395            (Rest, Model | Revision | Pooling | SearchEmbedder | IndexingEmbedder, _) => {
1396                FieldStatus::Disallowed
1397            }
1398            (Composite, SearchEmbedder | IndexingEmbedder, _) => FieldStatus::Mandatory,
1399            (Composite, Source, _) => FieldStatus::Allowed,
1400            (
1401                Composite,
1402                Model
1403                | Revision
1404                | Pooling
1405                | ApiKey
1406                | Dimensions
1407                | DocumentTemplate
1408                | DocumentTemplateMaxBytes
1409                | Url
1410                | Request
1411                | Response
1412                | Headers,
1413                _,
1414            ) => FieldStatus::Disallowed,
1415        }
1416    }
1417
1418    pub(crate) fn apply_default_source(setting: &mut Setting<EmbeddingSettings>) {
1419        if let Setting::Set(EmbeddingSettings {
1420            source: source @ (Setting::NotSet | Setting::Reset),
1421            ..
1422        }) = setting
1423        {
1424            *source = Setting::Set(EmbedderSource::default())
1425        }
1426    }
1427
1428    pub(crate) fn apply_default_openai_model(setting: &mut Setting<EmbeddingSettings>) {
1429        if let Setting::Set(EmbeddingSettings {
1430            source: Setting::Set(EmbedderSource::OpenAi),
1431            model: model @ (Setting::NotSet | Setting::Reset),
1432            ..
1433        }) = setting
1434        {
1435            *model = Setting::Set(openai::EmbeddingModel::default().name().to_owned())
1436        }
1437    }
1438
1439    pub(crate) fn check_nested_source(
1440        embedder_name: &str,
1441        source: EmbedderSource,
1442        context: NestingContext,
1443    ) -> Result<(), UserError> {
1444        match (context, source) {
1445            (NestingContext::NotNested, _) => Ok(()),
1446            (
1447                NestingContext::Search | NestingContext::Indexing,
1448                EmbedderSource::Composite | EmbedderSource::UserProvided,
1449            ) => Err(UserError::InvalidSourceForNested {
1450                embedder_name: context.embedder_name_with_context(embedder_name),
1451                source_: source,
1452            }),
1453            (
1454                NestingContext::Search | NestingContext::Indexing,
1455                EmbedderSource::OpenAi
1456                | EmbedderSource::HuggingFace
1457                | EmbedderSource::Ollama
1458                | EmbedderSource::Rest,
1459            ) => Ok(()),
1460        }
1461    }
1462}
1463
1464#[derive(
1465    Debug,
1466    Clone,
1467    Copy,
1468    Default,
1469    Serialize,
1470    Deserialize,
1471    PartialEq,
1472    Eq,
1473    Deserr,
1474    ToSchema,
1475    enum_iterator::Sequence,
1476)]
1477#[serde(deny_unknown_fields, rename_all = "camelCase")]
1478#[deserr(rename_all = camelCase, deny_unknown_fields)]
1479pub enum EmbedderSource {
1480    #[default]
1481    OpenAi,
1482    HuggingFace,
1483    Ollama,
1484    UserProvided,
1485    Rest,
1486    Composite,
1487}
1488
1489impl std::fmt::Display for EmbedderSource {
1490    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1491        let s = match self {
1492            EmbedderSource::OpenAi => "openAi",
1493            EmbedderSource::HuggingFace => "huggingFace",
1494            EmbedderSource::UserProvided => "userProvided",
1495            EmbedderSource::Ollama => "ollama",
1496            EmbedderSource::Rest => "rest",
1497            EmbedderSource::Composite => "composite",
1498        };
1499        f.write_str(s)
1500    }
1501}
1502
1503impl EmbeddingSettings {
1504    fn from_hugging_face(
1505        super::hf::EmbedderOptions {
1506        model,
1507        revision,
1508        distribution,
1509        pooling,
1510    }: super::hf::EmbedderOptions,
1511        document_template: Setting<String>,
1512        document_template_max_bytes: Setting<usize>,
1513        quantized: Option<bool>,
1514    ) -> Self {
1515        Self {
1516            source: Setting::Set(EmbedderSource::HuggingFace),
1517            model: Setting::Set(model),
1518            revision: Setting::some_or_not_set(revision),
1519            pooling: Setting::Set(pooling),
1520            api_key: Setting::NotSet,
1521            dimensions: Setting::NotSet,
1522            document_template,
1523            document_template_max_bytes,
1524            url: Setting::NotSet,
1525            request: Setting::NotSet,
1526            response: Setting::NotSet,
1527            headers: Setting::NotSet,
1528            search_embedder: Setting::NotSet,
1529            indexing_embedder: Setting::NotSet,
1530            distribution: Setting::some_or_not_set(distribution),
1531            binary_quantized: Setting::some_or_not_set(quantized),
1532        }
1533    }
1534
1535    fn from_openai(
1536        super::openai::EmbedderOptions {
1537            url,
1538            api_key,
1539            embedding_model,
1540            dimensions,
1541            distribution,
1542        }: super::openai::EmbedderOptions,
1543        document_template: Setting<String>,
1544        document_template_max_bytes: Setting<usize>,
1545        quantized: Option<bool>,
1546    ) -> Self {
1547        Self {
1548            source: Setting::Set(EmbedderSource::OpenAi),
1549            model: Setting::Set(embedding_model.name().to_owned()),
1550            revision: Setting::NotSet,
1551            pooling: Setting::NotSet,
1552            api_key: Setting::some_or_not_set(api_key),
1553            dimensions: Setting::some_or_not_set(dimensions),
1554            document_template,
1555            document_template_max_bytes,
1556            url: Setting::some_or_not_set(url),
1557            request: Setting::NotSet,
1558            response: Setting::NotSet,
1559            headers: Setting::NotSet,
1560            search_embedder: Setting::NotSet,
1561            indexing_embedder: Setting::NotSet,
1562            distribution: Setting::some_or_not_set(distribution),
1563            binary_quantized: Setting::some_or_not_set(quantized),
1564        }
1565    }
1566
1567    fn from_ollama(
1568        super::ollama::EmbedderOptions {
1569          embedding_model,
1570          url,
1571          api_key,
1572          distribution,
1573          dimensions,
1574        }: super::ollama::EmbedderOptions,
1575        document_template: Setting<String>,
1576        document_template_max_bytes: Setting<usize>,
1577        quantized: Option<bool>,
1578    ) -> Self {
1579        Self {
1580            source: Setting::Set(EmbedderSource::Ollama),
1581            model: Setting::Set(embedding_model),
1582            revision: Setting::NotSet,
1583            pooling: Setting::NotSet,
1584            api_key: Setting::some_or_not_set(api_key),
1585            dimensions: Setting::some_or_not_set(dimensions),
1586            document_template,
1587            document_template_max_bytes,
1588            url: Setting::some_or_not_set(url),
1589            request: Setting::NotSet,
1590            response: Setting::NotSet,
1591            headers: Setting::NotSet,
1592            search_embedder: Setting::NotSet,
1593            indexing_embedder: Setting::NotSet,
1594            distribution: Setting::some_or_not_set(distribution),
1595            binary_quantized: Setting::some_or_not_set(quantized),
1596        }
1597    }
1598
1599    fn from_user_provided(
1600        super::manual::EmbedderOptions { dimensions, distribution }: super::manual::EmbedderOptions,
1601        quantized: Option<bool>,
1602    ) -> Self {
1603        Self {
1604            source: Setting::Set(EmbedderSource::UserProvided),
1605            model: Setting::NotSet,
1606            revision: Setting::NotSet,
1607            pooling: Setting::NotSet,
1608            api_key: Setting::NotSet,
1609            dimensions: Setting::Set(dimensions),
1610            document_template: Setting::NotSet,
1611            document_template_max_bytes: Setting::NotSet,
1612            url: Setting::NotSet,
1613            request: Setting::NotSet,
1614            response: Setting::NotSet,
1615            headers: Setting::NotSet,
1616            search_embedder: Setting::NotSet,
1617            indexing_embedder: Setting::NotSet,
1618            distribution: Setting::some_or_not_set(distribution),
1619            binary_quantized: Setting::some_or_not_set(quantized),
1620        }
1621    }
1622
1623    fn from_rest(
1624        super::rest::EmbedderOptions {
1625            api_key,
1626            dimensions,
1627            url,
1628            request,
1629            response,
1630            distribution,
1631            headers,
1632        }: super::rest::EmbedderOptions,
1633        document_template: Setting<String>,
1634        document_template_max_bytes: Setting<usize>,
1635        quantized: Option<bool>,
1636    ) -> Self {
1637        Self {
1638            source: Setting::Set(EmbedderSource::Rest),
1639            model: Setting::NotSet,
1640            revision: Setting::NotSet,
1641            pooling: Setting::NotSet,
1642            api_key: Setting::some_or_not_set(api_key),
1643            dimensions: Setting::some_or_not_set(dimensions),
1644            document_template,
1645            document_template_max_bytes,
1646            url: Setting::Set(url),
1647            request: Setting::Set(request),
1648            response: Setting::Set(response),
1649            distribution: Setting::some_or_not_set(distribution),
1650            headers: Setting::Set(headers),
1651            search_embedder: Setting::NotSet,
1652            indexing_embedder: Setting::NotSet,
1653            binary_quantized: Setting::some_or_not_set(quantized),
1654        }
1655    }
1656}
1657
1658impl From<EmbeddingConfig> for EmbeddingSettings {
1659    fn from(value: EmbeddingConfig) -> Self {
1660        let EmbeddingConfig { embedder_options, prompt, quantized } = value;
1661        let document_template_max_bytes =
1662            Setting::Set(prompt.max_bytes.unwrap_or(default_max_bytes()).get());
1663        match embedder_options {
1664            super::EmbedderOptions::HuggingFace(options) => Self::from_hugging_face(
1665                options,
1666                Setting::Set(prompt.template),
1667                document_template_max_bytes,
1668                quantized,
1669            ),
1670            super::EmbedderOptions::OpenAi(options) => Self::from_openai(
1671                options,
1672                Setting::Set(prompt.template),
1673                document_template_max_bytes,
1674                quantized,
1675            ),
1676            super::EmbedderOptions::Ollama(options) => Self::from_ollama(
1677                options,
1678                Setting::Set(prompt.template),
1679                document_template_max_bytes,
1680                quantized,
1681            ),
1682            super::EmbedderOptions::UserProvided(options) => {
1683                Self::from_user_provided(options, quantized)
1684            }
1685            super::EmbedderOptions::Rest(options) => Self::from_rest(
1686                options,
1687                Setting::Set(prompt.template),
1688                document_template_max_bytes,
1689                quantized,
1690            ),
1691            super::EmbedderOptions::Composite(super::composite::EmbedderOptions {
1692                search,
1693                index,
1694            }) => Self {
1695                source: Setting::Set(EmbedderSource::Composite),
1696                model: Setting::NotSet,
1697                revision: Setting::NotSet,
1698                pooling: Setting::NotSet,
1699                api_key: Setting::NotSet,
1700                dimensions: Setting::NotSet,
1701                binary_quantized: Setting::some_or_not_set(quantized),
1702                document_template: Setting::NotSet,
1703                document_template_max_bytes: Setting::NotSet,
1704                url: Setting::NotSet,
1705                request: Setting::NotSet,
1706                response: Setting::NotSet,
1707                headers: Setting::NotSet,
1708                distribution: Setting::some_or_not_set(search.distribution()),
1709                search_embedder: Setting::Set(SubEmbeddingSettings::from_options(
1710                    search,
1711                    Setting::NotSet,
1712                    Setting::NotSet,
1713                )),
1714                indexing_embedder: Setting::Set(SubEmbeddingSettings::from_options(
1715                    index,
1716                    Setting::Set(prompt.template),
1717                    document_template_max_bytes,
1718                )),
1719            },
1720        }
1721    }
1722}
1723
1724impl SubEmbeddingSettings {
1725    fn from_options(
1726        options: SubEmbedderOptions,
1727        document_template: Setting<String>,
1728        document_template_max_bytes: Setting<usize>,
1729    ) -> Self {
1730        let settings = match options {
1731            SubEmbedderOptions::HuggingFace(embedder_options) => {
1732                EmbeddingSettings::from_hugging_face(
1733                    embedder_options,
1734                    document_template,
1735                    document_template_max_bytes,
1736                    None,
1737                )
1738            }
1739            SubEmbedderOptions::OpenAi(embedder_options) => EmbeddingSettings::from_openai(
1740                embedder_options,
1741                document_template,
1742                document_template_max_bytes,
1743                None,
1744            ),
1745            SubEmbedderOptions::Ollama(embedder_options) => EmbeddingSettings::from_ollama(
1746                embedder_options,
1747                document_template,
1748                document_template_max_bytes,
1749                None,
1750            ),
1751            SubEmbedderOptions::UserProvided(embedder_options) => {
1752                EmbeddingSettings::from_user_provided(embedder_options, None)
1753            }
1754            SubEmbedderOptions::Rest(embedder_options) => EmbeddingSettings::from_rest(
1755                embedder_options,
1756                document_template,
1757                document_template_max_bytes,
1758                None,
1759            ),
1760        };
1761        settings.into()
1762    }
1763}
1764
1765impl From<EmbeddingSettings> for SubEmbeddingSettings {
1766    fn from(value: EmbeddingSettings) -> Self {
1767        let EmbeddingSettings {
1768            source,
1769            model,
1770            revision,
1771            pooling,
1772            api_key,
1773            dimensions,
1774            document_template,
1775            document_template_max_bytes,
1776            url,
1777            request,
1778            response,
1779            headers,
1780            binary_quantized: _,
1781            search_embedder: _,
1782            indexing_embedder: _,
1783            distribution: _,
1784        } = value;
1785        Self {
1786            source,
1787            model,
1788            revision,
1789            pooling,
1790            api_key,
1791            dimensions,
1792            document_template,
1793            document_template_max_bytes,
1794            url,
1795            request,
1796            response,
1797            headers,
1798            distribution: Setting::NotSet,
1799            binary_quantized: Setting::NotSet,
1800            search_embedder: Setting::NotSet,
1801            indexing_embedder: Setting::NotSet,
1802        }
1803    }
1804}
1805
1806impl From<EmbeddingSettings> for EmbeddingConfig {
1807    fn from(value: EmbeddingSettings) -> Self {
1808        let mut this = Self::default();
1809        let EmbeddingSettings {
1810            source,
1811            model,
1812            revision,
1813            pooling,
1814            api_key,
1815            dimensions,
1816            document_template,
1817            document_template_max_bytes,
1818            url,
1819            request,
1820            response,
1821            distribution,
1822            headers,
1823            binary_quantized,
1824            search_embedder,
1825            mut indexing_embedder,
1826        } = value;
1827
1828        this.quantized = binary_quantized.set();
1829        if let Some((template, document_template_max_bytes)) =
1830            match (document_template, &mut indexing_embedder) {
1831                (Setting::Set(template), _) => Some((template, document_template_max_bytes)),
1832                // retrieve the prompt from the indexing embedder in case of a composite embedder
1833                (
1834                    _,
1835                    Setting::Set(SubEmbeddingSettings {
1836                        document_template: Setting::Set(document_template),
1837                        document_template_max_bytes,
1838                        ..
1839                    }),
1840                ) => Some((std::mem::take(document_template), *document_template_max_bytes)),
1841                _ => None,
1842            }
1843        {
1844            let max_bytes = document_template_max_bytes
1845                .set()
1846                .and_then(NonZeroUsize::new)
1847                .unwrap_or(default_max_bytes());
1848
1849            this.prompt = PromptData { template, max_bytes: Some(max_bytes) }
1850        }
1851
1852        if let Some(source) = source.set() {
1853            this.embedder_options = match source {
1854                EmbedderSource::OpenAi => {
1855                    SubEmbedderOptions::openai(model, url, api_key, dimensions, distribution).into()
1856                }
1857                EmbedderSource::Ollama => {
1858                    SubEmbedderOptions::ollama(model, url, api_key, dimensions, distribution).into()
1859                }
1860                EmbedderSource::HuggingFace => {
1861                    SubEmbedderOptions::hugging_face(model, revision, pooling, distribution).into()
1862                }
1863                EmbedderSource::UserProvided => {
1864                    SubEmbedderOptions::user_provided(dimensions.set().unwrap(), distribution)
1865                        .into()
1866                }
1867                EmbedderSource::Rest => SubEmbedderOptions::rest(
1868                    url.set().unwrap(),
1869                    api_key,
1870                    request.set().unwrap(),
1871                    response.set().unwrap(),
1872                    headers,
1873                    dimensions,
1874                    distribution,
1875                )
1876                .into(),
1877                EmbedderSource::Composite => {
1878                    super::EmbedderOptions::Composite(super::composite::EmbedderOptions {
1879                        // it is important to give the distribution to the search here, as this is from where we'll retrieve it
1880                        search: SubEmbedderOptions::from_settings(
1881                            search_embedder.set().unwrap(),
1882                            distribution,
1883                        ),
1884                        index: SubEmbedderOptions::from_settings(
1885                            indexing_embedder.set().unwrap(),
1886                            Setting::NotSet,
1887                        ),
1888                    })
1889                }
1890            };
1891        }
1892
1893        this
1894    }
1895}
1896
1897impl SubEmbedderOptions {
1898    fn from_settings(
1899        settings: SubEmbeddingSettings,
1900        distribution: Setting<DistributionShift>,
1901    ) -> Self {
1902        let SubEmbeddingSettings {
1903            source,
1904            model,
1905            revision,
1906            pooling,
1907            api_key,
1908            dimensions,
1909            // retrieved by the EmbeddingConfig
1910            document_template: _,
1911            document_template_max_bytes: _,
1912            url,
1913            request,
1914            response,
1915            headers,
1916            // phony parameters
1917            distribution: _,
1918            binary_quantized: _,
1919            search_embedder: _,
1920            indexing_embedder: _,
1921        } = settings;
1922
1923        match source.set().unwrap() {
1924            EmbedderSource::OpenAi => Self::openai(model, url, api_key, dimensions, distribution),
1925            EmbedderSource::HuggingFace => {
1926                Self::hugging_face(model, revision, pooling, distribution)
1927            }
1928            EmbedderSource::Ollama => Self::ollama(model, url, api_key, dimensions, distribution),
1929            EmbedderSource::UserProvided => {
1930                Self::user_provided(dimensions.set().unwrap(), distribution)
1931            }
1932            EmbedderSource::Rest => Self::rest(
1933                url.set().unwrap(),
1934                api_key,
1935                request.set().unwrap(),
1936                response.set().unwrap(),
1937                headers,
1938                dimensions,
1939                distribution,
1940            ),
1941            EmbedderSource::Composite => panic!("nested composite embedders"),
1942        }
1943    }
1944
1945    fn openai(
1946        model: Setting<String>,
1947        url: Setting<String>,
1948        api_key: Setting<String>,
1949        dimensions: Setting<usize>,
1950        distribution: Setting<DistributionShift>,
1951    ) -> Self {
1952        let mut options = super::openai::EmbedderOptions::with_default_model(None);
1953        if let Some(model) = model.set() {
1954            if let Some(model) = super::openai::EmbeddingModel::from_name(&model) {
1955                options.embedding_model = model;
1956            }
1957        }
1958        if let Some(url) = url.set() {
1959            options.url = Some(url);
1960        }
1961        if let Some(api_key) = api_key.set() {
1962            options.api_key = Some(api_key);
1963        }
1964        if let Some(dimensions) = dimensions.set() {
1965            options.dimensions = Some(dimensions);
1966        }
1967        options.distribution = distribution.set();
1968        SubEmbedderOptions::OpenAi(options)
1969    }
1970    fn hugging_face(
1971        model: Setting<String>,
1972        revision: Setting<String>,
1973        pooling: Setting<OverridePooling>,
1974        distribution: Setting<DistributionShift>,
1975    ) -> Self {
1976        let mut options = super::hf::EmbedderOptions::default();
1977        if let Some(model) = model.set() {
1978            options.model = model;
1979            // Reset the revision if we are setting the model.
1980            // This allows the following:
1981            // "huggingFace": {} -> default model with default revision
1982            // "huggingFace": { "model": "name-of-the-default-model" } -> default model without a revision
1983            // "huggingFace": { "model": "some-other-model" } -> most importantly, other model without a revision
1984            options.revision = None;
1985        }
1986        if let Some(revision) = revision.set() {
1987            options.revision = Some(revision);
1988        }
1989        if let Some(pooling) = pooling.set() {
1990            options.pooling = pooling;
1991        }
1992        options.distribution = distribution.set();
1993        SubEmbedderOptions::HuggingFace(options)
1994    }
1995    fn user_provided(dimensions: usize, distribution: Setting<DistributionShift>) -> Self {
1996        Self::UserProvided(super::manual::EmbedderOptions {
1997            dimensions,
1998            distribution: distribution.set(),
1999        })
2000    }
2001    fn rest(
2002        url: String,
2003        api_key: Setting<String>,
2004        request: serde_json::Value,
2005        response: serde_json::Value,
2006        headers: Setting<BTreeMap<String, String>>,
2007        dimensions: Setting<usize>,
2008        distribution: Setting<DistributionShift>,
2009    ) -> Self {
2010        Self::Rest(super::rest::EmbedderOptions {
2011            api_key: api_key.set(),
2012            dimensions: dimensions.set(),
2013            url,
2014            request,
2015            response,
2016            distribution: distribution.set(),
2017            headers: headers.set().unwrap_or_default(),
2018        })
2019    }
2020    fn ollama(
2021        model: Setting<String>,
2022        url: Setting<String>,
2023        api_key: Setting<String>,
2024        dimensions: Setting<usize>,
2025        distribution: Setting<DistributionShift>,
2026    ) -> Self {
2027        let mut options: ollama::EmbedderOptions =
2028            super::ollama::EmbedderOptions::with_default_model(
2029                api_key.set(),
2030                url.set(),
2031                dimensions.set(),
2032            );
2033        if let Some(model) = model.set() {
2034            options.embedding_model = model;
2035        }
2036
2037        options.distribution = distribution.set();
2038        SubEmbedderOptions::Ollama(options)
2039    }
2040}
2041
2042impl From<SubEmbedderOptions> for EmbedderOptions {
2043    fn from(value: SubEmbedderOptions) -> Self {
2044        match value {
2045            SubEmbedderOptions::HuggingFace(embedder_options) => {
2046                Self::HuggingFace(embedder_options)
2047            }
2048            SubEmbedderOptions::OpenAi(embedder_options) => Self::OpenAi(embedder_options),
2049            SubEmbedderOptions::Ollama(embedder_options) => Self::Ollama(embedder_options),
2050            SubEmbedderOptions::UserProvided(embedder_options) => {
2051                Self::UserProvided(embedder_options)
2052            }
2053            SubEmbedderOptions::Rest(embedder_options) => Self::Rest(embedder_options),
2054        }
2055    }
2056}