Development
Key points

Key points

  1. 1An embedding turns text into a list of numbers so that items with similar meaning sit close together, and vector search finds the nearest items to a query.
  2. 2Retrieval quality depends more on how documents are split into chunks than on the database, so start with simple fixed-size chunks and test before adding complexity.
  3. 3Exact search is perfectly accurate and fine for small tables, while HNSW and IVFFlat indexes trade a little recall for much faster queries on large ones.
  4. 4Vector search misses exact terms such as error codes and parameter names, so production systems usually combine it with keyword search and merge the results with reciprocal rank fusion.

What embeddings and vector search are

TigerData's beginner guide defines an embedding as a compact representation of raw data, such as an image or text, transformed into a vector comprising floating-point numbers. An embedding model is trained so that semantically similar items end up close together in that vector space. "How do I reset my password" and "I forgot my login" share almost no words, but a good model places them near each other.

Vector search uses that property. Elastic's explainer describes the contrast: traditional search relies on keywords, lexical similarity and word frequency, while vector search engines use distances in the embedding space to represent similarity. Finding related content becomes finding the nearest neighbors of the query vector. Both the documents and the query must be embedded the same way for the distances to mean anything.

Three distance measures are common.

MeasureWhat it comparesWhen to use it
Cosine distanceThe angle between vectorsThe default for most text embedding models
Inner productAngle and length togetherModels whose vectors are normalized to length 1, where it matches cosine and is cheaper
L2 (Euclidean) distanceStraight-line distanceModels documented for it, and many image models

Use the measure the embedding model's documentation recommends. A mismatched distance function quietly lowers result quality.

The pipeline from documents to results

Every vector search system has two halves: an ingestion pipeline that runs when content changes, and a query path that runs on every search.

Ingestion:

  1. Extract clean text from the source, removing navigation, boilerplate and markup.
  2. Chunk the text into passages small enough to embed and specific enough to be useful.
  3. Embed each chunk with the chosen model.
  4. Store the vector alongside the chunk text, the source document ID, and metadata such as title, URL, date and access permissions.
  5. Index the vectors once the table is large enough to need it.

Query:

  1. Embed the user's query with the same model.
  2. Search for the nearest chunks, applying any metadata filters.
  3. Optionally combine the results with keyword search and rerank.
  4. Return the chunks, or pass them to a language model as context.

Keep the model name and version stored with every vector. If you change embedding models, vectors from the old and new models are not comparable, and everything must be re-embedded.

Chunking decides what can be found

Pinecone's guide gives two reasons to chunk: so that embedding models can fit the data into their context windows, and so that the chunks themselves contain the information necessary for search. Text beyond a model's context window is usually truncated before embedding, and a chunk that mixes several topics produces a vector that represents none of them well.

The same guide offers a test that works better than any formula: if a chunk makes sense to a human without the surrounding context, it will make sense to the model too. If chunks are too small, they lose context. If they are too large, the meaning of any single passage is diluted.

The common strategies, roughly from simplest to most involved:

StrategyHow it splitsGood for
Fixed sizeA set number of tokens, often with overlapA first version for almost any corpus
Recursive characterParagraphs first, then sentences, then words, until under a size limitGeneral prose with uneven paragraph lengths
Document structureHeadings, sections, list items, code blocksMarkdown, HTML, documentation
SemanticBreaks where the topic changes, detected with embeddingsLong, loosely structured text

Pinecone recommends starting with fixed-size chunking: it will be the best path in most cases, and you should iterate only after finding it insufficient. A recursive splitter, such as the one LangChain implements, is a good middle ground because it tries paragraph and sentence boundaries before falling back to words.

Two habits improve almost any strategy.

  • Prepend context. Add the document title and section heading to each chunk before embedding, so "Step 3: restart the service" carries the name of the product and procedure it belongs to.
  • Use overlap. A small overlap between consecutive chunks keeps a sentence that straddles a boundary findable.

Storing and querying vectors in Postgres

If you already run Postgres, the pgvector extension lets you store vectors next to your other data, join them with normal tables, and filter with ordinary SQL. The pgvector README lists support for exact and approximate nearest neighbor search and for L2, inner product, cosine, L1, Hamming and Jaccard distances.

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE chunks (
  id          bigserial PRIMARY KEY,
  document_id bigint NOT NULL REFERENCES documents(id),
  content     text NOT NULL,
  model       text NOT NULL,
  embedding   vector(1024) NOT NULL
);

-- Nearest chunks by cosine distance
SELECT id, content, 1 - (embedding <=> $1) AS similarity
FROM chunks
ORDER BY embedding <=> $1
LIMIT 10;

The operators are <-> for L2 distance, <#> for negative inner product and <=> for cosine distance. The vector size in the column definition must match the dimension count of your embedding model; the value above is only an example.

An important default: pgvector performs exact nearest neighbor search, which provides perfect recall, until you add an index. For tens of thousands of rows, exact search is often fast enough, and it is always correct. Add an index when query latency, not accuracy, becomes the problem. The README also lists the maximum dimensions each vector type can index, which matters if your model produces very large vectors.

Choosing an index HNSW or IVFFlat

Approximate indexes speed up search by not comparing the query with every vector. The pgvector README warns that this trades some recall for speed and that, unlike typical indexes, you will see different results for queries after adding an approximate index.

