Skip to content
tnsaiDocs
GitHub

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 propertyDefaultMeaning
tnsai.vector.qdrant.collectiontnsaiCollection name prefix
tnsai.vector.qdrant.apiKeyemptySent 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 propertyDefaultMeaning
tnsai.vector.pgvector.useremptyJDBC user
tnsai.vector.pgvector.passwordemptyJDBC password
tnsai.vector.pgvector.tabletnsai_vectorsTable name prefix
tnsai.vector.pgvector.indexhnswhnsw, 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.

SettingValue
tnsai.vector.ownerLeaseISO-8601 duration, default PT24H, from 1 minute to 30 days. Out of range throws IllegalArgumentException.
HeartbeatEvery 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 ownersEvery lease duration
Retention of cleared records2 × 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.

  • 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.