Laws have versions, and most RAG pretends they don't
In short: a section of a statute is not one text, it's a series of texts, each in force for a stretch of time. If your retrieval only knows the current one, it will answer a question about 2021 with…
- published
- read time
- 5 min
- words
- 934
- lang
- en
- filed under
- Engineering
In short: a section of a statute is not one text, it's a series of texts, each in force for a stretch of time. If your retrieval only knows the current one, it will answer a question about 2021 with the law of today, and it will cite it with a straight face.
The question behind the question
Most legal questions are about a day. Someone was let go two years ago and the claim is being written now. A contract was signed before an amendment and the dispute started after it. The lawyer asking doesn't want "what does section 12 say". They want "what did section 12 say on the day it mattered".
People who practise do this without thinking. A retrieval pipeline doesn't. It knows whatever text was scraped, and that is almost always the latest consolidation. In the legal retrieval work I've been doing, this was one of the first things I had to fix, and the fix turned out to be mostly a data model, not a model.
How a normal pipeline gets it wrong
The usual ingestion loop looks harmless:
- Scrape the current consolidated text of each act.
- Split it into chunks, usually by section.
- Embed the chunks and write them to the vector store.
- Next month, do it again and overwrite.
Step four is where the history goes. The old text of a section is gone, so any question about the past gets today's wording. The second way to fail is quieter. Someone notices the overwrite, stops doing it, and now the index holds two versions of the same section with no dates on them. The retriever picks whichever one happens to sit closer to the wording of the question. Sometimes that's the right one. You can't tell which times.
Both failures look fine in a demo, because demo questions are about the present.
Store versions, not documents
The change is to make every chunk carry the window it was law for. Each row in my index has the instrument, the provision (for example section 12, subsection 3), a version number, an in-force-from date and an in-force-to date. The current version has an empty end date. When a new consolidation arrives and a section has changed, ingestion closes the old row by filling its end date, and writes the new text as a new row. Nothing is overwritten.
Here's what that looks like for one made-up section. The dates and changes are illustrative, not from a real act.
| Version | In force from | In force to | What changed |
|---|---|---|---|
| 1 | 2015-01-01 | 2019-06-30 | Original text |
| 2 | 2019-07-01 | 2023-02-28 | Notice period changed |
| 3 | 2023-03-01 | (current) | New subsection added |
Detecting "this section changed" is the boring part, and it's worth doing carefully. I compare normalised text, not raw HTML, because a publisher changing a stylesheet is not an amendment.
Filter by date before you rank
Once the rows have windows, retrieval takes one extra input: the date the question is about. The filter goes in the same query as the similarity search, so the ranking never sees text that wasn't in force on that day.
SELECT provision_id, version, body
FROM chunks
WHERE instrument_id = :instrument
AND valid_from <= :as_of
AND (valid_to IS NULL OR valid_to >= :as_of)
ORDER BY embedding <=> :query_vec
LIMIT 10;
Where does as_of come from? Three places, in this order:
- The user sets it. A date field next to the search box is not glamorous and it works.
- The question states it. "In 2021, could an employer..." has a date in it, and a small extraction step can pull it out.
- Neither. Then it defaults to today, and the answer says so out loud: "as in force today".
The date also goes into the citation. "Section 12, as in force on 4 May 2021" is a citation a lawyer can check. "Section 12" on its own invites them to check the wrong text.
The cases this doesn't solve
Versioned rows get you most of the way. Some things still need a human, and I'd rather show them than hide them:
- Coming into force by order. An amendment can be passed on one day and take effect on another, sometimes section by section. The consolidation date is not the in-force date.
- Transitional provisions. The new rule may apply only to matters started after a date, so the event date and the filing date can point at different versions.
- Case law about the old text. A decision interpreting version 1 is still cited while version 3 is in force. Cases need the version they interpreted, not just a link to the section.
- Repeal. A repealed section still matters for old events. It needs an end date, not a delete.
When the as_of date falls close to the edge of a window, I flag it in the answer rather than pick a side quietly. Near a boundary, "this changed around then, check which applies" is the useful sentence.
Check your own index this afternoon
You don't need to rebuild anything to find out if you have this problem.
- Pick three provisions you know were amended in the last few years.
- For each, write a question about a date before the amendment.
- Run them through your pipeline and read which text comes back.
If it's the current text every time, add two date columns, stop overwriting on re-ingest, and put the date in the citation. That's a day of work, and it's the difference between a search tool and something a lawyer can rely on for an old file.
related