Why I Built @farming-labs/grag: GraphRAG for TypeScript

This is the thinking behind @farming-labs/grag: why I wanted GraphRAG to feel native in TypeScript, why I chose relational storage instead of a mystery box, and why I think retrieval systems need to be inspectable, composable, and understandable.

Grag is a TypeScript-native tool for building inspectable, composable, and understandable retrieval systems.

Now you might be wondering "why build this when there are already a lot of RAG libraries, vector tools, orchestration frameworks, and GraphRAG discussions?"

Grag is not made to be yet another wrapper around embeddings and chat completion. It attempts to solve a real gap that I see constantly in Node and TypeScript systems:

The Problem

the moment you want serious GraphRAG, you usually end up leaving the main stack.

Most TypeScript RAG tools are very good at one narrow thing:

  • chunk retrieval
  • vector similarity
  • model orchestration
  • or framework-level agent flows

All useful, but all leave a hole.

Normal vector search is good at similarity, but it does not naturally explain the relationships between concepts.

It can also struggle when a question depends on exact wording, a specific date, an identifier, or some detail that is easy for humans to recognize but hard for semantic similarity alone to spot.

Graph databases are good at graph storage, but they do not give you a ready-made GraphRAG pipeline, chunking flow, community reports, or retrieval contract.

Chain frameworks help orchestrate models and tools, but they tend to own too much of the stack while still not giving you a faithful GraphRAG artifact model.

Many serious paths that lead to GraphRAG often stop feeling like part of your app and start feeling like a separate system you have to negotiate with.

That sounds manageable at first, but it becomes expensive fast. Once the indexing, retrieval, and storage flows stop living in the same stack, you usually end up with:

  • an extra runtime to operate
  • a boundary between systems that has to stay in sync
  • duplicated types and contracts
  • awkward storage handoffs
  • much harder debugging attempts when the answers look wrong

I wanted to avoid this experience. And that is why @farming-labs/grag was made.

One very practical reason for building it was docs.farming-labs.dev/cloud. I did not want that experience to only return the nearest chunk and call that an answer. I wanted it to be more answer-rich: better grounded, more connected, and more capable of pulling together the parts that actually explain something.

RAG vs GraphRAG

Before we go any deeper we need to understand what GraphRAG really is and how it is different from traditional RAG.

"GraphRAG" is an idea that does not get nearly as much explanation as it gets mentioned. So here is the basic distinction from traditional RAG:

Basic RAG:

  • break text into chunks
  • retrieve relevant chunks
  • give them to the model

GraphRAG:

  • break text into chunks
  • extract the important concepts from those chunks
  • connect those concepts to each other
  • cluster them into communities
  • write summaries about those communities
  • retrieve from both the local evidence and the larger graph structure

GraphRAG is not only about "finding similar chunks." It is a broader pipeline that:

  1. turns source documents into text units
  2. extracts entities and relationships
  3. groups those entities into communities
  4. generates community reports
  5. answers questions from graph neighborhoods or from those higher-level reports

Which means the retrieval layer can answer both:

  • "what exact chunk explains this?"
  • "what bigger pattern keeps showing up across all these documents?"

Traditional RAG really sucks at answering the second question.

An even simpler distinction is that basic RAG mostly asks "which pieces of text look relevant?" while GraphRAG would also ask (and answer) "how do these pieces connect, and what do they mean together?"

Another useful way to say it is this: traditional RAG often gives you isolated text fragments, while GraphRAG starts building structured domain knowledge. That is what makes graph-based retrievers more useful for complex multi-hop questions, because they can follow typed relationships instead of stopping at the first similar chunk.

Why does this matter? Because the first matching chunk is not always enough.

Sometimes the first useful chunk is just the entry point. After that, you may need to move from the chunk to the entity it mentions, from that entity to a relationship, and from there to the other source text that actually completes the answer.

In some domains, you also do not want to treat every piece of text as equally important. You want the system to remember the important things it extracted. Things like entities, claims, dates, relationships, or higher-level groupings.

These built-in distinctions reduce noise and give retrieval something more useful to work with than a flat pile of chunks.

GraphRAG gives you a better shot at answering questions like:

  • what themes keep appearing across our support issues?
  • what parts of this repository are tightly related?
  • what risks show up across many documents, not just one chunk?

That is what I want @farming-labs/grag to make practical.

Why Grag Lives In TypeScript

Not only do I want a GraphRAG to exist for my app, I want it to exist in the same language as the rest of the app.

For TypeScript this means:

  • the artifact model is typed
  • the storage layer is typed
  • the service layer is typed
  • the query results are typed

And app code does not have to pretend a separate retrieval system is part of the same codebase.

And it is for the same reason that the package starts with strict TypeScript models for:

  • documents
  • text units
  • entities
  • relationships
  • covariates
  • communities
  • community reports
  • embeddings

The package also isolates model choice from the rest of the system.

It exposes ChatModel and EmbeddingModel interfaces, which means the graph, storage, and retrieval parts are not locked to one provider. OpenAI and Anthropic adapters exist, but the contract is intentionally small so teams can swap providers without rebuilding the whole system.

That was important to me. I did not want a retrieval system that felt clean only until you decide to change model vendors.

Why Relational Storage Is The Center

One of the strongest architectural choices in grag is that the graph can live in a relational database as a first-class backend.

This is intentional.

A lot of systems treat the vector store as the main source of truth for the graph state, with chunks, embeddings, and metadata living there. The database is often just app data or a secondary storage layer. Grag challenges that by making the relational database the durable GraphRAG state.

That means storing things like:

  • documents
  • text units
  • entities
  • relationships
  • communities
  • community reports
  • embeddings

and also storing the important links between them.

Those link tables matter because they make the graph explainable.

They let the system move from:

  • a matching chunk
  • to the entities grounded in that chunk
  • to nearby relationships
  • to community summaries
  • and then back to the original source text for citations

The vector search is still useful, but it becomes one retrieval channel over that stored graph, not the whole system.

This creates a much stronger foundation for serious retrieval than "found a similar vector, hope it is enough."

Because the system is not forced to depend on one ranking signal or an isolated chunk, the general quality of the retrieval is improved. It can gather several kinds of evidence at once:

  • exact source chunks
  • connected entities
  • nearby relationships
  • community-level summaries

This usually leads to more grounded, less random, and easier-to-verify context when the final answer is shown to a user.

Why Multiple Sources Matter

It also helps that the graph does not have to come from only one place.

import { DataSourceLoader, source } from '@farming-labs/grag'

const loader = new DataSourceLoader([
  source.repo({ url: 'https://github.com/farming-labs/grag', maxFiles: 80 }),
  source.document({ files: ['./docs/design.md'] }),
])

A repo, local docs, database rows, or fetched pages can all feed the same graph. This happens when the graph is loaded and indexed, not by passing sources to ask().

That matters because a better answer often needs more than one kind of evidence. Especially for something like docs.farming-labs.dev/cloud, the goal is not just to retrieve something related. It is to return a more complete answer while still keeping source paths and citations attached to the evidence.

And if that wasn't good enough, having relational storage as the core means teams can use the infrastructure they already have.

If you already run Postgres, you do not need to introduce an entire second graph backend just to start using grag.

Keeping The Storage Boundary Small

The GraphRagStore contract is another intentional design choice that I put a lot of thought into.

Your app code shouldn't depend on one specific database tool. It should be flexible enough to handle different systems that live in different places. Which is why the GraphRagStore exists. Grag supports:

  • MemoryGraphRagStore for tests and prototypes
  • SqlGraphRagStore for Kysely and relational databases
  • OrmGraphRagStore through @farming-labs/grag/orm
  • custom stores when a team wants to own the backend themselves

Sometimes they are part of docs infrastructure. Sometimes they sit behind an internal support tool. Sometimes they live inside a product assistant. Sometimes they run in tests with no external services at all.

I wanted the same retrieval contract to survive across all of these cases.

The Service Layer

