# pgvector vector index

TnsAI 0.14.0 (`TnsAI@816ed1b9`, TAN-3096 / PR #141) ships an
optional pgvector backend for `VectorMemoryStore`. The types are
`@since 0.14.0`. They ship in Maven Central `0.14.1`.

Role RAG uses pgvector when a JDBC URL is configured and otherwise keeps
the in-memory index. No `@Retrieval` member turns this on.
`@VectorMemory(provider = "pgvector")` is a different path: fail-loud
SPI lookup, not this JDBC-URL adapter (TAN-2911). Qdrant and pgvector
cannot both be configured in one process — `VectorBackends` fails
startup rather than picking a winner.

## Enable

```bash
export TNSAI_VECTOR_PGVECTOR_URL=jdbc:postgresql://127.0.0.1:5432/tnsai
# or -Dtnsai.vector.pgvector.url=jdbc:postgresql://127.0.0.1:5432/tnsai
```

Optional knobs, all system properties only:

| Property | Default | Role |
| --- | --- | --- |
| `tnsai.vector.pgvector.user` | empty | JDBC user |
| `tnsai.vector.pgvector.password` | empty | JDBC password |
| `tnsai.vector.pgvector.table` | `tnsai_vectors` | Table prefix |
| `tnsai.vector.pgvector.index` | `hnsw` | `hnsw` or `ivfflat` / `ivf` |

The PostgreSQL JDBC driver is **not** a `tnsai-core` or required
`tnsai-intelligence` dependency. Put `org.postgresql:postgresql` on the
runtime classpath yourself. Production talks SQL through JDK
`DriverManager` (`JdbcPgvectorTransport` loads `org.postgresql.Driver`
reflectively). A missing driver is an `IllegalStateException`, not an
empty index.

The first upsert runs `CREATE EXTENSION IF NOT EXISTS vector` and
creates the scoped table plus a cosine index (`vector_cosine_ops`).
HNSW is the default; IVFFlat uses `lists = 100`. A second start is a
no-op (`IF NOT EXISTS`).

## Role bindings

`RoleRagBinding` asks `VectorBackends.configured(scope)` while building
the scoped vector store. That helper opens Qdrant **or** pgvector — never
both. A blank pgvector URL (and no Qdrant URL) returns empty and the
binding constructs `new VectorMemoryStore(embedding)` as before.

The scope is the Role class plus the selected source-name set. The table
becomes `prefix` + `_` + the first **sixteen** hex digits of the SHA-256
of that scope (`fingerprint` walks eight bytes and writes two hex chars
each), so two Roles never share a table. Each index instance also tags
every row with an owner derived from the **same** fingerprint (TAN-5821).
A restart therefore still sees the previous corpus. `clear()` deletes
those owner rows and never drops the table.

A `putIfAbsent` race loser calls `abandon()` / `discard()` — it does
**not** `clear()` the remote store. Upserts under the deterministic
owner are idempotent (`ON CONFLICT (point_id) DO UPDATE`).

Transport faults on `size()` and search are errors, not an empty index.
A missing table is the one quiet case: size is `0` and search is empty.

```java
@KnowledgeSource(name = "handbook", path = "knowledge/handbook")
@Retrieval(strategy = Retrieval.Strategy.SEMANTIC)
public final class HandbookRole extends Role {
}
```

With the JDBC URL set, that Role's semantic (and hybrid) index lands in
Postgres and survives a process restart. Without the URL, the same
declaration stays in-process.

## Standalone store

Programmatic callers can open the same adapter without a Role:

```java
PgvectorVectorMemoryStore.configured(embedding).ifPresentOrElse(
        store -> store.add(entry),
        () -> new VectorMemoryStore(embedding).add(entry));
```

Or name the table and index kind explicitly:

```java
PgvectorVectorMemoryStore store = PgvectorVectorMemoryStore.builder()
        .jdbcUrl("jdbc:postgresql://127.0.0.1:5432/tnsai")
        .table("tnsai_vectors")
        .indexKind(PgvectorTransport.IndexKind.HNSW)
        .embeddingFunction(embedding)
        .build();
```

`VectorMemoryStore` now accepts a `VectorIndex`. `PgvectorVectorIndex`
is the shipped JDBC adapter; `InMemoryVectorIndex` remains the no-URL
default. Search ranks with cosine distance (`1 - (embedding <=> query)`).

## Not this page

- `@VectorMemory(provider = "pgvector")` — after TAN-2911
  (`TnsAI@31f3c503`) this string is fail-loud unless a
  `VectorStoreProvider` named `pgvector` is registered. Core only
  ships `inmemory`. This page's adapter is still the JDBC-URL
  `PgvectorVectorIndex` path, not that annotation.
- The optional Qdrant REST adapter — same `VectorIndex` seam, different
  URL (`tnsai.vector.qdrant.url`). Setting both URLs is a deployment
  fault.
- Maven Central `0.14.1` ships `VectorIndex` and this adapter.
- A new parent-POM module — TAN-3096 shipped inside `tnsai-intelligence`.
- [Selective re-embed](selective-reembed.md) — local hash-keyed index,
  not this JDBC adapter.
