Graph projection block

Graph projection block #

Add a graph block to any search request and the response carries a graph — nodes and edges — built from the hit set, alongside the normal hits. Where the traverse query searches by walking edges, the graph block renders: it takes the documents a query already found and projects them into a node/edge structure a UI (or a GraphRAG pipeline) can consume directly.

graph LR subgraph walk["traverse — searches by walking"] S("seeds") -->|"walk edges
up to max_depth"| H1["hits — every reached node,
scored by distance"] end subgraph render["graph — renders what was found"] Q("any query") --> H2("hit set") H2 -->|"project the relation field"| G["nodes + edges
for a UI or GraphRAG"] end

Live over the articles teaching dataset (44 documents with a related_to relation field) — a term query picks the seeds, the graph block renders everything they reach:

And the ad-hoc form over a plain keyword field — no relation schema, every tags value on the hit documents becomes an entity node:

POST /articles/_search
{
  "size": 20,
  "query": { "match_all": {} },
  "graph": { "field": "related_to" }
}

How it works #

The block runs after the query: whatever the request selected — the hit page, capped at max_seeds — is the seed set, and each slice (one entry of relations, or the field shorthand) projects over it.

  • Adjacency tier. A relation field’s edges are stored as their own documents ({"related_to": {"from": "A01", "to": "A02"}}). At write time a writer hook resolves both endpoints through the key map to internal ids and records the pair in an in-memory adjacency index that keeps both directions; an unresolvable endpoint drops the adjacency entry, never the edge document. The projection walks that adjacency from the seed documents and emits one doc → doc edge per recorded edge whose endpoints it reached.
  • Column tier. For an ordinary field the projection reads each seed document’s values for the field (stored fields first, columnar storage as fallback), deduplicates them, and emits one entity node per distinct value plus a doc → ent:<field>:<value> edge — capped at max_values_per_doc values per document.

Slices merge into one graph: doc nodes are deduplicated across slices by key, entity nodes stay per-field, and the plan string records what actually executed per slice.

The adjacency walk inherits the same shard-local endpoint resolution as traverse — see Notes and traverse — shard placement.

Two projection tiers #

The engine picks a tier per relation slice:

  • Relation fields (adjacency). When the field is declared type: relation in the schema (or you pass the relation hint), the projection walks the field’s adjacency index — the same structure traverse uses — and emits document-to-document edges for every recorded edge whose endpoints are in the hit set.
  • Ad-hoc column projection. For an ordinary keyword field, every distinct value on the hit documents becomes a synthetic entity node and each document links to its values (doc → ent:<field>:<value>). This turns any faceting-style column into a bipartite graph — no relation schema needed.

Shapes #

Single relation (the shorthand):

{
  "query": { "term": { "field": "author", "value": "medcl" } },
  "graph": { "field": "genres", "max_values_per_doc": 8 }
}

Multiple relations in one round trip — each slice merges into one graph; doc nodes are deduplicated across slices, entity nodes stay per-field. Live: one projection over both the related_to edges and the tags column:

The equivalent request as JSON:

{
  "query": { "match_all": {} },
  "graph": {
    "relations": [
      { "field": "related_to" },
      { "field": "tags", "max_values_per_doc": 4 }
    ],
    "max_seeds": 100
  }
}

Response #

The response gains a graph object as a peer of hits and aggregations:

{
  "hits": { … },
  "graph": {
    "nodes": [
      { "id": "doc:A01", "kind": "doc", "key": "A01", "degree": 3 },
      { "id": "ent:tags:search", "kind": "ent", "field": "tags", "value": "search" }
    ],
    "edges": [
      { "from": "doc:A01", "to": "doc:A09", "via": "related_to" },
      { "from": "doc:A01", "to": "ent:tags:search", "via": "tags" }
    ],
    "plan": "slices=2 seeds=3 [tier=edge field=related_to reached=10, tier=column field=tags]"
  }
}
  • Node ids are stable identifiers for consumers to dedupe on: "doc:<key>" for document nodes, "ent:<field>:<value>" for entity nodes. Internal numeric document ids are never exposed — documents without a _key render as doc:<id>.
  • kind is "doc" or "ent"; doc nodes carry their key and a degree (edges touching the node), entity nodes carry the field they were derived from and its stringified value.
  • With a relation field, edge documents appear as doc nodes too — the endpoints they connect link through them, mirroring how the edges are stored.
  • Edges carry from, to (node ids) and via (the field the edge was derived from).
  • plan is a short human-readable summary of what actually executed (tier chosen per slice, field, seed count, reach).

Use cases #

  • GraphRAG context assembly. Retrieve candidate documents by text or vector similarity, then project the related_to edges among them so the prompt receives structure — which candidates cite which — not a flat list. Over the articles teaching set (20 nodes, 24 citation edges) one block renders both layers.
  • Knowledge-graph UIs. Render a hit set as a node-link diagram without standing up a graph database: doc nodes carry stable ids, optional labels (label_field) and metadata (extra_fields) for immediate rendering; entity nodes carry the shared values.
  • Facet exploration. The ad-hoc tier turns any keyword column into a bipartite doc-value graph — project articles.tags to see which topics co-occur inside one hit set, at facet-query cost.
  • One round trip, two views. The relations array renders citation edges and tag entities in a single response, so a UI paginates hits and graph together instead of issuing a second graph request.

Parameters for graph #

  • field
    (string) Single-relation shorthand — the field to project. Set this or relations, not both.
  • relation
    (Optional, string) Explicit hint naming a schema relation field, routing the slice through the adjacency tier even when field alone would be ambiguous.
  • relations
    (array) Multi-relation form — one entry per slice, each with field, optional relation hint and max_values_per_doc.
  • max_values_per_doc
    (Optional, integer, default: 16) Cap on distinct values projected per source document (ad-hoc tier) — keeps a long array field from blowing up the node set. In the relations form, set it per entry.
  • max_seeds
    (Optional, integer, default: the request’s size) Cap on how many hit documents feed the projection — prevents accidental projections over million-hit baselines.
  • seed_kind
    (Optional, string, default: "doc") Kind label for document nodes — set it to the collection name (e.g. "movie") for more semantic rendering.
  • key_field
    (Optional, string) _source field used to key document nodes that are NOT part of the returned hit page (reached only through edges) — the engine looks the value up from the document store so their ids carry a business key instead of the internal ordinal. Nodes in the hit page always use their _key.
  • label_field
    (Optional, string) _source field populating each doc node’s human-readable value (e.g. the title) — verified live: works for in-page and edge-reached nodes alike.
  • extra_fields
    (array of strings) Additional _source fields attached to each doc node under extras (e.g. ["author"]) so a UI can render metadata without a second round trip.

Notes #

  • The block composes with any query, sort, and size — the graph is derived from whatever the query selected.
  • Like traverse, relation-field projection resolves endpoints within the shard that stores the edge document — keep graph collections on a single shard (see traverse — shard placement).
  • traverse — the searching sibling: walks the same adjacency to decide which documents match, where this block renders documents a query already found.
  • join — cross-collection membership (semi-join) rather than in-collection edge walking.
  • nested — correlation within one document’s array elements, the intra-document counterpart of relation edges.
  • Relational parameters — declaring the relation field type these projections execute over.
  • Nested aggregation and children aggregation — bucket-side analytics over nested and parent/child structures.
Calendar October 6, 2026
Edit Edit this page