I didn't want people to have to learn every low-level primitive just to ask a useful question. That is why the services surface exists:

import { createGraphRagService } from '@farming-labs/grag'

const grag = createGraphRagService({ store, model, embeddingModel })

const result = await grag.ask('How does storage support retrieval?', {
  limit: 12,
  responseStyle: 'short answer with citations',
})

As you can see in the code above, you can create a grag instance with createGraphRagService() by passing it an object with a store, a model, and an embeddingModel.

Then you call it with a question and choose how much supporting evidence to keep with limit.

result is designed to be product-shaped, not only research-shaped.

It returns:

  • an answer
  • citations
  • ranked evidence
  • graph ids for highlighting
  • stats
  • timings

That means the same package can support a dashboard, a docs assistant, a support tool, or an API route without every team having to invent its own return shape.

This was a big part of the project for me.

I did not want GraphRAG to be technically impressive but operationally awkward. I wanted it to plug into real app surfaces.

How This Improves Retrieval Quality

GraphRAG is bigger than just "better search." It is more about a broader concept related to retrieval quality.

Better quality means:

  • the evidence is more relevant
  • the context is more grounded
  • the answer is easier to trace back to source text
  • broad questions and narrow questions can use different retrieval paths
  • and the whole system is easier to inspect when something goes wrong

A big part of ensuring quality is accepting that different questions need different retrieval behavior.

For example:

  • an exact date or identifier may need lexical matching
  • a vague conceptual question may need semantic recall
  • a relationship-heavy question may need graph traversal
  • a broad strategic question may need community-level summaries

I do not think one retriever shape should pretend to accommodate all of these.

That is also why hybrid retrieval makes so much sense to me.

Vector search is great for semantic similarity. Full-text or lexical matching is better when exact wording, identifiers, dates, versions, or error strings matter. Then graph traversal adds the missing connective tissue.

That gives you the best of both worlds: semantic recall when meaning matters, lexical precision when wording matters, and graph expansion when the answer lives across relationships rather than inside one chunk.

From there, the retrieval layer can do more than just rank chunks. It can do neighborhood traversal around a relevant node or path traversal across several connected nodes when the question needs more than one hop.

That is why I care about local and global retrieval modes. They solve different problems.

Local graph search is good when you need nearby evidence:

  • what relationship explains this failure?
  • what chunks support this entity?
  • what concepts are adjacent to this topic?

Global search is good when you need broader synthesis:

  • what are the biggest patterns across all these documents?
  • what problems keep showing up across many documents?
  • what does the graph say at the community level?

That is why the package includes both local context building and global community-report search.

I did not want to flatten everything into one retrieval mode and call it a day.

A chunk-only system often stops too early. It finds a few similar snippets and hopes they are enough.

GRAG can keep going:

  • chunk to entity
  • entity to relationship
  • relationship to nearby chunks
  • community to report
  • report back to supporting chunks

This does not magically make every answer correct. But it gives the retrieval layer more structure, better grounding, and more than one way to recover when a single ranking signal is weak.

That is the real improvement I care about. Not only should the search look smarter, the retrieval behind the answer is stronger and easier to trust.

The Bigger Picture: How Grag Fits In @farming-labs

This project fits a broader direction applied across Farming Labs:

  • typed contracts
  • portable storage
  • small but strong primitives
  • and tooling that can compose into larger systems

That is why @farming-labs/grag works with relational SQL directly, but can also plug into @farming-labs/orm when the storage layer needs to stay portable.

That is how I think these systems become useful over time.

Final Thoughts

@farming-labs/grag was built with the belief that GraphRAG should not require leaving the TypeScript world just to be taken seriously.

I wanted a package where the graph artifacts are explicit, the storage is durable, the retrieval is inspectable, the provider layer is swappable, and the output is usable inside real products.

A retrieval system that is more than just "RAG, but more complicated."

A system with stronger structure, better explainability, and a shape that feels native in a stack many teams already use.

✨ Schedule a call ✨

Let's talk and discuss more about my project and my experience on farming the modern technology

Schedule