Natural Language Processing

Shipping Text Embeddings in Production

Suresh Madhra·Jun 04, 2026·14 min read

The demo is easy, the system is not

Embedding a hundred documents and querying them takes an afternoon. Keeping millions of embeddings fresh, cheap and actually relevant is a different job. This is a tour of the decisions that determine whether your retrieval system works in production — chunking, storage, and the evaluation loop that most teams skip.

1. Chunking is a retrieval decision, not a preprocessing detail

An embedding compresses a passage into a single vector. Too long and the vector becomes an average of several topics that matches nothing precisely. Too short and it loses the context that makes it interpretable. Chunk boundaries determine what can ever be retrieved.

  • Split on structure first — headings, sections, list items — before falling back to token counts.
  • Overlap adjacent chunks so a fact spanning a boundary is not lost. Ten to fifteen percent is usually enough.
  • Prepend the document title and section path to every chunk. It costs a few tokens and sharply improves matching.
  • Store the parent document id and character offsets so you can expand a hit back into context at answer time.
  • Keep tables and code blocks intact; splitting them mid-structure destroys their meaning.
chunking.py

2. Vector store trade-offs

  • Postgres with pgvector: one database, transactional consistency, easy metadata filters. Excellent up to a few million vectors; HNSW indexing is essential past a hundred thousand.
  • Dedicated vector databases: better recall/latency at very large scale and richer hybrid search, at the cost of another system to operate and keep in sync.
  • In-memory brute force: unbeatable simplicity below roughly fifty thousand vectors. Exact results, no index tuning, no drift.
  • Approximate indexes trade recall for speed. Measure that recall — the default parameters are a guess about your data, not a promise.

Two operational details matter more than the choice of store: dimensionality (storage and latency scale with it, and many modern models support truncation to a shorter prefix) and metadata filtering, because most real queries are scoped to a tenant, a language or a date range.

3. Hybrid search beats pure vectors

Dense embeddings capture meaning but miss exact tokens — product codes, error numbers, rare names. Keyword search does the opposite. Run both and fuse the ranked lists; reciprocal rank fusion needs no score calibration and is hard to beat for the effort involved.

rrf.py

4. The evaluation loop nobody talks about

Retrieval quality is measurable, and if you do not measure it you are tuning blind. Build a small golden set — fifty to two hundred real queries with the passage that should be retrieved — and track recall@k and MRR on every change to the chunker, the model or the index.

retrieval_eval.py

5. Operating the thing

  • Version your embeddings. Store the model name and version with every vector; a model upgrade means a full re-index and you need to be able to run both during the migration.
  • Batch and cache. Embedding calls dominate cost; hash the normalised text and skip anything unchanged.
  • Re-rank the top 50 with a cross-encoder when precision matters. It is the single largest quality win after hybrid search.
  • Watch p99 latency, not the mean. Approximate indexes have long tails under concurrent writes.
  • Log every query with its retrieved ids. Those logs become next quarter's golden set.