I keep hitting the same question when a headline crosses my feed about a company I'm holding, or thinking about holding: does this news actually reach the thing I care about? A chip export restriction hits some supplier nobody's heard of. Two weeks later a manufacturer I do care about moves, and the headline that started it never mentioned them once. I wanted to see that chain, the actual event-to-event path, and no sentiment score was going to show it to me.
So I tried the obvious thing first: search my article database for the company name and read whatever comes back.
Where the article database stopped helping
I have a database of financial news articles, 9,414 documents at the moment, each indexed with the companies, people, and events mentioned. "Show me articles about Samsung" works fine. That's what search is for. "Show me what happens downstream when a supplier gets sanctioned" falls apart immediately, because a table of articles has no concept of downstream. Each row is independent. Nothing connects article 4,201 about a supplier to article 6,830 about a company three steps down the chain (those two numbers are made up, but that's the shape of the problem). Unless I read every row myself and hold the connections in my head, which is what I was already doing, badly.
Filtering and sorting, a table does great. Walking a chain of "this caused that caused that" just isn't something a flat table does. The information might be sitting right there across a dozen rows. There's no native way to say "A affects B affects C." Tables were never built for that question.
Swapping rows for nodes and edges
A knowledge graph fixes this at the structural level. Instead of rows you have nodes (companies, people, events) and edges (supplies, owns, sanctioned, competes with), and once that structure exists, "what's downstream of this event" stops being a research project. You start at a node and walk outward.
That's what I've been building: articles get parsed for entities and events, those get written into a graph store, and a question like "what's connected to this supplier three hops out" becomes something the graph itself can answer. It currently holds 84,962 nodes and 275,293 relationships, all extracted from those same articles.
None of this is a novel idea. Graphs for multi-hop reasoning are well established. The part that hurt showed up one layer down, before any traversal logic even mattered.
The part that actually blocked me: the entities were wrong
A traversal is only as good as the nodes it's walking. My first batches of extracted articles produced a mess I didn't expect to be the hard part: the same company kept landing in the graph as several disconnected nodes, because the articles never call it the same thing twice.
Here's one company, exactly as the pipeline extracted it:
- "Samsung"
- "Samsung Electronics"
- "Samsung Electronics Co."
- "Samsung Foundry"
- "Samsung's" (possessive, mid-sentence)
- "the Korean company" (a lazy second reference)
- "the largest memory chipmaker in the world" (same article, different lazy reference)
Seven surface forms, one real company. That last one is my favorite, I only caught it because a "company" node named an entire sentence looked absurd in the browser. Write each form into the graph as its own node and a query for everything connected to Samsung silently misses six of them. The causal chain I wanted literally can't form. The graph thinks it's looking at seven strangers.
And the failure runs the other way too, which took me longer to appreciate. Another article mentioned "삼성SDI": Samsung SDI, a real, separately listed subsidiary that makes batteries, not chips. Merge that into the Samsung Electronics node and the graph starts lying. SDI's battery contracts become evidence about a semiconductor business, and every chain through that node inherits the false premise.
So the bottleneck wasn't graph technology at all. It was entity resolution: given messy surface forms from unstructured text, decide which ones are the same real-world thing and which only look alike. Naive string matching fails both directions at once. "The largest memory chipmaker in the world" shares zero characters with "Samsung," and "Samsung SDI" is one string-edit away from a wrong merge. You need probabilistic matching over multiple signals, and a memory of what you've already resolved.
So I built it
I ended up building a standalone entity resolution service rather than a one-off cleanup function, mostly because this exact problem had already bitten me in every project that merges data from more than one source. It's built on Splink, the open source probabilistic record linkage library, wrapped in a FastAPI service. Feed it candidate mentions and it scores how likely two of them refer to the same entity, keeping a registry of resolved entities and known aliases so it gets better the more it sees. As of today the registry holds 47,853 resolved entities and 47,883 aliases. The Samsung example resolves correctly now: all seven mentions collapse into one node, Samsung SDI stays separate.
And it's live
The resolver runs as a deployed cloud service, sitting in front of the graph pipeline. Every incoming article gets its entity mess cleaned there before anything touches the investing knowledge graph.
If you're merging entities from more than one source, articles, KYC documents, supply chain records, customer files from separate systems, you will meet the Samsung problem eventually. That's what ER API is for. It's live and in beta. Next post: the resolver mechanics, how it decides "Samsung Foundry" and "the largest memory chipmaker in the world" are the same company without also swallowing Samsung SDI.
Top comments (0)