JournalHow a property knowledge graph makes an AI agent answerable
Journal · under the hood

Most Real Estate RAG Retrieves Text. Legal Questions Require Qualified Facts.

A retrieval layer that returns text cannot separate a statute from a guess. 173 rejuvenated entries, 10 unsourced figures, and the payload schema that fixes it.

Published 2026-10-259 min readSources verified 2026-09-13

A retrieval system that ingests a legal corpus as flat text has already erased the only distinction that matters. Not the wording. Not the ranking. Whether the sentence was read in the statute or reported by a law firm.

Both survive chunking. Both get embedded. Both come back at query time with comparable similarity scores and identical tone. The model cannot tell them apart, because by the time it sees them the difference no longer exists in the payload. It was not lost in transit. It was deleted at ingestion.

For a question about facade materials, that is tolerable. For a question about withholding tax on rent paid to a non-resident, it is a liability with a number attached.

What Breaks When a Corpus Is Refreshed Instead of Re-Verified?

Recall powers Reflexion, a property intelligence platform running on a Thai real estate corpus. During the knowledge-base remediation documented in the ingestion specification dated 2026-09-13, two categories of defect were measured directly.

173 entries carried artificially rejuvenated dates. A timestamp had been advanced without the underlying fact being re-checked against its source. Every one of those entries presented as current. None had been re-verified.

10 FAQ entries carried 2026 figures with no source behind them at all. Not a wrong source. No source.

Neither of these is an editorial problem, and treating them as one is the first mistake. An editorial defect is a sentence that reads badly. A structural defect is a schema that allows a fact to exist without the fields that would have made the defect visible. If the payload has no cutoff_date and no source_primary flag, nothing in the pipeline can object when a date moves or a figure appears from nowhere. The corpus accepts it because the corpus has no opinion.

The consequence is the expensive kind of failure. An agent built on an unqualified corpus does not throw an error, return empty, or degrade visibly. It answers. It answers fluently, in the same register, with the same confidence, whether the supporting fragment is a statute article with a Royal Gazette date or a practitioner's reading that two firms contradict. Loud failure costs an incident. Silent failure costs a decision that nobody flags for months.

Qualified Chunks: The Payload That Makes an Answer Auditable

The fix is to stop ingesting text and start ingesting facts that carry their own status. Every point in the vector store takes this payload. The first six fields are mandatory: without them, the fragment is not ingested.

{
  "fact_id": "C2.4",
  "source_pack": "factpack-juridique-fiscal",
  "confidence_level": "verified | partial | unverified",
  "cutoff_date": "2026-09-13",
  "source_primary": true,
  "source_url": "https://www.impots.gouv.fr/...",

  "legal_validation_required": true,
  "validation_domain": "fiscalite_francaise",
  "jurisdiction": ["FR", "TH"],
  "legal_reference": "Convention France-Thailande 27/12/1974, art. 23 §1 a)",
  "rule_status": "in_force | draft | repealed | contested",
  "in_force_since": "1975-08-29",
  "supersedes": null,
  "contested_by": ["Formichella & Sritawat 2026"],
  "publishable": true,
  "figure_unit": "THB | EUR | % | m2 | ug/m3",
  "figure_period": "S1 2026"
}

Metadata that only describes is decoration. These fields are read at answer time and they constrain what the agent is permitted to say.

Payload stateWhat the agent may do
confidence_level: verifiedServed normally, with legal_reference or source_url cited
confidence_level: partialServed only with the divergence named and a referral to a professional. Cannot be presented as established
confidence_level: unverifiedNever reaches an end user. Internal monitoring and gap detection only
legal_validation_required: trueCite the text and its date, direct to a lawyer or tax adviser. No conclusive answer
rule_status: draftThe response opens with "not adopted" before any other element
source_primary: falseCan never be the sole support for a figure. Requires a concordant primary fragment
cutoff_date past 180 daysAutomatically downgraded to partial and flagged for re-verification

Read the second and sixth rows together. A fragment marked partial because two firms give different withholding rates and neither cites an article does not become usable by being retrieved three times. A secondary source reporting a figure does not become primary by being confidently worded. The constraint is on the output, not on the tone.

The same discipline governs the graph side of the system. An edge carrying confidence != verified forces the agent to expose the uncertainty in its response, and no edge is created without a legal_reference or a source_url. That is the design principle behind the property ontology and knowledge graph: a rule encoded as a hard triplet is a rule the agent will state without hedging, so anything unsettled has to be encoded as unsettled.

Why Does a Knowledge Base That Ages in Silence Cost More Than an Empty One?

Because it keeps answering.

An empty knowledge base fails in a way the operator sees on the first query. An aging one returns a tax threshold from eleven months ago with the same latency and the same phrasing it used when the threshold was correct. Nothing in the system is aware that time has passed, because nothing in the system was instructed to care.

The rule is mechanical: any fragment whose cutoff_date is more than 180 days old is automatically downgraded to partial and flagged for re-verification. It does not wait for a reviewer to notice. It does not require the fact to be wrong. A tax rule six months past its last verification is not a rule any more — it is a record of what was true once.

Three refresh cadences carry the re-verification back up: monthly for macro and market figures, quarterly for transaction costs, schedules and thresholds, and event-driven for anything whose adoption is pending in the Royal Gazette or the Journal Officiel. The event-driven tier exists because draft provisions are exactly the material an unqualified corpus reports as law.

Isolation by Collection, Not by Filter

Measured at 2026-07-27, the vector index holds four collections.

