Mindket

What are embeddings? Models, dimensions and pgvector for developers

A black banner titled "RAG" over the line "Embeddings, chunking, retrieval and evals".

An embedding is a list of floating point numbers that represents the meaning of a text, so texts with related meanings end up close together. Search then becomes a distance calculation. Normalized vectors make cosine similarity and the dot product agree, the model and dimension you pick set the storage cost, and pgvector stores and queries the vectors in Postgres.

In this lesson

  • Explain what an embedding is and why distance measures relatedness.
  • Choose a model and an output dimension, and shorten a stored vector safely.
  • Create, index and query a vector column in Postgres with pgvector.
  • Spot the limits that bite: the 2,000-dimension index cap and filters on approximate indexes.
All courses
On this page
  1. Before you start
  2. What an embedding is
  3. Cosine, dot product and distance
  4. Choose a model
  5. Pick a dimension and shorten it correctly
  6. Store and query vectors in Postgres
  7. HNSW or IVFFlat
  8. The dimension limits that decide your model
  9. Why a filtered query returns fewer rows
  10. What Voyage AI's co-founder and team say about one shared embedding space
  11. Common mistakes
  12. Try it yourself
  13. Next
  14. Check yourself
  15. Frequently asked questions
  16. Sources

Before you start

You need Python 3, a Voyage AI key as in lesson 1, and a Postgres server with pgvector. The pgvector README ships a Docker image for each Postgres version, so docker pull pgvector/pgvector:pg18-trixie adds the extension to the Postgres 18 image. Its tag list names release 0.8.7 as the newest, which is the version this lesson follows.

Run the SQL in psql or any client. The Python snippets use the voyageai and numpy packages.

Take the newest patch release, not an old image. Three of the 2026 releases fixed buffer overflows in index builds. The next entry in the changelog, 0.9.0, is still marked unreleased and drops support for Postgres 13 and 14.

pgvector releases behind this lesson
  1. 0.7.0: new types

    halfvec and sparsevec arrive, bit vectors can be indexed, and binary_quantize is added.

  2. 0.8.0: iterative index scans

    Filtered queries can keep scanning until enough rows match, with better index choice when filtering.

  3. 0.8.1: Postgres 18

    Adds support for Postgres 18 rc1 and speeds up binary_quantize.

  4. 0.8.2: parallel HNSW fix

    Fixes a buffer overflow in parallel HNSW index builds.

  5. 0.8.7: IVFFlat fix

    Fixes a buffer overflow in IVFFlat index builds. The newest release on 5 November 2026.

Source: pgvector CHANGELOG (read November 5, 2026)

What an embedding is

An embedding is a vector, a list of numbers, and the distance between two vectors measures their relatedness, as OpenAI's guide defines it. A small distance means high relatedness and a large one means low. An embedding model is trained so that text with similar meaning lands close together.

A keyword engine compares words, and an embedding compares meaning. The query "When did we go to the moon?" from the retrieval guide shows the gap. The best answer shares no word with it, and the sentence about moon cake shares the most.

Keyword similarity is intersection over union, and semantic similarity is cosine similarity with text-embedding-3-small. From OpenAI's retrieval guide.
TextKeyword similaritySemantic similarity
The first lunar landing occurred in July of 1969.0%65%
The first man on the moon was Neil Armstrong.27%43%
When I ate the moon cake, it was delicious.40%28%

Cosine, dot product and distance

OpenAI recommends cosine similarity and notes that the choice of distance function typically does not matter much. Its embeddings are normalized to length 1, and so are Voyage AI's. That has two consequences, and both vendors list them: cosine similarity reduces to a dot product, which is slightly faster, and cosine similarity and Euclidean distance rank results identically.

pgvector builds on this. Its performance notes say that for vectors normalized to length 1, inner product is the fastest operator. One operator reads backward: <#> returns the negative inner product because Postgres index scans only support ascending order, so you multiply by -1 to read it as a similarity.

