-
Notifications
You must be signed in to change notification settings - Fork 0
Add validity to the document #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
5217d8b
feat: add an approver-set validity window to the network catalog [#111]
shruti0025 e2dcff1
feat: filter search results by document validity period [#111]
shruti0025 39dc95a
feat: remove the network catalog validity window [#111]
shruti0025 52595c0
refactor: move the validity default out of the db layer [#111]
shruti0025 02e48bd
fix: zero-pad dates so validity parses on glibc [#111]
shruti0025 172b012
fix: rename-adr-file [#111]
shruti0025 882a66e
fix: remove-dead-code-method-get-validity [#111]
shruti0025 3706d7a
fix: fix-tests [#111]
shruti0025 a79fa4a
fix: use-valid-dates [#111]
shruti0025 fd07bcb
fix: add-tests-for-format-variations [#111]
shruti0025 d49db83
fix: disable-ui-inputs-and-change-defaults [#111]
shruti0025 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| # Document validity filters search | ||
|
|
||
| Context: a chunk, once ingested, answered searches forever. Agricultural content does not work that way — a rabi sowing advisory is wrong advice in June, and a scheme circular superseded last year is worse than no answer. Nothing in the pipeline could express "this stopped being true", so the only way to retire content was to disable the whole document, losing it from the operator console too. | ||
|
|
||
| A short-lived predecessor (ADR 0005, since removed along with its code) put a *catalog* validity window on the Network Catalog Envelope at the prod gate. That window was kind-level and about whether this provider still claims to serve a knowledge kind at all; it never spoke to whether a given document's content was current, which is what this ADR is about. | ||
|
|
||
| Decision: | ||
| - **A document-level concept, owned in one place.** `documents.valid_from` / `documents.valid_to`, stamped onto every chunk's Qdrant payload as `start_date` / `end_date`, owned by a pure module `pipeline/document_validity.py` that holds the rules so the API, the ingest activity and the vector store never re-derive them. | ||
| - **Defaulted on upload, not asked for.** The upload endpoints stamp the upload day as the start, with no end. An uploader is not asked a question they cannot answer, and no document reaches the index without a period. An end nobody chose would retire content on a date nobody meant, so the default expires never and narrowing it is a deliberate act. The default is computed in `api.py` and passed down: `db.upsert_document` stores the dates verbatim on INSERT and has no opinion on what they should be, keeping `db.py` free of domain knowledge as the rest of that layer is. A row created around the upload path (a script, a backfill) therefore has no period, which search reads as "no stated period" — the same as the pre-validity corpus. | ||
| - **Confirmed or moved by the reviewer, in the form that already exists.** The dates sit beside the Knowledge Kind selector and travel on the same `PATCH /documents/{id}/scheme-metadata`. Choosing the period is part of classifying a document, not a separate errand, and the reviewer sees real prefilled dates rather than an empty field. Either end may be sent alone; the other is taken from what is stored, so moving the end date does not require restating the start. | ||
| - **Validated like a bad kind, not like a bad type.** `document_validity.parse_period` raises, `apply_scheme_metadata` lets it through as a `ValueError`, and the endpoint answers 400 — the same shape a bad `document_kind` gets, not a Pydantic 422. A rejected period stores nothing, kind included. | ||
| - **Both ends inclusive.** A document uploaded this morning starts today and must answer this afternoon; an end date names the last day it answers rather than the first day it does not. The requirement was written as "today greater than start and less than end", but exclusive bounds would hide a document on its own first day, which is the default every document gets. | ||
| - **Search filters on it by default**, as `(start_date missing OR start_date <= today) AND (end_date missing OR end_date >= today)`. Per-end rather than one window clause, so a partially-dated point behaves sensibly instead of vanishing. | ||
| - **Undated chunks stay searchable.** The missing-field branches are the whole reason the rule is shaped this way: every point already in Qdrant carries no dates, and a change that silently emptied the live index would rightly be called a regression. Such a document picks up a period the next time it is ingested — anchored on its own upload day, not on today, so reingesting a two-year-old document does not quietly extend its life by another year. | ||
| - **The clock is injected.** `QdrantVectorStore(clock=...)` decides what "today" means, defaulting to the real clock. Tests pin a day instead of writing fixtures relative to whenever they run, and `POST /search` exposes `valid_on` (answer as of another day) and `include_expired` (drop the filter) so an operator can inspect what an expired document still holds. Both default to the safe behaviour: a caller that asks for nothing gets today's valid documents only. | ||
| - **Document-scoped reads ignore validity.** `list_by_doc_id`, `delete_by_doc_id` and `delete_chunk` pass no date: an expired chunk is still that document's chunk, and a purge that skipped expired chunks would orphan them in the index. | ||
| - **Indexed as Qdrant `DATETIME`**, created idempotently on every `ensure_collection` including one that already exists — a collection created before the field existed has no index for it, and ingest is the one path that reliably runs against every live collection. The filter was verified to work unindexed too, so a collection still being backfilled filters correctly, just more slowly. | ||
|
|
||
| Known limitation, accepted: expiry is enforced at **read** time, not by a sweeper. An expired document's vectors stay in Qdrant, still costing storage and still visible to anything that queries Qdrant directly without this filter (including `include_expired`). That is the right trade for now — deleting on expiry would make un-expiring a document a reingest — but a caller bypassing `/search` is not protected. | ||
|
|
||
| Alternatives considered: | ||
| - **Reuse the catalog validity window from ADR 0005.** Rejected at the time, and moot since that feature was removed: those dates defaulted to today/today and carried a kind-level meaning on the wire, so reusing them would have expired every document the day it was approved. | ||
| - **Collect the dates at the prod gate.** Rejected: DEV search is filtered too, and the prod gate is reached long after ingestion — a document would be searchable with no period for most of its life. | ||
| - **Store epoch integers instead of `YYYY-MM-DD` strings.** Rejected: Qdrant's `DatetimeRange` accepts date-only strings (verified against a live instance), and an operator reading a chunk payload can see `2027-03-31` rather than decoding a number. | ||
| - **Filter after retrieval, in the API.** Rejected: it silently shrinks the candidate set below `top_k`, so an index full of expired documents would return a short page of results rather than the valid ones further down. | ||
| - **Ask the uploader for the dates.** Rejected: the uploader is often not the person who knows how long content holds, and blocking upload on it would slow the common case for a value the reviewer sets anyway. | ||
| - **Default to no end date (never expires).** Rejected: it makes expiry opt-in, so content would go stale by default — the same failure this ADR exists to fix. | ||
|
|
||
| ## Amendment — open-ended by default, and gated in the UI (2026-09-25) | ||
|
|
||
| The original decision gave every upload a one-year end date. Business revised that: a document should stay answerable until someone decides otherwise, so the default is now **start at the upload day, no end**. `end_date is None` means the document never expires, and the search filter already read a missing end that way — the `(end_date missing OR end_date >= today)` clause was written for the pre-validity corpus and covers this unchanged, which is why no filter logic moved. | ||
|
|
||
| Consequences worth naming: | ||
| - `period_from_row` now treats the **start** as what decides whether a period exists. A row with a start and a NULL end rebuilds as an open-ended period rather than as "no period", which is the shape every upload now stores. | ||
| - A chunk with no expiry carries **no** `end_date` key in its Qdrant payload rather than a null one, so the missing-field branch matches. | ||
| - `add_years` and `DEFAULT_VALIDITY_YEARS` existed only to derive the old default end. Nothing derives one any more, so they were removed rather than left as dead code. | ||
| - Clearing an end date is how a reviewer returns a document to never-expiring: a blank end is a real answer, not a missing one. | ||
|
|
||
| The reviewer-facing date fields sit behind `VITE_DOCUMENT_VALIDITY_ENABLED` (default off), following the `VITE_AUTH_ENABLED` pattern. The flag gates **only the entry fields** — the backend stamps a period on every upload and search filters on it regardless — so enabling it exposes an existing capability rather than switching one on. With it off, classifying a document sends no dates at all and leaves the stored period untouched. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.