CollectionPointsContent
kb_public_v17,438Chunks of entries classified public, only
kb_internal_v11,711Internal corpus
projects_v16,659Project vectors
units_v114,730Deliberate premium subset

Two of these are design decisions, and both are worth stating plainly because both are commonly done the other way.

Public and internal content are separated at collection level, not by query filter. A filter is a runtime condition. It lives in code, it is constructed in more than one place, and it is one refactor, one default argument or one cache key away from being absent. A collection boundary is not a condition — there is no query against kb_public_v1 that can return an internal fragment, regardless of what the filter says. The cost is a second index to maintain. The alternative is a leak whose blast radius is the entire internal corpus.

units_v1 holds 14,730 points, not the full unit inventory. At the same measurement date the A-Box held 83,707 units. The vector collection is a premium subset by design, because semantic retrieval over an undifferentiated inventory returns plausible neighbours rather than relevant ones. Vectorising everything is not a completeness gain, it is a precision loss.

And the detail that belongs in the same paragraph rather than a footnote: projects_v1 holds 6,659 of 6,660 projects at 2026-07-27. One project is unvectorised. The gap is known, tracked, and stated — an architecture that can only report round numbers is an architecture that is not measuring itself.

                          ingestion
                              │
              ┌───────────────▼───────────────┐
              │      QUALIFICATION GATE       │
              │  fact_id · source_pack        │  six mandatory fields
              │  confidence_level             │  missing one → rejected,
              │  cutoff_date · source_primary │  not ingested unqualified
              │  source_url                   │
              └───────────────┬───────────────┘
                              │
      ┌──────────────┬────────┴───────┬──────────────┐
      ▼              ▼                ▼              ▼
 kb_public_v1   kb_internal_v1    projects_v1    units_v1
    7,438           1,711            6,659         14,730
  public only    never joined     6,659/6,660    premium subset
                 at query time     (1 tracked)    (not 83,707)
      │
      ▼
  retrieval ──►  ENFORCEMENT  ──►  answer
                 unverified   → dropped before the user
                 partial      → divergence named, referral
                 draft        → "not adopted" stated first
                 non-primary  → requires primary match
                 >180 days    → downgraded, flagged

  collection counts measured 2026-07-27

1,250 Files, 1,128 Documents: Discrepancy or Bug?

The knowledge corpus holds 1,250 files on disk and produces 1,128 documents in the graph. The gap is 122.

That is deduplication on a hash of the document body, with frontmatter excluded from the hash. Two files whose metadata differs but whose content is identical resolve to one document. Intended behaviour.

The wider point is the one operators tend to skip. A number that does not match another number is a discrepancy, and a discrepancy is only a bug once you have failed to explain it. Systems that are audited produce discrepancies constantly: derived indexes lag sources, dedup collapses rows, subsets are smaller than supersets by construction. The engineering obligation is not to eliminate the gaps. It is to be able to name the mechanism behind each one, in a sentence, on demand. A team that cannot do that will eventually either chase a phantom defect or dismiss a real one. The same reasoning applies to score movement in the auditable property scoring model, where a change in output is not by itself evidence that the calculation changed.

What the Retrieval Stack Is Designed to Do

This section is deliberately written in the descriptive and the conditional, and the reason is worth stating.

The architecture is specified around a multilingual embedding model, BGE-M3, for the knowledge base. The model observed in the intelligence pipeline code is bge-large-en-v1.5, served through a local Ollama instance at 1,024 dimensions, over chunks of 256 to 512 tokens. Reranking is a stated architectural requirement — the specification calls for the best available reranking model — and no reranker was found in the code at audit. Recall does not describe either configuration as a production state, and does not claim a reranker is running or that the knowledge base is multilingually embedded, because that gap is open and unresolved as of 2026-09-13.

What can be stated without qualification is the general engineering point underneath it, because it is true of the technique regardless of any one system: an English-only embedding model applied to a corpus spanning several languages degrades recall on the non-English portions, and it degrades it silently. Nothing errors. The index returns results, the latency is unchanged, and the answers are fluent. What disappears is the fragments the query never surfaced — and a fragment that was never retrieved leaves no trace in any log. Recall measured per language is the only way to see it. Recall measured overall is dominated by whichever language the model was trained on.

Publishing an open architecture means publishing the parts that are settled and marking the parts that are not. The alternative is to describe an intention in the present tense, which is the same defect as a rejuvenated cutoff_date, applied to a company instead of a corpus.

Five Checks to Run on Your Own Retrieval Stack

  1. Trace ten answers back to fields. Take ten figures your agent has produced and ask, for each, which payload field supplied the source and which supplied the date. If the answer cannot name a source_url and a cutoff_date, your layer retrieves text, not facts.
  2. Plot the age distribution of your corpus. Count fragments whose last verification is more than 180 days old. If that count is zero and your corpus is older than six months, the decay rule is not running — it is not that your data is fresh.
  3. Count the code paths that build your isolation filter. If public and restricted content share a collection, every one of those paths is a leak surface. Separation at collection level removes the question.
  4. Measure recall per language, not in aggregate. Compare your embedding model's training languages against the actual language distribution of your corpus, then build a small ground-truth set per language and score them separately.
  5. Reconcile every count you publish. Files on disk against documents indexed, source rows against vectors, subset against superset. Explain each delta in one sentence before anyone calls it a defect.

Run those five and the answer you get is not a score. It is a list of the places where your system currently cannot tell a statute from a guess. Recall's full data governance and measurement approach is published in the methodology, and the market work that runs on top of this layer is in market intelligence.