# Knowledge Base & RAG

TnsAI provides a built-in Retrieval-Augmented Generation (RAG) system through the `KnowledgeBase` interface, `Document` model, and `@KnowledgeSource` annotation.

**Package:** `com.tnsai.knowledge`

The only shipped `KnowledgeBase` implementation is `InMemoryKnowledgeBase`.
Pinecone, Weaviate, Milvus, Elasticsearch, and OpenSearch are **not**
framework adapters for this interface. `WeaviateTools` / `QdrantTools` are
`tnsai-tools` POJO toolkits (LLM-callable HTTP helpers), not
`KnowledgeBase` backends.

Role and action RAG indexes live on `VectorMemoryStore` (an in-process
`VectorIndex`). Optional Qdrant and pgvector indexes attach through their
environment-backed adapters (see [Qdrant](qdrant.md) /
[pgvector](pgvector.md)). They are not `KnowledgeBase` replacements.

`@VectorMemory.provider` resolves through
`com.tnsai.memory.vector.VectorStoreProvider`. Core provides `inmemory`
(`InMemoryVectorStoreProvider`), whose default searches a real
`VectorMemoryStore`. A name with no SPI
(`qdrant`, `pgvector`, `milvus`, …) throws `IllegalStateException`
at agent init — there is no silent TF-IDF fallback. Optional Role
Qdrant/pgvector adapters open from env URL; they do **not** register as
`VectorStoreProvider`.

`com.tnsai.intelligence.rag.vector.SelectiveReembedIndex` is a hash-keyed
**local** index that
re-embeds dirty chunks only. It is not a `VectorIndex` backend and not
a `KnowledgeBase`. See [Selective re-embed](selective-reembed.md).

## KnowledgeBase Interface

`KnowledgeBase` is the programmatic document store. You can implement
the interface yourself; TnsAI does not ship a second adapter.

