Create a persistent Chroma collection
GitHub: https://github.com/scossar/find_command/blob/master/create_embeddings.py
Embed the descriptions of tmux key bindings from the tmux_key_bindings.sqlite3 database (populated here:
populate.py
) and save them to a
persistent Chroma collection
.
"""Embed tmux descriptions and save them in a persistent Chroma collection."""
import argparse
import sqlite3
from contextlib import closing
from pathlib import Path
from typing import cast
import chromadb
from chromadb.api.types import Embeddable, EmbeddingFunction
from chromadb.utils.embedding_functions import DefaultEmbeddingFunction
def create_embeddings(
database: Path,
chroma_path: Path,
collection_name: str = "tmux-key-bindings",
) -> int:
"""Generate and upsert embeddings for the descriptions stored in SQLite."""
with closing(
sqlite3.connect(database.resolve().as_uri() + "?mode=ro", uri=True)
) as connection:
connection.row_factory = sqlite3.Row
rows = connection.execute("""
SELECT id, key, description, prefix, tmux_version, source
FROM key_bindings
ORDER BY id
""").fetchall()
if not rows:
return 0
# Chroma's default model is all-MiniLM-L6-v2, running locally via ONNX.
# The model files are downloaded and cached on the first use if needed.
embedding_function = DefaultEmbeddingFunction()
client = chromadb.PersistentClient(path=str(chroma_path))
# Chroma types this parameter as accepting documents OR images, but its
# default embedding function accepts only documents. This collection is
# text-only, so cast at the API boundary to bridge that typing mismatch.
# cast does not change the function or add image support at runtime.
collection = client.get_or_create_collection(
name=collection_name,
embedding_function=cast(EmbeddingFunction[Embeddable], embedding_function),
metadata={"embedding_model": "all-MiniLM-L6-v2"},
)
# Batching limits how many descriptions are embedded and written at once.
for start in range(0, len(rows), 128):
batch = rows[start : start + 128]
documents = [row["description"] for row in batch]
# This converts each original description into a numeric vector.
# Do not stem the text: the model works with the original language.
embeddings = embedding_function(documents)
# Store both the vectors and original text. The key is a stable ID
# within this dataset; metadata connects results back to SQLite.
# Upsert inserts new IDs and updates existing IDs without duplicates.
collection.upsert(
ids=[f"tmux:{row['key']}" for row in batch],
embeddings=embeddings,
documents=documents,
metadatas=[
{
"sqlite_id": row["id"],
"key": row["key"],
"prefix": row["prefix"],
"tmux_version": row["tmux_version"],
"source": row["source"],
}
for row in batch
],
)
return len(rows)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--database",
type=Path,
default=Path("tmux_key_bindings.sqlite3"),
help="Populated SQLite source (default: tmux_key_bindings.sqlite3)",
)
parser.add_argument(
"--chroma",
type=Path,
default=Path("data/chroma"),
help="Persistent Chroma directory (default: data/chroma)",
)
parser.add_argument(
"--collection",
default="tmux-key-bindings",
help="Chroma collection name (default: tmux-key-bindings)",
)
args = parser.parse_args()
count = create_embeddings(args.database, args.chroma, args.collection)
print(f"Stored {count} descriptions and embeddings in {args.chroma} ({args.collection}).")
if __name__ == "__main__":
main()
The Chroma embedding function #
What’s going on with:
embedding_function = DefaultEmbeddingFunction()
# ...
embeddings = embedding_function(documents)
I’ll ignore the details related to batch processing.
documents is the value of the "description" rows from the tmux_key_bindings.sqlite3 database:
- “Send the prefix key (C-b) through to the application.”
- “Rotate the panes in the current window forwards.”
- “Suspend the tmux client.”
- etc.
Chroma DefaultEmbeddingFunction #
Docs: https://docs.trychroma.com/docs/embeddings/embedding-functions#default-all-minilm-l6-v2
Chroma’s default embedding function uses the Sentence Transformers all-MiniLM-L6-v2 model to create embeddings. When the code calls embedding_function(documents), the flow is going to be something like:
Python strings
↓
tokenizer
↓
token IDs / attention masks
↓
all-MiniLM-L6-v2
↓
token-level representations
↓
pooling / normalization
↓
one 384-dimensional vector per document
The attention mask is dealing with padding that’s added to input tokens so that all inputs have the same length. E.g., shorter sequences get padded with zeros:
[101, 2243, 345, 9]
[101, 23, 885, 0] # shorter sequence padded with a zero
The corresponding masks might be:
[1, 1, 1, 1]
[1, 1, 1, 0] # the 0 tells the model that the final value shouldn't participate in attention or pooling
(I thought the masks were created inside the model(?). In any case, it’s an implementation detail.)
Difference between a sentence-embedding model and a “normal” GPT model #
The difference is the “pool into one vector” step:
text
→ tokens
→ token IDs
→ learned token embeddings
→ transformer
→ contextual token representations
→ pooling # the difference starts here
→ one vector for the whole text input
What does get_or_create_collection create? #
It creates an SQLite database and some binary files:
find_command/data master
find-command ❯ tree
.
├── chroma
│ ├── 0ce30325-7299-4b73-94fd-573c1d2488e8
│ │ ├── data_level0.bin
│ │ ├── header.bin
│ │ ├── length.bin
│ │ └── link_lists.bin
│ └── chroma.sqlite3
The chroma.sqlite3 database has a lot of tables:
sqlite> .tables
acquire_write embedding_fulltext_search_config embedding_metadata maintenance_log tenants
collection_metadata embedding_fulltext_search_content embedding_metadata_array max_seq_id
collections embedding_fulltext_search_data embeddings migrations
databases embedding_fulltext_search_docsize embeddings_queue segment_metadata
embedding_fulltext_search embedding_fulltext_search_idx embeddings_queue_config segments
The embeddings table doesn’t store the embeddings (the embeddings aren’t in the SQLite database):
sqlite> SELECT * FROM embeddings ORDER BY id ASC LIMIT 1;
╭────┬──────────────────────────────────────┬──────────────┬────────┬─────────────────────╮
│ id │ segment_id │ embedding_id │ seq_id │ created_at │
╞════╪══════════════════════════════════════╪══════════════╪════════╪═════════════════════╡
│ 1 │ d4e48acc-25e0-42a5-8297-a88aad40bdca │ tmux:C-b │ 109 │ 2026-09-16 04:01:05 │
╰────┴──────────────────────────────────────┴──────────────┴────────┴─────────────────────╯
sqlite> SELECT * FROM segments WHERE id = 'd4e48acc-25e0-42a5-8297-a88aad40bdca';
╭────────────────────────────────────┬──────────────────────────────────┬────────┬────────────────────────────────────╮
│ id │ type │ scope │ collection │
╞════════════════════════════════════╪══════════════════════════════════╪════════╪════════════════════════════════════╡
│d4e48acc-25e0-42a5-8297-a88aad40bdca│urn:chroma:segment/metadata/sqlite│METADATA│c63eb109-a92b-4c7f-a5ba-277b59cd00cc│
╰────────────────────────────────────┴──────────────────────────────────┴────────┴────────────────────────────────────╯
sqlite> SELECT * FROM collections WHERE id = 'c63eb109-a92b-4c7f-a5ba-277b59cd00cc';
╭────────────────────────────────────┬─────────────────┬─────────┬────────────────────────────────────┬───────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ id │ name │dimension│ database_id │config_json_str│ schema_str │
╞════════════════════════════════════╪═════════════════╪═════════╪════════════════════════════════════╪═══════════════╪═══════════════════════════════════════════════════════════════════════════════════════════════════════╡
│c63eb109-a92b-4c7f-a5ba-277b59cd00cc│tmux-key-bindings│ 384│00000000-0000-0000-0000-000000000000│{} │{"defaults":{"string":{"fts_index":{"enabled":false,"config":{}},"string_inverted_index":{"enabled": │
│ │ │ │ │ │true,"config":{}}},"float_list":{"vector_index":{"enabled":false,"config":{"space":"l2"," │
│ │ │ │ │ │embedding_function":{"type":"known","name":"default","config":{}},"hnsw":{"ef_construction":100," │
│ │ │ │ │ │max_neighbors"... │
╰────────────────────────────────────┴─────────────────┴─────────┴────────────────────────────────────┴───────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────╯
Somewhere there’s a vector database, separate from the SQLite database. I think the issue is that to find the nearest neighbor between the vector that’s generated for a query, and every stored vector would be very inefficient if it was done with a normal database:
query vector
↓
distance to vector 1
↓
distance to vector 2
↓
distance to vector 3
...
distance to vector N
↓
sort
Vector databases create a specialized nearest-neighbor index, conceptually kind of similar to SQLite FTS5 indexes.