# RAG Strategy SPI

TnsAI 0.14.1 provides eight retrieval strategies in
`com.tnsai.intelligence.rag`. You can construct them directly for an
application-owned pipeline or select the matching mode through `@Retrieval`.
Those entry points share strategy names, but they do not share every default or
provider requirement.

TnsAI 0.14.0 (`@since 0.14.0`, TAN-5326) lets `@Retrieval.queryParam` and
`RetrievalConfig.queryParam` name the action argument that becomes the query.
Blank — the default — still binds the first `String` in declaration order.
Named selection needs `-parameters`. A class-level name must exist on every
action of that Role; otherwise put `@Retrieval` on each method. Missing names
fail before `@Retrieval.onFailure`. Details: [Declarative RAG](index.md).

## RAGStrategy Interface

Every programmatic strategy implements this contract:

```text
String name();
List<RetrievedDocument> retrieve(String query, RAGContext context);
```

`RetrievedDocument` contains `content`, `source`, `score`, and an unmodifiable
`Map<String, String> metadata` view. `RAGContext` carries `maxResults` (default
`10`), `minScore` (default `0.0`), and exact metadata filters. Use
`new RAGContext()` or derive a new context with `withMaxResults`,
`withMinScore`, and `withMetadataFilters`. `RAGContext` defensively copies its
filter map. `RetrievedDocument` exposes an unmodifiable view of the metadata
map supplied to its constructor, so copy that input first if its owner may
mutate it later.

## Programmatic Setup

The three base strategies use the in-memory stores from `tnsai-core`. A
`VectorMemoryStore` accepts an application-provided `EmbeddingFunction`; use a
production embedding provider whose output has a stable dimension. The small
function below is deterministic only so the complete example can run without
external services.

<!-- java-contract: src/main/java/RagStrategyExample.java -->
```java
import com.tnsai.intelligence.rag.HybridRAGStrategy;
import com.tnsai.intelligence.rag.KeywordRAGStrategy;
import com.tnsai.intelligence.rag.RAGContext;
import com.tnsai.intelligence.rag.RAGPipeline;
import com.tnsai.intelligence.rag.RAGStrategy;
import com.tnsai.intelligence.rag.RetrievedDocument;
import com.tnsai.intelligence.rag.VectorRAGStrategy;
import com.tnsai.memory.advanced.BM25Index;
import com.tnsai.memory.advanced.EmbeddingFunction;
import com.tnsai.memory.advanced.VectorMemoryStore;
import com.tnsai.memory.strategy.MemoryEntry;
import com.tnsai.memory.strategy.MemoryTier;

import java.util.List;
import java.util.Map;

public final class RagStrategyExample {
    private RagStrategyExample() {
    }

    public static void main(String[] args) {
        EmbeddingFunction embedding = text -> new float[] {
            text.contains("agent") ? 1.0f : 0.0f,
            text.contains("memory") ? 1.0f : 0.0f
        };
        VectorMemoryStore vectors = new VectorMemoryStore(embedding);
        BM25Index keywords = new BM25Index();

        MemoryEntry semantic = new MemoryEntry(
            "memory-guide",
            "Agents can persist semantic memory.",
            MemoryTier.SEMANTIC
        );
        MemoryEntry working = new MemoryEntry(
            "working-note",
            "Agents use memory while running.",
            MemoryTier.WORKING
        );
        vectors.add(semantic);
        vectors.add(working);
        keywords.add(semantic);
        keywords.add(working);

        RAGStrategy vector = new VectorRAGStrategy(vectors);
        RAGStrategy keyword = new KeywordRAGStrategy(keywords);
        RAGStrategy hybrid = new HybridRAGStrategy(keywords, vectors);

        RAGContext context = new RAGContext()
            .withMaxResults(5)
            .withMinScore(0.0)
            .withMetadataFilters(Map.of("tier", "SEMANTIC"));
        List<RetrievedDocument> documents =
            hybrid.retrieve("agent memory", context);
        if (documents.isEmpty() || documents.stream().anyMatch(document ->
                !"SEMANTIC".equals(document.metadata().get("tier")))) {
            throw new IllegalStateException("tier filter was not applied");
        }

        RAGPipeline pipeline = RAGPipeline.builder(hybrid)
            .defaultContext(context)
            .queryPreprocessor(String::strip)
            .reranker(results -> results)
            .build();
        String formattedContext = pipeline.execute("agent memory");

        System.out.println(vector.name());
        System.out.println(keyword.name());
        System.out.println(documents.size());
        System.out.println(formattedContext);
    }
}
```

