×marble
all posts
May 28, 2026·14 min read

How to Build a Personalization Knowledge Graph from Scratch: Schema, Signals, and Serving in 2026

A schema-first guide to building a personalization knowledge graph: entity design, signal ingestion, traversal patterns, and graph DB selection for teams starting from zero.

Alex Shrestha·Founder, ×marble

How to Build a Personalization Knowledge Graph from Scratch: Schema, Signals, and Serving in 2026

TL;DR.

  • A personalization knowledge graph stores users, items, and the typed semantic relationships between them — it captures why a recommendation fits, not just that it statistically correlates.
  • The bottleneck is almost never the graph database; it's the schema: wrong entity granularity or missing edge types compound into retrieval dead-ends you cannot patch later without a rebuild.
  • Most teams reach for collaborative filtering first, then discover it fails at cold-start, tail items, and cross-domain transfer — graph structure solves all three through shared concept nodes.
  • In 2026, the practical production pattern is a property graph (Neo4j or TigerGraph) for traversal plus a vector index for embedding-based fallback, served under 30 ms via pre-computed candidate sets.
  • If you only remember one thing: design the schema around the questions the graph must answer, not around the data you currently have.

Recommendation systems fail in predictable ways. Matrix factorization misses long-tail items because they lack interaction density. Vector similarity retrieves semantically close items but cannot express "the user already owns this" or "this requires a prerequisite." The two-tower model learns user and item embeddings independently, severing the semantic chain between entities. A personalization knowledge graph fixes all three failure modes in a single representation — but only if you build it right from the start. The schema decisions you make in week one determine the query patterns you can support in year three, which is why this post is schema-first, then pipeline, then serving.

Why a Graph and Not a Matrix or a Vector Store?

The graph wins when relationships between entities are first-class data, not derived statistics. A user-item interaction matrix knows that user A rated item B; a graph knows that user A rated item B because they are interested in topic C, which is a subgenre of D, which has three other unexplored items the user has never seen. That chain of typed edges is the difference between "users like you also liked" and "here is the next step in your progression."

This is not a theoretical advantage. Pinterest's PinSage, which operates on a graph of two billion nodes and eighteen billion edges, outperformed matrix-factorization baselines by 40% on engagement-based offline metrics because it could propagate feature information across the graph neighborhood of each item (Ying et al., 2018). RippleNet, from Renmin University, demonstrated that injecting knowledge graph triples as a propagation regularizer improved CTR prediction on MovieLens and Book-Crossing by 3–8% over deep collaborative filtering alone (Wang et al., 2018). The mechanism is the same in both cases: typed semantic edges propagate signal to sparse or zero-interaction regions of the embedding space that interaction matrices cannot reach.

The matrix and the vector store are not wrong — they are projections of the graph. We use both. But the graph is the source of truth, and everything else is a derived serving artifact.

What Entities and Edge Types Does Your Schema Actually Need?

Start with four canonical entity types: User, Item, Concept, and Context. Everything else is a specialization of one of these, and adding domain-specific subtypes before you have query evidence for them is premature. The schema should grow in response to traversal queries that fail, not in anticipation of queries you have not written yet.

User. One node per identity, or per session if the user is anonymous. Properties: first_seen, tier, locale, device_type, cohort. Do not store behavioral state on the user node — that belongs on edges. The most common early mistake is stuffing a JSON blob of preference scores onto the user node; this destroys traversal performance because every update requires a full node write and makes incremental signal incorporation expensive.

Item. One node per addressable piece of content or product. Properties: id, type (article, video, product, track), created_at, author_id, duration, language. Do not embed taxonomies as properties. Tags, genres, categories, and topics belong as edges to Concept nodes — treating them as properties eliminates the graph's ability to find items that share a concept without scanning every item's property bag.

Concept. The semantic backbone of the graph. Concepts are entities extracted from item metadata and content: topics, genres, named entities, skills, brands, moods, difficulty levels. Critically, concepts form a hierarchy (Modal JazzJazzMusic), which means they carry topology that flat properties cannot express. This is where most teams underinvest: a flat tag list is not a concept hierarchy. The depth and structure of your concept ontology determines how far signal propagates from a single interaction — a shallow ontology means a video completion does not generalize to a related genre; a deep one means it does.

