Retrieval strategies
TnsAI 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 @Search(strategy = ...) on a @Knowledge source. Those entry points share strategy names, but they do not share every default or provider requirement.
The RAGStrategy interface
Every programmatic strategy implements this contract:
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.
Build strategies in code
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.
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.
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.
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 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: Graph as a third hybrid stream.
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:
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 Search.QueryExpansion mode, an exact
provider model name, and a positive expansion limit:
MultiQueryRAGStrategy(
RAGStrategy baseStrategy,
QueryExpander expander,
Search.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
ReasoningRAGStrategy is the opposite walk from HierarchicalRAGStrategy:
root-to-leaf descent with an installed TreeNavigator choosing the
branch. TnsAI bundles no navigator.
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.
The package-private DeduplicatingRagStrategy used by the declarative runtime
is a post-retrieval decorator, not an additional 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 declares @Knowledge sources on the Role or agent
and names them from an action with @ActionSpec(knowledge = ...). The runtime
ingests the named sources and resolves the strategy from their @Search at
dispatch time. The RAG overview shows the action and builder entry
points, and the declarative RAG tutorial
provides the complete action-level walkthrough.
@Search 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, plus 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 |
TnsAI 0.14.0: declarative embedding provider
Programmatic vector and hybrid retrieval use the EmbeddingFunction you pass to VectorMemoryStore. Declarative Role bindings instead discover one process-wide provider through Java ServiceLoader, registered in this service file:
META-INF/services/com.tnsai.memory.advanced.EmbeddingFunctionWith no provider, declarative vector retrieval uses a 128-dimensional hash embedding and warns. With more than one, binding initialization fails. The rules and the warning are on Embeddings.
RAGPipeline
RAGPipeline.builder(strategy) composes four phases in this order:
queryPreprocessor(UnaryOperator<String>)RAGStrategy.retrieve(query, RAGContext)reranker(UnaryOperator<List<RetrievedDocument>>)- 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 |
Related
- RAG: declaring
@Knowledgesources and@Search. - Install retrieval providers: rerankers, query expanders, tree navigators and graph stores.
- Embeddings: the embedding provider and the hash fallback.
- Graph stores: graph retrieval and the third hybrid stream.
- Vectorless retrieval: reasoning retrieval over a tree.