## Metadata Filtering

`RAGContext.metadataFilters()` uses exact, case-sensitive string equality
against `RetrievedDocument.metadata()`. Every filter entry must match. A
missing key excludes the document, so two entries in the filter map are an
AND predicate, not alternatives. Null or blank keys and values are rejected
when the context is created.

The direct memory-backed `VectorRAGStrategy`, `KeywordRAGStrategy`, and
`HybridRAGStrategy` emit two filterable fields for every result:

| Key | Value |
| --- | --- |
| `tier` | The exact `MemoryTier.name()`, such as `SEMANTIC` or `WORKING` |
| `entryId` | The exact `MemoryEntry.id()` |

These strategies filter the ranked candidate set before applying
`maxResults`. For example, a context with `maxResults = 1` and
`tier = WORKING` can return the highest-ranked WORKING entry even when an
unfiltered SEMANTIC entry ranks first. Filtering by a key that these direct
strategies do not emit, such as `tenant`, returns no results.

The unified annotation/declarative path has a wider metadata surface. It
attaches source and custom-loader metadata after the base memory strategy
returns, applies the same exact-match predicate to that attributed metadata,
and only then limits the results to the configured top-K, or to top-N when
reranking is enabled. Reserved
`tnsai.chat.*` lifecycle fields are request context rather than document
predicates and are removed before this filtering step. This separation lets a
custom `SourceLoader` expose additional filterable fields without claiming
that direct programmatic memory strategies emit them.

