RAG Pipeline
The server provides a per-session Retrieval-Augmented Generation pipeline that indexes local codebases, chunks source files by language boundaries, and retrieves relevant context using hybrid BM25 + vector search with Reciprocal Rank Fusion.
Architecture Overview
The RAG pipeline has three stages: indexing (scanning files and splitting them into chunks), storage (keeping chunks in an in-memory knowledge base with BM25 and vector indexes), and retrieval (finding the most relevant chunks for a user's query using hybrid search).
Directory --> FileIndexer --> FileIngestionService --> KnowledgeBase (in-memory)
|
User Query --> HybridRetriever --> [BM25Stream 60%] -+ RRF --> Results
--> [VectorStream 40%] -+Each session gets its own RagService, lazily created by SessionManager.getRag(sessionId). The service is thread-safe: indexing is serialized via a ReentrantLock, while reads (search) run concurrently.
RagService
The central orchestrator for a session's RAG pipeline.
RagService rag = sessionManager.getRag("my-session");
// Index a directory
rag.indexDirectory(Path.of("/project/src"), progress -> {
System.out.printf("Indexed %d/%d: %s%n",
progress.indexedFiles(), progress.totalFiles(), progress.currentFile());
});
// Search
List<SearchResult> results = rag.search("authentication middleware", 5);
// Build augmented prompt (auto-prepends context)
String prompt = rag.buildContextPrompt("How does auth work?", 5);
// Document management
String docId = rag.addDocument("Custom knowledge...", Map.of("source", "manual"));
rag.removeDocument(docId);
List<RagService.DocumentInfo> docs = rag.listDocuments();The hybrid retriever is configured at construction with BM25 at 60% weight and the vector knowledge base at 40%:
this.hybridRetriever = HybridRetriever.builder()
.stream(bm25Stream, 0.6)
.stream(new KnowledgeBaseStream(knowledgeBase), 0.4)
.build();FileIndexer
FileIndexer is the Server adapter over
com.tnsai.intelligence.rag.binding.FileIngestionService. Discovery,
.gitignore / .tnsignore, default skip lists, format detection,
extraction, and chunking are the same contract as declarative
@KnowledgeSource(type = FILE). The class does not call
CodeChunker. Chunks come from NormalizedDocumentChunker — see
File formats.
This path is in TnsAI 0.14.0 (TnsAI@b7f3a098,
TAN-5806) and ships in Maven Central 0.14.1. Maven Central 0.13.0
used a Server-local walk.
new FileIndexer() and new FileIngestionService() share
FileIngestionService.Limits.defaults(). Pass the same Limits
record to both constructors when you tighten a deployment.
Formats
The walk does not have a private "28 extensions" list. Candidates
follow DocumentFormat / AUTO on File formats.
include / exclude on a KnowledgeSourceConfig still run before
the format check.
Ignore files and default skips
Each directory may carry .gitignore and .tnsignore. Rules from a
nested file apply under that directory, not as if they were
written at the corpus root.
| Pattern | Meaning |
|---|---|
leading / | Anchored to the ignore file's directory. /build in src/.gitignore does not match docs/build. |
trailing / | Directory tree only. tmp/ skips the tmp directory and its children; a regular file named tmp stays. |
unanchored name (*.log, target) | Matches at any depth under that ignore file. |
# | Comment. |
! | Unsupported. The ignore file is a configuration error. |
.gitignore and .tnsignore themselves are never ingested.
Default directory names (case-insensitive) are also skipped:
.aws, .git, .gnupg, .gradle, .hg, .idea, .kube,
.next, .ssh, .svn, .vscode, __pycache__, build,
coverage, dist, node_modules, out, target, vendor.
A subset (.git, .ssh, node_modules, vendor, and the other
secret/VCS ancestors) also matches when it appears as any path
component.
Default file names skipped anywhere: .env, .env.*,
.git-credentials, .netrc, .npmrc, .pypirc, id_dsa,
id_ecdsa, id_ed25519, id_rsa.
Ignore files are capped at 64 KiB and 1 024 compiled selectors.
Admission caps
FileIngestionService.Limits is one policy object:
| Field | Default | Meaning |
|---|---|---|
maxDecodedBytes | 64 MiB | Aggregate decoded-character budget for the source |
maxFiles | 4 096 | Regular files that may be admitted |
maxEntries | 8 192 | Walk entries (files and directories) |
maxDepth | 64 | Maximum relative path depth |
A single file is also bounded at 16 MiB raw input
(ExtractionContext). There is no 512 KB skip threshold.
Symbolic links are never followed. An empty regular file is not a special skip rule — format detection still decides.
Incremental indexing
Reuse is ingestionFingerprint (content hash plus format, extractor,
chunker, and selectors), not a content-only SHA-256. Unchanged
fingerprints keep the previous chunks.
FILE ingest stages a complete generation before mutating TF-IDF or
BM25 (TAN-5620). A source-level format or extraction failure discards
the staged replacement — file N cannot publish chunks from files
1..N-1 of the new attempt. FileIngestionService.IngestionResult
carries fingerprints (Map<String, String>); the previous
eight-argument form is gone. The atomic swap is
KnowledgeBase.replaceDocuments. InMemoryKnowledgeBase and
BM25Stream hold their write locks across that generation. The
interface default walks remove-then-add sequentially — concurrent
readers can see a mix unless the store overrides.
Call fileIndexer.clearHashes() to drop the fingerprint map and force
a full re-index.
CodeChunker
com.tnsai.server.rag.CodeChunker is the Server facade for one-file
source chunking. TnsAI 0.14.0 (TnsAI@a652e847, TAN-5618) delegates
to SourceCodeContentExtractor and NormalizedDocumentChunker, so
callers share symbol boundaries, IDs, limits, and provenance with
declarative FILE ingest.
FileIndexer still does not call CodeChunker. Live directory
ingest goes through FileIngestionService and the same extractor.
Do not treat a leftover regex language table as the live path.
Symbol units and metadata: File formats.
BM25Stream
The BM25Stream provides keyword-based search using the Okapi BM25 algorithm, which is the same ranking function used by search engines like Elasticsearch. It scores documents based on how well their terms match the query, accounting for term frequency and document length.
Parameters
These BM25 parameters control how the scoring behaves. The defaults work well for code search and rarely need tuning.
| Parameter | Value | Description |
|---|---|---|
| K1 | 1.2 | Term frequency saturation |
| B | 0.75 | Document length normalization |
Text Processing Pipeline
Before scoring, queries and documents go through a text processing pipeline that normalizes, tokenizes, and stems terms. This improves recall by matching different forms of the same word.
- Tokenization: Lowercase, strip non-alphanumeric (except
_), split on whitespace, drop tokens with 1 character or fewer - Stop word removal: 50 common English stop words
- Stemming: Suffix-stripping rules for 14 suffixes (
-ies,-ing,-tion,-sion,-ment,-ness,-able,-ous,-ful,-less,-ly,-ed,-er,-es,-s) - Synonym expansion (query-time only): 20 coding-domain synonym pairs
Synonym Pairs
At query time, common coding abbreviations are expanded to their full forms (and vice versa) so that searching for "auth" also finds documents containing "authentication".
| Term | Synonyms |
|---|---|
| db | database |
| auth | authentication, authorization |
| config | configuration |
| perf | performance |
| impl | implementation |
| req | request |
| res | response |
| err | error |
| msg | message |
| fn | function |
| param | parameter |
| repo | repository |
| env | environment |
| async | asynchronous |
| sync | synchronous |
HybridRetriever
The HybridRetriever combines results from multiple search strategies (like BM25 keyword search and vector similarity search) into a single ranked list. This hybrid approach gives better results than either method alone because keyword search finds exact term matches while vector search captures semantic similarity.
Fusion Algorithm
The retriever merges results using Reciprocal Rank Fusion (RRF), which combines rankings without needing normalized scores. For each document appearing in any stream's results:
score(doc) = SUM over streams: weight(stream) / (K + rank(doc, stream) + 1)Where K = 60 (the RRF constant). Documents are then sorted by fused score.
Diversification
To prevent a single large file from dominating search results, the retriever limits output to a maximum of 3 chunks per source file. This ensures the agent sees context from multiple relevant files.
HybridRetriever retriever = HybridRetriever.builder()
.stream(bm25Stream, 0.6) // 60% weight
.stream(vectorStream, 0.4) // 40% weight
.build();
List<SearchResult> results = retriever.retrieve("authentication flow", 10);Context Prompt Format
When the agent asks a question, RagService.buildContextPrompt searches for relevant code and prepends it to the user's query. This gives the LLM the codebase context it needs to answer accurately.
[Relevant code context]
--- file: src/auth/Middleware.java (lines 15-45) ---
public class AuthMiddleware {
private final TokenValidator validator;
...
}
--- file: src/auth/TokenValidator.java (lines 1-30) ---
public class TokenValidator {
...
}
[User question]
How does the authentication middleware work?If no context is found (empty knowledge base or no matches), the original query is returned unchanged.
Document Management API
Beyond automatic directory indexing, you can manually add, list, and remove documents in the knowledge base. This is useful for injecting custom knowledge (like deployment procedures or domain-specific documentation) that is not part of the codebase.
// Add a document with metadata
String docId = rag.addDocument("Custom knowledge content",
Map.of("source", "user", "topic", "deployment"));
// List documents (returns preview, length, metadata)
List<RagService.DocumentInfo> docs = rag.listDocuments();
// DocumentInfo(id, preview(100chars), contentLength, metadata)
// Get a specific document
Optional<Document> doc = rag.getDocument(docId);
// Remove
boolean removed = rag.removeDocument(docId);
// Clear everything
rag.clear();Documents added via addDocument are tracked separately and appear in listDocuments(). Both manually added documents and file-indexed chunks are searchable through the same hybrid retriever.
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.
Agent-level action RAG
@AgentSpec.knowledge() and @AgentSpec.retrieval() are the annotation counterparts of AgentBuilder.addKnowledgeSource and AgentBuilder.retrieval. Both members are @since 0.14.0 in TnsAI 0.14.0 (TnsAI@c2bb5306). They ship in Maven Central 0.14.1.