A simple semantic layer a retrieval agent can call

An agent that answers questions about a company’s numbers has to return a figure it was allowed to say, taken from a definition someone signed, and show where that figure came from. The failures are concrete when that constraint is missing. The model invents a metric name. It averages two rows because the question said “on average.” It presents one breakdown as if it were a slice of another, because the words lined up.

A semantic layer is how those failures get closed. The term already covers four jobs. A metrics layer answers whether every tool computes revenue the same way. A catalog answers who owns a dataset, and whether this caller may see it. An ontology answers what a region is, and which other terms mean the same thing. A knowledge graph answers what an order is connected to. Product pages treat these as neighbors. An architecture review stacks them into one platform and then puts the agent on the front.

Drawn in full, the layer is an intricate stack rather than a single box.

Common semantic layer architecture

At request time the agent needs three calls: resolve the words to a governed concept, read that concept’s metric definition, including the query it is legal to ask, and run the query on the certified table. The rest of this article is those three calls when the stack behind them is a compiled bundle, rather than a catalog, a triplestore, or a metric engine invoked on the question.

Overview of data retrieval agents

Rather than query the warehouse directly, the agent calls tools, in order, and it may only state what those tools returned.

The question arrives as a sentence: “Which regions had the worst on-time delivery this year and last?” At that point the model does not have the machine name, the table, or the filters the definition always applies. Three calls close that gap.

Data retrieval flow

Tool 1 resolves the business terms. resolve_business_request takes the sentence and returns a short menu of governed concepts: a URI, a label, a definition, and, when a published metric measures that concept, the metric’s machine name. For this question the name that matters is region_on_time_rate. Draft metrics stay off the menu, so the model is not handed a name the next tool would reject.

Tool 2 fetches the certified definition. get_metric_definition takes that machine name and nothing else. There is no search on this call. The response is the registry entry: description, formula, unit, version, and a query block. The block is what the model needs next. It records whether the metric is computed or listed as rows, which columns may be grouped, which sorts are legal, and which filters the definition applies on every query. For on-time delivery the measure is a count of deliveries, the dimensions include region and year, and the required filters keep only completed shipments. The model does not repeat those filters, and it cannot drop them.

Tool 3 runs the query. query_certified_metric takes the metric name, a grouping, a sort, and a limit. It does not take a table name. The server looks the table up from the same compiled definition, writes the SQL, adds the required filters, and refuses a dimension the registry never allowed. A ratio, such as on-time rate as on-time deliveries divided by all deliveries, is a single call. The model does not fetch both sides and divide them in the prompt.

What comes back is rows: the dimension values, the value, and a count. The model copies the value at the precision shown, attaches the unit from Tool 2, and cites the source and version. A later check withholds an answer that contains a figure no tool returned.

Choosing the simpler path

Every layer in that intricate architecture exists as a full-fledged product: Cube handles the metric layer, DataHub or OpenMetadata serve as the catalog, Fuseki or Stardog power the ontology, Neo4j manages the relationships, and MetricFlow orchestrates the joins. Each is a standalone service with its own definition of a metric and its own operational footprint to maintain.

Rather than employ heavyweight catalog, semantic, or ontology products at query time, we chose a single compiled bundle as the heart of the semantic layer. The workflow is streamlined into three calls: resolve business terms, fetch the certified definition, and run the query. All the logic, rules, and metadata needed for these steps are pre-baked into this bundle. No external product is invoked as part of serving a user request.

A bundler as simpler semantic layer

Definitions are still authored in human-readable files. What differs is that these files are not consulted at runtime. Instead, a compiler reconciles all ontology modules and per-metric registry entries, validating integrity: it will fail the build if a metric refers to a nonexistent concept, to an invalid column, or if a name has silently vanished without deprecation. This yields a fully versioned, immutable JSON bundle—versioned by both content hash and date—ensuring consistency between code, metrics, and ontology. Deploys simply update a pointer to the latest bundle; services poll this pointer and reload within about ten seconds, automatically adopting new or rolled-back versions without needing a restart. Only the latest five bundles are retained for immediate rollback.

The compiled bundle encapsulates the critical logic and metadata in-memory, completely eliminating runtime dependency on external tools or product APIs. This approach achieves the reliability, speed, and integrity that the more complex solutions promise—without their operational overhead.

