Why a model cannot just read the schema
A harmonized lakehouse layer easily holds hundreds of tables and thousands of columns. Sending all of that to a language model with every question is slow and expensive, and it makes the answers worse: the model has to guess which of many similar tables is meant, and then guess how to join them.
GraphRAG in OrionBelt® Analytics (OBA) answers those two questions separately, with two different kinds of search:
- Vector search decides which tables matter. It finds tables and columns whose meaning is close to the question, including ones that share no word with it.
- Graph search decides how to join them. It walks the keys between those tables and returns the join path, the neighbouring tables, and the joins that multiply rows.
Neither is enough alone. Vectors know the meaning but not the joins. The graph knows the joins but not the meaning.
How a table becomes a vector
When OBA discovers a schema, every table, column, relationship and view is written out as a short piece of text. A table's text holds its name split into words, its comment, each column's name and type, and a line such as relates to customers for each foreign key. A column's text holds its table and column name split into words, its type, its comment, and primary key identifier or references customers where that applies.
orders net amt DECIMAL (the column orders.net_amt)
|
v all-MiniLM-L6-v2, on the server
[-0.01, 0.00, 0.07, ...] (384 numbers)
Each text is turned into 384 numbers by all-MiniLM-L6-v2, a small open sentence-embedding model (Apache-2.0, about 22 million parameters). It was trained so that texts with similar meaning land close together: "revenue", "sales" and "turnover" are neighbours, "order date" is far away. That is why a question about revenue can find a column called net_amt at all.
- It runs on your server. The model runs on the CPU, inside the OBA process, through the ONNX runtime that comes with ChromaDB. No GPU, no external service, no API key. Schema text never leaves the server.
- It ships in the Docker image. The model files are part of the image, so the server works in a network without internet access from the first start.
- It is stored once per connection. Vectors live in ChromaDB with an HNSW nearest-neighbour index on the server's disk, one index per database connection, and are restored when you reconnect.
For installs that cannot use the model, a keyword-only TF-IDF backend is available. It matches words, not meaning, and that difference is large in practice: against columns named salesamount, unitcost and returnquantity, a question about the "most profitable products that get returned the most" shares no word with the schema at all, every element scores zero, and the ranking is just index order. The default model finds productname, returnquantity and salesamount for the same question.
An honest example: the question "revenue"
The scores below are real measurements with this model: how close each stored text is to the question "revenue", where 1.0 would mean the same meaning.
| Stored element | Closeness to "revenue" |
|---|---|
orders.net_amt with the business name "Net revenue" | 0.48 |
orders.ship_cost | 0.26 |
orders.net_amt with the comment "net amount" | 0.22 |
orders.net_amt, bare | 0.17 |
orders.order_date | 0.04 |
The lesson is a limitation worth knowing. The bare net_amt loses to an unrelated shipping cost, because the model has never seen the abbreviation. A comment saying "net amount" barely helps. The business name "Net revenue" makes it the clear winner. The model knows language, not your abbreviations.
That is what the naming step is for. suggest_semantic_names finds cryptic and abbreviated identifiers and proposes business names, apply_semantic_names writes them into the ontology, and the applied names are indexed as their own entries. The one-call retrieval described below searches them together with the tables and columns, resolves each matching name to its table or column, and ranks it with the ordinary hits. A result found this way says which business name matched. Before the name, "revenue" ranked ship_cost first; with net_amt named "Net revenue", orders.net_amt comes first. A name that fits tables in two schemas is left out rather than guessed. The same tool family lets the model index other business vocabulary ("profit", "churn") for a table or column with add_semantic_context.
Questions in another language
all-MiniLM-L6-v2 is trained on English. A German question such as "Umsatz" scores 0.06 against the English name "Net revenue": no match. For users who ask in a different language than the schema is named in, set GRAPHRAG_EMBEDDING_MODEL=multilingual. That selects paraphrase-multilingual-MiniLM-L12-v2 (Apache-2.0), which puts more than 50 languages into one space of 384 numbers. With it, "Umsatz" scores 0.40 against "Net revenue" and ranks it first.
It runs the same way as the default model, on the CPU through the ONNX runtime, in an 8-bit version of 118 MB. The Docker image contains both models, so either works without network access. Outside Docker it is downloaded once from a pinned revision, and each file is checked against a pinned SHA-256 checksum. If it cannot be loaded, the server warns and uses the default model. Within one language the two rank alike; the default is smaller and slightly sharper on English.
How the graph connects
The second half is a graph: tables are nodes, keys are edges. Edges come from three sources, ranked in this order:
| Source | Scope | Marked in the join path |
|---|---|---|
| Foreign keys the database declares | Every session on the connection | No extra fields |
Relationships inferred from column names, such as customer_id to customers (high or medium confidence) | Every session on the connection | source: inferred, with the confidence |
Relationships in an ontology a user loaded with load_my_ontology | That user's session only | source: ontology |
A declared key always outranks the other two between the same pair of tables. Lakehouse layers often declare no keys at all, and join paths still work there; Join Discovery Without Declared Keys covers the three ways to give OBA the joins.
On that graph, OBA computes:
- The shortest join path between two tables, up to 12 hops, including paths where the key arrows point in different directions (
graphrag_find_join_path). Each join on the path carries its data check verdict if it has one; a join thatvalidate_relationshipfound refuted, partial or non-unique comes with a warning. - Ambiguity, said out loud. When two routes are equally short, the result says so and lists the alternatives with their joins. An order reaching a region through its customer or through its warehouse are two different questions, and the model is told not to pick one silently.
- Fan-trap risks. Joins that would repeat rows of a measure are flagged before any SQL is written. OBQC then checks the finished query and blocks the ones that would inflate totals.
- Grain closures.
reachable_fromlists the tables that can serve as dimensions for a given grain (the many-to-one direction),measurable_fromthe tables whose measures can be aggregated to it (one-to-many), andplan_composite_queryproposes a fan-trap-safe decomposition of a multi-fact question into separately aggregated parts combined withUNION ALL.
Large schemas are also clustered into communities of related tables, with central tables and suggested domain names, so a model or a person can get a map of an unfamiliar layer before asking anything.
One call per question
graphrag_query_context runs the whole flow in a single tool call:
- Turn the question into a vector, once.
- Find the nearest tables and columns, and the business names that match.
- Add the directly neighbouring tables.
- Compute the join paths between the hits from the graph.
- Flag fan-trap risks along those paths.
- Return only that, with a token estimate.
question
-> vector search: the relevant tables and columns
-> graph search: joins and risks
-> compact context for the model
The model writes its SQL from this context instead of from the full schema, and the joins it uses are grounded in keys rather than guessed from names. Database views are indexed too and are found by graphrag_search, which is often the best answer to a business question, since a view is SQL an analyst already wrote and checked.
Related OrionBelt® Concepts
- OrionBelt® Analytics: the MCP server GraphRAG is part of, with architecture, tools and setup.
- Join Discovery Without Declared Keys: inferred joins, your own ontology, and informational constraints.
- OBQC: Ontology-Based Query Check: the deterministic check every query passes before it runs.
- Governed Text-to-SQL: where retrieval, validation and execution fit together.
Frequently Asked Questions
What does GraphRAG mean in OrionBelt® Analytics?
Retrieval that uses two searches. Vector search finds the tables and columns whose meaning is close to the question. Graph search walks the keys between them to find how they join. The model receives only that subset of the schema, with its join paths and fan-trap risks.
Does schema data leave the server for embedding?
No. The embedding model, all-MiniLM-L6-v2 by default or a multilingual model if you choose it, runs inside the OBA server on the CPU through the ONNX runtime. The Docker image ships with both, so no internet access, external API or API key is needed.
Why do business names matter for vector search?
The model knows language, not your abbreviations. A bare net_amt scores lower against "revenue" than an unrelated ship_cost; with the business name "Net revenue" it becomes the clear best match. suggest_semantic_names and apply_semantic_names add those names, and every retrieval ranks them together with the schema's own names.
Can users ask in German when the schema is in English?
Yes, with GRAPHRAG_EMBEDDING_MODEL=multilingual. The default model is English-only: "Umsatz" scores 0.06 against "Net revenue". The multilingual model scores it 0.40 and ranks it first.
Does it work when the database declares no foreign keys?
Yes. The graph also holds relationships inferred from column names, marked as inferred with a confidence, and, for one user's session, relationships from an ontology that user loaded. A declared key always wins. See Join Discovery Without Declared Keys.
What happens when two join paths are equally short?
graphrag_find_join_path reports the path as ambiguous and lists the alternatives with their joins, so the model asks or chooses deliberately instead of silently.
Can it run in an air-gapped network?
Yes. The Docker image already contains both embedding models. A keyword-only TF-IDF backend is also available, but it matches words rather than meaning, so questions phrased in business terms often find nothing useful.