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
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
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:
VectorMemoryStore fallback = new VectorMemoryStore(embedding);
QdrantVectorMemoryStore.configured(embedding).ifPresentOrElse(
store -> store.add(entry),
() -> fallback.add(entry));Or configure one explicitly:
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: declaring the knowledge that these stores hold.
- Embeddings: the embedding function the vectors come from.
- Selective re-embed: a local hash-keyed index, separate from these adapters.