Context. Time buckets (morning, weekend), device classes (mobile, smart-TV), location regions, or session-level intent signals. Context nodes let you express "this user watches long-form video on weekends from their TV" without duplicating the user node or polluting the main behavioral edge weights with temporal confounding. They are optional for an MVP but essential once you want context-aware ranking.

The edge types that carry the semantic load

INTERACTED (User → Item): typed by interaction kind — viewed, clicked, completed, skipped, rated, shared. Each instance carries timestamp, weight, and optionally a context_id foreign key linking to a Context node. Do not merge all interaction types into one edge type distinguished by a property; you lose the ability to query "show me only completions" without a full edge-type scan. Separate edge types are the right granularity here.

HAS_CONCEPT (Item → Concept): the semantic bridge between content and meaning. Weight = tf-idf relevance score or extraction confidence from your NLP pipeline. This edge is how the graph achieves cross-item transfer: two items that share a Concept node receive mutual signal from any user who interacted with either one.

SIMILAR_TO (Item ↔ Item): pre-computed similarity edges from embedding distance, editorial curation, or co-occurrence in sessions. The fast-path for "more like this." Recompute on item ingestion and weekly for the full catalog.

IS_A (Concept → Concept): the ontology hierarchy. Traversal up this edge enables generalization when concept-level signal is sparse — if a user has interacted with three Modal Jazz items, propagating up via IS_A to Jazz and then down to sibling subgenres finds adjacent candidates they have not yet discovered.

INTERESTED_IN (User → Concept): derived from interaction history via signal aggregation. Update incrementally as new INTERACTED edges arrive. This is the primary cold-start bridge: a new item with matching HAS_CONCEPT edges immediately scores against existing user interest vectors without needing direct user-item co-occurrence.

How to Ingest Signals and Keep the Graph Current

Signal ingestion is a streaming problem. The graph is not a batch artifact you rebuild nightly — it is a live representation of user state, and staleness compounds directly into bad recommendations. The target latency between "user completed a video" and "that completion weights the next recommendation" should be under sixty seconds.

The pipeline has three stages.

1. Event capture

Capture raw events at the client layer: view_start, view_end, click, skip, search_query, add_to_cart, share. Emit to a Kafka topic keyed by user_id for ordering guarantees within a user's event stream. Do not attempt to infer intent at the capture layer — raw events are the ground truth; all inference happens downstream. The capture layer should be dumb and fast; the processing layer is where the semantics live.

2. Edge derivation

A stream processor (Flink, Kafka Streams, or a lightweight consumer for smaller scale) reads raw events and emits graph mutations:

  • A view_end with completion_ratio > 0.8 → upsert INTERACTED {type: "completed", weight: 1.0, timestamp: now()} from user to item.
  • A click followed by view_end with completion_ratio < 0.2INTERACTED {type: "clicked", weight: 0.2} (low-quality click).
  • A skip on auto-played content → INTERACTED {type: "skipped", weight: -0.5}. Negative signal is essential; omitting it biases the graph toward over-recommending high-volume content regardless of rejection rate.

Edge weights are not static. Apply exponential decay at query time or during periodic recomputation: effective_weight = base_weight * exp(-λ * days_since_interaction). Tune λ per interaction type — completions decay slower than clicks; skips can be treated as permanent low-weight signals since preferences rarely reverse. A completion from three years ago should not dominate a skip from yesterday.

3. Concept roll-up

After each INTERACTED edge upsert, incrementally update the INTERESTED_IN edges from the user to concepts. For each concept C linked to item I via HAS_CONCEPT, add interaction_weight * HAS_CONCEPT.weight to the user's running interest score for C. Propagate one hop up the IS_A hierarchy with a decay factor of 0.5 — a completed jazz video raises the user's Jazz interest by half as much as it raises Modal Jazz. This is a simplified version of RippleNet's ripple propagation and runs in milliseconds for concept hierarchies of three to five levels deep.

Traversal Patterns for Personalization Queries