This behavior landed in framework PR
[#97](https://github.com/TnsAI-Framework/TnsAI/pull/97) for TAN-5625. The
eight-strategy inventory was established separately under TAN-5329.

## The Eight Strategies

### VectorRAGStrategy

`VectorRAGStrategy(VectorMemoryStore)` ranks entries by cosine similarity. It
is the programmatic semantic option, and its quality depends on the
`EmbeddingFunction` supplied to the store.

### KeywordRAGStrategy

`KeywordRAGStrategy(BM25Index)` performs lexical BM25 retrieval. It needs no
embedding provider and works well for identifiers, exact terms, and code-like
queries.

TnsAI 0.14.0 also has a **package-private** write-back overlay that
uses BM25 inside `ControlledWriteBackStore`. That store is default-off,
not this public constructor, and does not join named-source or
non-`KEYWORD` requests. See [Controlled write-back](write-back.md).

### HybridRAGStrategy

`HybridRAGStrategy(BM25Index, VectorMemoryStore)` combines lexical and vector
results with Reciprocal Rank Fusion using the default RRF constant of `60`.
Alternatively, pass a configured `HybridMemoryRetriever` to its one-argument
constructor.

A third constructor (`@since 0.14.0`) also accepts a source-fenced
`GraphRAGStrategy` and RRF-fuses that ranking with BM25 and vector. A missing
graph store does not collapse HYBRID into vector-only; the two lexical and
semantic streams still fuse. Details: [Hybrid graph stream](hybrid-graph.md).

### GraphRAGStrategy

`GraphRAGStrategy` finds seed nodes in a `GraphStore`, then follows outgoing
edges with bounded depth, score decay, cycle prevention, and an optional source
fence. Its constructor accepts:

```text
GraphRAGStrategy(
    String providerName,
    GraphStore store,
    Set<String> allowedSources,
    int maxDepth,
    double depthDecay,
    int maxVisitedNodes
)
```

Defaults exposed by the class are depth `2`, depth decay `0.85`, and `1,024`
visited nodes. Apply those constants explicitly when constructing the strategy.

### MultiQueryRAGStrategy

`MultiQueryRAGStrategy` asks a `QueryExpander` for query variations, runs each
unique query through one base strategy, and merges the results with reciprocal
rank fusion. It requires a non-`NONE` `Retrieval.QueryExpansion` mode, an exact
provider model name, and a positive expansion limit:

```text
MultiQueryRAGStrategy(
    RAGStrategy baseStrategy,
    QueryExpander expander,
    Retrieval.QueryExpansion mode,
    String model,
    int maxExpansions
)
```

An overload adds a `CancellationToken`. `QueryExpander` implementations own
model inference and are discovered through `ServiceLoader` on the declarative
path; TnsAI does not bundle an expansion model.

### HierarchicalRAGStrategy

`HierarchicalRAGStrategy` starts with child hits and expands their parent chain
through a validated `HierarchyIndex`. Its constructor accepts a child strategy,
the index, an optional source fence, maximum parent depth, and per-level score
decay. The class defaults are depth `3` and decay `0.85`.

Documents in the index need stable hierarchy IDs. A child with a parent sets
`HierarchyMetadata.PARENT_ID` to that parent's ID; duplicate IDs, missing
parents, and cycles are configuration errors.

### TemporalRAGStrategy

`TemporalRAGStrategy` takes candidates from a child strategy and applies a
bounded freshness multiplier using a `TemporalIndex`. Its constructor accepts
the child strategy, index, optional source fence, `Clock`, half-life, relevance
floor, and maximum future clock skew. Defaults are a 30-day half-life, `0.8`
relevance floor, and five-minute future skew.

Timestamped documents use `TemporalMetadata.TIMESTAMP` with an ISO-8601
instant. Undated documents retain the relevance floor; invalid timestamps and
excessive future dates fail validation or retrieval instead of being silently
accepted.

### ReasoningRAGStrategy (TnsAI 0.14.0)

`ReasoningRAGStrategy` is `@since 0.14.0` and ships in Maven Central
`0.14.1`. It is the opposite walk from `HierarchicalRAGStrategy`:
root-to-leaf descent with an installed `TreeNavigator` choosing the
branch. TnsAI bundles no navigator.

```text
ReasoningRAGStrategy(
    HierarchyIndex index,
    TreeNavigator navigator,
    String model,
    Set<String> allowedSources,
    int maxNodesVisited,
    int maxDepth
)
```

A three-argument overload (`index`, `navigator`, `model`) uses empty sources
and the class defaults: `15` visited nodes and depth `6`. See
[vectorless reasoning retrieval](reasoning.md).

The package-private `DeduplicatingRagStrategy` used by the declarative runtime
is a post-retrieval decorator, not an additional 0.14.1 declarative mode.

## Programmatic and Declarative Retrieval

Direct construction gives the application ownership of stores, providers,
indexes, clocks, and strategy composition. `RAGPipeline` is a programmatic
orchestrator; it is not installed with an `AgentBuilder.ragPipeline(...)`
method. Call the pipeline from application code or a custom action/executor.

The annotation-first path uses `@KnowledgeSource` plus `@Retrieval` on an
action. The runtime ingests the selected sources and resolves the corresponding
strategy at dispatch time. The table below is the immutable Maven Central
0.14.1 surface. The [RAG overview](index.md) shows the action and builder entry
points; the [declarative RAG tutorial](../../tutorials/declarative-rag.md)
provides the complete action-level walkthrough.

| `@Retrieval` strategy | Runtime composition | Additional requirement |
| --- | --- | --- |
| `SEMANTIC` | Vector retrieval | Loaded knowledge sources; 128-slot hash / token overlap unless one `EmbeddingFunction` is installed |
| `KEYWORD` | BM25 retrieval | Loaded knowledge sources |
| `HYBRID` | Vector + BM25 with RRF; optional graph stream | Loaded knowledge sources; one claiming `GraphStoreProvider` for the third stream |
| `GRAPH` | Graph traversal | One matching `GraphStoreProvider` |
| `MULTI_QUERY` | Query expansion + semantic base strategy | Matching `QueryExpander` and model |
| `HIERARCHICAL` | Semantic child hits + parent expansion | Valid hierarchy metadata |
| `TEMPORAL` | Semantic child hits + freshness reranking | Valid temporal metadata |
| `REASONING` | Root-to-leaf tree descent | Matching `TreeNavigator` and `navigatorModel` — `@since 0.14.0` |

### TnsAI 0.14.0: reasoning retrieval

Maven Central `0.14.1` adds an eighth declarative mode, `REASONING`, backed by
`ReasoningRAGStrategy`. It performs root-to-leaf tree descent through one
installed `TreeNavigator`; `navigatorModel` identifies the provider-owned
model. The mode is `@since 0.14.0`.

### TnsAI 0.14.0: declarative embedding provider

Programmatic vector and hybrid retrieval use the `EmbeddingFunction` supplied
directly to `VectorMemoryStore`. Declarative Role bindings instead discover one
process-wide application provider through Java `ServiceLoader`. Register the
implementation class in exactly this service file:

```text
META-INF/services/com.tnsai.memory.advanced.EmbeddingFunction
```

The file contains the fully qualified name of one public, no-argument
`EmbeddingFunction` implementation. The runtime behavior is deterministic:

- no installed provider uses the built-in, offline 128-dimensional hash
  fallback; it primarily measures token overlap
- exactly one provider is wrapped, cached for the process, and shared by all
  vector-backed declarative strategies and embedding-based deduplication
- multiple providers fail binding initialization as a configuration error;
  classpath order is never used to choose one
- concurrent initialization or embedding calls fail immediately instead of
  waiting in a queue, so the application must control call concurrency and the
  provider must return promptly
- returned vectors must be non-null, non-empty, at most 16,384 values, finite,
  and dimensionally stable; the wrapper snapshots the returned buffer before
  finite-value validation and before returning it

The first shape-valid vector fixes the process provider's dimension even if a
later finite-value check rejects that vector. Provider transport failures keep
their typed `RetrievalException`; other provider exceptions are classified as
execution failures.

This ServiceLoader path is separate from direct programmatic construction. An
application that passes an `EmbeddingFunction` to `VectorMemoryStore` owns that
specific store and does not use the declarative process-provider selection.

## RAGPipeline

`RAGPipeline.builder(strategy)` composes four phases in this order:

1. `queryPreprocessor(UnaryOperator<String>)`
2. `RAGStrategy.retrieve(query, RAGContext)`
3. `reranker(UnaryOperator<List<RetrievedDocument>>)`
4. numbered text formatting

`execute(query)` returns the formatted context string. Use
`retrieveDocuments(query, context)` when you need the raw documents after
optional reranking. Set the reusable default with `defaultContext(context)` or
pass a context to `execute(query, context)`.

## Choosing a Strategy

| Strategy | Strengths | Tradeoffs | Best fit |
| --- | --- | --- | --- |
| Vector | Semantic ranking when the caller supplies embeddings | Declarative `SEMANTIC` uses 128-slot hash / token overlap unless one `EmbeddingFunction` is installed | Natural-language similarity |
| Keyword | Fast lexical BM25 matching | Does not understand paraphrases | Technical terms, IDs, code |
| Hybrid | Combines semantic, exact, and optional graph ranking | Two retrieval paths, or three when a graph store is supplied | Mixed production corpora |
| Graph | Traverses explicit relationships | Requires a graph provider and bounded traversal design | Connected entities and dependencies |
| Multi-query | Improves recall through query variations | Adds model calls and fusion cost | Ambiguous or underspecified questions |
| Hierarchical | Returns relevant children with parent context | Requires valid parent metadata | Sections, manuals, and nested documents |
| Temporal | Preserves relevance while favoring fresh material | Requires trustworthy timestamps | News, incidents, and changing policies |
| Reasoning | Root-to-leaf descent without embeddings | Requires an installed `TreeNavigator` and `navigatorModel` | Long structured corpora where vocabulary overlap fails |