pgvector's distance operators for the vector type, from its README.
OperatorReturnsTo get a similarity
<->L2 (Euclidean) distanceSmaller is closer
<#>Negative inner productMultiply by -1
<=>Cosine distanceSubtract it from 1
<+>L1 distanceSmaller is closer

Choose a model

Anthropic does not offer an embedding model. Its guide names three factors for choosing a provider: how large and how specific to your domain the training data is, inference speed and latency, and whether you can continue training on private data. Its own pick is Voyage AI, with voyage-4-large for quality, voyage-4 for balance and voyage-4-lite for latency and cost. Domain models exist for code (voyage-code-4), finance and law, and the Voyage models are described as multilingual.

OpenAI's two current models differ by size and benchmark score. Both accept up to 8192 tokens per input, against 32,000 for the Voyage 4 series. That series has a fourth member, voyage-4-nano, with open weights on Hugging Face, and Voyage's model page says all embeddings from the 4 series are compatible with each other.

From OpenAI's embeddings guide, Anthropic's embeddings guide and the Voyage AI model page. Prices change, so check the vendors for those.
ModelDefault dimensionLongest inputNotes
text-embedding-3-small15368192 tokensMTEB score 62.3%
text-embedding-3-large30728192 tokensMTEB score 64.6%
voyage-4-lite102432,000 tokensOptimized for latency and cost
voyage-4102432,000 tokensBalanced quality and efficiency
voyage-4-large102432,000 tokensBest general-purpose and multilingual retrieval quality
voyage-4-nano51232,000 tokensOpen weights on Hugging Face, shortens to as few as 128 dimensions

Pick a dimension and shorten it correctly

Longer vectors cost more storage, memory and compute, as OpenAI's guide says. Both vendors trained their current models so that you can keep the first numbers of a vector and lose little. OpenAI exposes this as the dimensions parameter, and says a text-embedding-3-large embedding shortened to 256 numbers still beats an unshortened text-embedding-ada-002 embedding of 1536 on MTEB. Voyage exposes it as output_dimension, with 2048, 1024 (the default), 512 and 256 for the 4 series.

Shortening a stored vector by hand works if you normalize afterward, and OpenAI and Voyage both warn about that step. Voyage also quantizes: output_dtype can return 8-bit integers, or bit-packed binary values at one eighth of the dimension, which its FAQ puts at 4x and 32x smaller storage than 32-bit floats.

import voyageai
import numpy as np

vo = voyageai.Client()

# 1. Ask the API for a shorter vector
short = vo.embed(
    ["Sample text 1", "Sample text 2"],
    model="voyage-4",
    input_type="document",
    output_dimension=256,
).embeddings
print(len(short[0]))  # 256


# 2. Or shorten a vector you already stored: keep the leading values, then normalize again
def embd_normalize(v: np.ndarray) -> np.ndarray:
    row_norms = np.linalg.norm(v, axis=1, keepdims=True)
    if np.any(row_norms == 0):
        raise ValueError("Cannot normalize rows with a norm of zero.")
    return v / row_norms


full = vo.embed(
    ["Sample text 1", "Sample text 2"], model="voyage-4", input_type="document"
).embeddings  # 1024 numbers each by default
resized = embd_normalize(np.array(full)[:, :256]).tolist()

Store and query vectors in Postgres

pgvector adds vector similarity search to Postgres, so the vectors sit beside the rest of your data with transactions, joins and replication. Out of the box it runs exact nearest neighbor search, which gives perfect recall. The SQL below follows the README's getting-started steps with a column sized for the Voyage default.

-- once per database
CREATE EXTENSION vector;

-- the dimension has to match your embedding model
CREATE TABLE chunks (
    id bigserial PRIMARY KEY,
    source text NOT NULL,
    content text NOT NULL,
    embedding vector(1024)
);

-- vectors go in as text literals
INSERT INTO chunks (source, content, embedding)
VALUES ('kb://refund-policy', 'You may return an item within 30 days.', '[0.01,-0.02, ...]');

