1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use bumpalo::Bump;
5use hf_hub::api::sync::ApiError;
6
7use super::parsed_vectors::ParsedVectorsDiff;
8use super::rest::ConfigurationSource;
9use super::MAX_COMPOSITE_DISTANCE;
10use crate::error::FaultSource;
11use crate::update::new::vector_document::VectorDocument;
12use crate::{FieldDistribution, PanicCatched};
13
14#[derive(Debug, thiserror::Error)]
15#[error("Error while generating embeddings: {inner}")]
16pub struct Error {
17 pub inner: Box<ErrorKind>,
18}
19
20impl<I: Into<ErrorKind>> From<I> for Error {
21 fn from(value: I) -> Self {
22 Self { inner: Box::new(value.into()) }
23 }
24}
25
26impl Error {
27 pub fn fault(&self) -> FaultSource {
28 match &*self.inner {
29 ErrorKind::NewEmbedderError(inner) => inner.fault,
30 ErrorKind::EmbedError(inner) => inner.fault,
31 }
32 }
33}
34
35#[derive(Debug, thiserror::Error)]
36pub enum ErrorKind {
37 #[error(transparent)]
38 NewEmbedderError(#[from] NewEmbedderError),
39 #[error(transparent)]
40 EmbedError(#[from] EmbedError),
41}
42
43#[derive(Debug, thiserror::Error)]
44#[error("{fault}: {kind}")]
45pub struct EmbedError {
46 pub kind: EmbedErrorKind,
47 pub fault: FaultSource,
48}
49
50#[derive(Debug, thiserror::Error)]
51pub enum EmbedErrorKind {
52 #[error("could not tokenize:\n - {0}")]
53 Tokenize(Box<dyn std::error::Error + Send + Sync>),
54 #[error("unexpected tensor shape:\n - {0}")]
55 TensorShape(candle_core::Error),
56 #[error("unexpected tensor value:\n - {0}")]
57 TensorValue(candle_core::Error),
58 #[error("could not run model:\n - {0}")]
59 ModelForward(candle_core::Error),
60 #[error("attempt to embed the following text in a configuration where embeddings must be user provided:\n - `{0}`")]
61 ManualEmbed(String),
62 #[error("model not found. Meilisearch will not automatically download models from the Ollama library, please pull the model manually{}", option_info(.0.as_deref(), "server replied with "))]
63 OllamaModelNotFoundError(Option<String>),
64 #[error("error deserializing the response body as JSON:\n - {0}")]
65 RestResponseDeserialization(std::io::Error),
66 #[error("expected a response containing {0} embeddings, got only {1}")]
67 RestResponseEmbeddingCount(usize, usize),
68 #[error("could not authenticate against {embedding} server{server_reply}{hint}", embedding=match *.1 {
69 ConfigurationSource::User => "embedding",
70 ConfigurationSource::OpenAi => "OpenAI",
71 ConfigurationSource::Ollama => "Ollama"
72 },
73 server_reply=option_info(.0.as_deref(), "server replied with "),
74 hint=match *.1 {
75 ConfigurationSource::User => "\n - Hint: Check the `apiKey` parameter in the embedder configuration",
76 ConfigurationSource::OpenAi => "\n - Hint: Check the `apiKey` parameter in the embedder configuration, and the `MEILI_OPENAI_API_KEY` and `OPENAI_API_KEY` environment variables",
77 ConfigurationSource::Ollama => "\n - Hint: Check the `apiKey` parameter in the embedder configuration"
78 })]
79 RestUnauthorized(Option<String>, ConfigurationSource),
80 #[error("sent too many requests to embedding server{}", option_info(.0.as_deref(), "server replied with "))]
81 RestTooManyRequests(Option<String>),
82 #[error("sent a bad request to embedding server{}{}",
83 if ConfigurationSource::User == *.1 {
84 "\n - Hint: check that the `request` in the embedder configuration matches the remote server's API"
85 } else {
86 ""
87 },
88 option_info(.0.as_deref(), "server replied with "))]
89 RestBadRequest(Option<String>, ConfigurationSource),
90 #[error("received internal error HTTP {} from embedding server{}", .0, option_info(.1.as_deref(), "server replied with "))]
91 RestInternalServerError(u16, Option<String>),
92 #[error("received unexpected HTTP {} from embedding server{}", .0, option_info(.1.as_deref(), "server replied with "))]
93 RestOtherStatusCode(u16, Option<String>),
94 #[error("could not reach embedding server:\n - {0}")]
95 RestNetwork(ureq::Transport),
96 #[error("error extracting embeddings from the response:\n - {0}")]
97 RestExtractionError(String),
98 #[error("was expecting embeddings of dimension `{0}`, got embeddings of dimensions `{1}`")]
99 UnexpectedDimension(usize, usize),
100 #[error("no embedding was produced")]
101 MissingEmbedding,
102 #[error(transparent)]
103 PanicInThreadPool(#[from] PanicCatched),
104}
105
106fn option_info(info: Option<&str>, prefix: &str) -> String {
107 match info {
108 Some(info) => format!("\n - {prefix}`{info}`"),
109 None => String::new(),
110 }
111}
112
113impl EmbedError {
114 pub fn tokenize(inner: Box<dyn std::error::Error + Send + Sync>) -> Self {
115 Self { kind: EmbedErrorKind::Tokenize(inner), fault: FaultSource::Runtime }
116 }
117
118 pub fn tensor_shape(inner: candle_core::Error) -> Self {
119 Self { kind: EmbedErrorKind::TensorShape(inner), fault: FaultSource::Bug }
120 }
121
122 pub fn tensor_value(inner: candle_core::Error) -> Self {
123 Self { kind: EmbedErrorKind::TensorValue(inner), fault: FaultSource::Bug }
124 }
125
126 pub fn model_forward(inner: candle_core::Error) -> Self {
127 Self { kind: EmbedErrorKind::ModelForward(inner), fault: FaultSource::Runtime }
128 }
129
130 pub(crate) fn embed_on_manual_embedder(texts: String) -> EmbedError {
131 Self { kind: EmbedErrorKind::ManualEmbed(texts), fault: FaultSource::User }
132 }
133
134 pub(crate) fn ollama_model_not_found(inner: Option<String>) -> EmbedError {
135 Self { kind: EmbedErrorKind::OllamaModelNotFoundError(inner), fault: FaultSource::User }
136 }
137
138 pub(crate) fn rest_response_deserialization(error: std::io::Error) -> EmbedError {
139 Self {
140 kind: EmbedErrorKind::RestResponseDeserialization(error),
141 fault: FaultSource::Runtime,
142 }
143 }
144
145 pub(crate) fn rest_response_embedding_count(expected: usize, got: usize) -> EmbedError {
146 Self {
147 kind: EmbedErrorKind::RestResponseEmbeddingCount(expected, got),
148 fault: FaultSource::Runtime,
149 }
150 }
151
152 pub(crate) fn rest_unauthorized(
153 error_response: Option<String>,
154 configuration_source: ConfigurationSource,
155 ) -> EmbedError {
156 Self {
157 kind: EmbedErrorKind::RestUnauthorized(error_response, configuration_source),
158 fault: FaultSource::User,
159 }
160 }
161
162 pub(crate) fn rest_too_many_requests(error_response: Option<String>) -> EmbedError {
163 Self {
164 kind: EmbedErrorKind::RestTooManyRequests(error_response),
165 fault: FaultSource::Runtime,
166 }
167 }
168
169 pub(crate) fn rest_bad_request(
170 error_response: Option<String>,
171 configuration_source: ConfigurationSource,
172 ) -> EmbedError {
173 Self {
174 kind: EmbedErrorKind::RestBadRequest(error_response, configuration_source),
175 fault: FaultSource::User,
176 }
177 }
178
179 pub(crate) fn rest_internal_server_error(
180 code: u16,
181 error_response: Option<String>,
182 ) -> EmbedError {
183 Self {
184 kind: EmbedErrorKind::RestInternalServerError(code, error_response),
185 fault: FaultSource::Runtime,
186 }
187 }
188
189 pub(crate) fn rest_other_status_code(code: u16, error_response: Option<String>) -> EmbedError {
190 Self {
191 kind: EmbedErrorKind::RestOtherStatusCode(code, error_response),
192 fault: FaultSource::Undecided,
193 }
194 }
195
196 pub(crate) fn rest_network(transport: ureq::Transport) -> EmbedError {
197 Self { kind: EmbedErrorKind::RestNetwork(transport), fault: FaultSource::Runtime }
198 }
199
200 pub(crate) fn rest_unexpected_dimension(expected: usize, got: usize) -> EmbedError {
201 Self {
202 kind: EmbedErrorKind::UnexpectedDimension(expected, got),
203 fault: FaultSource::Runtime,
204 }
205 }
206 pub(crate) fn missing_embedding() -> EmbedError {
207 Self { kind: EmbedErrorKind::MissingEmbedding, fault: FaultSource::Undecided }
208 }
209
210 pub(crate) fn rest_extraction_error(error: String) -> EmbedError {
211 Self { kind: EmbedErrorKind::RestExtractionError(error), fault: FaultSource::Runtime }
212 }
213}
214
215#[derive(Debug, thiserror::Error)]
216#[error("{fault}: {kind}")]
217pub struct NewEmbedderError {
218 pub kind: NewEmbedderErrorKind,
219 pub fault: FaultSource,
220}
221
222impl NewEmbedderError {
223 pub fn open_config(config_filename: PathBuf, inner: std::io::Error) -> NewEmbedderError {
224 let open_config = OpenConfig { filename: config_filename, inner };
225
226 Self { kind: NewEmbedderErrorKind::OpenConfig(open_config), fault: FaultSource::Runtime }
227 }
228
229 pub fn deserialize_config(
230 model_name: String,
231 config: String,
232 config_filename: PathBuf,
233 inner: serde_json::Error,
234 ) -> NewEmbedderError {
235 match serde_json::from_str(&config) {
236 Ok(value) => {
237 let value: serde_json::Value = value;
238 let architectures = match value.get("architectures") {
239 Some(serde_json::Value::Array(architectures)) => architectures
240 .iter()
241 .filter_map(|value| match value {
242 serde_json::Value::String(s) => Some(s.to_owned()),
243 _ => None,
244 })
245 .collect(),
246 _ => vec![],
247 };
248
249 let unsupported_model = UnsupportedModel { model_name, inner, architectures };
250 Self {
251 kind: NewEmbedderErrorKind::UnsupportedModel(unsupported_model),
252 fault: FaultSource::User,
253 }
254 }
255 Err(error) => {
256 let deserialize_config =
257 DeserializeConfig { model_name, filename: config_filename, inner: error };
258 Self {
259 kind: NewEmbedderErrorKind::DeserializeConfig(deserialize_config),
260 fault: FaultSource::Runtime,
261 }
262 }
263 }
264 }
265
266 pub fn open_pooling_config(
267 pooling_config_filename: PathBuf,
268 inner: std::io::Error,
269 ) -> NewEmbedderError {
270 let open_config = OpenPoolingConfig { filename: pooling_config_filename, inner };
271
272 Self {
273 kind: NewEmbedderErrorKind::OpenPoolingConfig(open_config),
274 fault: FaultSource::Runtime,
275 }
276 }
277
278 pub fn deserialize_pooling_config(
279 model_name: String,
280 pooling_config_filename: PathBuf,
281 inner: serde_json::Error,
282 ) -> NewEmbedderError {
283 let deserialize_pooling_config =
284 DeserializePoolingConfig { model_name, filename: pooling_config_filename, inner };
285 Self {
286 kind: NewEmbedderErrorKind::DeserializePoolingConfig(deserialize_pooling_config),
287 fault: FaultSource::Runtime,
288 }
289 }
290
291 pub fn open_tokenizer(
292 tokenizer_filename: PathBuf,
293 inner: Box<dyn std::error::Error + Send + Sync>,
294 ) -> NewEmbedderError {
295 let open_tokenizer = OpenTokenizer { filename: tokenizer_filename, inner };
296 Self {
297 kind: NewEmbedderErrorKind::OpenTokenizer(open_tokenizer),
298 fault: FaultSource::Runtime,
299 }
300 }
301
302 pub fn new_api_fail(inner: ApiError) -> Self {
303 Self { kind: NewEmbedderErrorKind::NewApiFail(inner), fault: FaultSource::Bug }
304 }
305
306 pub fn api_get(inner: ApiError) -> Self {
307 Self { kind: NewEmbedderErrorKind::ApiGet(inner), fault: FaultSource::Undecided }
308 }
309
310 pub fn pytorch_weight(inner: candle_core::Error) -> Self {
311 Self { kind: NewEmbedderErrorKind::PytorchWeight(inner), fault: FaultSource::Runtime }
312 }
313
314 pub fn safetensor_weight(inner: candle_core::Error) -> Self {
315 Self { kind: NewEmbedderErrorKind::SafetensorWeight(inner), fault: FaultSource::Runtime }
316 }
317
318 pub fn load_model(inner: candle_core::Error) -> Self {
319 Self { kind: NewEmbedderErrorKind::LoadModel(inner), fault: FaultSource::Runtime }
320 }
321
322 pub fn could_not_determine_dimension(inner: EmbedError) -> NewEmbedderError {
323 Self {
324 kind: NewEmbedderErrorKind::CouldNotDetermineDimension(inner),
325 fault: FaultSource::Runtime,
326 }
327 }
328
329 pub(crate) fn rest_could_not_parse_template(message: String) -> NewEmbedderError {
330 Self {
331 kind: NewEmbedderErrorKind::CouldNotParseTemplate(message),
332 fault: FaultSource::User,
333 }
334 }
335
336 pub(crate) fn ollama_unsupported_url(url: String) -> NewEmbedderError {
337 Self { kind: NewEmbedderErrorKind::OllamaUnsupportedUrl(url), fault: FaultSource::User }
338 }
339
340 pub(crate) fn composite_dimensions_mismatch(
341 search_dimensions: usize,
342 index_dimensions: usize,
343 ) -> NewEmbedderError {
344 Self {
345 kind: NewEmbedderErrorKind::CompositeDimensionsMismatch {
346 search_dimensions,
347 index_dimensions,
348 },
349 fault: FaultSource::User,
350 }
351 }
352
353 pub(crate) fn composite_test_embedding_failed(
354 inner: EmbedError,
355 failing_embedder: &'static str,
356 ) -> NewEmbedderError {
357 Self {
358 kind: NewEmbedderErrorKind::CompositeTestEmbeddingFailed { inner, failing_embedder },
359 fault: FaultSource::Runtime,
360 }
361 }
362
363 pub(crate) fn composite_embedding_count_mismatch(
364 search_count: usize,
365 index_count: usize,
366 ) -> NewEmbedderError {
367 Self {
368 kind: NewEmbedderErrorKind::CompositeEmbeddingCountMismatch {
369 search_count,
370 index_count,
371 },
372 fault: FaultSource::Runtime,
373 }
374 }
375
376 pub(crate) fn composite_embedding_value_mismatch(
377 distance: f32,
378 hint: CompositeEmbedderContainsHuggingFace,
379 ) -> NewEmbedderError {
380 Self {
381 kind: NewEmbedderErrorKind::CompositeEmbeddingValueMismatch { distance, hint },
382 fault: FaultSource::User,
383 }
384 }
385}
386
387#[derive(Debug, Clone, Copy)]
388pub enum CompositeEmbedderContainsHuggingFace {
389 Both,
390 Search,
391 Indexing,
392 None,
393}
394
395impl std::fmt::Display for CompositeEmbedderContainsHuggingFace {
396 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397 match self {
398 CompositeEmbedderContainsHuggingFace::Both => f.write_str(
399 "\n - Make sure the `model`, `revision` and `pooling` of both embedders match.",
400 ),
401 CompositeEmbedderContainsHuggingFace::Search => f.write_str(
402 "\n - Consider trying a different `pooling` method for the search embedder.",
403 ),
404 CompositeEmbedderContainsHuggingFace::Indexing => f.write_str(
405 "\n - Consider trying a different `pooling` method for the indexing embedder.",
406 ),
407 CompositeEmbedderContainsHuggingFace::None => Ok(()),
408 }
409 }
410}
411
412#[derive(Debug, thiserror::Error)]
413#[error("could not open config at {filename}: {inner}")]
414pub struct OpenConfig {
415 pub filename: PathBuf,
416 pub inner: std::io::Error,
417}
418
419#[derive(Debug, thiserror::Error)]
420#[error("could not open pooling config at {filename}: {inner}")]
421pub struct OpenPoolingConfig {
422 pub filename: PathBuf,
423 pub inner: std::io::Error,
424}
425
426#[derive(Debug, thiserror::Error)]
427#[error("for model '{model_name}', could not deserialize config at {filename} as JSON: {inner}")]
428pub struct DeserializeConfig {
429 pub model_name: String,
430 pub filename: PathBuf,
431 pub inner: serde_json::Error,
432}
433
434#[derive(Debug, thiserror::Error)]
435#[error("for model '{model_name}', could not deserialize file at `{filename}` as a pooling config: {inner}")]
436pub struct DeserializePoolingConfig {
437 pub model_name: String,
438 pub filename: PathBuf,
439 pub inner: serde_json::Error,
440}
441
442#[derive(Debug, thiserror::Error)]
443#[error("model `{model_name}` appears to be unsupported{}\n - inner error: {inner}",
444if architectures.is_empty() {
445 "\n - Note: only models with architecture \"BertModel\" are supported.".to_string()
446} else {
447 format!("\n - Note: model has declared architectures `{architectures:?}`, only models with architecture `\"BertModel\"` are supported.")
448})]
449pub struct UnsupportedModel {
450 pub model_name: String,
451 pub inner: serde_json::Error,
452 pub architectures: Vec<String>,
453}
454
455#[derive(Debug, thiserror::Error)]
456#[error("could not open tokenizer at {filename}: {inner}")]
457pub struct OpenTokenizer {
458 pub filename: PathBuf,
459 #[source]
460 pub inner: Box<dyn std::error::Error + Send + Sync>,
461}
462
463#[derive(Debug, thiserror::Error)]
464pub enum NewEmbedderErrorKind {
465 #[error(transparent)]
467 OpenConfig(OpenConfig),
468 #[error(transparent)]
469 OpenPoolingConfig(OpenPoolingConfig),
470 #[error(transparent)]
471 DeserializeConfig(DeserializeConfig),
472 #[error(transparent)]
473 DeserializePoolingConfig(DeserializePoolingConfig),
474 #[error(transparent)]
475 UnsupportedModel(UnsupportedModel),
476 #[error(transparent)]
477 OpenTokenizer(OpenTokenizer),
478 #[error("could not build weights from Pytorch weights:\n - {0}")]
479 PytorchWeight(candle_core::Error),
480 #[error("could not build weights from Safetensor weights:\n - {0}")]
481 SafetensorWeight(candle_core::Error),
482 #[error("could not spawn HG_HUB API client:\n - {0}")]
483 NewApiFail(ApiError),
484 #[error("fetching file from HG_HUB failed:\n - {0}")]
485 ApiGet(ApiError),
486 #[error("could not determine model dimensions:\n - test embedding failed with {0}")]
487 CouldNotDetermineDimension(EmbedError),
488 #[error("loading model failed:\n - {0}")]
489 LoadModel(candle_core::Error),
490 #[error("{0}")]
491 CouldNotParseTemplate(String),
492 #[error("unsupported Ollama URL.\n - For `ollama` sources, the URL must end with `/api/embed` or `/api/embeddings`\n - Got `{0}`")]
493 OllamaUnsupportedUrl(String),
494 #[error("error while generating test embeddings.\n - the dimensions of embeddings produced at search time and at indexing time don't match.\n - Search time dimensions: {search_dimensions}\n - Indexing time dimensions: {index_dimensions}\n - Note: Dimensions of embeddings produced by both embedders are required to match.")]
495 CompositeDimensionsMismatch { search_dimensions: usize, index_dimensions: usize },
496 #[error("error while generating test embeddings.\n - could not generate test embedding with embedder at {failing_embedder} time.\n - Embedding failed with {inner}")]
497 CompositeTestEmbeddingFailed { inner: EmbedError, failing_embedder: &'static str },
498 #[error("error while generating test embeddings.\n - the number of generated embeddings differs.\n - {search_count} embeddings for the search time embedder.\n - {index_count} embeddings for the indexing time embedder.")]
499 CompositeEmbeddingCountMismatch { search_count: usize, index_count: usize },
500 #[error("error while generating test embeddings.\n - the embeddings produced at search time and indexing time are not similar enough.\n - angular distance {distance:.2}\n - Meilisearch requires a maximum distance of {MAX_COMPOSITE_DISTANCE}.\n - Note: check that both embedders produce similar embeddings.{hint}")]
501 CompositeEmbeddingValueMismatch { distance: f32, hint: CompositeEmbedderContainsHuggingFace },
502}
503
504pub struct PossibleEmbeddingMistakes {
505 vectors_mistakes: BTreeMap<String, u64>,
506}
507
508impl PossibleEmbeddingMistakes {
509 pub fn new(field_distribution: &FieldDistribution) -> Self {
510 let mut vectors_mistakes = BTreeMap::new();
511 let builder = levenshtein_automata::LevenshteinAutomatonBuilder::new(2, true);
512 let automata = builder.build_dfa("_vectors");
513 for (field, count) in field_distribution {
514 if *count == 0 {
515 continue;
516 }
517 if field.contains('.') {
518 continue;
519 }
520 match automata.eval(field) {
521 levenshtein_automata::Distance::Exact(0) => continue,
522 levenshtein_automata::Distance::Exact(_) => {
523 vectors_mistakes.insert(field.to_string(), *count);
524 }
525 levenshtein_automata::Distance::AtLeast(_) => continue,
526 }
527 }
528
529 Self { vectors_mistakes }
530 }
531
532 pub fn vector_mistakes(&self) -> impl Iterator<Item = (&str, u64)> {
533 self.vectors_mistakes.iter().map(|(misspelling, count)| (misspelling.as_str(), *count))
534 }
535
536 pub fn embedder_mistakes<'a>(
537 &'a self,
538 embedder_name: &'a str,
539 unused_vectors_distributions: &'a UnusedVectorsDistribution,
540 ) -> impl Iterator<Item = (&'a str, u64)> + 'a {
541 let builder = levenshtein_automata::LevenshteinAutomatonBuilder::new(2, true);
542 let automata = builder.build_dfa(embedder_name);
543
544 unused_vectors_distributions.0.iter().filter_map(move |(field, count)| {
545 match automata.eval(field) {
546 levenshtein_automata::Distance::Exact(0) => None,
547 levenshtein_automata::Distance::Exact(_) => Some((field.as_str(), *count)),
548 levenshtein_automata::Distance::AtLeast(_) => None,
549 }
550 })
551 }
552
553 pub fn embedder_mistakes_bump<'a, 'doc: 'a>(
554 &'a self,
555 embedder_name: &'a str,
556 unused_vectors_distribution: &'a UnusedVectorsDistributionBump<'doc>,
557 ) -> impl Iterator<Item = (&'a str, u64)> + 'a {
558 let builder = levenshtein_automata::LevenshteinAutomatonBuilder::new(2, true);
559 let automata = builder.build_dfa(embedder_name);
560
561 unused_vectors_distribution.0.iter().filter_map(move |(field, count)| {
562 match automata.eval(field) {
563 levenshtein_automata::Distance::Exact(0) => None,
564 levenshtein_automata::Distance::Exact(_) => Some((*field, *count)),
565 levenshtein_automata::Distance::AtLeast(_) => None,
566 }
567 })
568 }
569}
570
571#[derive(Default)]
572pub struct UnusedVectorsDistribution(BTreeMap<String, u64>);
573
574impl UnusedVectorsDistribution {
575 pub fn new() -> Self {
576 Self::default()
577 }
578
579 pub fn append(&mut self, parsed_vectors_diff: ParsedVectorsDiff) {
580 for name in parsed_vectors_diff.into_new_vectors_keys_iter() {
581 *self.0.entry(name).or_default() += 1;
582 }
583 }
584}
585
586pub struct UnusedVectorsDistributionBump<'doc>(
587 hashbrown::HashMap<&'doc str, u64, hashbrown::DefaultHashBuilder, &'doc Bump>,
588);
589
590impl<'doc> UnusedVectorsDistributionBump<'doc> {
591 pub fn new_in(doc_alloc: &'doc Bump) -> Self {
592 Self(hashbrown::HashMap::new_in(doc_alloc))
593 }
594
595 pub fn append(&mut self, vectors: &impl VectorDocument<'doc>) -> Result<(), crate::Error> {
596 for res in vectors.iter_vectors() {
597 let (embedder_name, entry) = res?;
598 if !entry.has_configured_embedder {
599 *self.0.entry(embedder_name).or_default() += 1;
600 }
601 }
602 Ok(())
603 }
604}