Graph traversal for personalization is not graph search — you are not finding the shortest path to a single target node. You are ranking a candidate set retrieved via typed edge walks, then passing that set to a lightweight ranking model. Three patterns cover the majority of real-world use cases.

Pattern 1: Concept bridge retrieval. Walk from the user via INTERESTED_IN (top-K concepts by weight) → HAS_CONCEPT (items linked to those concepts, excluding items with existing INTERACTED edges) → rank by aggregated concept weight times a recency discount on item freshness. This is the primary pattern for day-zero personalization: a user with no interaction history can be seeded with inferred interests from onboarding signals, and the concept bridge immediately returns ranked candidates without any user-item co-occurrence history.

Pattern 2: Neighborhood expansion. Walk from a seed item via SIMILAR_TO to neighbor items, then score each neighbor by its overlap with the user's INTERESTED_IN concept weights via HAS_CONCEPT. This is "more like this" — it does not require user history against the seed item, only pre-computed item similarity edges and existing user interest signals. Combine with Pattern 1 in a candidate union before ranking.

Pattern 3: Personalized PageRank. Start a random walk from the user node, follow edges with probability proportional to their weight, and tally which item nodes the walk visits most frequently across many iterations. PPR captures second-order effects — items connected through shared users and shared concepts — that single-hop traversal misses entirely (Haveliwala, 2002). Full PPR at query time is too expensive for a p50 < 30 ms serving budget. Pre-compute PPR vectors for active users in a nightly or incremental offline job and serve from a key-value cache. PPR scores then become a feature in the online ranking model rather than a live traversal result.

In production, Patterns 1 and 2 handle real-time candidate generation (fast, incremental, suitable for synchronous query-path execution in Neo4j at the scale of millions of users), and Pattern 3 contributes a pre-computed relevance signal that improves ranking quality without adding synchronous traversal cost.

Cold-Start Without History: The Synthetic Clone Approach

Cold-start is the failure mode that breaks every matrix-factorization system and most vector-store approaches. A new user has no edges. The standard fallback is global popularity ranking, which is a personalization abdication dressed up as a recommendation.

The knowledge graph enables a different strategy: synthetic clone. Instead of falling back to the global leaderboard, build a synthetic user node that represents an inferred archetype derived from onboarding signals — stated preferences, referral source, device class, locale, time of day. Populate INTERESTED_IN edges from a pre-built archetype profile that was derived from clustering users who did have behavioral history. Then run the exact same traversal patterns — Patterns 1 and 2 — as you would for a user with real history.

The archetype profiles are built offline: cluster your existing users by their top INTERESTED_IN concept weights, extract the centroid concept weights for each cluster, and label the clusters with a short onboarding questionnaire or referral-source classifier. When a new user arrives, assign them to the nearest archetype and instantiate their synthetic INTERESTED_IN edges from the archetype profile. The graph traversal does the rest. The synthetic clone dissolves into the real behavioral profile as the user accumulates interactions — you incrementally replace synthetic edge weights with observed ones and taper the archetype contribution over the first few sessions using a blending parameter that decreases with interaction count.

This works at day zero because the semantic structure of the knowledge graph provides the connective tissue between a stated preference and a ranked item list. A new user who selects "jazz" during onboarding is immediately linked to the Jazz concept subtree, and the graph traversal returns actual jazz items ranked by concept-level engagement signals from users in the same archetype cluster — a meaningfully better start than "most popular overall."

Choosing Your Graph Database in 2026

The graph DB landscape has consolidated around three realistic production choices: Neo4j, TigerGraph, and Amazon Neptune. Vector-graph hybrids — a dedicated graph DB alongside a separate Qdrant or Weaviate embedding index — are the standard serving architecture for teams that need both semantic fallback and structured traversal.

Neo4j is the right choice for teams bootstrapping and iterating on schema. Cypher is expressive, readable, and well-documented. ACID transactions prevent edge-weight corruption under concurrent writes. The Aura managed service removes operational overhead. The weakness is write throughput at scale — above roughly 50K writes per second you will hit contention on heavily connected nodes (your high-cardinality concept nodes become hot spots as every item ingestion writes HAS_CONCEPT edges). This is acceptable for most B2B products and early-stage B2C.

