Prefer Swagger UI? Click hereThe same API in the classic OpenAPI explorer.
// concept · 8 min read

GraphRAG

GraphRAG layers a knowledge graph over the same vector store you already query. Set graphrag_enabled: true and ingest builds a graph of the entities and relationships across your files; file_search then returns a facts array of {subject, predicate, object} triples next to the usual chunk citations. It catches the multi-hop questions where the answer is spread across two or three documents and no single chunk holds it.

Plain RAG embeds each chunk of your documents, embeds the question and returns the nearest chunks by cosine similarity. That works when the answer lives in one passage. It struggles when the answer is a chain: document A says Acme acquired Pico, document B says Pico builds the BL-9 sensor, and the question is "who owns the BL-9 sensor?". No single chunk contains both hops, so nearest-chunk search returns the two halves and hopes the model joins them. Often it does not, because the two halves never both rank in the top results for that one query.

A graph stores those hops explicitly. During ingest GraphRAG extracts entities (Acme, Pico, BL-9) and the relationships between them (acquired, builds) and writes them as edges. At query time it takes the chunks vector search just matched, walks the graph outward from them and hands back the connected facts as triples. The model gets the passages and the explicit links, so the join is in the context rather than left to luck.

Note

GraphRAG is additive and opt-in. With it off, or when the graph has nothing relevant for a query, file_search output is byte-identical to vector-only. You never lose chunk citations; you only gain a facts array when the graph found connected triples.

When it helps, when plain vector search is enough

Turn it on when your questions cross documents. Leave it off when they do not. The graph adds an ingest-time extraction pass and a storage line, so it earns its keep only on corpora where relationships matter.

Reach for GraphRAG whenPlain vector search is enough when
Answers chain across two or more documents (multi-hop)Each answer sits inside one passage
You ask about relationships: who owns, reports to, depends on, supersedesYou ask for facts stated verbatim somewhere
Entities recur under different phrasings across filesThe corpus is a single doc or a flat FAQ
Users complain the model "missed the obvious connection"Recall is already good and you want to keep cost flat

A useful tell is the retrieval stats on a store. A high empty_result_rate or low top_3_score_avg on questions you know the corpus answers usually means the answer is spread across documents rather than missing, and that is exactly where a graph helps.

How it works end to end

ingest (graphrag_enabled: true)
  file -> chunk -> embed ----------------> vector index
                 \-> entity + relation extraction -> knowledge graph (edges)

file_search (inside an Assistants run)
  query -> embed -> nearest chunks ----------> citations[]
                          |
                          \-> walk graph N hops from those chunk_ids
                                   -> dedupe by (subject,predicate,object)
                                   -> facts[]

  tool output = { results:[...citations], facts:[...triples] }
  1. 01At create or via PATCH you set graphrag_enabled: true on the store.
  2. 02Ingest runs the normal chunk-and-embed path AND an entity/relationship extraction pass, writing graph edges keyed to the chunks they came from.
  3. 03A file_search call (inside an Assistants run) embeds the query and pulls the nearest chunks exactly as before, producing citations.
  4. 04Those matched chunk_ids seed a graph walk, default 2 hops, that collects connected triples scoped to this store only.
  5. 05Triples are deduped by the exact (subject, predicate, object) tuple and attached to the tool output as a facts array alongside the citations.

The graph walk is per-store and tenant-scoped. A chunk_id only resolves edges that were ingested under the same store, so attaching several vector stores to one assistant never bleeds one customer's graph into another's answer. Hop depth defaults to 2 and is a platform-level setting, not a per-request parameter.

The facts array

Each entry in facts is a flat triple. No nesting, no scores, no ids:

{ "subject": "Acme", "predicate": "acquired", "object": "Pico" }
subject
The entity the relationship starts from, as extracted from your documents.
predicate
The relationship, normalized to a short verb phrase (acquired, builds, reports_to).
object
The entity the relationship points to.

Facts arrive only when the graph found connected triples for the matched chunks. An empty graph result drops the facts key entirely rather than sending "facts": [], so a store with GraphRAG off and a store whose query hit nothing produce the same shape. Treat the presence of facts as optional in your parsing.

Worked example