-- the four nearest chunks by cosine distance, with the similarity (1 - distance)
SELECT id, source, 1 - (embedding <=> '[0.03,0.01, ...]') AS cosine_similarity
FROM chunks
ORDER BY embedding <=> '[0.03,0.01, ...]'
LIMIT 4;

-- an approximate index for cosine distance, built once the table has data
CREATE INDEX ON chunks USING hnsw (embedding vector_cosine_ops);
  1. Start Postgres with pgvector, then run CREATE EXTENSION vector; once in each database that will use it.
  2. Create the table. vector(1024) fixes the dimension, so it must match your model. Keep the text and the source beside the vector so one query returns everything the prompt needs.
  3. Insert vectors as text literals such as [0.1,0.2,0.3]. For bulk loads the README recommends COPY, with indexes added after the first load.
  4. Query with ORDER BY embedding <=> query LIMIT k. Without an index, pgvector scans every row.
  5. Add an index once the table has data. An approximate index trades some recall for speed, so its results can differ from the exact ones.
  6. Match the index to the operator. vector_cosine_ops serves <=>, and the README says to add one index for each distance function you query with.

HNSW or IVFFlat

pgvector offers two approximate indexes. HNSW builds a multilayer graph. IVFFlat divides the vectors into lists and searches only the lists closest to the query. The settings that move recall and speed, with the README's defaults:

  • HNSW build: m is 16 and ef_construction is 64. A higher ef_construction gives better recall at the cost of build time and insert speed.
  • HNSW query: hnsw.ef_search is 40. A higher value gives better recall at the cost of speed.
  • IVFFlat build: choose lists, starting at rows / 1000 up to one million rows and the square root of rows beyond that.
  • IVFFlat query: ivfflat.probes is 1. The README suggests starting at the square root of lists.
HNSW or IVFFlat in pgvector
HNSW Our pickIVFFlat
Better speed and recall trade-offYesNo
Faster to buildNoYes
Uses less memoryNoYes
Needs data in the table before you create itNoYes

Our pick: HNSW. It has the better speed and recall trade-off and no training step, at the price of build time and memory.

Source: pgvector README (read November 4, 2026)

The dimension limits that decide your model

An index on a plain vector column accepts up to 2,000 dimensions. That rules out the 3072 numbers a default text-embedding-3-large returns. The README gives three ways out: store or index the column as halfvec, which reaches 4,000 dimensions, quantize to bits, which reaches 64,000, or ask the model for a shorter vector.

Half precision is a cast inside the index: CREATE INDEX ON items USING hnsw ((embedding::halfvec(3)) halfvec_l2_ops);, queried with the same cast in the ORDER BY. If you must keep different dimensions in one column, the README's answer is a model_id column and one partial index per model.

HNSW limits from the pgvector README. IVFFlat accepts the first three.
TypeMost dimensions an index accepts
vector2,000
halfvec4,000
bit64,000
sparsevec1,000 non-zero elements

Why a filtered query returns fewer rows

With an approximate index, pgvector applies the WHERE clause after it scans the index. The README works an example: if a condition matches 10% of rows and hnsw.ef_search is the default 40, only 4 rows match on average. A RAG query that filters by tenant or document type hits this directly.

Starting with 0.8.0, iterative index scans keep scanning until enough rows match or a scan cap is reached. A different route is a B-tree index on the filter column, which the README says gives fast, exact search when the condition matches a low percentage of rows.

-- with an approximate index, the WHERE runs after the index scan, so this can return fewer than 5 rows
SELECT * FROM items WHERE category_id = 123 ORDER BY embedding <-> '[3,1,2]' LIMIT 5;

-- pgvector 0.8.0 and later: keep scanning the index until enough rows match
SET hnsw.iterative_scan = strict_order;

-- a higher ef_search gives better recall at the cost of speed (the default is 40)
SET hnsw.ef_search = 100;

What Voyage AI's co-founder and team say about one shared embedding space

Tengyu Ma co-founded Voyage AI, ran it as CEO and is now chief AI scientist at MongoDB, by his X bio. He listed the Voyage 4 launches on X on 15 January 2026. voyage-4, voyage-4-lite and voyage-4-nano share an embedding space with voyage-4-large, which he said enables flexible cost-benefit optimization. Voyage's launch post spells out the trade: embed your documents once with voyage-4-large, then embed queries with the cheaper voyage-4-lite.

