GitHub

Embedders

An embedder turns chunk text into vectors for semantic and hybrid search. RCE Code defines a tiny Embedder protocol and ships four implementations behind it — a zero-dependency offline default, an OpenAI-compatible HTTP adapter, a local sentence-transformers adapter, and a content-addressed disk cache that wraps any of them.

You pick an embedder when you open the index, by passing it to CodeEngine.open_or_create. If you pass nothing, RCE Code uses the offline HashingEmbedder, so semantic and hybrid search work out of the box with no network and no extra dependencies.

choose_embedder.py
from rce.code import CodeEngine
# Default: offline HashingEmbedder — no network, no extra deps.
engine = CodeEngine.open_or_create("/path/to/repo")
# Or supply your own embedder:
# engine = CodeEngine.open_or_create("/path/to/repo", embedder=my_embedder)
engine.index()
engine.embed() # embed chunks that lack a current embedding

The Embedder protocol

Any object with three attributes and one method satisfies the protocol — there is no base class to subclass. embed takes a list of texts and returns an (n, dim) float32 numpy array, one row per input.

rce/code/embed/__init__.py
from typing import Protocol
import numpy as np
class Embedder(Protocol):
model_id: str
model_version: str
dim: int
def embed(self, texts: list[str]) -> np.ndarray: ...

The three identity attributes matter for freshness. An embedding row is stored with its model_id, model_version, and the chunk's content hash, so the embed pipeline re-embeds a chunk only when its content changed or the model identity changed. Bump model_version and the next engine.embed() transparently re-embeds everything.

All embeddings are L2-normalized

Every built-in adapter returns L2-normalized vectors, so cosine similarity reduces to a plain dot product. That is exactly the assumption the vector index makes — if you write a custom embedder, normalize your output too. See Vector Backends.

The built-in adapters

AdapterImportExtra neededWhen to use
HashingEmbedderrce.code.embed.hashingnone (default)Offline tests, CI, getting started — coarse but free
OpenAICompatEmbedderrce.code.embed.openai_compatembeddings-apiOpenAI / Voyage / local Ollama or LM Studio over HTTP
SentenceTransformerEmbedderrce.code.embed.sentence_transformersembeddings-localFully local, GPU/CPU model, no network
CachedEmbedderrce.code.embed.cachednoneWrap any embedder to skip re-embedding identical content

HashingEmbedder (the default)

A deterministic feature-hashing stub: it hashes word tokens into a fixed dim=256 signed vector and L2-normalizes. There is no model and no network, so texts that share tokens get correlated vectors — meaningful, if coarse, similarity for exercising the whole vector pipeline offline. It takes no constructor arguments.

hashing.py
from rce.code import CodeEngine
from rce.code.embed.hashing import HashingEmbedder
# This is the default; passing it explicitly is equivalent to passing nothing.
engine = CodeEngine.open_or_create("/path/to/repo", embedder=HashingEmbedder())
engine.index()
engine.embed()

OpenAICompatEmbedder

Speaks the OpenAI /embeddings HTTP API, which means it works against OpenAI itself, Voyage, or any local server that mirrors that API (Ollama, LM Studio, llama.cpp). You pass the model name and its vector dim positionally, plus a base_url and (optionally) an api_key as keyword arguments. It batches inputs (128 at a time by default) and L2-normalizes the returned vectors.

openai_compat.py
from rce.code import CodeEngine
from rce.code.embed.openai_compat import OpenAICompatEmbedder
# OpenAI
embedder = OpenAICompatEmbedder(
"text-embedding-3-small",
1536,
base_url="https://api.openai.com/v1",
api_key="sk-...",
)
# Local Ollama / LM Studio (OpenAI-compatible endpoint, no key needed)
local = OpenAICompatEmbedder(
"nomic-embed-text",
768,
base_url="http://localhost:11434/v1",
)
engine = CodeEngine.open_or_create("/path/to/repo", embedder=embedder)
engine.index()
engine.embed()

The constructor signature is OpenAICompatEmbedder(model, dim, *, base_url=None, api_key=None, client=None, batch_size=128, timeout=60.0). You must supply a base_url unless you inject your own httpx.Client. The reported model_id is openai-compat:<model>, and the adapter raises if the server returns a different vector dimension than the dim you declared.

SentenceTransformerEmbedder

Runs a sentence-transformers model fully locally. The heavy dependency is lazy-imported, so this module imports fine without it; instantiating the adapter without the package installed raises a clear ImportError pointing you at the optional extra. If you omit dim, it is read from the model.

sentence_transformers.py
from rce.code import CodeEngine
from rce.code.embed.sentence_transformers import SentenceTransformerEmbedder
# dim is inferred from the model when not given.
embedder = SentenceTransformerEmbedder("all-MiniLM-L6-v2")
engine = CodeEngine.open_or_create("/path/to/repo", embedder=embedder)
engine.index()
engine.embed()

The constructor signature is SentenceTransformerEmbedder(model_name, *, dim=None, model=None). It encodes with normalize_embeddings=True so output is L2-normalized, and reports model_id = st:<model_name>. Install it with the embeddings-local extra:

install the local extra
pip install 'rce-code[embeddings-local]'

CachedEmbedder

A wrapper, not an embedder of its own: it takes any inner embedder plus a cache directory and embeds identical content only once — across chunks and across runs. The cache key is blake3(model_id + model_version + text), and each vector is written to a raw .f32 file atomically (temp-then-rename), so an interrupted run cannot leave a torn entry. A missing, unreadable, or wrong-length entry is simply treated as a miss and re-embedded, so the cache self-heals. It mirrors the inner embedder's model_id, model_version, and dim.

cached.py
from pathlib import Path
from rce.code import CodeEngine
from rce.code.embed.cached import CachedEmbedder
from rce.code.embed.openai_compat import OpenAICompatEmbedder
inner = OpenAICompatEmbedder(
"text-embedding-3-small",
1536,
base_url="https://api.openai.com/v1",
api_key="sk-...",
)
embedder = CachedEmbedder(inner, cache_dir=Path(".rce/code/embed-cache"))
engine = CodeEngine.open_or_create("/path/to/repo", embedder=embedder)
engine.index()
engine.embed() # paid API calls only for never-seen content

The constructor signature is CachedEmbedder(inner, cache_dir). Because the cache is keyed by model identity, two models, or two versions of one model, never collide in the same directory.

Combine adapters

Wrapping a paid or slow embedder (OpenAI-compatible or sentence-transformers) in a CachedEmbedder is the common production setup: you pay to embed a given chunk once, and every later re-index of unchanged content is served from disk.

How embedding fits the pipeline

Indexing populates chunks; engine.embed() embeds the ones that need it. The embed pipeline asks the vector index which chunk ids are pending — those with no vector, a stale content hash, or a different model identity — fetches their content in batches, calls your embedder, and upserts the vectors. It returns the number of chunks embedded, so a no-op re-embed returns 0.

embed.py
n = engine.embed()
print(f"embedded {n} chunks")
for hit in engine.search_semantic("retry with backoff", top_k=5):
print(round(hit.score, 3), hit.chunk.file_path, hit.chunk.name)

Where that cosine KNN runs — in numpy or inside SQLite via sqlite-vec — is the subject of Vector Backends.