Create a store with the graph on, ingest the two documents, then ask the cross-document question through an assistant. The store id below is vs_handbook; the customer is cus_42.

  1. 1
    Create the store with GraphRAG enabled
    curl https://api.fightclub.pro/v1/vector_stores \
      -H "Authorization: Bearer ko_<key>" \
      -H "FC-Customer: cus_42" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "acme-handbook",
        "embedding_model": "text-embedding-3-small",
        "graphrag_enabled": true
      }'
    
    # => { "id": "vs_handbook", "object": "vector_store",
    #      "graphrag_enabled": true, "status": "active", ... }
  2. 2
    Turn it on later instead (PATCH)

    If a store already exists, flip the flag without recreating it. New ingests build the graph. You do not need to re-upload to opt in going forward.

    curl -X PATCH https://api.fightclub.pro/v1/vector_stores/vs_handbook \
      -H "Authorization: Bearer ko_<key>" \
      -H "FC-Customer: cus_42" \
      -H "Content-Type: application/json" \
      -d '{"graphrag_enabled": true}'
  3. 3
    Query through file_search and read the facts

    Inside an Assistants run the model calls file_search; Ringside resolves it server-side and the tool output carries both arrays. The relevant slice of that output:

    {
      "query_id": "...",
      "empty": false,
      "results": [
        { "index": 0, "file_id": "file_corp", "chunk_id": "c_8a1",
          "text": "Acme Corp completed its acquisition of Pico Systems in 2024.",
          "score": 0.71, "source": { "chunk_index": 4, "page": 2, "type": "pdf" } },
        { "index": 1, "file_id": "file_specs", "chunk_id": "c_3f0",
          "text": "The BL-9 sensor is manufactured by Pico Systems.",
          "score": 0.64, "source": { "chunk_index": 1, "page": 9, "type": "pdf" } }
      ],
      "facts": [
        { "subject": "Acme Corp", "predicate": "acquired", "object": "Pico Systems" },
        { "subject": "Pico Systems", "predicate": "builds", "object": "BL-9 sensor" }
      ]
    }

The two facts make the hop explicit. Acme acquired Pico, Pico builds the BL-9, so Acme owns the BL-9. Vector-only would have returned the same two passages but no link tying them, and the model would have had to infer the join from two chunks that each only carry half the chain.

Tip

GraphRAG is invisible to your client code path. You call file_search the same way; the facts array just appears in the tool output when the graph has something. If you build the assistant with the OpenAI SDK against api.fightclub.pro/v1, the only change versus stock OpenAI is the fc: model prefix.

Sealed stores

GraphRAG works on sealed (encrypted) stores. The mechanics underneath are different, but the contract you see is the same facts array.

A plaintext store walks the graph through the GraphRAG sidecar over mTLS. A sealed store has no sidecar tenant, so the graph is resolved in-tier against Postgres. The entity labels and predicates are encrypted at rest under the store's data key. Only the topology stays in the clear, and the matched facts are decrypted in-process the same way the chunk text is. Set encryption: managed (or byok) and graphrag_enabled: true together and both work. See Sealed RAG and encryption for what each sealing mode protects.

Fail-closed on sealed stores

If the data key is unavailable (a byok store with no active key lease, or any unwrap failure) the graph walk yields nothing rather than surfacing ciphertext. The same fail-closed rule that governs sealed chunk text governs sealed facts. With no key the facts are dropped, never leaked.

Failure modes and edge cases

Graph expansion is best-effort and never breaks a query. It is layered so that a slow or down graph backend degrades you to plain vector search instead of failing the call.

  • ·Graph backend down, slow or returns a bad body: the call degrades to vector-only and you get citations with no facts key. The retrieval does not error.
  • ·A repeated run of graph failures opens a circuit breaker so subsequent queries skip the graph entirely (no timeout cost) until it recovers, then resume automatically.
  • ·No relevant triples for this query: facts is omitted. This is the normal empty case, not an error.
  • ·Multiple stores attached to one assistant: each store is expanded against its own graph and the results merge, deduped by the full triple. One store can have GraphRAG on and another off in the same run.
  • ·Cross-tenant access to a store id you do not own returns 404 not_found_error, never 403, so ids stay non-enumerable. The graph honors the same tenant boundary as the vectors.

Billing

The knowledge graph is its own storage line, separate from the vector and raw-file lines. It is metered per GB-day, billed strictly day by day off a daily snapshot, so a store you prune stops accruing that same day. The line has the same shape as the vector line. Same byte estimate and same free tier; only the rate differs.

Rates are admin-configurable defaults; the GraphRAG free tier is independent of the vector free tier.
Storage lineDefault rate (USD per GB-day)Free tier per store per day
Vector chunks0.051 GB-day
Raw files0.04none (billed from byte 1)
GraphRAG graph0.101 GB-day

The first 1 GB-day per store per day of graph storage is free, and that allowance is separate from the vector store's own 1 GB-day. The default graph rate is 0.10 USD per GB-day, twice the vector rate, reflecting the extra extraction and index. Leave graphrag_enabled off and there is no graph, no extraction pass and nothing extra to pay. Most graphs for modest corpora sit inside the free tier, so for many stores enabling GraphRAG costs nothing on the storage line and you pay only when the graph itself grows past 1 GB-day.

Note

Query-time embedding tokens are billed the same whether or not the graph is on. GraphRAG adds a storage line, not a per-query model charge. The graph walk itself is not separately metered.

Limits and defaults

graphrag_enabled
Boolean, default false. Settable at create or via PATCH on the store.
Hop depth
Default 2 hops from the matched chunks. Platform-level, not a per-request parameter.
facts shape
Flat {subject, predicate, object} strings. No scores, no ids, no nesting. Omitted entirely when empty.
Free tier
1 GB-day per store per day of graph storage, independent of the vector free tier.
Default rate
0.10 USD per GB-day for the graph line (admin-configurable).
Tenant scope
A graph walk only resolves edges ingested under the same store; never merged across tenants.