A fleet maintenance manager pays for a repair. Months later, it turns out the same failure was covered by a recall and the repair could have been free. The information existed the whole time in two public government files. Nobody connected them.
This post builds that connection: a knowledge graph over every NHTSA vehicle complaint and recall, with a query engine that answers “is this repair covered by a recall?” by citing a campaign number, or refuses when there’s no evidence. No LLM reads any document. The graph is parsed, not extracted, because the data already ships with its structure.

From two government text files to an evidence engine
The data: two files, already structured
NHTSA publishes two tab-delimited flat files:
• Complaints: 2,246,958 rows covering 1,619,872 complaints from January 1995 to September 2026. Each row has make, model, year, failure date, crash/fire/injury flags, a free-text narrative, and a component label.
• Recalls: 327,036 rows covering 30,313 campaigns from 1966 to September 2026. Each row has the campaign number, vehicle, component, report date, and the number of units affected.
Parsing both into DuckDB took 28.8 seconds for complaints, 2.9 seconds for recalls, and 14.1 seconds to type and clean them. That’s about 46 seconds total, with zero model calls.
The ontology we didn’t have to build
The component field is not free text. It’s a colon-separated hierarchy:
SERVICE BRAKES, HYDRAULIC : FOUNDATION COMPONENTS : DISC : CALIPER
Both files use the same vocabulary. Across them there are 816 distinct component paths, which expand to 912 nodes once every ancestor is included. Those nodes sit under 54 top-level systems and go up to five levels deep. The deepest path is ELECTRICAL SYSTEM:ADAS:BLINDSPOT:WARNING:CHIME.
As with MeSH’s tree numbers, ancestry is a string-prefix test, so no reasoner is needed:
rel = lambda a, b: a == b or b.startswith(a + “:”) or a.startswith(b + “:”)
But the two files use this tree very differently.

67% of complaint rows are labelled only at the top level, such as ENGINE or ELECTRICAL SYSTEM. Consumers pick a broad label; manufacturers file precise ones. Only 20% of recall rows stop at the top level. Exact string matching between the two would miss most links, so ancestry matching is required.
Vocabulary drift: same system, two names
The first link attempt showed an odd gap. Complaints used the label ENGINE 169,752 times, but recalls used it only 693 times. Recalls overwhelmingly use the older label ENGINE AND ENGINE COOLING (9,890 rows).
The vocabulary changed over time. Complaint forms adopted newer labels, while recall filings mostly kept the old ones.

The fuel system is the extreme case. 53% of fuel complaints use FUEL/PROPULSION SYSTEM, and exactly 8 recall rows do. Four normalization rules fix it, the fleet equivalent of spelling-normalization rules in text search:
CASE WHEN t IN (‘ENGINE’,’ENGINE AND ENGINE COOLING’) THEN ‘ENGINE’
WHEN t LIKE ‘SERVICE BRAKES%’ THEN ‘SERVICE BRAKES’
WHEN t = ‘FUEL/PROPULSION SYSTEM’
OR t LIKE ‘FUEL SYSTEM%’ THEN ‘FUEL SYSTEM’
WHEN t IN (‘VISIBILITY’,’VISIBILITY/WIPER’) THEN ‘VISIBILITY’
ELSE t END
Those four rules connected 96,765 more complaints to a recall in the same component branch.
Citation edges hiding in free text
While sampling narratives, I noticed owners write recall numbers directly into complaints, such as “18V160000 recall…”. NHTSA campaign numbers follow a fixed pattern, so one regex extracts them:
regexp_extract_all(upper(cdescr), ‘([0–9]{2}V[0–9]{6})’)
That produced 166,200 citation edges from 149,043 complaints to 6,368 campaigns, and 96.7% of them resolve to a real campaign in the recall file. These are the fleet equivalent of a paper’s reference list: explicit, human-written links that no model had to infer. The most-cited campaigns are airbag inflator recalls, led by 14V373000 with 4,283 citing complaints.
91% of complaints sit on a recalled vehicle, so that proves nothing
The obvious linking rule is “this complaint’s vehicle has a recall.” That’s nearly useless: 91.1% of the 2,043,706 usable complaint rows are on a make/model/year with at least one recall. A connection that links almost everything means almost nothing.
To tell real matches from coincidence, I built a control. Each complaint kept its real vehicle, but its component was swapped for one drawn from a different random complaint. If matching by component meant nothing, real and shuffled complaints would match recalls at the same rate.


Real complaints match more often at every meaningful level, but the control is uncomfortably high. The reason is specificity. A complaint labelled only ELECTRICAL SYSTEM matches any electrical recall on the vehicle.

Split by depth, the signal sharpens. At depth 1, real complaints match 1.7× as often as the control (39.7% vs 23.7%). At depth 4 it’s 3.0× (20.8% vs 6.9%). Specific labels do the work, which is why the query engine refuses generic ones.
Complaints arrive before recalls
Among 120,632 specific complaints (depth 2 or deeper) that match a recall branch, 46,485 (38.5%) describe a failure that happened before the first matching recall existed. The median lead time is 631 days, with an interquartile range of 230 to 1,483 days.

