Booking · Q4 2026 · 2 slots open20+ yrs shipping systemsIdea → prototype → production
All guides
September 14, 202615 min readWhere AI Is Worth It

I Built a Knowledge Graph Next to My Vector Search

A knowledge graph and a vector search answer different questions. How I built a graph next to my vector search, and the query that shows which one you need.

On August 8 I asked my own AI office a question its search could not answer.

Which decisions does record #35 depend on, and what did they change?

I keep every structural decision I make as a numbered, dated record, written the day I make it. Sixty-two of them sat in that office at the time, chunked and embedded so I could search them by meaning.

The search ranked record #35's own text seventh, at a similarity score of 0.514, behind five records that only mentioned amendments in passing. It never surfaced record #34, the one #35 replaced.

Nothing was broken. A similarity search ranks text that reads like the question. The word "supersedes" in record 35's header is a link to record 34, and that link is not in either document's own text.

I reach for a vector search and a knowledge graph to solve two different problems. I hand a similarity search a sentence. It has no way to follow a stated link between two documents. I give a knowledge graph a specific record instead, and it walks every connection that record names.

So I built the second path next to the first: a knowledge graph, in the same Postgres, in two tables, with no new service to run. The RAG guide covers embeddings and similarity search in full.

Which One Answers Your Question?

Vector search Knowledge graph
What you hand it A sentence One specific record
Matched on How the wording reads A relationship somebody wrote down
What comes back Ranked passages with a score Connected records with a named relation
Finds a link nobody wrote Sometimes Never
Needs an entry point No, a question is enough Yes, a specific starting record

When Is a Knowledge Graph the Wrong Build?

Skip it when your documents do not point at each other. A graph over records with no stated relationships is an empty table with a migration attached, and no amount of clever extraction invents a cross-reference that was never written. Three more signs the answer is no:

  1. Every real question you get is "what do we say about X." A vector search answers those on its own, and a graph has nothing to start from.
  2. Your documents cite each other so often that most things connect to most other things. Run the count in the section below first. A densely-linked corpus hands back most of itself after two or three hops, which stops being a useful answer.
  3. The relationships would have to be guessed rather than read. If nobody ever wrote "this replaces that" anywhere in your documents, every edge needs a language-model extraction pass instead, and that is a separate build with its own schema and error handling.

What a Knowledge Graph Is

A knowledge graph stores documents as nodes and the stated relationships between them as typed edges. "Record 35 supersedes record 34" is one row. Record 35 comes back with every connected record in the graph, each labeled with its relationship name and the source sentence it was read from.

It is two tables in the Postgres I run. One table holds the nodes, keyed on a stable identifier like record:0035 so loading a document twice updates it instead of duplicating it. The other holds directed, typed edges, plus the phrase each edge was read from, so a wrong edge can be traced back to the sentence that produced it.

-- the nodes. `key` is stable and human-readable, so loading is an upsert.
CREATE TABLE graph_entities (
    id    uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    key   text  NOT NULL UNIQUE,        -- 'record:0035' or 'file:some/path.py'
    kind  text  NOT NULL,
    name  text  NOT NULL,
    attrs jsonb NOT NULL DEFAULT '{}'::jsonb
);

-- the edges. `note` is the phrase this edge was read from, for the audit trail.
CREATE TABLE graph_relations (
    id        uuid NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
    source_id uuid NOT NULL REFERENCES graph_entities (id) ON DELETE CASCADE,
    target_id uuid NOT NULL REFERENCES graph_entities (id) ON DELETE CASCADE,
    type      text NOT NULL,
    note      text,
    UNIQUE (source_id, target_id, type)
);

I looked at two other shapes first. A dedicated graph database adds three costs I didn't want:

  • A second service to run
  • A second backup to maintain
  • A second copy of the same facts to keep in sync with everything else

An in-memory structure built fresh on every run is fine for a handful of files, but it cannot be joined to anything else, and nothing outside the one script that built it can query it. Two plain tables in the same Postgres I run carry none of those costs.

Where Do the Edges Come From?

From a sentence the corpus carries. Every record I write opens with a header naming the other records it touches, in a fixed spot, with a real verb: supersedes, amends, extends, refines. A parser reads that one line into nodes and edges. Every edge keeps the phrase it was read from.

Here is one real line, unedited:

Related: supersedes the earlier ruling and record 0034; leaves record 0016's
exclusions in place

Two edges come out of it, each with its source phrase attached:

record:0035 --supersedes--> record:0034   (read from: "supersedes ... and record 0034")
record:0035 --relates_to--> record:0016   (read from: "leaves")

This works because my own records share one fixed header. Meeting notes, support tickets, and a wiki carry no such contract. Loading those needs a language model reading each document and returning typed relationships:

  • A schema for what it is allowed to say
  • A check on each relationship it returns
  • A record of what it could not parse

Whether I parse a fixed header or run a language model over the documents, the two tables and the recursive query below stay the same. Only the parsing step changes.

How Does the Walk Work?

One recursive query. It reads every edge in both directions, so following a record's history and following what came after it are the same kind of step, then it carries the path taken so far and refuses to step onto a record already on that path. That is what stops A -> B -> C -> A from running forever.

WITH RECURSIVE edges AS (
    SELECT source_id AS a, target_id AS b, type, note, 'out' AS dir FROM graph_relations
    UNION ALL
    SELECT target_id AS a, source_id AS b, type, note, 'in'  AS dir FROM graph_relations
),
walk AS (
    SELECT e.id AS node_id, 0 AS depth, ARRAY[e.id] AS path
    FROM graph_entities e WHERE e.key = %(start)s
    UNION ALL
    SELECT x.b, w.depth + 1, w.path || x.b
    FROM walk w JOIN edges x ON x.a = w.node_id
    WHERE w.depth < %(max_hops)s AND NOT (x.b = ANY(w.path))
)
SELECT DISTINCT ON (node_id) node_id, depth FROM walk ORDER BY node_id, depth;

The walk follows edges in both directions. Each row in the results still says which way the edge it came from was pointing: "record 35 supersedes record 34" and its reverse are different facts.

What Did the Graph Find That the Search Missed?

Eight records, and the search found none of the same ones. Asked which decisions record 35 depends on, the two paths overlapped on nothing:

Path Count Which records
Found by both 0 -
Graph only 8 0003, 0005, 0013, 0014, 0016, 0017, 0033, 0034
Search only 6 0015, 0020, 0026, 0043, 0059, 0062

Those numbers came from the same run that ranked record 35 seventh: a two-hop graph walk against a search over the same 62 records on August 8.

Record 34, the one record 35 replaced, shares almost no wording with the question that names it. No amount of adjusting the similarity floor reaches it, because the fact that matters is not in the wording. It is in a link somebody wrote down.

Where Does a Search Win Instead?

Asked the same underlying question in ordinary words instead of by number, the vector search won. I asked: why was an earlier security rule retired, and what else did that change?

The top result came back at similarity 0.665, the second at 0.606, both the record that answers "why." A graph has no way to start from a sentence like that. It needs a specific record to walk from, and this question does not name one.

Path Count Which records
Found by both 2 0033, 0034
Graph only 6 0003, 0005, 0013, 0014, 0016, 0017
Search only 3 0011, 0021, 0062

A graph has no entry point of its own. I give it a specific record to start from. The search is how I find that record when all I have is a question in ordinary language.

How Do You Know Before You Build?

Count the edges per record and look at the median, then run a two-hop walk from one record and see what share of your total nodes comes back.

WITH e AS (
    SELECT source_id AS a FROM graph_relations
    UNION ALL SELECT target_id FROM graph_relations
)
SELECT g.id, count(e.a) AS degree
FROM graph_entities g LEFT JOIN e ON e.a = g.id
GROUP BY g.id;

On August 8, those 62 decision records plus the files they named made 109 total nodes in the graph. A two-hop walk from record 35 reached 11 of them, about 10%: a specific, useful set.

Six weeks and roughly 170 more records later, I reran the same measurement. The corpus is now 345 nodes and about 1,200 edges. The median record links to 9 others, and the busiest one links to 64. A two-hop walk from record 35 today reaches 114 of those 345 nodes, about a third of everything I have ever decided.

Once a two-hop walk starts returning close to a third of your corpus, that is the signal to check the walk at one hop instead. As my corpus grows denser, I shorten the hop limit to keep it useful.

How Many Edges Name a Real Relationship?

Twenty-seven out of 283, on August 8. The rest said only that two records belonged together, without saying how:

Relationship Edges Share
No verb stated (default) 183 64.7%
Names a file the record touches 73 25.8%
Amends 11 3.9%
Extends 10 3.5%
Supersedes 4 1.4%
Refines 2 0.7%

Supersession was the exact relationship I built this to answer, and only four edges in the whole graph carried it. A typed edge only gets written where the source sentence uses that verb. Ask to see this same breakdown before trusting anyone's diagram of neatly labelled arrows, including mine.

What Did My Own Parser Get Wrong?

It invented relationships out of a list. One record's header read "refines record A, record B, record C, record D," a single relationship followed by a plain list of three more records. My first version read each comma as a repeat of the verb and typed all four the same way.

The fix is one rule: a verb carries forward to the next reference only when a real connector like "and" sits between them, never a bare comma.

This is not a defect specific to my own parsing rule. Researchers building a supply-chain graph from public company profiles measured their own pipeline stage by stage: [1]

Pipeline stage Score
Named-entity recognition 0.95
Entity disambiguation 0.98
Relation extraction 0.82