Voyage AI's own account described voyage-4-nano on X on 5 February 2026. It has open weights under the Apache 2.0 license and the same embedding space as its larger siblings. It also supports Matryoshka Representation Learning, the training that lets a shortened vector lose little, plus int8 and binary quantization.

Compatible vectors still have to be the same length to compare, and a vector(1024) column takes no other length. Nano's sizes overlap only its siblings' two smallest. Set output_dimension to one of those on both models and size the column to match before you mix them.

Tom Aarsen, who leads Sentence Transformers at Hugging Face by his X bio, welcomed nano on X on 28 January 2026 with one reservation. It was not yet on many benchmarks beside similar models such as embeddinggemma-300m. With no public score to lean on, the shortlist test from Choose a model decides: embed your own corpus with each candidate and compare retrieval.

This means you can use voyage-4-nano locally alongside our flagship models without re-indexing a single vector.

Voyage AI by MongoDB, Maker of the Voyage embedding models · X (read November 5, 2026)
Tengyu Ma (@tengyuma) on X, 15 January 2026, listing the launches by Voyage AI by MongoDB. First voyage-4-large, an MoE embedding model with new SOTA accuracy and 40% lower costs. Then voyage-4, voyage-4-lite and the open-weight voyage-4-nano with shared embedding space that enables flexible cost-benefit optimization. Below it, the Voyage AI post it quotes.
Tengyu Ma on X, 15 January 2026
Voyage AI by MongoDB (@VoyageAI) on X, 5 February 2026: Meet voyage-4-nano: Our first open-weight embedding model. voyage-4-nano is ideal for local development and prototyping while providing an easy path to production. Shared Embedding Space: voyage-4-nano shares an embedding space as its larger siblings.
Voyage AI on X, 5 February 2026

Screenshots of X's own embeds of the posts, taken on 5 November 2026, with the time in UTC. X cuts both posts short with Show more. Select one to open the post on X.

Common mistakes

  • Cutting a vector and using it as is. After you keep the leading numbers, normalize again. Without that the vector no longer has length 1, and the dot product stops equalling cosine similarity.
  • Reading <#> output as a similarity. It is the negative inner product.
  • Creating an IVFFlat index on an empty table. The README lists this as the usual cause of too few results, and says to drop the index until the table has more data.
  • Counting tokens by characters. OpenAI's FAQ points to tiktoken with the cl100k_base encoding for the third-generation models, so you stay under the 8192-token input limit.
  • Assuming a missing row is a miss. NULL vectors are not indexed, and neither are zero vectors for cosine distance.
  • Changing the model without re-embedding. LlamaIndex says the same model must embed documents and queries, and that a changed model means re-indexing. Voyage's 4 series is the documented exception, at a dimension both models share.

Try it yourself

  • Embed the three moon sentences with your model and compare your scores with OpenAI's table. Where do the rankings differ?
  • Shorten a vector to a quarter of its length with and without embd_normalize, then print np.linalg.norm for both.
  • Load a few hundred chunks, run EXPLAIN (ANALYZE, BUFFERS) on the query, then add the HNSW index and run it again. The README names this as the way to debug performance.
  • Run a filtered query with hnsw.ef_search at its default and at 100 and count the rows each returns.

Next

Lesson 3 goes back to the step before embedding: how large to cut each chunk, how to split by structure, and how to add the context that makes a chunk findable. The token limits above are counted the way what are tokens in AI describes.