Both the tool server and the query service operate from the same compiled bundle, held in memory as a unified source of truth at runtime.

  • Tool 1 performs concept resolution by searching an index pre-built when the bundle loads. It uses BM25 over the labels, synonyms, and definitions—giving double weight for label matches. Rare words contribute more, and relevance increases with the coverage of the question’s terms. Common function words are ignored, so spurious matches on words like “the” don’t inflate results simply because of their rarity in a small ontology.
  • If BM25 lexical ranking does not sufficiently distinguish among candidates—particularly when terms are shared across domains—the top BM25 matches are then reranked using pretrained sentence embeddings. Each concept’s label, synonyms, and definition are encoded up front (with all-MiniLM-L6-v2), producing a normalized vector matrix, which is cached keyed by the bundle’s hash. At bundle load, this embedding process takes about ten seconds at the 5,000-concept scale, but happens only once per bundle load. For each question, only those concepts shortlisted by BM25 are compared—using cosine similarity (dot product) to the encoded query. The final ordering blends BM25 and embedding ranks via reciprocal rank fusion (1/(60 + rank) from each list). The output still exposes match_score, which simply counts overlapping terms.
  • Tool 2 becomes a fast dictionary lookup: given a metric name, it retrieves the corresponding definition directly.
  • Tool 3 reads the compute block for the requested metric from its own loaded copy of the bundle. It checks the requested arguments against the allowed aggregation, measures, dimensions, and required filters—ensuring only valid queries are constructed. The corresponding SQL is assembled within the same service context (on Trino for development, Athena in production), with the table details managed internally and never exposed directly to the model.

Critically, this design reduced the number of concepts returned in Tool 1 to 8, as the most relevant matches already appeared at the top. This eliminates spurious single-term hits and cuts wasted model processing time for every request.

Lessons

Definitions are still authored in human-readable files, and the request serves a compiled bundle.

Ontology modules and per-metric registry entries remain the place a person edits a metric. What differs is that these files are not consulted at runtime. Instead, a compiler reconciles all ontology modules and per-metric registry entries, validating integrity: it will fail the build if a metric refers to a nonexistent concept, to an invalid column, or if a name has silently vanished without deprecation. This yields a fully versioned, immutable JSON bundle—versioned by both content hash and date—ensuring the tool server and the query service load the same artifact. Deploys simply update a pointer to the latest bundle; services poll this pointer and reload within about ten seconds, automatically adopting new or rolled-back versions without needing a restart. Only the latest five bundles are retained for immediate rollback.

Resolve, define, and query are lookups into that same bundle.

Tool 1 performs concept resolution by searching an index pre-built when the bundle loads. It uses BM25 over the labels, synonyms, and definitions—giving double weight for label matches. Rare words contribute more, and relevance increases with the coverage of the question’s terms. Common function words are ignored, so spurious matches on words like “the” don’t inflate results simply because of their rarity in a small ontology. If BM25 lexical ranking does not sufficiently distinguish among candidates—particularly when terms are shared across domains—the top BM25 matches are then reranked using pretrained sentence embeddings. Each concept’s label, synonyms, and definition are encoded up front (with all-MiniLM-L6-v2), producing a normalized vector matrix, which is cached keyed by the bundle’s hash.

At bundle load, this embedding process takes about ten seconds at the 5,000-concept scale, but happens only once per bundle load.

For each question, only those concepts shortlisted by BM25 are compared—using cosine similarity (dot product) to the encoded query. The final ordering blends BM25 and embedding ranks via reciprocal rank fusion (1/(60 + rank) from each list). The output still exposes match_score, which simply counts overlapping terms.

Tool 2 becomes a fast dictionary lookup: given a metric name, it retrieves the corresponding definition directly.

Tool 3 reads the compute block for the requested metric from its own loaded copy of the bundle. It checks the requested arguments against the allowed aggregation, measures, dimensions, and required filters—ensuring only valid queries are constructed. The corresponding SQL is assembled within the same service context (on Trino for development, Athena in production), with the table details managed internally and never exposed directly to the model.

This design returns eight concepts from Tool 1, as the most relevant matches already appear at the top.

The longer menu was single-term noise, and the model reread those concepts on the definition call and again on the query. Reducing the list to 8 eliminates spurious single-term hits and cuts wasted model processing time for every request, while the metric the question needed stays on the menu.

Conclusion

There is a practical way to implement a semantic layer, and it carries less complexity than the full drawing.

The bundle does not remove the work of authorship. Definitions are still authored in human-readable files: ontology modules for the concepts, and a registry entry for each metric’s formula, dimensions, and required filters.

The simpler path still serves the purpose a semantic layer is asked to serve. The agent returns a figure it was allowed to say, taken from a definition someone signed, and shows where that figure came from. Tool 1 resolves the words to a governed concept and a published metric name. Tool 2 returns the formula, the unit, and the query it is legal to ask. Tool 3 runs that query on the certified table, adds the filters the definition always applies, and refuses a dimension the registry never allowed. The model may only state what those tools returned.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.