pgvector offers two index types.

HNSWIVFFlat
StructureMultilayer graphVectors grouped into lists around centroids
Speed and recallBetter speed-recall tradeoffLower query performance for the same recall
Build time and memorySlower to build, uses more memoryFaster to build, uses less memory
Needs data before buildingNoYes, it learns its lists from existing rows
Main query settinghnsw.ef_searchivfflat.probes

HNSW is the usual default because it gives better results per millisecond and can be created on an empty table. IVFFlat suits very large tables where build time or memory is the constraint. The README suggests starting IVFFlat with rows / 1000 lists for up to a million rows and probes around the square root of the list count.

CREATE INDEX ON chunks USING hnsw (embedding vector_cosine_ops);
SET hnsw.ef_search = 100;  -- higher means better recall, slower queries

Filtering needs special care. With approximate indexes, the README, as read on 2026-10-17, explains that filtering is applied after the index is scanned, so with the default ef_search of 40 and a filter matching 10% of rows, only 4 rows will match on average. The fixes it lists are iterative index scans, which keep scanning until enough rows match, partial indexes for a few common filter values, and partitioning for many values. A B-tree index on a selective filter column can also make exact search fast enough to skip the vector index entirely.

Hybrid search with keywords and rank fusion

Vector search is weak at exact matches. pgEdge's hybrid search article shows the problem with a technical query: searching for a specific configuration parameter, the model may rank a general replication article above the one that specifically explains that parameter, because it treats all replication content as similar. BM25, the classic keyword ranking algorithm, has the opposite profile: it finds exact terms reliably and fails at conceptual questions.

Hybrid search runs both and merges the lists. The difficulty is that the scores are in different units. Reciprocal rank fusion (RRF) avoids the problem by ignoring scores and using only positions. OpenSearch's announcement of RRF explains that, unlike min-max or L2 normalization, it uses a rank-based aggregation strategy, which keeps one method's score distribution from dominating and favors documents that rank well in several lists.

The formula is short. Each document's score is the sum, over every result list it appears in, of 1 / (k + rank). A hybrid search reference from Digital Applied notes that k = 60 is Elasticsearch's production default and recommends starting there.

def rrf(result_lists, k=60):
    scores = {}
    for results in result_lists:
        for rank, doc_id in enumerate(results, start=1):
            scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
    return sorted(scores, key=scores.get, reverse=True)

merged = rrf([vector_results, keyword_results])

In Postgres, the keyword side can use built-in full-text search with a GIN index on a tsvector column, so both searches run in the same database. A cross-encoder reranker over the top merged results can add further precision when latency allows.

Evaluate before you tune

Changing chunk sizes, models, index settings or fusion weights without measurement is guesswork. Digital Applied's practical recommendation is to evaluate on your own query set with NDCG or MRR before adding complexity such as reranking.

A small evaluation set goes a long way.

  1. Collect 50 to 200 real queries from search logs, support tickets or users.
  2. For each, record which documents or chunks a correct answer should include.
  3. Measure recall at k (did the right chunk appear in the top results?) and a rank-aware metric such as MRR or NDCG.
  4. Change one thing at a time and rerun the set.
  5. Compare approximate index results with exact search on the same queries to see how much recall the index costs.

Keep the set under version control and rerun it whenever you change the model, chunking or index settings. Quanta Intelligence routes requests to the right agent with this same approach, embeddings plus curated terms, and evaluation queries are what show whether a change helped.

FAQ

FAQ

Do I need a dedicated vector database?

Not necessarily. If you already use Postgres, pgvector stores vectors next to your relational data and lets you filter and join with SQL, which covers many applications. Dedicated vector databases can make sense at very large scale or when you need features your database does not offer, but start with what you already operate.

What chunk size should I use?

There is no universal number. Start with fixed-size chunks that fit comfortably within your embedding model's context window, with a small overlap, and prepend the document title and heading. Then measure retrieval on real queries and adjust, keeping chunks that read sensibly on their own.

Should I add an HNSW index right away?

Only when you need it. Without an index, pgvector does exact search with perfect recall, which is often fast enough for smaller tables. Add HNSW when latency becomes a problem, then compare its results against exact search to confirm recall is acceptable.

Why does vector search miss results that contain the exact words I searched for?

Embeddings capture overall meaning, so a passage that is broadly about the same topic can outrank one containing the exact term, such as an error code or product name. Combining vector search with keyword search, and merging the lists with reciprocal rank fusion, fixes most of these cases.

What happens if I change embedding models?

Vectors from different models live in different spaces and cannot be compared. You must re-embed every stored chunk with the new model before switching queries to it. Storing the model name with each vector makes it easy to run both side by side during the migration.

Sources

Sources

  1. https://www.tigerdata.com/blog/a-beginners-guide-to-vector-embeddings
  2. https://www.elastic.co/what-is/vector-search
  3. https://www.pinecone.io/learn/chunking-strategies/
  4. https://github.com/pgvector/pgvector
  5. https://www.pgedge.com/blog/hybrid-search-in-postgresql-bm25-sparse-vectors-and-reciprocal-rank-fusion
  6. https://opensearch.org/blog/introducing-reciprocal-rank-fusion-hybrid-search/
  7. https://www.digitalapplied.com/blog/hybrid-search-bm25-vector-reranking-reference-2026
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles