Building a Knowledge Graph for My Blog

How I built an interactive knowledge graph connecting blog posts through semantic similarity and shared tags. A deep dive into embeddings, NER, and D3 force-directed layouts.



I wanted to go beyond simple “related posts” recommendations. In my previous post on semantic blog clustering, I built a system that groups posts by topic similarity. But what if readers could explore my blog like a map of connected ideas? A knowledge graph enables precisely this: a navigable network where every post, tag, and concept is a node, and relationships are edges you can traverse.

Why a Knowledge Graph?

Traditional blog navigation is linear: browse by date, search by keyword, or follow tag links. But ideas don’t work that way. A post about Python might connect to one about embeddings through shared techniques, even if they have different tags. A knowledge graph captures these semantic connections.

My goals were:

  • Discover hidden connections: Find posts that are conceptually similar even without shared tags
  • Visualize topic clusters: See how my writing groups into themes
  • Enable exploration: Let readers navigate by curiosity rather than categories

The Graph Structure

The knowledge graph has three types of nodes and four types of edges:

Nodes:

  • Posts (35): Individual blog articles, colored by topic cluster
  • Tags (76): Explicit labels like “python”, “mathematics”, “react”
  • Concepts (0 currently): Automatically extracted entities from post content

Edges:

  • Similar: Posts connected by semantic similarity (cosine similarity ≥ 0.3)
  • Tagged: Posts linked to their tags
  • Mentions: Posts that reference extracted concepts
  • Co-occurs: Concepts that frequently appear together

Interactive Visualization

Below is the live knowledge graph. Posts are colored by their k-means cluster assignment. You can see how embeddings group related content. Click any post to read it, or toggle “Tags” to see how topics connect through shared labels.

Understanding the colors: Posts are colored by k-means cluster assignment. The algorithm groups posts with similar embeddings into 10 clusters, each with a distinct color: blue, red, green, amber, violet, pink, cyan, orange, lime, and indigo. Posts about related topics cluster together automatically and share the same color. Tags appear as gray rounded rectangles to avoid competing visually with post nodes. Concepts (currently none) would appear as purple diamonds. The colors reveal semantic groupings discovered automatically from content, not manual categories.

Technical Implementation

Embeddings with Sentence Transformers

I use @xenova/transformers with the Xenova/all-MiniLM-L6-v2 model to generate 384-dimensional embeddings for each post. The embedding captures semantic meaning by:

  1. Weighting the title 3x (titles are information-dense)
  2. Including all tags
  3. Adding the description
  4. Sampling the first 500 characters of content
function preparePostText(post: BlogPost): string {
  const titleWeight = [post.title, post.title, post.title].join(' ');
  const tagsText = post.tags.join(', ');
  const contentPreview = post.content.slice(0, 500);

  return [titleWeight, tagsText, post.description, contentPreview]
    .filter(Boolean)
    .join('\n\n');
}

Pairwise Similarity Computation

With embeddings in hand, I compute cosine similarity between all post pairs. Posts with similarity ≥ 0.3 receive a “similar” edge. This threshold balances density and quality, ensuring enough connections to navigate while maintaining meaningful relationships.

export function computePairwiseSimilarity(
  ids: string[],
  embeddings: number[][],
  threshold: number = 0.3
): SimilarityEdge[] {
  const edges: SimilarityEdge[] = [];

  for (let i = 0; i < ids.length; i++) {
    for (let j = i + 1; j < ids.length; j++) {
      const similarity = cosineSimilarity(embeddings[i], embeddings[j]);

      if (similarity >= threshold) {
        edges.push({
          source: ids[i] < ids[j] ? ids[i] : ids[j],
          target: ids[i] < ids[j] ? ids[j] : ids[i],
          weight: similarity,
        });
      }
    }
  }

  return edges;
}

Deduplication ensures each pair has one undirected edge, not two directed ones. The logic checks that source < target lexicographically.

Named Entity Recognition

I attempted to extract concepts using Xenova/bert-base-NER, a BERT-based model that identifies organizations, technologies, and entities. The problem: my content spans mathematics, React, operational research, and military AI.

The NER model found 29 concepts (PostgreSQL, Python, Monte Carlo), but only 3 appeared in multiple posts. My threshold requires a concept to span at least two posts to become a node, leaving zero concept nodes in the current graph.

Lesson learned: NER works best with homogeneous content. A focused blog about PostgreSQL would surface dozens of shared concepts. For diverse content like mine, tags remain the better explicit signal.

Build-Time Generation

The build process generates the entire graph:

pnpm generate:graph    # Run generation script
pnpm dev:graph         # Regenerate and start dev server

This approach offers:

  • Zero runtime overhead: The graph is a static JSON file
  • Cacheable: The 50MB embedding model downloads once
  • Reviewable: Changes to knowledge-graph.json are visible in PRs

The first build takes roughly 90 seconds (downloading models), while subsequent builds take 35 seconds. This overhead is acceptable for an infrequent build step.

Visualization with D3

The interactive visualization uses D3’s force-directed layout. Each node repels others (charge force) while edges pull connected nodes together (link force). The simulation settles into an equilibrium where clusters emerge naturally.

Key implementation details:

  1. Collision detection: Posts receive a larger radius (20px) than tags (15px), preventing overlap
  2. Edge strength: Similarity edges use their weight as strength, with higher similarity pulling nodes closer
  3. Dynamic layers: Toggling tags adds or removes nodes and re-heats the simulation with smooth transitions
  4. Cluster coloring: Ten distinct colors map to the k-means cluster assignments from the generation step

The component renders client-side only because D3 requires DOM access.

Lessons Learned

1. Tags Beat NER for Diverse Content

I initially hoped NER would surface implicit connections: concepts I wrote about without explicitly tagging. In practice, only 3 of 29 extracted concepts appeared in multiple posts. Explicit tags remain more useful.

For a focused blog (all React tutorials), NER works better. For eclectic writing, it produces noise.

2. Embeddings Capture Semantic Similarity Well

Despite the NER limitation, embeddings effectively capture semantic similarity. Posts about naval combat and Warhammer 40k connect (both use probabilistic modeling). Posts about PostgreSQL cluster together despite different specific topics. The 0.3 threshold strikes a good balance.

3. Build-Time Generation Is The Right Choice

Generating the graph at build time means:

  • Readers get instant interactivity (no loading spinner)
  • I can review changes to the graph structure in version control
  • No server costs for embedding computation

The tradeoff is a slower build, but 35 seconds is acceptable for a step I run once per blog post.

4. Force Layouts Require Careful Tuning

D3’s force simulation is powerful but unpredictable. Small parameter tweaks can drastically change the layout. I spent several hours tuning charge strength, link distance, and collision radius to achieve readable results.

For production use, consider pre-computing node positions (for example, using a graph layout algorithm like Fruchterman-Reingold) and animating transitions.

What’s Next?

The knowledge graph unlocks several potential features:

  • Shortest path: “How is this post connected to that post?”
  • Subgraph filtering: Show only posts within 2 edges of the current one
  • Temporal view: Animate how the graph evolved over time as I published posts
  • Concept evolution: If I write more focused series, concepts might actually appear and show how ideas develop

For now, it remains a tool for exploration.


Code & Resources: