# Asking for an embedding

To search by meaning, your app needs each text's embedding.
`ai.embed(text, as: V)` asks the model that `ai: { models: }` calls
`embedding` for a text's embedding, a `V`. `ai.embed_all(texts, as: V)`
asks for the embeddings of a list of texts at once, in the same order.

The text is a value built on `Text`, or a `Markdown`, never one that may
be `none`. `V` is a vector type with `dimensions:`, as many as
`embedding_dimensions:` says the model makes. So a model that changes
length is caught before a column holds two kinds of vector. Here the
model runs on this machine:

```jadpo
application {
    connections: {
        local_model: {
            driver: Driver.openai_compatible
            url: config.model_url
        }
    }
    ai: {
        connection: local_model
        models: {
            default: config.drafting_model
            embedding: config.search_model
        }
        embedding_dimensions: 768
        limits: {
            per_day: {
                tokens: 2000000
            }
        }
    }
}
```

```jadpo
/// A question's meaning, as the configured embedding model makes it.
type QuestionMeaning = Vector {
    dimensions: 768
}
```

Like `ai.generate`, it is an outside call only an action makes. The
action maps each of its three failures to a failure of the app's, of
kind `unavailable`:

```jadpo
use support/types { CustomerQuestion, QuestionMeaning }
use support/failures { AssistantUnavailable }

/// What a customer's question means, to search the help articles with.
action meaning_of(asked: CustomerQuestion) fails AssistantUnavailable -> QuestionMeaning {
    return attempt ai.embed(asked, as: QuestionMeaning) {
        timed_out: AssistantUnavailable
        over_budget: AssistantUnavailable
        too_long: AssistantUnavailable
    }
}
```

What the server is sent for "Does your sourdough contain sesame?":

```text
POST http://localhost:8000/v1/embeddings
{"input": ["Does your sourdough contain sesame?"], "model": "nomic-ai/nomic-embed-text-v1.5"}
```

- **Each vector is read as `V`,** through `json.read`'s decoding. So a
  vector of another length, or one of zeros where `V` compares by cosine,
  never becomes a `V`. It means the model isn't the one `ai:` says, and
  the call is `misconfigured`. The built-in driver never asks a model for
  fewer dimensions: it sends `model` and `input` alone.
- **The failures:**
  - `timed_out`: no answer within `timeout:`, 5 seconds unless you write
    one. It is counted once in a route's deadline.
  - `too_long`: a text longer than the model takes, or more than 100
    texts in one `embed_all`. Nothing is sent.
  - `over_budget`: the call would pass a limit ([Budgets](safety-and-privacy.md#budgets)).

  `ai.embed_all` of no texts gives no vectors, without asking.
- **In a test,** the `ai` fake makes each embedding from the text's
  SHA-256 and the test's seed. The same text always gives the same vector
  of unit length, and two texts give vectors far apart, so a search for a
  text finds what was stored for the same text. `fakes: { ai.embed: [...] }`
  scripts the failures of `ai.embed` and `ai.embed_all` calls, in the
  order they are made:

```jadpo
use support/types { QuestionMeaning }
use support/failures { AssistantUnavailable }

test "the same question always means the same" {
    var asked: QuestionMeaning = attempt meaning_of("Does your sourdough contain sesame?")
    var asked_twice: QuestionMeaning = attempt meaning_of("Does your sourdough contain sesame?")
    var other: QuestionMeaning = attempt meaning_of("Do you deliver to Jersey?")
    assert asked == asked_twice
    assert ai.distance(asked, other) > 0.5
}

test "a question the model can't embed in time goes to a person" {
    fakes: {
        ai.embed: [timed_out]
    }
    assert meaning_of("Sesame?") fails AssistantUnavailable
}
```
