Graph stores
Graph retrieval searches a knowledge graph instead of a document index. An action reaches a graph by naming a @Knowledge(type = Knowledge.Type.GRAPH) entry. The entry is a handle, not a corpus: nothing is loaded, and an installed GraphStoreProvider serves the graph for the Role.
Declare a graph handle
@RoleSpec(knowledge = {
@Knowledge(name = "handbook", path = "knowledge/handbook"),
@Knowledge(name = "graph", type = Knowledge.Type.GRAPH)
})
public final class HandbookGraphRole extends Role {
@ActionSpec(
type = ActionType.LLM,
description = "Answer from the handbook graph",
knowledge = @Knowledge(name = "graph"))
public String answer(String question) {
return question;
}
}Rules for a GRAPH entry:
- The strategy is
GRAPH. When neither the entry nor the action sets one,GRAPHapplies; any other strategy fails at init. useisSEARCH.DIRECTandSEQUENTIALfail.- Location and loader members (
path,connection,query,format,include,exclude,unit,chunkContext,chunkContextModel,select,maxTokens) fail. - An action names at most one
GRAPHentry, and no document entry beside it. - Chat turns leave
GRAPHentries out. Only actions search them.
Which provider serves the graph
Three providers ship in tnsai-rag:
| Provider | Name | Serves a Role when | Default |
|---|---|---|---|
InMemoryGraphStoreProvider | in-memory | An enabled FILE source is in reach: the Role's own, or an agent FILE source with loaded documents. A Role FILE source that loaded nothing still claims, and the graph is empty. | Yes |
KnowledgeToolsGraphStoreProvider | knowledge-tools | tnsai-tools is on the classpath and no enabled FILE source is in reach | Yes |
Neo4jGraphStoreProvider | neo4j | A Neo4j URI is set and the Neo4j driver is on the classpath | No |
When a non-default provider (Neo4j, or one of your own) also claims the Role, the defaults are dropped and that provider serves the graph. Two or more non-default claimants are a configuration error.
When no provider claims the Role, the action fails with GraphCapabilityException. This happens before @Search.onFailure applies: graph retrieval never falls back to vector search.
Graph from FILE sources
InMemoryGraphStoreProvider builds nodes and edges from the Role's FILE documents, and each entity keeps a link to the chunks it came from. Node identities are folded with InMemoryGraphStore.identityKey: NFKC normalization, lower case in Locale.ROOT, everything except letters and digits replaced by spaces, and a leading English article removed. Auth Service and the auth service become one node. Letters outside ASCII are kept, so 東京 stays 東京. Turkish letters do not fold to ASCII: ılık stays ılık, and İstanbul becomes i stanbul, not istanbul. Hits carry the source name in-memory.
A source selection that names no FILE source leaves this provider silent.
Graph from KnowledgeTools
KnowledgeToolsGraphStoreProvider serves the triples that the KnowledgeTools tools (such as kg_add_triple) keep in memory. It is the same in-process data, not a copy. Every live KnowledgeTools instance is read at retrieval time, so a triple added after the Role was bound is still found. Hits carry the source name knowledge-tools.
To use it, keep the graph handle on a Role with no FILE knowledge, in an agent whose @AgentSpec declares none either. With FILE knowledge in reach, the in-memory provider serves the Role instead.
Query matching here is ASCII only: the query is lower-cased and split on everything that is not a-z or 0-9. A query with no ASCII letters or digits produces no tokens and matches nothing:
| Query | Tokens |
|---|---|
Auth Service | auth, service |
İstanbul | i, stanbul |
ıslak | slak |
東京 | none |
Neo4j
export TNSAI_GRAPH_NEO4J_URI=bolt://localhost:7687
# or -Dtnsai.graph.neo4j.uri=bolt://localhost:7687
export TNSAI_GRAPH_NEO4J_USER=neo4j
export TNSAI_GRAPH_NEO4J_PASSWORD=...Add org.neo4j.driver to your runtime classpath; TnsAI does not bring it. When the URI is set but the driver is missing, the provider logs a warning and does not claim the Role.
Graph as a third hybrid stream
@Search(strategy = HYBRID) fuses BM25 and vector rankings. When a graph provider claims the Role, the graph ranking joins as a third stream:
- One claiming provider: the three streams are fused.
- No claiming provider: BM25 and vector are fused as before. HYBRID does not become vector only.
- Two or more non-default claimants: a configuration error, before
@Search.onFailureapplies.
Programmatically, pass a GraphRAGStrategy as the third argument:
RAGStrategy twoStreams = new HybridRAGStrategy(bm25Index, vectorStore);
RAGStrategy threeStreams = new HybridRAGStrategy(bm25Index, vectorStore, graphStrategy);Each stream is ranked on its own, then fused with unweighted reciprocal rank fusion (k = 60). Scores are scaled to 0.0 to 1.0 by the number of non-empty streams before RAGContext.minScore() applies. graphStream() reports which case applied.
Each hybrid hit records its streams in metadata:
| Key | Values |
|---|---|
retrievalStreams | bm25, vector, graph: the streams that found the hit |
graphStream | fused, or skipped when no graph store was available |
Related
- RAG: declaring knowledge and naming it from actions.
- Retrieval strategies: the other
@Searchstrategies. - Install retrieval providers: registering your own
GraphStoreProvider. - Tool catalog: the
KnowledgeToolstoolkit.