milli_core/vector/
error.rs

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    // hf
466    #[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}