Skip to main content

haematite_cli/serve/embed/
question.rs

1//! V8b (ruled 5 Oct 09:34): a question in words made into the one vector a
2//! search compares, by a worker that takes questions only.
3//!
4//! The vector is checked against the field's declaration before any search
5//! reads it: exactly one vector, made by the declared model, of the declared
6//! length. Each check fails by the name `make` gives the same failing of a
7//! document worker, so a caller reads one vocabulary for both.
8
9use haem_frames::embedding::Kind;
10
11use super::{Answer, Embedded, Embedder};
12use crate::serve::storage::Failure;
13use crate::serve::storage::embedded::{Declaration, ModelName};
14
15/// A question in, its vector out: what a search needs of a worker.
16pub trait Ask {
17    /// The vectors of `text` embedded as a question, and the model that
18    /// made them.
19    ///
20    /// # Errors
21    /// Whatever the worker answers.
22    fn question(&self, text: &str) -> Result<Embedded, Failure>;
23}
24
25impl Ask for Embedder {
26    fn question(&self, text: &str) -> Result<Embedded, Failure> {
27        self.embed(Kind::Query, &[text.to_owned()])
28    }
29}
30
31/// The vector of `text` as a question for a field declared as
32/// `declaration`.
33///
34/// # Errors
35/// The worker's failure; `embed_vector_count` unless exactly one vector came
36/// back; `embed_model_mismatch` for another model than the declared one;
37/// `text_longer_than_model` for a question of more tokens than the model
38/// reads, never searched by a part of it (EMB1); `embed_vector_length` for a
39/// vector of another length than declared.
40pub fn question_for(
41    asker: &dyn Ask,
42    text: &str,
43    declaration: &Declaration,
44) -> Result<Vec<f32>, Failure> {
45    let embedded = asker.question(text)?;
46    let count = embedded.answers.len();
47    let model = ModelName {
48        name: embedded.model.name,
49        revision: embedded.model.revision,
50        manifest_sha256: embedded.model.manifest_sha256,
51    };
52    let mut answers = embedded.answers.into_iter();
53    let (Some(answer), None) = (answers.next(), answers.next()) else {
54        return Err(Failure::new(
55            "embed_vector_count",
56            format!("one question was sent and {count} vectors came back"),
57        ));
58    };
59    if model != declaration.model {
60        return Err(Failure::new(
61            "embed_model_mismatch",
62            format!(
63                "the question worker runs {}; the field is declared for {}",
64                model.name, declaration.model.name
65            ),
66        ));
67    }
68    let vector = match answer {
69        Answer::Vector(vector) => vector,
70        // A question is never windowed (EMB-E2 Out); a worker that windows
71        // it is answering as though it were a document.
72        Answer::Windows(_) => {
73            return Err(Failure::new(
74                "embed_vector_count",
75                "the question worker answered windows for one question",
76            ));
77        }
78        Answer::Longer(longer) | Answer::OverCap(longer) => {
79            return Err(Failure::new(
80                "text_longer_than_model",
81                format!(
82                    "the question is {} tokens; the model's limit is {}",
83                    longer.tokens_total, longer.tokens_limit
84                ),
85            ));
86        }
87    };
88    let dimensions = declaration.dimensions.get();
89    if vector.len() != dimensions {
90        return Err(Failure::new(
91            "embed_vector_length",
92            format!(
93                "a question vector of {} members came back; {dimensions} are declared",
94                vector.len()
95            ),
96        ));
97    }
98    Ok(vector)
99}
100
101#[cfg(test)]
102#[path = "question_tests.rs"]
103mod tests;