# What are embeddings? Models, dimensions and pgvector for developers

> 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.

Mindket editors · Published 2026-11-04 · Updated 2026-11-05 · https://mindket.com/learn/rag/embeddings-explained

## In short

- 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.

## Before you start

You need Python 3, a Voyage AI key as in [lesson 1](https://mindket.com/learn/rag/rag-tutorial), 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](https://github.com/pgvector/pgvector/blob/master/CHANGELOG.md), 0.9.0, is still marked unreleased and drops support for Postgres 13 and 14.

**pgvector releases behind this lesson**

1. **April 29, 2024** 0.7.0: new types. halfvec and sparsevec arrive, bit vectors can be indexed, and binary_quantize is added.
2. **October 30, 2024** 0.8.0: iterative index scans. Filtered queries can keep scanning until enough rows match, with better index choice when filtering.
3. **September 4, 2025** 0.8.1: Postgres 18. Adds support for Postgres 18 rc1 and speeds up binary_quantize.
4. **February 25, 2026** 0.8.2: parallel HNSW fix. Fixes a buffer overflow in parallel HNSW index builds.
5. **October 1, 2026** 0.8.7: IVFFlat fix. Fixes a buffer overflow in IVFFlat index builds. The newest release on 5 November 2026.

Source: [pgvector CHANGELOG](https://github.com/pgvector/pgvector/blob/master/CHANGELOG.md) (read 2026-11-05)

## 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](https://developers.openai.com/api/docs/guides/embeddings) 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.

| Text | Keyword similarity | Semantic 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.

| Operator | Returns | To get a similarity |
| --- | --- | --- |
| <-> | L2 (Euclidean) distance | Smaller is closer |
| <#> | Negative inner product | Multiply by -1 |
| <=> | Cosine distance | Subtract it from 1 |
| <+> | L1 distance | Smaller 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.

> **Our view on model leaderboards**
>
> A benchmark score tells you which models to try, not which one wins on your documents. We'd shortlist two, embed your own corpus with each and compare retrieval on your own questions with the metrics in lesson 4.

| Model | Default dimension | Longest input | Notes |
| --- | --- | --- | --- |
| text-embedding-3-small | 1536 | 8192 tokens | MTEB score 62.3% |
| text-embedding-3-large | 3072 | 8192 tokens | MTEB score 64.6% |
| voyage-4-lite | 1024 | 32,000 tokens | Optimized for latency and cost |
| voyage-4 | 1024 | 32,000 tokens | Balanced quality and efficiency |
| voyage-4-large | 1024 | 32,000 tokens | Best general-purpose and multilingual retrieval quality |
| voyage-4-nano | 512 | 32,000 tokens | Open 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.

````python
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.

````sql
-- 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 pick) | IVFFlat |
| --- | --- | --- |
| Better speed and recall trade-off | ✓ Yes | ✗ No |
| Faster to build | ✗ No | ✓ Yes |
| Uses less memory | ✗ No | ✓ Yes |
| Needs data in the table before you create it | ✗ No | ✓ Yes |

**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](https://github.com/pgvector/pgvector) (read 2026-11-04)

## 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.

| Type | Most dimensions an index accepts |
| --- | --- |
| vector | 2,000 |
| halfvec | 4,000 |
| bit | 64,000 |
| sparsevec | 1,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.

````sql
-- 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](https://x.com/tengyuma/status/2011935594130391344) 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](https://blog.voyageai.com/2026/01/15/voyage-4/) 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](https://x.com/VoyageAI/status/2019395836153655581) 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](https://x.com/tomaarsen/status/2016532114871611776) 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](https://x.com/VoyageAI/status/2019395836153655581) (read 2026-11-05)

[![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.](https://mindket.com/blog/shots/x-tengyuma-voyage-4-shared-space.webp?v=634a460c0dd1)](https://x.com/tengyuma/status/2011935594130391344)

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.](https://mindket.com/blog/shots/x-voyageai-voyage-4-nano-open-weights.webp?v=fd2988031fd2)](https://x.com/VoyageAI/status/2019395836153655581)

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](https://developers.llamaindex.ai/python/framework/optimizing/basic_strategies/basic_strategies/) 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](https://mindket.com/learn/rag/rag-chunking) 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](https://mindket.com/blog/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?

   A. Nothing, the result is already normalized
   B. Normalize it again to length 1
   C. Re-embed the original text
   D. Round the values to integers

   Answer: B. 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?

   A. embedding <-> query
   B. embedding <#> query
   C. embedding <+> query
   D. 1-(embedding <=> query)

   Answer: D. 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?

   A. It fails, because an index accepts at most 2,000 dimensions on a vector column
   B. It works and uses half precision on its own
   C. It works with the dimensions truncated to 2,000
   D. It works but only for exact search

   Answer: A. 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?

   A. Drop the index permanently
   B. Switch the operator to <#>
   C. Enable iterative index scans
   D. Lower hnsw.ef_search

   Answer: C. 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

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

[Browse agents](https://mindket.com/agents)
