> For the complete documentation index, see [llms.txt](https://docs.telys.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.telys.ai/embeddings/recipe-bge.md).

# BGE recipe

Wrap a local `sentence-transformers` BGE model in a [`CallableEmbedder`](/embeddings/callable-byo.md). After the weights download once, the whole pipeline — embedding, indexing, and search — runs offline: no API key, no per-call cost, nothing leaves the host.

This recipe uses `BAAI/bge-base-en-v1.5`, which is **768-dimensional**. Always match your profile's `dimension` to the model you load: `bge-small-en-v1.5` is 384-d and `bge-large-en-v1.5` is 1024-d.

## Prerequisites

```bash
pip install telys sentence-transformers numpy
telys login            # free telys_developer tier
telys runtime verify   # confirm the signed runtime is active
```

The first `SentenceTransformer(...)` call downloads the weights; later runs load them from cache and need no network.

## The full script

```python
import numpy as np
from sentence_transformers import SentenceTransformer
from telys import Telys
from telys.embedding import CallableEmbedder, EmbeddingProfile

MODEL = "BAAI/bge-base-en-v1.5"
DIM = 768
model = SentenceTransformer(MODEL)

# BGE v1.5 recommends prefixing the QUERY (not documents) with a retrieval instruction.
INSTRUCTION = "Represent this sentence for searching relevant passages: "

def embed_docs(texts):
    return model.encode(list(texts), normalize_embeddings=True).astype("float32")

def embed_query(texts):
    prompted = [INSTRUCTION + t for t in texts]
    return model.encode(prompted, normalize_embeddings=True).astype("float32")

profile = EmbeddingProfile(
    provider="bge",
    model_id=MODEL,
    model_version="1.5",
    dimension=DIM,
    normalization="l2",                 # normalize_embeddings=True returns unit vectors
    distance="ip",                      # inner product == cosine on unit vectors
)
embedder = CallableEmbedder(embed_docs, profile, query_fn=embed_query)

ame = Telys("./telys-bge")
col = ame.create_collection("docs", dim=DIM, partition_by="scope", embedder=embedder,
                            filter_columns=("scope",))  # declare "scope" to read it back via with_metadata

col.add_texts(
    texts=[
        "Telys runs entirely on-device.",
        "Vectors are stored locally and never uploaded.",
        "The signed runtime is verified before it loads.",
    ],
    ids=["a", "b", "c"],
    metadata=[{"scope": "demo"}, {"scope": "demo"}, {"scope": "demo"}],
)
col.save()

hits = col.search_text("is my data uploaded anywhere?", top_k=2, with_metadata=True)
for doc_id, score, meta in zip(hits["ids"], hits["scores"], hits["metadata"]):
    print(f"{doc_id}  {score:.3f}  {meta}")
```

## What you will see

The two nearest documents, ranked by score. Because `query_fn` prepends the BGE instruction, `search_text` embeds the query differently from the stored documents while both land in the same 768-d space:

```
b  0.744  {'scope': 'demo'}
a  0.681  {'scope': 'demo'}
```

Scores depend on the model, but the ordering is stable. `normalize_embeddings=True` makes each vector unit length, so `distance="ip"` behaves as cosine similarity.

{% hint style="info" %}
`CallableEmbedder` never normalizes for you — it stores exactly what your function returns. If you drop `normalize_embeddings=True`, either normalize inside the function or set `normalization="none"` and `distance="ip"` and accept raw inner-product scores.
{% endhint %}

## Reopen later

```python
ame = Telys("./telys-bge")
ame.register_provider(embedder.profile.model_id, embedder)   # key must equal model_id (or space_id()) to auto-attach on open
col = ame.open_collection("docs")
print(len(col))
```

## Next steps

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>Recipe: OpenAI embeddings</strong></td><td>The same pattern against a hosted embedding API.</td><td><a href="/pages/osxpAbHUV1iJmzTnmdX3">/pages/osxpAbHUV1iJmzTnmdX3</a></td></tr><tr><td><strong>CallableEmbedder</strong></td><td>The BYO wrapper and its fn/query_fn contract.</td><td><a href="/pages/Xzd0BXJbxW07haNz0M1J">/pages/Xzd0BXJbxW07haNz0M1J</a></td></tr><tr><td><strong>Dimensions &#x26; space IDs</strong></td><td>Why the model's dimension must match your collection.</td><td><a href="/pages/MIVK3hfr15xmitNeowAz">/pages/MIVK3hfr15xmitNeowAz</a></td></tr></tbody></table>

Next: compare this with a hosted API in the [OpenAI recipe](/embeddings/recipe-openai.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.telys.ai/embeddings/recipe-bge.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
