You've got 500 documents. Some are contracts. Some are meeting notes. Some are design specs. They're connected—a contract references a project, which references a meeting, which references a person. But your tools don't know that.
Most document systems treat files as isolated objects. Search one, get results for that one. You have to manually connect the dots. AiFiler's knowledge graph does the connecting for you, automatically, and does it fast enough that you don't notice it happening.
This isn't a theoretical exercise. We built this because the alternative—traditional graph databases like Neo4j—either choke on scale or require you to pre-define every relationship type. We needed something that could infer relationships from content, store them efficiently, and query them without latency.
The Problem We Solved
Graph databases are powerful. They're also slow when you have thousands of documents and millions of potential relationships. A naive implementation would look like:
Document A → Document B → Document C → Document D
Query "show me everything connected to Document A" and you're traversing edges. Add enough documents and you're looking at seconds of latency. That's unacceptable when you're trying to search in real-time.
The second problem: relationship types. Traditional graphs require you to decide upfront: "We have REFERENCES, MENTIONS, BELONGS_TO, CREATED_BY." But documents are messy. A contract might reference a project in three different ways. A meeting note might mention a person as an attendee, a decision-maker, and a stakeholder. You can't predict all of these.
We needed a system that:
- Infers relationships automatically from document content and metadata
- Stores them efficiently without pre-defining every edge type
- Queries them fast without traversal latency
- Scales to thousands of documents without degradation
How We Built It
The architecture sits on three layers: inference, storage, and query.
Layer 1: Inference (Claude + Intent Heuristics)
When you upload a document, AiFiler's intelligence system (powered by Claude and our intent heuristics engine) reads it and extracts potential relationships. This happens in lib/intelligence/intentHandlers.ts and lib/intentHeuristics.ts.
The system asks:
- What entities are mentioned? (people, projects, dates, companies)
- What's the document's purpose? (contract, meeting note, analysis)
- What other documents in your workspace might relate to this?
Instead of storing raw "Person X mentioned in Document Y," we store a relationship tuple:
{
source_doc_id: "doc_abc123",
target_doc_id: "doc_xyz789",
edge_type: "REFERENCES",
confidence: 0.92,
context: "Contract references Project Acme",
inferred_at: "2025-06-15T10:30:00Z"
}
The confidence score is crucial. It tells us how sure the system is. A 0.92 confidence relationship is weighted differently than a 0.65 one.
Layer 2: Storage (Supabase + Denormalization)
This is where most systems get it wrong. They try to store a pure graph in a relational database, which creates join hell.
We denormalize. Heavily.
In Supabase, we store relationships in a document_relationships table with these columns:
id (UUID)
source_doc_id (UUID, indexed)
target_doc_id (UUID, indexed)
edge_type (text enum: REFERENCES, MENTIONS, BELONGS_TO, CREATED_BY, RELATED_TO, etc.)
confidence (float 0-1)
context (text)
inferred_at (timestamp)
But here's the trick: we also denormalize relationship counts into the documents table itself:
documents.relationships_count (int)
documents.primary_relationships (JSONB array of top 5 relationships)
This means when you open a document, we don't query the relationships table. We already have the most important ones in the document record. It's a cache, but a smart one.
For deeper relationship exploration (like "show me everything connected to this document"), we do query the relationships table. But because we index heavily on source_doc_id and target_doc_id, and because we filter by confidence threshold, the query is fast.
Layer 3: Query (SWR + Smart Filtering)
On the frontend, we use SWR (stale-while-revalidate) to fetch relationship data. The pattern lives in lib/knowledge/hooks/ and uses localStorage prefixing for offline-first behavior.
When you click on a document in AiFiler and open the Knowledge View, the system:
- Fetches the document's
primary_relationshipsfrom the document record (instant, cached) - In the background, fetches the full relationship graph from the API
- If you're offline, you see the primary relationships. When you're back online, you see the full graph
The API endpoint filters relationships by:
- Confidence threshold: Only show relationships above 0.7 by default (user-configurable)
- Relationship type: The Knowledge View lets you toggle edge types on/off
- Depth: How many hops away from the current document do you want to go?
This is where the architecture shines. Instead of a single "give me the graph" query, we have intelligent filtering that respects both performance and relevance.
The Edge Types: 8 Relationships That Matter
We support 8 edge types, inferred automatically:
| Edge Type | Meaning | Example |
|---|---|---|
| REFERENCES | Document A cites Document B | Contract references a Statement of Work |
| MENTIONS | Document A talks about Document B | Meeting notes mention a person |
| BELONGS_TO | Document is part of a collection | Invoice belongs to a project |
| CREATED_BY | Document authored by a person | Report created by Sarah |
| RELATED_TO | Thematic similarity, no direct link | Two analyses on the same topic |
| DEPENDS_ON | Document requires another to be complete | Implementation plan depends on design doc |
| SUPERSEDES | Document replaces an older one | v2 contract supersedes v1 |
| CHILD_OF | Hierarchical relationship | Email thread item is child of thread |
Each relationship is scored independently. A single pair of documents might have multiple edge types with different confidence scores.
Why This Matters for You
When you use Universal Command (Ctrl+Shift+A) to search, the system doesn't just find documents by keyword. It understands relationships. Ask "show me everything related to the Acme project" and you get:
- Documents that explicitly mention Acme
- Documents created by people assigned to Acme
- Documents that reference contracts tied to Acme
- Documents related to deliverables for Acme
All in one search, ranked by relevance.
Without the knowledge graph, you'd get keyword matches. With it, you get context.
The graph also powers the Knowledge View—click the "Knowledge" tab on any document and you see its relationship network visually. You can explore outward, toggle edge types, and jump between related documents. It's the difference between finding a document and understanding its place in your work.
What We Learned
Building this taught us three things:
First: Denormalization isn't cheating. It's pragmatism. A pure graph is elegant. A fast system is useful. We chose useful.
Second: Confidence scoring matters more than you think. A relationship you're 65% sure about is different from one you're 95% sure about. Surfacing both, but weighting them differently, gives users agency. They can explore uncertain connections if they want, but they're not buried in noise.
Third: Inference at ingest time is better than inference at query time. We pay the cost upfront (Claude reads the document, extracts relationships, stores them). You benefit at search time (queries are instant). This is the right trade-off for a document tool.
The Technical Trade-offs
We could have built a traditional graph database. It would have been more "pure." It would also have been slower and more expensive to operate at scale.
We could have done relationship inference on-demand, at query time. It would have been simpler. It would also have meant every search waits for Claude to think.
Instead, we built a hybrid: infer at ingest, denormalize for speed, query with intelligence. It's not textbook graph database design. It's pragmatic architecture for how people actually work.
The result: you search, you find not just documents but their context. That context lives in the knowledge graph, and the knowledge graph is fast enough that it disappears into the background. You just get better results.
Enjoyed this article?
Get more articles like this delivered to your inbox. No spam, unsubscribe anytime.