The more striking result is who initiated the recall. Across 18,306 vehicle campaigns reported from 2000 to 2025:

• Manufacturer-initiated recalls: 7.8% had a specific matching complaint beforehand (mean 1.2).
• Compliance office (OVSC) recalls: 15.1% (mean 2.7).
• NHTSA defect-investigation (ODI) recalls: 30.1% (mean 14.4).
Complaints are an early-warning signal, and they’re strongest for exactly the defects manufacturers didn’t recall on their own.
Grounding free text without a model
A fleet user won’t type POWER TRAIN:DRIVELINE:DRIVESHAFT. They’ll type “driveshaft vibration at highway speed.” The grounder maps text to component nodes by lexical match, so it can never invent a component.
Evaluating it needed ground truth I didn’t write. NHTSA already labels every complaint’s component, so I grounded 3,000 complaint narratives and checked the results against NHTSA’s labels.


v1 had a real bug. It matched only the final word of a path and preferred deeper nodes, so an early version sent “fuel pump failure” to the engine’s oil pump, because PUMP was the leaf and the oil-pump node was deeper. Scoring the whole path fixed it.
v3 fixed a second bug. Words like “rear” matched nodes such as SUSPENSION:REAR, so “rear view camera screen goes blank” pulled in suspension and seat-belt recalls. Ignoring nodes whose leaf is only a position word (rear, front, driver, side) gave up 3.5 points of coverage for higher precision. Coverage and precision pull in opposite directions, and precision is what matters when the output is a claim about money.
The refusal ladder
Five gates run before any answer is given. Each is a simple lookup, and no model decides anything.

1. Vehicle in graph? A “2031 Ford Transit” is refused as vehicle_not_in_graph.
2. Text maps to a component? “Flux capacitor overheating” is refused as component_not_grounded.
3. Specific enough? “Engine” is refused as component_too_generic.
4. Matching recall exists? “2018 Freightliner Cascadia, air brake compressor leak” returns NO_RECALL, with 3 similar complaints on file.
5. Recall existed on the repair date? “2020 Transit, rear view camera blank, repaired 2020–06–01” is refused as recall_issued_after_repair. The matching campaign, 20V575000 covering 620,246 vehicles, was reported on 2020–09–23, after the repair.
When all five pass, the answer is a citation. “2020 Ford Transit, driveshaft vibration at highway speed” returns campaign 21V978000, reported 2021–12–16, covering 4,548 vehicles, with 17 similar complaints.
The specificity gate had its own bug. It first counted matched words, so the single word “driveshaft” was refused as too generic, even though POWER TRAIN:DRIVELINE:DRIVESHAFT is three levels deep. The fix was to judge specificity by depth in the tree, not by word count.
The index loads in 3.4 seconds and covers 86,903 vehicle keys. A recall lookup takes about 2.4 microseconds.
Bugs that produced plausible answers
None of these raised an error:
1. Row alignment by luck. I joined complaint narratives to their labels using row_number() over () with no ORDER BY. It happened to align all 2,043,706 rows because the scan ran in file order. That’s luck, not a guarantee, so I rebuilt the join on NHTSA’s unique complaint ID.
2. Oil pump for fuel pump. Leaf-only matching returned a confident, wrong component.
3. “Rear” means suspension. Position words matched unrelated systems.
4. “Driveshaft” too generic. The specificity gate measured the wrong thing.
Every one produced a reasonable-looking output. Only checking against independent labels caught them.
What public data can’t do for truck fleets
Here’s the finding that matters for a product.


Heavy trucks have plenty of recalls but almost no complaints. Truck owners and fleets don’t file NHTSA complaints the way car owners do. For a Class 8 fleet, the recall layer is rich and the evidence layer is nearly empty.
That gap is where a product lives. A fleet’s own work orders, fault codes, and parts history are the complaint layer heavy trucks are missing. Connect them to this recall graph and you get warranty and recall recovery: “this $1,800 driveshaft repair matches campaign 21V978000; check whether it was covered.”
Honest limits
• Matching by make, model, and year over-approximates. Recalls cover specific build-date ranges and VINs. A match here is a candidate, and a real product must confirm it against the VIN through NHTSA’s recall lookup.
• Complaints are self-reported. Narratives, failure dates, and component labels come from owners and are not verified.
• 23.3% of complaints match a recall branch that existed before the failure date, versus 13.5% for the shuffled control. That gap is a real signal, but given the first point, it’s an upper bound, not a recovery rate.
• No fleet work-order data has been tested yet. The product thesis is untested until it runs on one fleet’s real repair history.
Cost ledger

The graph is a parse, not an extraction. The next step is adding the one layer the public data can’t provide: the fleet’s own repair history.
Data: NHTSA ODI complaints and recalls flat files, downloaded September 18, 2026.
This article was originally published on Medium by Ponvannan P.