TigerGraph is purpose-built for real-time deep-link traversal at high write throughput. Its distributed architecture handles the hot-node problem and sustains the write volumes a large consumer product generates. GSQL has a steeper learning curve than Cypher. Use TigerGraph if you are operating above hundreds of millions of nodes and need multi-hop traversal results under 10 ms with concurrent writes.

Amazon Neptune suits teams already on AWS who want managed infrastructure and can accept some query expressiveness trade-offs. Neptune supports both openCypher and SPARQL. Its latency profile is adequate for offline candidate generation but typically too high for synchronous real-time retrieval — use Neptune as the knowledge base and pre-compute candidate sets into DynamoDB or ElastiCache for the hot request path.

The serving architecture that achieves p50 < 30 ms in 2026 follows a pattern established by large-scale graph embedding research (Hamilton et al., 2017): graph DB for knowledge maintenance and offline candidate generation → PPR and neighborhood pre-computation → candidates written to a low-latency store (Redis, DynamoDB) → embedding index (Qdrant or Weaviate) for semantic fallback when graph traversal returns an insufficient candidate set → lightweight online ranker that merges all signals. The graph is the knowledge base; the serving layer is a set of pre-computed projections optimized for speed.

Do not serve deep graph traversal synchronously in the user-request path. It will not meet latency budgets at production scale regardless of which database you choose.

Where to Start: The 4-Week Build Sequence

The graph is not a big-bang migration. It grows alongside your product, and you do not need the complete schema or the full serving stack to get value from it. Here is a realistic sequence for a team starting from zero.

Week 1: Schema lock and event capture. Define the five node types (User, Item, Concept, Context, Archive for retired items). Define INTERACTED, HAS_CONCEPT, IS_A, and INTERESTED_IN edges with their weight and timestamp properties. Stand up a Neo4j Aura instance. Write a backfill job that ingests your existing item catalog and extracts concepts — a batched LLM call (or a small NLP pipeline) per item batch is adequate for initial concept extraction; refine the ontology later. Instrument client-side event capture for view_start, view_end, click, skip. You do not need the graph to be serving recommendations yet — you need the schema locked and events flowing into a Kafka topic.

Week 2: Signal ingestion pipeline. Wire a stream processor to consume raw events and write edge mutations to Neo4j. Implement the weight and decay rules. Implement the concept roll-up for INTERESTED_IN. By the end of week 2, you should be able to query "top concepts for user X" and receive a meaningful ranked list for any user who has generated events. This is your first real test of schema correctness — if the top concepts for a jazz listener are not jazz concepts, your HAS_CONCEPT weights or your extraction pipeline need debugging.

Week 3: Concept bridge retrieval and offline evaluation. Implement Pattern 1 traversal in Cypher or your chosen query language. Expose it as an internal API: given a user_id, return a ranked list of item candidates with scores. Do not deploy to production yet. Evaluate against your existing recommendation system offline using held-out interactions (predict the last interaction from history). Expect the graph to win on long-tail item recall; it may lose on head-item precision until you tune concept weights and add the SIMILAR_TO neighbor expansion from Pattern 2.

Week 4: Cold-start profiles and A/B test. Build three to five archetype profiles from historical user clusters. Wire them to your onboarding flow so that new users receive a synthetic INTERESTED_IN profile from the first session. Run a 10% A/B test comparing the graph-based system against your existing baseline. Measure click-through rate and completion rate at day 0 and day 3 separately — the graph should show its largest advantage in the day-0 cohort, where the baseline has no signal and falls back to popularity. The day-3 delta tells you how quickly real behavioral signal overtakes the synthetic bootstrap.

After week four, the graph is a production system with a real feedback loop. Schema evolution from this point is additive — new edge types, new concept hierarchy branches, new context nodes. You do not need to rebuild. The entity model locked in week one remains the foundation; every subsequent change extends it rather than replacing it.

The common mistake is waiting until the schema feels "complete" before building the pipeline. It will never be complete. Start narrow — four edge types, two traversal patterns, one A/B test — and let the data surface what to add next.

the product behind these notes

×marble is the personalization graph.

One API. A living knowledge graph per user. Day-zero ready, explainable by construction. We built it so you don't have to.

See ×marble