# Remote vector stores

By default, semantic and hybrid retrieval keep their vectors in memory. Set one URL and `tnsai-rag` stores them in Qdrant or in PostgreSQL with pgvector instead, so the index survives a restart. No annotation member turns this on, and nothing in your `@Knowledge` declarations changes.

`@VectorMemory(provider = "qdrant")` or `provider = "pgvector"` is a different mechanism: an SPI lookup that fails unless a `VectorStoreProvider` with that name is registered. Only `inmemory` ships.

## Enable Qdrant

```bash
export TNSAI_VECTOR_QDRANT_URL=http://localhost:6333
# or -Dtnsai.vector.qdrant.url=http://localhost:6333
```

| System property | Default | Meaning |
|---|---|---|
| `tnsai.vector.qdrant.collection` | `tnsai` | Collection name prefix |
| `tnsai.vector.qdrant.apiKey` | empty | Sent as the `api-key` header when set |

The adapter speaks REST through the JDK `HttpClient`, with a 5 second connect timeout and a 15 second request timeout. The Qdrant Java client is not needed.

## Enable pgvector

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

| System property | Default | Meaning |
|---|---|---|
| `tnsai.vector.pgvector.user` | empty | JDBC user |
| `tnsai.vector.pgvector.password` | empty | JDBC password |
| `tnsai.vector.pgvector.table` | `tnsai_vectors` | Table name prefix |
| `tnsai.vector.pgvector.index` | `hnsw` | `hnsw`, or `ivfflat` (also `ivf`) |

Add `org.postgresql:postgresql` to your runtime classpath; TnsAI does not bring it. Without the driver, opening the store throws `IllegalStateException`.

On first use the adapter runs `CREATE EXTENSION IF NOT EXISTS vector`, creates the table and a cosine index (`vector_cosine_ops`; IVFFlat uses `lists = 100`). Every statement is `IF NOT EXISTS`, so later starts change nothing. Search scores are `1 - (embedding <=> query)`.

## One backend per process

Set the Qdrant URL or the pgvector URL, not both. With both set, the first binding that opens a vector index throws `IllegalStateException`: "Both tnsai.vector.qdrant.url and tnsai.vector.pgvector.url are set; pick one vector backend".

## Collections, tables and owners

Each binding gets its own namespace: the configured prefix, an underscore, and 32 hex characters derived from the binding's scope by SHA-256. Bindings with the same configuration share one collection or table. A blank scope uses the prefix itself.

Inside that namespace, every point or row carries an owner, the full SHA-256 of the owner scope. `clear()` removes only that owner's data. It never drops the collection or the table.

- **Role bindings** use a deterministic owner, built from the Role and its selected sources. A restarted process sees the corpus it stored before.
- **Engines a provider creates** (programmatic bindings) get an owner that is unique to that engine's lifecycle, so two live engines never overwrite each other.

Transport failures on `size()` and search are errors, not an empty result. A missing Qdrant collection (HTTP 404) or a missing pgvector table is the one quiet case: size is 0 and search returns nothing.

## Owner lease

Lifecycle-managed indexes keep their owner alive with a lease, so an owner that a crashed process left behind can be found and removed.

| Setting | Value |
|---|---|
| `tnsai.vector.ownerLease` | ISO-8601 duration, default `PT24H`, from 1 minute to 30 days. Out of range throws `IllegalArgumentException`. |
| Heartbeat | Every lease/3, on a daemon thread named `tnsai-vector-owner-timer`. Each renewal times out after the shorter of the interval and 15 seconds. |
| Sweep of expired owners | Every lease duration |
| Retention of cleared records | 2 × lease. `clear()` keeps non-searchable fence records for this long, so a delayed write cannot bring deleted content back; a later sweep removes them. |

## Standalone stores

Without a Role, open the same adapters as a memory store. `configured(...)` returns empty when the URL is not set:

```java
VectorMemoryStore fallback = new VectorMemoryStore(embedding);

QdrantVectorMemoryStore.configured(embedding).ifPresentOrElse(
        store -> store.add(entry),
        () -> fallback.add(entry));
```

Or configure one explicitly:

```java
QdrantVectorMemoryStore qdrant = QdrantVectorMemoryStore.builder()
        .url("http://localhost:6333")
        .apiKey(System.getenv("QDRANT_API_KEY"))
        .collection("tnsai")
        .embeddingFunction(embedding)
        .build();

PgvectorVectorMemoryStore pgvector = PgvectorVectorMemoryStore.builder()
        .jdbcUrl("jdbc:postgresql://127.0.0.1:5432/tnsai")
        .user("tnsai")
        .password(System.getenv("PGPASSWORD"))
        .table("tnsai_vectors")
        .indexKind(PgvectorTransport.IndexKind.HNSW)
        .embeddingFunction(embedding)
        .build();
```

Both stores offer `add`, `search`, `searchByVector`, `remove`, `size`, `clear` and `index()`. A standalone store has no lease.

## Related

- [RAG](index.md): declaring the knowledge that these stores hold.
- [Embeddings](embedding-prefixes.md): the embedding function the vectors come from.
- [Selective re-embed](selective-reembed.md): a local hash-keyed index, separate from these adapters.
