Metadata & filters
Оваа содржина сè уште не е достапна на вашиот јазик.
Tag your documents, then narrow a search to the ones that matter.
A collection can declare typed fields — a department, an effective date, a set of tags — and every document in it can carry those, plus any extra labels you invent on the spot. Agents can filter on the declared ones; you can filter on both.
Two halves, on purpose
Section titled “Two halves, on purpose”Declared fields are the vocabulary. They are typed and validated, they
appear in dropdowns and facet counts, they support ranges and set
operators, and — this is the part that matters for agents — an assistant’s
search tool is told they exist. A model cannot filter on a field it has
never heard of.
Extras are anything else: a plain key and a string value, accepted with no ceremony. Use them when you want to label something now and decide whether it deserves a schema later. Extras are stored, shown and filterable by exact match, but they are not advertised to agents and they do not support ranges.
The short version: declare a field when an agent should be able to filter on it; use an extra when it is just a label.
Declaring the schema
Section titled “Declaring the schema”In the console, open the collection’s Settings tab and use Metadata fields: Add field, give it a name (lowercase letters, digits and underscores, starting with a letter), pick a type, and for a One of a list field enter its options one per line. Add a description if the name alone would leave a model guessing — it is shown to the model as guidance, both to agents choosing filters and to auto-metadata proposing values. Nothing is stored until you press Save settings, together with the rest of the tab.
Required means every upload, bulk import and metadata edit must supply a value or it is refused. Uploads from the console’s own upload dialog carry no metadata, so on a collection with a required field they are refused too — use the API or a bulk import with a metadata sidecar. Connector syncs are exempt, and documents already stored are not re-checked.
Once saved, a field’s name and type are fixed. Its description, options and required flag stay editable. Changing the type or the name would not touch the values documents already carry, so a field would end up holding values of the wrong type, and a range filter on it would fail. For a different name or type, add a new field under a new name; documents keep their values under the old one until they are edited.
Removing a field deletes nothing from your documents. They keep their values, which become undeclared extras: agents are no longer told about them, they have no facet counts, and they filter by exact match only. An extra must be text, so a document still holding a number, yes/no or tags value for a removed field cannot have its metadata edited until you delete that value or re-enter it as text. Taking an option away from a One of a list field works the same way: documents that carry it keep it, and their next metadata edit must pick a remaining option. Pending auto-metadata proposals for a removed number, yes/no or tags field can no longer be accepted, since accepting checks the value against the current schema.
Adding the field back under the same name declares those values again, so it must come back with the same type — the console refuses a different one, and re-adding it in the same edit carries its saved description, options and settings over. If Ranking uses the field for recency, choose another recency source there before saving; the console will not save a recency source that is no longer a date field.
The same schema can be set through the API, as a whole list — a PATCH replaces every field, so send the ones you want to keep as well:
curl -s -X PATCH "$KOVAC_BASE/api/manage/collections/$COLLECTION_ID" \ -H "Authorization: Bearer $KOVAC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "metadata_schema": [ {"name": "department", "type": "enum", "options": ["HR", "Finance", "Legal"], "description": "Owning department", "required": true}, {"name": "effective_date", "type": "date"}, {"name": "tags", "type": "tags"} ] }'| Type | Values | Operators beyond equality |
|---|---|---|
string |
any text | — |
enum |
one of options |
in |
number |
a number | gt gte lt lte |
date |
YYYY-MM-DD |
gt gte lt lte |
boolean |
true / false |
— |
tags |
a list of strings | any |
The API also accepts "embed": true on a field, meant to fold its value
into the text that gets embedded. It is stored and kept on every save, but
nothing applies it yet, so the console does not offer it.
Reserved names cannot be declared or set: page, page_end,
section, section_path, order, title, filename, mime_type,
page_count, author, created, modified, language, code_language,
uploaded_by, folders. These come from the document itself or from who you are
signed in as, and anything you set would be overwritten on the next
ingest. language is the natural language a chunk is written in (see
the Collections page); a source-code file’s programming language
is code_language. folders is derived from a document’s path; see
Organise documents in folders.
Tagging documents
Section titled “Tagging documents”At upload, as a JSON field alongside the file:
curl -s -X POST "$KOVAC_BASE/api/manage/collections/$COLLECTION_ID/documents" \ -H "Authorization: Bearer $KOVAC_API_KEY" \ -F "file=@handbook.pdf" \ -F 'metadata={"department": "HR", "effective_date": "2025-04-01", "cost_centre": "CC-12"}'cost_centre was never declared — that is an extra, and it is fine.
Afterwards, on one document or a batch:
curl -s -X PATCH ".../documents/$DOC_ID" -d '{"metadata": {"department": "Finance"}}'curl -s -X PATCH ".../documents:batch" \ -d '{"document_ids": ["...", "..."], "metadata": {"department": "HR"}}'Metadata replaces rather than merges, so send the whole object. Editing metadata never re-embeds anything: the text has not changed, only the labels on it.
Letting a model propose metadata
Section titled “Letting a model propose metadata”Filling declared fields by hand for hundreds of documents is what nobody does. Turn on auto-metadata and a model reads the first ~8,000 tokens of each new or re-parsed document and proposes a value for every declared field that has none:
curl -s -X PATCH "$KOVAC_BASE/api/manage/collections/$COLLECTION_ID" \ -H "Authorization: Bearer $KOVAC_API_KEY" \ -H "Content-Type: application/json" \ -d '{"extract_metadata": true}'Three things are true by construction. A field that already has a value —
set at upload, by a sidecar, by a connector or by hand — is never asked
about, so the model never writes over anything you set. Every proposed
value passes the same validation a hand-set value does, so an enum value
outside its options is never written; a value that fails is dropped and
named in the ingestion attempt’s warnings. And nothing is applied without
you, unless you say so per field with auto_apply — this field’s proposal
is applied as soon as the model is at least 80% sure of it:
{"name": "document_type", "type": "enum", "auto_apply": true, "options": ["policy", "contract", "manual", "invoice", "slide deck", "report", "correspondence", "other"]}Proposals wait on the document (metadata_proposed on GET …/documents/{id}, or the Metadata tab in the UI). Accept or reject
them by name or all at once, and list the documents that still have some:
curl -s -X POST ".../documents/$DOC_ID/metadata-proposals/accept" -d '{"fields": ["department"]}'curl -s -X POST ".../documents/$DOC_ID/metadata-proposals/reject" -d '{"fields": "all"}'curl -s ".../collections/$COLLECTION_ID/documents?has_proposals=true"Accepting re-checks the value against the schema as it is now, writes it
onto the document and its chunks without re-embedding, and replaces any
value the field has picked up in the meantime — you asked for this one.
Setting a field by hand clears its pending proposal. A rejected value is
remembered and not proposed again (for a tags field, the same tags in
any order count as the same value).
Your decisions are the only labels there are:
GET …/collections/{id}/metadata-schema/stats reports, per field, how
many proposals are pending, accepted, rejected or auto-applied, and
accepted-over-decided as accuracy — null until you have decided any.
Classification and “who is mentioned” need nothing extra: declare an
enum field (document_type with the options above is a good start) and
a tags field (entities, say), and the same call fills them. The call
runs once per document and is billed to the tenant’s ingestion usage; each
ingestion attempt’s llm_steps carries it as metadata_extraction. If
the call fails or the provider refuses it, the document still indexes, its
earlier proposals stay as they were, and the attempt’s warnings say which.
It runs on ingest and on re-parse — not on re-chunk or re-index, which
never re-read the text.
Filtering
Section titled “Filtering”The same filter object works for search, for search preview, and for
listing documents:
{ "department": "HR", "effective_date": {"gte": "2025-01-01", "lt": "2026-01-01"}, "region": {"in": ["EU", "UK"]}, "tags": {"any": ["policy"]}, "cost_centre": "CC-12"}To find documents that are missing a field, or that have one, use
exists:
{"category": {"exists": false}}{"exists": true} matches documents where the field holds a value.
{"exists": false} matches documents where it is absent, empty ("" or
an empty list) or null. It works on declared fields and on extras, and
takes only true or false. Agents are told which fields support it, but
inferred filters never use it. A workflow can use it
to find the documents that still need a field filled in, see
the documents step.
All conditions must hold — there is no OR yet. A range on an undeclared
extra is rejected rather than guessed at: without a declared type there is
nothing to order by, and comparing strings that were never claimed to be
dates would quietly return the wrong rows.
curl -s -G "$KOVAC_BASE/api/manage/collections/$COLLECTION_ID/documents" \ --data-urlencode 'filters={"department":"HR"}'Inferred filters
Section titled “Inferred filters”An agent does not have to name a filter for one to apply. Turn on Infer
filters from questions on the collection’s Settings tab and the query
planner reads the question itself (“the HR parental leave policy from
2025”) and proposes a filter over this collection’s own declared fields —
the same ones an agent’s search tool is told about.
Only typed fields are inferred — enum, date, number, boolean
and tags. A Text field (string with no options) is never offered to
the planner: the model would have to guess the exact stored spelling, and
a wrong guess matches nothing while still costing the fallback search
above. Every typed field has its own May be inferred checkbox, on by
default once the collection switch is on, so a field you never want
guessed at can be left out; a Text field shows a muted note instead of a
checkbox — make it a list (enum) to make it inferable.
Three things are true by construction:
- An explicit filter always wins. When the agent (or a search-preview
request) supplies its own
filters, nothing is inferred for that call — there is no second-guessing a filter someone actually asked for. - A proposed entry is validated, not trusted. Every entry the model proposes goes through the same filter grammar a hand-written one does — an undeclared field, a field not marked inferable, an enum value outside its options, or an operator that does not fit the field’s type is dropped rather than applied. A dropped entry can only narrow what gets searched less than the model intended; it can never widen a search past what the grammar already allows.
- An over-narrow filter falls back, it never empties the answer. If the filtered search finds nothing, or finds only weak matches, the search runs again with the same sub-questions and the inferred filter removed — not a broader rewrite of the question, since the filter, not the wording, was the problem. The unfiltered results stand whatever they score.
This costs no extra model call: the planner already asks a model to split the question into sub-queries, and the filter rides the same reply when the collection has opted in.
Seeing it. Every search’s trace carries a retrieval.query_plan
observation: whether inference ran at all, the sub-queries used, every
filter entry the model proposed, what was dropped and why, what was
actually applied per collection, and — when the inferred filter was
removed on retry — that it was. Search preview shows the same thing as an
Inferred filter panel next to the results, with a button to re-run the
same question without the inferred filter for comparison.
Facets
Section titled “Facets”Value counts for a declared field, for building a filter UI:
curl -s "$KOVAC_BASE/api/manage/collections/$COLLECTION_ID/facets?field=department"# {"field": "department", "values": [{"value": "HR", "count": 12}, ...]}Declared fields only — a facet list is a vocabulary, which is what declaring a field is for.
What agents see
Section titled “What agents see”An assistant attached to a collection gets its declared fields listed in
the search tool description: names, types, enum options, your own field
descriptions and which operators each supports. Extras are not listed, so
an agent will not invent them.
That is the practical reason to declare a field rather than leave it an extra: an agent can only filter on what it has been told exists.
Fields a spreadsheet declares for you
Section titled “Fields a spreadsheet declares for you”Uploading a CSV, spreadsheet or JSON export in records mode adds a
typed field to this collection’s schema for every column you gave a
metadata or both role — see
Collections for the roles themselves.
The type comes from the values: all numbers makes a number, all dates a
date, a small repeated vocabulary an enum with those options, anything
else a string.
Two things it never does. It never marks a field required, so a later
upload missing that column still succeeds. And it never changes a
field you already declared — if this collection says priority is a
string and a spreadsheet arrives whose priority column looks numeric, the
values are validated as strings. Change the type yourself if that is what
you want.
