The vector index
You don't write an index for a vector search. The compiler derives
every index your queries need
(How the derived indexes are chosen),
and a vector is no different. A query with nearest on a field gets an
HNSW index on it, with the operator class of the type's metric, in its
migration and the schema report. HNSW is the kind of approximate index
pgvector uses to find near vectors fast. The query's equality tests get
a B-tree beside it, as any query's do:
-- store: mainCREATE INDEX "help_article_body_meaning_idx" ON "help_article" USING hnsw ("body_meaning" vector_cosine_ops);CREATE INDEX "help_article_shown_publicly_idx" ON "help_article" ("shown_publicly");
- HNSW only, with pgvector's defaults (
m16,ef_construction64). pgvector's other kind, IVFFlat, isn't offered: it needs the rows to exist before it is built, and a migration builds it on an empty table. - Beside the B-tree, so the database can choose an exact scan when a filter matches few rows. An entity with a lifecycle gets a partial HNSW index, as its other indexes are (Lifecycle).
Tuning the index
Most apps never tune it. On a large table that is searched often, an
indexes: entry with kind: IndexKind.hnsw declares the index on one
vector field, with either or both of:
m:, the links each vector keeps, 2 to 100;ef_construction:, the candidates weighed as each vector is linked, 4 to 1,000, and at least twicem.
More of either finds the nearest more reliably on a large table, but builds slower and takes more space. A query searching the field uses the declared index in place of the one it would derive:
use support/types { ArticleMeaning }/// An article on the help site, with its meaning once it is embedded.entity HelpArticle { id: Uuid body_meaning: ArticleMeaning? indexes: { by_meaning: { fields: [body_meaning] kind: IndexKind.hnsw m: 24 // a million articles, searched often ef_construction: 128 } }}
-- store: mainCREATE INDEX "help_article_by_meaning_idx" ON "help_article" USING hnsw ("body_meaning" vector_cosine_ops) WITH (m = 24, ef_construction = 128);
- A declared HNSW index is on exactly one vector field of fixed
length and at most 4,000 dimensions, with the operator class its type's
metric gives (
type.setting_valueotherwise; over 4,000 its error namesquery.nearest_exact). It may stand without a query, as any declared index may (Indexes). A query searching the same field uses it, tuned or not, rather than deriving a second index on the column. - The bounds are pgvector's:
m2 to 100,ef_construction4 to 1,000, andef_constructionat least twicem, counting pgvector's default for the one not written, since pgvector refuses to build the index otherwise. A value that isn't a whole number, or is out of bounds, istype.setting_value;m:oref_construction:on an index withoutkind: IndexKind.hnswistype.unknown_setting; either twice isdata.duplicate_rule. Only what the entry writes goes in the migration'sWITH (...), in the order written; changing it drops and rebuilds the index, as any index change does.
Vectors of more than 2,000 dimensions
pgvector indexes a vector of up to 2,000 dimensions, and a
half-precision halfvec of up to 4,000. Where the provider allows it,
you can stay under 2,000 by asking for fewer dimensions with
embedding_dimensions:.
-
2,001 to 4,000 dimensions, such as the 3,072 of OpenAI's large embedding model. Each item must be within a half-precision float's ±65,504, so the index takes every vector the type holds. An item past that breaks
max(ormin), at its place in JSON, and a list written out with items in range passes. (pgvector refuses to index a larger item, rounding it up to infinity from 65,520, so without the rule a write would fail on the index, a defect no caller could map.) The vector is stored at full precision asvector(n)and indexed through a half-precision expression, with the metric'shalfvec_operator class. Its query orders by the same expression, so PostgreSQL matches the index to it. The distance it reads back and itswhere:thresholds stay at full precision, asai.distancegives them. The parameter is cast tovector(n)first, so the server infersvectorfor it, which is the binary format the runtime sends:CREATE INDEX "help_article_body_meaning_idx" ON "help_article" USING hnsw (("body_meaning"::halfvec(3072)) halfvec_cosine_ops);SELECT "id", "title", "body_meaning" <=> $1 AS "jadpo_distance" FROM "help_article" WHERE "body_meaning" IS NOT NULL ORDER BY "body_meaning"::halfvec(3072) <=> $1::vector(3072)::halfvec(3072) LIMIT 5
-
Over 4,000 dimensions, no index can hold the vector, so a
nearestquery on it reads every record. That is the errorquery.nearest_exact, which you can decline (Declining a judgement call). A type with fewer dimensions fixes it. A decline above the query'sorder:, inside the query, accepts the cost, for a table small enough to read in full. Declined, the query measures every record, orders by the distance andidin one statement, and limits, so ties at the limit are broken byidas everywhere else. It sets no search settings and derives no index, so it doesn't need pgvector 0.8.0.
Markdown for agents: /docs/vector-index.md.Built from Jadpo main at a707efb.