# 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

```java
@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, `GRAPH` applies; any other strategy fails at init.
- `use` is `SEARCH`. `DIRECT` and `SEQUENTIAL` fail.
- Location and loader members (`path`, `connection`, `query`, `format`, `include`, `exclude`, `unit`, `chunkContext`, `chunkContextModel`, `select`, `maxTokens`) fail.
- An action names at most one `GRAPH` entry, and no document entry beside it.
- Chat turns leave `GRAPH` entries 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

```bash
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.onFailure` applies.

Programmatically, pass a `GraphRAGStrategy` as the third argument:

```java
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](index.md): declaring knowledge and naming it from actions.
- [Retrieval strategies](retrieval-strategies.md): the other `@Search` strategies.
- [Install retrieval providers](providers.md): registering your own `GraphStoreProvider`.
- [Tool catalog](../tools/catalog.md): the `KnowledgeTools` toolkit.
