# 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](indexes.md#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:

```sql
-- store: main
CREATE 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 (`m` 16, `ef_construction` 64).
  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](lifecycle.md)).

## 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 twice `m`.

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:

```jadpo
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
        }
    }
}
```

```sql
-- store: main
CREATE 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_value` otherwise; over 4,000 its error names
  `query.nearest_exact`). It may stand without a query, as any declared
  index may ([Indexes](indexes.md)). 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: `m` 2 to 100, `ef_construction` 4 to
  1,000, and `ef_construction` at least twice `m`, 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,
  is `type.setting_value`; `m:` or `ef_construction:` on an index without
  `kind: IndexKind.hnsw` is `type.unknown_setting`; either twice is
  `data.duplicate_rule`. Only what the entry writes goes in the
  migration's `WITH (...)`, 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` (or `min`), 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 as `vector(n)` and indexed through a
  half-precision expression, with the metric's `halfvec_` operator class.
  Its query orders by the same expression, so PostgreSQL matches the
  index to it. The distance it reads back and its `where:` thresholds
  stay at full precision, as `ai.distance` gives them. The parameter is
  cast to `vector(n)` first, so the server infers `vector` for it, which
  is the binary format the runtime sends:

  ```sql
  CREATE INDEX "help_article_body_meaning_idx" ON "help_article" USING hnsw (("body_meaning"::halfvec(3072)) halfvec_cosine_ops);
  ```

  ```sql
    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
  `nearest` query on it reads every record. That is the error
  [`query.nearest_exact`](../errors/query.nearest_exact.md), which you
  can decline
  ([Declining a judgement call](declining-a-judgement-call.md)).
  A type with fewer dimensions fixes it. A decline above the query's
  `order:`, 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 and `id` in one statement, and limits, so ties at the
  limit are broken by `id` as everywhere else. It sets no search settings
  and derives no index, so it doesn't need pgvector 0.8.0.