Relation extraction is the step that decides what kind of connection two entities have. Their model recognized related entities easily. It got the specific relationship between them wrong far more often.

So every edge in my graph keeps the sentence it was read from.

How Deep Should a Walk Go?

Two hops, on the August 8 corpus. Past that, the walk stops answering the question and starts returning the corpus:

Depth Records reached Query time
2 11 of 109 0.44 ms
3 41 of 109 0.47 ms
4 89 of 109 1.40 ms
6 109 of 109 47.99 ms

Cost climbs with depth too, because the query enumerates full paths rather than tracking which records it has already visited. At depth 4, 89 of 109 records come back. Past that, it's the whole corpus.

Where Does This Pay Off Outside My Own Case?

Anywhere the important fact about a document lives in a different, later document. Law is the clearest version, and the tooling for it predates computers.

Case law. Shepard's Citations has listed every later case citing a given one, and what each did to it, since 1873. [2] A red flag in the system today means "reserved for opinions that have been overruled, reversed, or negatively impacted in some other significant way." [3]

A case's own text never says it was overruled. A later case says it, and a citator is a knowledge graph that has been sold as a product for over 150 years.

Statutes. The UK's official legislation site models what it calls an effect: "any impact that one legislative provision may have on another," including a change to meaning with no change to the text. [4]

Applying a new effect after a law passes "typically take[s] from four to eight weeks." [4] A compliance team needs to know which effects have been applied to a rule since it passed. That answer is a walk from the original text through every later provision that touched it.

Supply chains. Researchers built a graph from public company filings with 732 entities and 433 relationships after cleanup, and used it to trace parts back through two and three tiers of suppliers to a named source. [1] A similarity search can't answer "what feeds into this." It ranks passages; it can't walk a chain of parts.

Industry classification. Ramp Engineering scores customer classification with a "fuzzy accuracy" metric built specifically because their categories sit in a hierarchy: a prediction that lands close in the tree counts for more than one that is simply wrong. [5] A vector search finds the candidates. The tree is what assigns partial credit to a near miss.

Quick Recap

  • A knowledge graph is two tables: records as nodes, stated relationships as typed edges, both sitting in whatever Postgres you run.
  • I run one recursive query for the full walk. It carries the path taken so far so a cycle cannot run forever.
  • Measure the median edges per record before you build. A low median means a two-hop walk comes back with a specific, useful set. Once the median climbs, shorten the hop limit rather than widen the query.
  • Most edges will not name a real relationship. On my own corpus, 183 of 283 said only that two records belonged together.
  • I give the graph a specific record to start from. The vector search is how I find that record from a question in ordinary language, and it also turns up links nobody ever wrote down.
  • This pays off wherever the deciding fact sits in a different document than the one you're reading: case law, statute, a supplier network, or a category tree.

Start Here

The intake at daisyguti.ai/work-with-me is about nine questions and takes a few minutes. I read it and reply with whether a retrieval build like this one, or a different kind of workflow automation, fits what you're trying to solve.

The RAG guide covers the vector side of this build in full, and choosing a retrieval cutoff is the deeper version of the similarity floor mentioned above. The AI buzzwords guide has plain definitions for embeddings and retrieval if either term is new.

Sources

  1. Sara AlMahri, Liming Xu, Alexandra Brintrup, "Enhancing Supply Chain Visibility with Knowledge Graphs and Large Language Models," arXiv:2408.07705 - https://arxiv.org/html/2408.07705v1 - reports a graph of 732 entities and 433 relationships, and pipeline accuracy of 0.95 (entity recognition), 0.98 (disambiguation), and 0.82 (relation extraction).
  2. Shepard's Citations, Wikipedia - https://en.wikipedia.org/wiki/Shepard%27s_Citations - "The name comes from a service begun by Frank Shepard (1848-1900) in 1873."
  3. USC Gould School of Law, Law Library guide, "Shepard's Citations - How to Confirm That Your Case Is Good Law" - https://lawlibguides.usc.edu/c.php?g=542695&p=3718771 - "This symbol is reserved for opinions that have been overruled, reversed, or negatively impacted in some other significant way."
  4. legislation.gov.uk, Help and glossary - https://www.legislation.gov.uk/help - defines an effect as "any impact that one legislative provision may have on another," and states new effects "typically take from four to eight weeks to complete."
  5. Ramp Engineering, "Industry classification at Ramp" - https://engineering.ramp.com/post/industry_classification - describes NAICS as hierarchical and a custom "fuzzy-accuracy" metric built around that hierarchy.
  6. pgvector, open-source vector search for Postgres - https://github.com/pgvector/pgvector

Start a project

Ready to scope it and ship it?

Have an AI workflow, reporting gap, or system you can't seem to get off the ground? Scope it with me. You'll get a real read on what to build first and what it should cost.

I review every submission and reply with a real read on fit.