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
relationfield’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 onedoc → docedge 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 atmax_values_per_docvalues 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: relationin the schema (or you pass therelationhint), the projection walks the field’s adjacency index — the same structuretraverseuses — 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_keyrender asdoc:<id>. kindis"doc"or"ent"; doc nodes carry theirkeyand adegree(edges touching the node), entity nodes carry thefieldthey were derived from and its stringifiedvalue.- 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) andvia(the field the edge was derived from). planis 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_toedges among them so the prompt receives structure — which candidates cite which — not a flat list. Over thearticlesteaching 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.tagsto see which topics co-occur inside one hit set, at facet-query cost. - One round trip, two views. The
relationsarray 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 orrelations, not both.relation
(Optional, string) Explicit hint naming a schemarelationfield, routing the slice through the adjacency tier even whenfieldalone would be ambiguous.relations
(array) Multi-relation form — one entry per slice, each withfield, optionalrelationhint andmax_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 therelationsform, set it per entry.max_seeds
(Optional, integer, default: the request’ssize) 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)_sourcefield 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)_sourcefield populating each doc node’s human-readablevalue(e.g. the title) — verified live: works for in-page and edge-reached nodes alike.extra_fields
(array of strings) Additional_sourcefields attached to each doc node underextras(e.g.["author"]) so a UI can render metadata without a second round trip.
Notes #
- The block composes with any
query,sort, andsize— 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).
Related #
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
relationfield type these projections execute over. - Nested aggregation and children aggregation — bucket-side analytics over nested and parent/child structures.