Flattening model
- Last UpdatedMay 14, 2026
- 6 minute read
Knowledge Graph uses a flattening model to represent properties and relationships. This design enables efficient querying, filtering, and paging at fine granularity but differs fundamentally from how data is stored in the underlying Graph Store. Understanding flattening is essential for writing correct queries and interpreting results.
What is flattening?
In the Knowledge Graph, entities and events are composed of components (typed sub-structures). Each component contains properties and relationships.
The Graph Store stores components as nested objects:
Entity {
id: "E1"
properties: [...]
relationships: [
{
id: "R1"
targets: [
{ id: "T1", base: "Entity", type: "Parent" },
{ id: "T2", base: "Event", type: "Child" }
]
}
]
components: [
{
typeId: "Pump"
properties: [...]
relationships: [...]
},
{
typeId: "Motor"
properties: [...]
relationships: [...]
}
]
}
Knowledge Graph flattens this structure into streams of atomic records at the entity level:
-
Property records: one row per property across all components, indexed by TypeId (component type).
-
Relationship records: one row per relationship target, indexed by TypeId and TargetId.
This means:
-
If a component has 5 properties, Knowledge Graph emits 5 property records.
-
If a relationship has 3 targets, Knowledge Graph emits 3 relationship records.
-
A single entity can expand into many rows when queried.
Why flattening?
Flattening enables:
-
Component-aware filtering: Query properties or relationships by component type.
-
Target-level filtering: Filter relationships by their specific target (not just "has a relationship").
-
Efficient pagination: Page through flattened records at the property/relationship granularity instead of re-fetching entire components.
-
Unified schema: Clients work with a flat property/relationship stream, not nested component payloads.
Impact on query results
Result cardinality
A single entity query may return multiple rows due to flattening.
Example:
-
Entity E1 has 2 components (Pump, Motor).
-
Pump has 3 properties; Motor has 2 properties.
-
Querying entities { id, properties { id, typeId } } returns 5 rows, one per property.
Query response:
{
"entities": [
{ "id": "E1", "properties": { "id": "P1", "typeId": "Pump" } },
{ "id": "E1", "properties": { "id": "P2", "typeId": "Pump" } },
{ "id": "E1", "properties": { "id": "P3", "typeId": "Pump" } },
{ "id": "E1", "properties": { "id": "P4", "typeId": "Motor" } },
{ "id": "E1", "properties": { "id": "P5", "typeId": "Motor" } }
]
}
Relationship flattening by target
Relationships are flattened per target. A relationship with multiple targets produces multiple relationship records.
Example:
-
Relationship R1 targets 3 entities: T1 (Parent), T2 (Child), T3 (Related).
-
Querying relationships { id, targetId, relationshipType } returns 3 rows.
{
"relationships": [
{ "id": "R1", "targetId": "T1", "relationshipType": "Parent" },
{ "id": "R1", "targetId": "T2", "relationshipType": "Child" },
{ "id": "R1", "targetId": "T3", "relationshipType": "Related" }
]
}
The relationship id is duplicated; each row represents one target link.
Filtering with flattening
Flattening allows powerful component-aware filtering.
Property filters
Filter entities by component property presence or value:
query {
entities(
input: {
filter: {
where: {
properties: [
{
id: "temperature"
typeId: "Sensor" # Only from Sensor component
numberValue: { ge: 50, le: 100 }
}
]
}
}
}
) {
items {
id
properties {
id
typeId
stringValue
numberValue
}
}
}
}
Result: Returns entities that have a temperature property in the Sensor component with value between 50 and 100. Only the matching properties are included in the response.
Component filters
Filter by component type presence:
query {
entities(
input: {
filter: {
where: {
components: [
{
typeId: "Pump"
},
{
typeId: "Motor"
}
]
}
}
}
) {
items {
id
componentTypes {
id
}
}
}
}
Result: Returns only entities that have both Pump and Motor components.
Relationship filters
Filter relationships by target:
query {
entities(
input: {
filter: {
where: {
relationships: [
{
id: "contains"
targetBase: "Entity"
relationshipType: "Parent"
}
]
}
}
}
) {
items {
id
relationships {
id
targetId
relationshipType
}
}
}
}
Result: Returns entities with a contains relationship targeting an entity and labeled Parent. Each matched relationship target is a separate row.
Pagination with flattening
Because results are flattened, pagination operates on property and relationship records, not entities.
Pagination cursor semantics
A pagination continuationToken represents a position in the flattened stream, not in an entity list.
Example scenario:
-
Entity E1 has 10 properties; Entity E2 has 5 properties.
-
Page 1 (limit: 8) returns properties 1โ8 from E1.
-
continuationToken points to property 9 in E1.
-
Page 2 returns properties 9โ10 from E1, then properties 1โ5 from E2.
Important: You may receive partial entity data across pages.
Pagination best practices with flattening
-
Always include ordering: Use orderBy to ensure stable pagination.
query {
entities(
input: {
filter: {
where: { ... },
orderBy: [
{ path: "id", direction: ASC },
{ path: "properties.id", direction: ASC }
]
}
}
limit: 20
continuationToken: "<token>"
) {
continuationToken
items { ... }
}
}
-
Use deterministic sorting to handle partial entities: If you care about entity-level grouping, sort by entity id first so flattened records for the same entity stay together across page boundaries.
-
Don't assume one row per entity: With flattening, expect many rows per entity. A limit: 100 may return only 10 entities if each has ~10 properties.
-
De-duplicate and reconstruct at client: If you need entities as composite objects, collect all flattened records for the same entity before reconstruction.
const entitiesMap = new Map();
for (const item of pageResults) {
if (!entitiesMap.has(item.id)) {
entitiesMap.set(item.id, { id: item.id, properties: [], relationships: [] });
}
entitiesMap.get(item.id).properties.push(item.properties);
}
const reconstructedEntities = Array.from(entitiesMap.values());
Implications for result size
Flattening increases response row count but does not increase actual data size significantly because:
-
Each row is small (property or relationship record with metadata).
-
Duplicate entity metadata (id, common fields) is minimal in each row.
However, requesting deep nested fields (such as all properties plus all relationships plus type info) per row can inflate response payload. Optimize by requesting only the fields you need.
Discovery and composition filters
Discovery filters (hasComponent, hasPropertyId, doesNotHavePropertyId) are automatically translated to component and property filters when you query.
Example:
Knowledge Graph GraphQL input:
{
components: [{ typeId: "Pump" }]
properties: [{ id: "serialNumber", exists: true }]
}
Internally translated to:
DiscoveryFilter(Operation=HasComponent, Values=["Pump"])
DiscoveryFilter(Operation=HasPropertyId, Values=["serialNumber"])
Entities matching both conditions are returned; flattened results include only the matching properties/components.
Ordering with flattening
Ordering operates on flattened records. By default, results are ordered by entity id, then by property/relationship id within each entity.
You can customize ordering:
query {
entities(
input: {
filter: {
orderBy: [
{ path: "id", direction: ASC },
{ path: "properties.stringValue", direction: DESC }
]
}
}
) {
items { ... }
}
}
This orders entities by id, then properties within each entity by their string value (descending).
Common pitfalls
-
Forgetting partial entity rows: A single entity may span multiple pages. Do not assume a new entity after a continuationToken.
-
Over-requesting fields: Flattening already produces many rows. Requesting large nested selections per row amplifies response size.
-
Misinterpreting result cardinality: A limit: 100 query may return 5 entities with ~20 flattened rows each, not 100 entities.
-
Unstable pagination across complex filters: If filter criteria change mid-pagination (such as a component type is removed), you may see inconsistent results. Rerun the full query with updated filters.
-
Assuming relationship id uniqueness per row: Relationship id repeats once per target. Identify a specific link by the combination of id + targetId + relationshipType.
Summary
Flattening is a powerful model that enables efficient component-aware filtering and fine-grained pagination. The tradeoff is that clients must understand:
-
Entities expand into multiple rows.
-
Pagination operates on flattened records, not entities.
-
Relationships are indexed by target. A relationship with multiple targets appears multiple times.
-
Reconstruction of composite objects must happen at the client if needed.
For most query workloads (filtering, aggregation, export), flattening is transparent and beneficial. For interactive UI retrieval of entities as composite objects, consider batch reconstruction or client-side composition strategies.