Chat grounding uses `ChatKnowledgeBinding` and `RetrievalEngine`; the
orchestrator does not call `KnowledgeBase.search` directly. Application code
can call `search` on its own store. To ground agent chat, choose the declarative
or application-supplied binding described under
[Integration with AgentBuilder](#integration-with-agentbuilder).

### Methods

These are the operations every `KnowledgeBase` implementation must support. The most important ones are `addDocument` (to ingest content) and `search` (to retrieve relevant context for an LLM call).

| Method | Signature | Description |
|--------|-----------|-------------|
| `addDocument` | `void addDocument(Document document)` | Adds a single document. Throws `KnowledgeBaseException` on failure. |
| `addDocuments` | `default void addDocuments(List<Document> documents)` | Adds multiple documents. Default implementation iterates `addDocument`. |
| `getDocument` | `Optional<Document> getDocument(String id)` | Retrieves a document by ID. Returns empty if not found. |
| `removeDocument` | `boolean removeDocument(String id)` | Removes a document by ID. Returns `true` if removed, `false` if not found. |
| `replaceDocuments` | `default void replaceDocuments(Collection<String> removeIds, List<Document> add)` | One generation: drop then add. Default walks sequentially. `InMemoryKnowledgeBase` holds the write lock and rolls back on add failure so readers never see a mix. |
| `search` | `List<SearchResult> search(String query, int topK)` | Natural language search. Returns results ordered by relevance (highest first). |
| `search` | `List<SearchResult> search(String query, int topK, Map<String, Object> filters)` | Search with metadata filtering. Filter entries are key-value pairs that must match. |
| `searchByEmbedding` | `List<SearchResult> searchByEmbedding(float[] embedding, int topK)` | Similarity search using a pre-computed embedding vector. |
| `size` | `int size()` | Returns the total number of documents. |
| `isEmpty` | `default boolean isEmpty()` | Returns `true` if the knowledge base has no documents. Delegates to `size() == 0`. |
| `clear` | `void clear()` | Removes all documents from the knowledge base. |
| `contains` | `default boolean contains(String id)` | Checks if a document with the given ID exists. Delegates to `getDocument(id).isPresent()`. |

## Document

`Document` is an immutable value object representing a document or document chunk. Each document has:

- **id** -- Unique identifier (auto-generated UUID if not specified)
- **content** -- The text content (required, cannot be null or empty)
- **metadata** -- Arbitrary key-value pairs for filtering and context (immutable copy)
- **embedding** -- Optional vector representation for similarity search (defensive copy)

### Factory Methods

The quickest way to create a `Document` is with the static `of()` methods. These are convenient for simple use cases where you do not need to set an explicit ID or embedding.

```java
// Simple document (auto-generated ID)
Document doc = Document.of("This is the document content");

// Document with a single metadata entry
Document doc = Document.of("Product docs...", "source", "docs/product.md");
```

### Builder

For full control over the document's ID, metadata, and embedding vector, use the builder. This is the recommended approach when you need to attach metadata for filtered searches or pre-computed embeddings for similarity search.

```java
Document doc = Document.builder()
    .id("doc-001")                          // optional, UUID generated if omitted
    .content("Product documentation...")     // required
    .metadata("source", "docs/product.md")  // single entry
    .metadata("category", "documentation")  // chainable
    .metadata(Map.of("version", "2.0"))     // bulk metadata
    .embedding(embeddingVector)              // optional float[]
    .build();
```

Builder method `content(String)` throws `NullPointerException` if null. `build()` throws `IllegalStateException` if content is null or empty.

### Accessors

These getter methods let you read the document's fields. Metadata is accessed through the `getSpec` methods, and embeddings are returned as defensive copies to preserve immutability.

| Method | Return Type | Description |
|--------|-------------|-------------|
| `getId()` | `String` | Unique document ID |
| `getContent()` | `String` | Document text content |
| `getSpec()` | `Map<String, Object>` | Unmodifiable metadata map |
| `getSpec(String key)` | `Object` | Single metadata value, or `null` |
| `getSpec(String key, Class<T> type)` | `T` | Type-safe metadata value, returns `null` if missing or wrong type |
| `hasEmbedding()` | `boolean` | Whether an embedding is present |
| `getEmbedding()` | `float[]` | Copy of embedding array, or `null` |
| `getEmbeddingDimension()` | `int` | Embedding vector length, or `0` if none |

### Immutable Copy with Embedding

Since `Document` is immutable, attaching an embedding returns a new `Document` instance rather than modifying the original. This is useful when you compute embeddings separately after initial document creation.

```java
// Attach an embedding to an existing document (returns a new Document)
Document withVector = doc.withEmbedding(embeddingVector);
```

Equality is based on `id` only.

## SearchResult

`SearchResult` wraps a matched `Document` with a relevance score. Implements `Comparable<SearchResult>` -- natural ordering is by score descending (highest first).

| Method | Return Type | Description |
|--------|-------------|-------------|
| `getDocument()` | `Document` | The matched document |
| `getScore()` | `double` | Relevance score (higher = more relevant) |
| `getContent()` | `String` | Convenience: delegates to `document.getContent()` |
| `getDocumentId()` | `String` | Convenience: delegates to `document.getId()` |

Constructor: `new SearchResult(Document document, double score)` -- document cannot be null.

```java
List<SearchResult> results = knowledgeBase.search("query", 5);
for (SearchResult result : results) {
    System.out.printf("Score: %.4f | %s%n", result.getScore(), result.getContent());
}
```

## InMemoryKnowledgeBase

`InMemoryKnowledgeBase` is a thread-safe, in-memory implementation suitable for testing and small datasets. It provides:

- **ConcurrentHashMap** storage for thread safety
- **TF-IDF keyword search** with stop-word removal for `search()`
- **Cosine similarity** for both TF-IDF vectors and raw embeddings (`searchByEmbedding`)
- **Metadata filtering** support

```java
KnowledgeBase kb = new InMemoryKnowledgeBase();

kb.addDocument(Document.of("Java is a programming language"));
kb.addDocument(Document.of("Python is also a programming language"));
kb.addDocument(Document.builder()
    .content("Rust is a systems programming language")
    .metadata("category", "systems")
    .build());

// Keyword search
List<SearchResult> results = kb.search("programming language", 5);

// Filtered search
List<SearchResult> filtered = kb.search("programming", 5,
    Map.of("category", "systems"));

// Embedding search
List<SearchResult> similar = kb.searchByEmbedding(queryEmbedding, 3);
```

`InMemoryKnowledgeBase` is the built-in store for this interface. Large or
durable vector indexes are a different type: `VectorMemoryStore`, not a
drop-in `KnowledgeBase`.
See [Strategies](strategies.md) and [Pipeline](pipeline.md).

FILE ingest uses `replaceDocuments` as the atomic swap after a staged
generation. See [Pipeline — incremental indexing](pipeline.md#incremental-indexing).

## @KnowledgeSource Annotation

**Package:** `com.tnsai.annotations`

Declarative configuration for RAG knowledge sources. Can be applied to types (agent classes) or methods (individual actions). Repeatable via `@KnowledgeSources`.

**Targets:** `ElementType.TYPE`, `ElementType.METHOD`
**Retention:** `RetentionPolicy.RUNTIME`

### Fields

`@KnowledgeSource` says **where documents come from**. It does not say how they
are searched -- result counts, score thresholds, caching and strategy are
[`@Retrieval`](../../tutorials/declarative-rag.md)'s, applied per action.

That split is enforced, not merely advised. A source loader runs **once**, when
the agent's knowledge binding is built, and returns the documents it found; the
framework indexes those and searches the index thereafter. Per-request result
limits, score thresholds, caching, and strategies belong to `@Retrieval`.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `name` | `String` | (required) | Unique identifier. `@Retrieval(sources = ...)` selects sources by this name |
| `type` | `KnowledgeType` | `FILE` | Which loader ingests this source |
| `path` | `String` | `""` | Directory or file to read (for `FILE`, `URL`) |
| `connection` | `String` | `""` | Database connection string (for `DATABASE`) |
| `query` | `String` | `""` | Query template with `${query}` placeholder (for `DATABASE`) |
| `enabled` | `boolean` | `true` | Whether this source is ingested at all |

### KnowledgeType Enum

`type` selects the loader. TnsAI bundles exactly one, for `FILE`.

| Value | Bundled loader | Notes |
|-------|----------------|-------|
| `FILE` | **yes** | Text files on disk. The default. |
| `URL` | no | Documents fetched once from a remote URL or REST API |
| `DATABASE` | no | Rows ingested from a SQL or NoSQL database |
| `MEMORY` | no | A snapshot of the agent's own memory |

The three types without a bundled loader are **extension points**, not
placeholders: the framework resolves them through the `SourceLoader` SPI and
your application supplies the loader from an optional module, following the rule
that integrations are never bundled into core.

A source whose type has no installed loader is **skipped with a warning**, and
the binding still builds from whichever sources did load. Only when *every*
enabled source is unloadable does binding fail, with an error naming the types
and what is registered. The line is drawn there so you can install optional
loaders one at a time -- but it does mean a missing loader costs you part of the
corpus without failing anything. Read the warning.

> **Retrieval against a remote index is not a knowledge source.** A vector
> database or a search API is queried *per request*, which the ingest-time
> `SourceLoader` seam cannot express at any level of effort. Those are served by
> `com.tnsai.rag.RetrievalEngineProvider`, which is invoked per retrieval. This
> is why `VECTOR_DB` and `WEB_SEARCH` are not in the table above.

### What the FILE loader reads

`LocalFileSourceLoader` is text-only, and this list is exhaustive:

`.txt` · `.md` · `.markdown` · `.json` · `.yaml` · `.yml` · `.csv`

**PDF and DOCX are not included.** They need a loader of their own, and none is
bundled today. How that shows up depends on how you declared the source:

* **A directory** -- unsupported files are filtered out before they are read.
  They are simply absent from the corpus, while the `loaded N document(s)` line
  still counts whatever text files sat beside them, so nothing looks wrong.
* **A single file** -- the filter does not guard this path, so the loader tries
  to read the binary as text and logs `failed to read file`. That reads like a
  permissions or I/O problem, but it is a format problem.

The loader also does **no chunking**: one file becomes one document however long
it is. Since `@Retrieval.topK` counts documents, on a corpus of whole files it
counts *files* -- `topK = 5` over five long manuals is the entire corpus, and
`contextWindow` then truncates mid-document. Until the framework chunks for you,
splitting large files into smaller ones on disk is what makes those knobs mean
what they say.

### Annotation Examples

These examples show how to attach knowledge sources to an agent class or an
individual action method. Multiple sources combine on the same class through the
repeatable annotation pattern.

```java
// On a class -- multiple sources via @Repeatable
@KnowledgeSource(name = "product-docs", path = "knowledge/products")
@KnowledgeSource(name = "faq", path = "knowledge/faq")
public class SupportAgent extends Agent { ... }

// How the corpus is searched is @Retrieval's, per action
@ActionSpec(type = ActionType.LLM, description = "Answer question")
@Retrieval(sources = "product-docs", topK = 3, minScore = 0.8)
public String answerQuestion(String question) {
    // Relevant context is automatically retrieved before the LLM call
}

// A type whose loader your application registers through the SourceLoader SPI
@KnowledgeSource(
    name = "customer-data",
    type = KnowledgeType.DATABASE,
    connection = "jdbc:postgresql://localhost/mydb",
    query = "SELECT content FROM docs WHERE content ILIKE '%${query}%' LIMIT 10"
)
```

### Refreshing a corpus

A binding -- its documents, its indexes and its retrieval-result cache -- is
built once per agent class per JVM and kept for the process lifetime. Nothing
re-reads your knowledge sources on its own, and `@Retrieval.cacheTTL` does not
cover it: an expired cache entry is recomputed against the same index and
returns the same documents.

When every source owned by the Role may have changed, invalidate the whole
binding:

```java
RoleRagBinding.invalidate(SupportAgent.class);   // next dispatch re-ingests
```

When only one enabled source changed, use its exact, case-sensitive name:

```java
RoleRagBinding.invalidate(SupportAgent.class, "product-docs");
```

That overload rebuilds bindings that contain `product-docs` and resets only
that source's sequential cursor. Sibling bindings and cursors are preserved.
A null, blank, disabled, or unknown source name fails loudly. Retrieval already
in flight completes against the snapshot it started on.

## Integration with AgentBuilder

`KnowledgeBase` is a programmatic store. Agent chat accepts two binding paths,
depending on what describes the source:

- **A source you can declare** — a directory, a file set, a query.
  Declare it with `addKnowledgeSource(...)`, supply `retrieval(...)`,
  and name it with
  [`chatKnowledge(name)`](https://tnsai.dev/docs/capabilities/rag#programmatic-builder).
  This is the
  canonical path.
- **A corpus the application owns and keeps mutating** — a session's
  uploaded documents, a workspace index. `KnowledgeSourceConfig` is a
  declarative descriptor and cannot wrap a live object, so this case
  uses [`liveChatKnowledge(name)` with
  `ChatKnowledgeBinding.live(...)`](https://tnsai.dev/docs/capabilities/rag#application-supplied-live-sources).
  This is the current path for application-supplied live sources.

⚠️ Reaching for `chatKnowledge(name)` with a runtime corpus does not
leave chat ungrounded — it makes `build()` throw, because the named
source is never found. That is the signal to use the live path instead.

## Full RAG Example

This end-to-end example shows the complete RAG workflow: creating a knowledge base, adding documents with metadata, searching for relevant context, building an augmented prompt, and sending it to the agent. It also demonstrates filtered search to narrow results by metadata category.

```java
// 1. Create and populate knowledge base
KnowledgeBase kb = new InMemoryKnowledgeBase();
kb.addDocuments(List.of(
    Document.builder()
        .content("Product X supports features A, B, and C.")
        .metadata("source", "product-docs")
        .metadata("category", "features")
        .build(),
    Document.builder()
        .content("Pricing starts at $99/month for the Basic plan.")
        .metadata("source", "pricing-page")
        .metadata("category", "pricing")
        .build(),
    Document.builder()
        .content("Enterprise plan includes SSO and dedicated support.")
        .metadata("source", "pricing-page")
        .metadata("category", "pricing")
        .build()
));

// 2. Search for relevant context
String query = "What features does Product X have?";
List<SearchResult> context = kb.search(query, 3);

// 3. Build augmented prompt
String augmentedPrompt = "Context:\n" +
    context.stream()
        .map(r -> r.getContent())
        .collect(Collectors.joining("\n")) +
    "\n\nQuestion: " + query;

// 4. Send to agent
String answer = agent.chat(augmentedPrompt);

// Or use filtered search for specific categories
List<SearchResult> pricingResults = kb.search("plan cost", 3,
    Map.of("category", "pricing"));
```

## Embedding prefixes

`EmbeddingFunctions.matryoshka` keeps an L2-normalized prefix of a
Matryoshka-compatible `EmbeddingFunction`.
`VectorMemoryStore` pins the first admitted dimension and rejects mixed
add/query vectors. See [Embedding prefixes](embeddings.md).

## Related pages

- [Agent-level action RAG](agent-knowledge.md)
  `@AgentSpec.knowledge()` / `retrieval()` (not this `KnowledgeBase`).
- [Embedding prefixes](embeddings.md) Matryoshka helper on
  `VectorMemoryStore`.
- [Strategies](strategies.md) — retrieval algorithms and installable
  `EmbeddingFunction` providers.
- [Controlled write-back](write-back.md) package-private
  overlay. Not a `KnowledgeBase` backend and not a public write API.
- [Pipeline](pipeline.md) — file ingest, chunking, and hybrid search.
