This episode covers embeddings and embedding functions in ChromaDB: the built-in ONNX MiniLM model that runs locally without an API key, switching to sentence-transformers or OpenAI, using pre-computed embeddings, and writing a custom embedding function.

Everything you did in episode 5 runs on top of one component we have not dissected yet: the embedding function. Embeddings are the bridge between human language and vector space — and the quality of this bridge determines everything. A poor embedding model makes relevant documents seem far away; a good model makes semantic search feel accurate.
Episode 6 covers the embedding function from all angles: the ONNX MiniLM default, switching to sentence-transformers and OpenAI, using pre-computed embeddings, and writing a custom function. Let's start with the most commonly used one.
ChromaDB ships with the DefaultEmbeddingFunction based on the ONNX MiniLM model. This model runs entirely locally via onnxruntime — no API key, no internet connection, no per-request cost. This is the main reason ChromaDB has been so easy to use since episode 0.
import chromadb
default_ef = chromadb.utils.embedding_functions.DefaultEmbeddingFunction()
vektor = default_ef(["Halo dunia"])
print(len(vektor[0]))default_ef(["Halo dunia"]) returns a list of vectors — one per text. The MiniLM model produces 384-dimensional vectors, good enough for most use cases and very resource-efficient.
The default suits: prototypes, mixed-language applications with normal needs, and deployments that want to avoid external dependencies. For production RAG with high-accuracy demands, a larger model is usually better — and that is where replacing the embedding function comes in.
Info
The key thing to remember: the embedding function must be consistent between when the data is embedded and when the query is embedded. Replacing the embedding function in a collection that already contains data will make retrieval inaccurate. That function is embedded in the collection — get_collection will reject a different embedding function.
sentence-transformers provides far stronger local models, for example all-MiniLM-L6-v2 or multilingual models like paraphrase-multilingual-MiniLM-L12-v2. ChromaDB provides a built-in adapter:
pip install sentence-transformersfrom chromadb.utils.embedding_functions import SentenceTransformerEmbeddingFunction
ef = SentenceTransformerEmbeddingFunction(model_name="paraphrase-multilingual-MiniLM-L12-v2")
collection = client.create_collection(
name="dokumen-id",
embedding_function=ef,
)SentenceTransformerEmbeddingFunction(model_name="paraphrase-multilingual-MiniLM-L12-v2") uses a model optimized for many languages — highly relevant for Indonesian-language content. This model is downloaded once on first use.
For state-of-the-art quality with larger dimensions, the OpenAI embedding API is a popular choice. ChromaDB provides an adapter that reads the OPENAI_API_KEY environment variable:
export OPENAI_API_KEY="sk-..."from chromadb.utils.embedding_functions import OpenAIEmbeddingFunction
ef = OpenAIEmbeddingFunction(
api_key="sk-...",
model_name="text-embedding-3-small",
)OpenAIEmbeddingFunction(api_key="sk-...", model_name="text-embedding-3-small") produces 1536-dimensional embeddings. Note: every embedding call uses the API — there is a per-token cost and network latency. Consider pre-computed embeddings (below) for large volumes.
For full control and manageable costs, you can embed outside ChromaDB and store the results as embeddings:
import numpy as np
vectors = model.encode(teks_list)
collection.add(
ids=ids_list,
documents=teks_list,
embeddings=vectors.tolist(),
)This pattern separates the embedding pipeline from storage. collection.add(ids=ids_list, documents=teks_list, embeddings=vectors.tolist()) saves repeated calls and makes it easy to use any embedding model — including ones without a ChromaDB adapter.
If your model has no adapter, write a custom class that implements EmbeddingFunction:
from chromadb.api.types import EmbeddingFunction, Documents, Embeddings
class CustomEF(EmbeddingFunction):
def __init__(self, encoder):
self.encoder = encoder
def __call__(self, input: Documents) -> Embeddings:
return self.encoder.encode(input).tolist()
ef = CustomEF(encoder)
collection = client.create_collection(name="custom", embedding_function=ef)The class CustomEF(EmbeddingFunction) simply defines a __call__ that accepts Documents and returns Embeddings. The contract is simple: text in, list of vectors out. This opens ChromaDB up to models from anywhere — Cohere, Gemini, Ollama, even fine-tuned local models.
A summary in table form:
| Embedding Function | Dimensions | Connection | Cost | Best for |
|---|---|---|---|---|
| ONNX MiniLM (default) | 384 | Local | Free | Prototypes, resource efficiency |
| Sentence-Transformers | 384-768 | Local | Free | Better quality, multilingual |
| OpenAI | 1536 | API | Per token | Highest quality |
| Pre-computed custom | Any | Any | Any | Large volumes, custom models |
For Indonesian-language content with a zero budget, the multilingual sentence-transformers combination is the sweet spot. If you have millions of documents, seriously consider pre-computed embeddings in batches.
Episode 6 put you in control of retrieval quality: from the local ONNX MiniLM default, to sentence-transformers for quality and multilingual support, OpenAI for state-of-the-art, pre-computed embeddings for cost control, and custom functions for any model. The embedding function is not an afterthought — it is what determines the quality of semantic search.
Key takeaways:
__call__(Documents) -> Embeddings.In the next episode, episode 7, we will discuss metadata filtering (where) — the $eq, $ne, $in, $nin, $gt, $lt, $gte, $lte operators, $and and $or logic, plus searching within documents with $contains and regex. These filters are what make retrieval precise.