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.
0.7.0: new types
halfvec and sparsevec arrive, bit vectors can be indexed, and binary_quantize is added.
0.8.0: iterative index scans
Filtered queries can keep scanning until enough rows match, with better index choice when filtering.
0.8.1: Postgres 18
Adds support for Postgres 18 rc1 and speeds up binary_quantize.
0.8.2: parallel HNSW fix
Fixes a buffer overflow in parallel HNSW index builds.
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.
| 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.
| 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.
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);- Start Postgres with pgvector, then run
CREATE EXTENSION vector;once in each database that will use it. - 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. - Insert vectors as text literals such as
[0.1,0.2,0.3]. For bulk loads the README recommendsCOPY, with indexes added after the first load. - Query with
ORDER BY embedding <=> query LIMIT k. Without an index, pgvector scans every row. - 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.
- Match the index to the operator.
vector_cosine_opsserves<=>, 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:
mis 16 andef_constructionis 64. A higheref_constructiongives better recall at the cost of build time and insert speed. - HNSW query:
hnsw.ef_searchis 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.probesis 1. The README suggests starting at the square root oflists.
| 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 (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.
| 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.
-- 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.
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
tiktokenwith thecl100k_baseencoding for the third-generation models, so you stay under the 8192-token input limit. - Assuming a missing row is a miss.
NULLvectors 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 printnp.linalg.normfor 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_searchat 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
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.
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.
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.
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
- OpenAI Docs: Vector embeddings https://developers.openai.com/api/docs/guides/embeddings
- OpenAI Docs: Retrieval https://developers.openai.com/api/docs/guides/retrieval
- Claude Docs: Embeddings https://platform.claude.com/docs/en/build-with-claude/embeddings
- Voyage AI Docs: Text Embeddings https://www.mongodb.com/docs/voyageai/models/text-embeddings
- Voyage AI blog: The Voyage 4 model family (15 January 2026) https://blog.voyageai.com/2026/01/15/voyage-4/
- pgvector README https://github.com/pgvector/pgvector
- pgvector CHANGELOG https://github.com/pgvector/pgvector/blob/master/CHANGELOG.md
- LangChain Docs: Retrieval https://docs.langchain.com/oss/python/langchain/retrieval
- LlamaIndex Docs: Basic strategies https://developers.llamaindex.ai/python/framework/optimizing/basic_strategies/basic_strategies/
- Tengyu Ma on X: the Voyage 4 launches (15 January 2026) https://x.com/tengyuma/status/2011935594130391344
- Voyage AI on X: voyage-4-nano (5 February 2026) https://x.com/VoyageAI/status/2019395836153655581
- Tom Aarsen on X: voyage-4-nano and benchmarks (28 January 2026) https://x.com/tomaarsen/status/2016532114871611776