Check yourself

  1. You shorten a stored 1024-number vector to its first 256 numbers. What must you do before comparing it?

    Show answer

    B. Normalize it again to length 1 Both OpenAI and Voyage AI say a manually shortened vector has to be normalized again, or the dot product no longer equals cosine similarity.

  2. In pgvector, which expression gives cosine similarity for a stored vector and a query vector?

    Show answer

    D. 1-(embedding <=> query) The operator <=> returns cosine distance, and the README says to use 1 minus that distance for cosine similarity.

  3. You store 3072-number vectors in a vector column and build an HNSW index. What happens?

    Show answer

    A. It fails, because an index accepts at most 2,000 dimensions on a vector column A vector index accepts up to 2,000 dimensions. The README names halfvec, binary quantization or a shorter vector as the ways out.

  4. A filtered query with an HNSW index returns fewer rows than the LIMIT. What does the README suggest?

    Show answer

    C. Enable iterative index scans Filtering runs after the index scan, so rows can be lost. Iterative scans, available from 0.8.0, keep scanning until enough rows match.

Frequently asked questions

What is the difference between an embedding and a vector?

An embedding is a vector that an embedding model produced from a piece of content. Any list of numbers is a vector, but only vectors whose distances mean relatedness are embeddings.

Which embedding model should I use for RAG?

Shortlist two from the vendor guides and test them on your own questions. Anthropic points to Voyage AI, where voyage-4 balances quality and cost. OpenAI offers text-embedding-3-small and text-embedding-3-large.

Which dimension should I choose?

Start with the model's default and shorten only when storage or latency forces you to. A shortened vector loses little, and each vendor lists the sizes it supports. Normalize again after you cut a vector yourself.

Do I have to normalize embeddings myself?

Not for OpenAI or Voyage AI, because both return vectors of length 1. You only normalize again after you shorten a vector yourself, since cutting numbers off changes its length. Pick the operator that matches your index.

Do I need a separate vector database?

Not to start. pgvector keeps vectors beside your other rows, so one query can join them, and its README lists exact and approximate search, replication and point-in-time recovery. For more room it points to replicas and sharding with Citus or PgDog.

Do embeddings work for Chinese text?

Yes, with a multilingual model. Voyage AI describes its text embedding models as inherently multilingual, so similarity holds across languages.

How many texts can I embed in one request?

Voyage AI takes at most 1,000 texts in one embed call, and the total number of tokens across them is capped by model. Batch your chunks to stay under both limits.

Sources

  1. OpenAI Docs: Vector embeddings (read November 5, 2026)https://developers.openai.com/api/docs/guides/embeddings
  2. OpenAI Docs: Retrieval (read November 4, 2026)https://developers.openai.com/api/docs/guides/retrieval
  3. Claude Docs: Embeddings (read November 4, 2026)https://platform.claude.com/docs/en/build-with-claude/embeddings
  4. Voyage AI Docs: Text Embeddings (read November 5, 2026)https://www.mongodb.com/docs/voyageai/models/text-embeddings
  5. Voyage AI blog: The Voyage 4 model family (15 January 2026) (read November 5, 2026)https://blog.voyageai.com/2026/01/15/voyage-4/
  6. pgvector README (read November 5, 2026)https://github.com/pgvector/pgvector
  7. pgvector CHANGELOG (read November 5, 2026)https://github.com/pgvector/pgvector/blob/master/CHANGELOG.md
  8. LangChain Docs: Retrieval (read November 4, 2026)https://docs.langchain.com/oss/python/langchain/retrieval
  9. LlamaIndex Docs: Basic strategies (read November 4, 2026)https://developers.llamaindex.ai/python/framework/optimizing/basic_strategies/basic_strategies/
  10. Tengyu Ma on X: the Voyage 4 launches (15 January 2026) (read November 5, 2026)https://x.com/tengyuma/status/2011935594130391344
  11. Voyage AI on X: voyage-4-nano (5 February 2026) (read November 5, 2026)https://x.com/VoyageAI/status/2019395836153655581
  12. Tom Aarsen on X: voyage-4-nano and benchmarks (28 January 2026) (read November 5, 2026)https://x.com/tomaarsen/status/2016532114871611776

Share this lesson

See agents built on retrieval

Browse the agents listed on Mindket, each with a preview, to see what an agent with your own data can do.

Browse agents

This lesson as Markdown · RAG course: build retrieval-augmented generation · All courses