Data & analytics
Knowledge Base
Collections of documents an agent can search by meaning and quote with citations. This is how you stop an agent inventing your policies.
Open Build → Knowledge Base. A collection is a named group of documents; agents attach to collections, not to individual files.
Adding sources#
Add source covers one-shot ingestion — files, a web page, a repository. Connect links an external service that is synced on a schedule and kept deduplicated. Both land documents in the same collection and the same retrieval pipeline.
File upload#
Accepted extensions, exactly:
.txt .md .markdown .csv .tsv .log
.html .htm .xml .yaml .yml .json
.rtf .pdf .docx
.png .jpg .jpeg .webp .gifScanned PDFs and images are read with a vision model
kb.document.ocr, with the pages, the model and the cost) apply. The upload shows the page being read; the document keeps a [page N] marker per page and records the model and cost in its metadata. The model and the Pages per document limit live under Admin → Developer runtime → Document intelligence (DOCUMENT_VISION_MODEL, DOCUMENT_VISION_MAX_PAGES). A page the model reads as empty is dropped; if every page is, the upload says so instead of adding a document with nothing in it.Web page / crawl#
Give a URL and the page is fetched and converted to text. Use it for public documentation and policy pages. Pages behind a login cannot be fetched.
GitHub repository#
Ingests source and docs from a repository so an agent can answer questions about a codebase.
Public repositories work with no credentials at GitHub's anonymous limit of 60 requests an hour, which a large repository can exhaust mid-import. Set GITHUB_TOKEN on the deployment to raise that to 5,000 and to allow private repositories the token can read.
Pasted text#
The Manual tab takes a name and a body of text typed or pasted straight into the form — no file, no URL. It is the quickest way to add something that exists only in someone's head or in a chat thread: a support answer, an internal convention, a correction to a document you cannot edit. The text is chunked and embedded exactly like an uploaded file, so it is searchable on the same terms as everything else in the collection.
Connected services — Drive, Notion, SharePoint, Dropbox, Confluence, Website#
Connect opens a wizard for six providers. Credentials are pasted tokens (the platform's BYOK pattern — no OAuth consent screens to register), validated against the provider at save time, encrypted at rest, and never sent back to the browser: editing a source shows empty credential fields, and leaving them empty keeps what is stored.
| Provider | Credentials | What syncs | Mirrors sharing? |
|---|---|---|---|
| Google Drive | Access token, or refresh token + OAuth client pair for unattended syncs | A folder (subfolders to depth 5): Google Docs/Sheets/Slides exported as text, plus text-format files | Yes — per-file permissions |
| Notion | Internal-integration secret; share the pages with the integration | Listed page IDs and every page of listed databases | No — the API exposes none |
| SharePoint | Entra app registration: tenant + client ID + secret (Files.Read.All, admin-consented) | A document library or folder path, text-format files | Yes — per-item permissions |
| Dropbox | Access token, or refresh token + app key/secret for unattended syncs | A folder path (or everything); native content hashes make change detection exact | Yes — file members, best-effort |
| Website | None — a public site | Pages from the sitemap (lastmod is the change marker), else same-site links followed from the start URL; robots.txt honoured; up to 500 pages | No — public content |
| Confluence | Cloud: email + API token. Data Center: personal access token — the host decides which | Every page in the listed spaces; version.number is the change marker; storage-format macros flattened to text | No — not read |
Per source, listings cap at 500 items and each item's text at 400k characters. Anything the connector saw but did not ingest — an unsupported binary, a too-deep folder, the cap — is listed on the source card with its reason, never silently dropped.
Short-lived tokens and schedules don't mix
Scheduled sync — indexing without duplicates#
Each connected source has a schedule: manual, hourly, daily or weekly. Scheduled syncs run on the platform's maintenance pass (the same engine that refreshes BI and SaaS data), and a claim on the source's next-run time guarantees that multiple app instances never sync the same source twice.
Change detection is two-level, and both levels exist to make an hourly schedule cheap:
- 1
Version skip — unchanged files are not downloaded
The provider's change marker (modified time, revision, native hash) is stamped on each document. Unchanged marker ⇒ the item is skipped without being downloaded — a 400-file folder re-syncs for the price of a listing. - 2
Content skip — unchanged text is not re-embedded
Providers bump modified times on moves, permission edits and comments. Downloaded text is hashed (sha256); if it matches what is stored, the marker is refreshed and the document is not re-chunked or re-embedded. Embedding spend follows actual content change, nothing else.
Files deleted at the provider delete their documents (and chunks) here. The source card shows each sync's outcome as +added ~updated =unchanged −removed, and a database uniqueness constraint on (source, remote item) makes duplicate documents impossible even if everything above were wrong.
Sync statuses
ok is a clean pass. error names the provider's refusal verbatim — a revoked token reads "Dropbox 401: invalid_access_token", not "0 documents". embed failed means documents were saved but semantic indexing didn't finish: retrieval falls back to keyword search for them until a re-sync succeeds, and the owner gets a notification either way.What happens on ingest#
file ──▶ extract text ──▶ split into chunks ──▶ embed each chunk ──▶ store
(few hundred words, (vector = position
overlapping) in meaning-space)
question ──▶ embed ──▶ nearest chunks ──▶ pasted into the prompt as [1] [2] …Chunking matters more than people expect. Retrieval returns chunks, not documents — so if the sentence answering a question is split across two chunks, neither answers it well. Chunks overlap slightly to soften this.
Why it works this way
Which model does the embedding#
Set this per collection under RAG settings → Embedding. Embeddings come from a connected model provider — the same place your chat models come from. There is no separate embeddings key and no dependency on an OpenAI account. OpenRouter is the suggested default, not a requirement: it works either through your own integration or, with no integration at all, through the operator's OPENROUTER_API_KEY, so a fresh install gets retrieval for free from the same account that already makes chat work. Any other connected provider exposing an OpenAI-compatible /embeddings endpoint can be selected instead.
Not every embedding model fits this store
dimensions parameter — and whether a given one honours it cannot be told from its name.So don’t guess: press Test embedding beside the picker. It calls the provider once and reports the width it actually returned. Without that check the failure lands at ingest instead, after documents are already saved — and a collection whose embeddings never ran still answers, quietly, using keyword search alone.
Changing the model means re-embedding
Note
text-embedding-3-* models truncate to any size on request, and a narrower local model is zero-padded, which is exact for cosine similarity. A model that returns something wider fails the embed with a message saying so rather than writing unusable vectors.The OpenRouter default is openai/text-embedding-3-small because it is the same 1536-d space the operator's OpenAI key produces: moving a collection onto OpenRouter to get off an exhausted OpenAI quota costs nothing and leaves existing chunks searchable. The other options are different spaces, so choosing one means re-indexing.
| Model | Native width | Notes |
|---|---|---|
openai/text-embedding-3-small | 1536 | Default. Same vector space as the built-in OpenAI key. |
openai/text-embedding-3-large | 3072 | Truncates to 1536 on request. |
google/gemini-embedding-001 | 3072 | Truncates to 1536 on request. |
qwen/qwen3-embedding-8b | 4096 | Truncates to 1536 on request. |
qwen/qwen3-embedding-4b | 2560 | Truncates to 1536 on request. |
Every model in that list was called against OpenRouter's live endpoint and confirmed to return 1536 dimensions. That check is not ceremony: OpenRouter does not list embedding models in its public /models catalogue, so a plausible-looking id is no evidence the model exists. Two NVIDIA nemotron ids used to be offered here and both returned 404 No endpoints found — selecting one produced a failed embed with nothing to indicate the model had never been available.
Where the vectors are searched#
By default, in your own Postgres — kb_chunks.embedding is a pgvector column with an HNSW cosine index, and the permission check is the row-level security already protecting those rows. Nothing to connect, nothing extra to back up, and a collection that cannot half-exist because two systems disagree. For most deployments that is the end of it.
A self-hosted deployment can point the vector search at Qdrant instead, by setting VECTOR_STORE=qdrant and QDRANT_URL. The reason is capacity, not availability: an HNSW index wants RAM, and by default it wants it from the same instance serving your traces, audit, BI results and every other query. Past a few million chunks it is the largest thing in there, and the only way to feed it is to resize the whole database. Qdrant is a place to put the index that scales — and replicates — on its own. It does not make retrieval survive a Postgres outage: every hit is hydrated from the chunk rows in Postgres, so the database going down takes retrieval with it wherever the vectors live.
That setting is the default for every collection, not a decision for the whole deployment. RAG Settings → Retrieval → Vector index sets it per knowledge base, which is usually how to adopt Qdrant at all: the one collection that outgrew Postgres moves, every other one stays where its rows are. A collection pointed at Qdrant on a deployment that has none falls back to Postgres, and the picker says so rather than letting you find out from a server log.
Changing the index moves the vectors, and costs nothing to do
kb_chunks.embedding and are read back from there — so the only cost is the time to read every chunk.The order is copy, save, clear, and it is the order because the other one fails badly: a setting saved before the copy finished would leave the collection searching an index it was never written to, which returns nothing and raises nothing. This way a failure leaves the vectors in two stores — disk, not wrong answers — and saving again finishes it.
Qdrant holds vectors and two ids — that is all
One Qdrant node is not high availability. A single node is the right shape for a small install, and losing it degrades retrieval to keyword search rather than breaking it — but surviving the loss of a node means a Qdrant cluster with QDRANT_REPLICATION at 2 or more. Admin → Developer runtime → AI services shows which store is in use, whether it is answering, how many vectors it holds against how many chunks the database has, and the replication the collection actually got.
That same page has Re-index, which drops every vector in the store and writes them back from the chunks. It is the answer to every way an external index can drift — a restored-from-empty volume, a store switched on after documents were already embedded, a delete that happened while it was unreachable. It moves vectors that already exist; it does not re-embed, so a document that was never indexed stays keyword-only until it is.
Chunking modes#
Retrieval and generation want opposite things from a chunk. Matching is most precise when chunks are small and about one idea; answering is best when the model can see the whole passage. RAG Settings → Chunking → Chunking Mode decides how that tension is resolved for a document.
| Mode | What is embedded | What the model reads | Use it when |
|---|---|---|---|
| Flat | The chunk | The same chunk | Short documents, FAQs, anything where one chunk is already a complete thought. This is the default and was the only behaviour before. |
| Parent-child | Small child chunks | The child's parent | Long reference material — manuals, contracts, policies — where the sentence that matches is meaningless without the section around it. |
| Q&A | A generated question | The question and its answer | Support content and policy documents that people query in natural questions. Costs one model call per passage at index time. |
Parent-child sets two sizes: the parent is what reaches the model, and the existing chunk size becomes the child. A child is capped at half the parent, because a child the same size as its parent is flat chunking with extra bookkeeping. Parents do not overlap each other; children overlap within a parent.
Q&A exists because a question and a statement are different kinds of text, and that difference is a real part of the distance between their vectors. Asking “How do I rotate a key?” against a paragraph that says “Rotation issues a replacement…” is a harder match than asking it against the generated question “How do I rotate a key?”. It needs OPENROUTER_API_KEY; if generation fails, the run reports it rather than quietly writing flat chunks, so a collection never disagrees with the mode shown in its own settings.
Changing the mode does not rewrite existing chunks
Hybrid search and weighting#
Vector search finds meaning and blurs exact strings; an error code, a part number or a surname is exactly the kind of token embeddings smooth away. Keyword search is the opposite. RAG Settings → Retrieval sets which of them runs, per knowledge base. The same tab chooses the vector index that collection is searched in.
| Mode | What runs |
|---|---|
| Semantic | Vector search only. What every collection did before hybrid existed. |
| Hybrid | Vector and Postgres full-text search over the same chunks, merged by weight. The default for a collection that has never saved retrieval settings, weighted 0.7 toward meaning — measured: semantic-only lost exact-term questions to look-alike paragraphs. |
| Keyword | Full-text search only. |
The weighting slider splits the score between them. Each retriever’s scores are normalised within its own list first, because cosine similarity (roughly 0.3–0.9) and text rank (roughly 0.0–0.3) are not comparable numbers — added raw, the slider would do almost nothing across most of its range. A chunk found by bothretrievers scores above one found by only one, which is usually the result you want.
Each list's best hit always scores 1.0
Changing retrieval mode takes effect immediately and needs no re-embedding: it changes how the existing index is queried, not how it was built. Note that keyword search also indexes the generated question on Q&A rows, because the answer text often does not contain the words someone would search for.
Retrieval settings — the real numbers#
| Setting | Default | Range | Notes |
|---|---|---|---|
| top-K | 5 | 1 – 8 (hard cap) | How many documents are cited per turn. Asking for more than 8 is clamped. |
| Chunks per document | 3 | 1 – 10 (KB_CHUNKS_PER_DOCUMENT) | Each cited document carries its best few chunks in reading order — adjacent ones joined, gaps marked with an ellipsis. One chunk was not enough: a policy's table and the prose about it rank separately, and the prose won. |
| Characters per chunk | 1,600 | 100 – 20,000 (KB_CITATION_CHARS_PER_CHUNK) | A flat chunk reaches the prompt whole; a default chunk is about 1,024 characters. Parent passages keep their own 4,000-character budget. |
| Grounding budget | 12,000 characters | 500 – 1,000,000 (KB_GROUNDING_MAX_CHARS) | Per turn, across every citation, applied in rank order: a later citation is shortened, then dropped; an earlier one is never trimmed to make room. |
| Similarity floor | 0.3 | 0 – 1 (KB_MIN_SIMILARITY; 0 = off) | Below this best-chunk similarity with no keyword hit, the turn is not grounded and the model is told the search found nothing — an off-topic question stops carrying five documents to ignore. Measured with text-embedding-3-small: document questions 0.38–0.75, off-topic 0.10–0.32. Never applied to the kb_search tool. |
| Candidate pool (over-fetch) | 3 × top-K, capped at 30 | — | A wider first pass, fetched whenever the list is about to be re-ordered — either by a reranker or by hybrid fusion. Re-ordering can only promote what it was given, so fetching exactly top-K would leave the keyword side nothing to rescue. |
| Merged results kept | 3 × top-K, capped at 20 | — | After the vector and keyword lists are fused and deduplicated by document. The final answer still sees top-K. |
| Snippet radius | 280 characters | — | Either side of a keyword match in a document that has no embeddings yet — the fallback while indexing is incomplete. |
Reranking#
Configured per agent on the Knowledge tab: a Provider and a Re-rank model (for example llama-nemotron-rerank-vl-1b-v2).
- Cost — one extra model call per retrieval.
- When it pays — collections with many near-identical passages: long contracts, several revisions of one policy, product manuals for a family of similar products.
- When it doesn't — a small collection of clearly distinct documents. The first pass is already right.
Configuration by what you are indexing#
The settings interact, so choosing them one at a time tends to produce a collection that is individually reasonable and collectively wrong. These are five shapes of content that come up constantly, with the whole configuration for each and the reasoning behind it. Start from the closest one and adjust.
Policies, contracts, handbooks#
Long documents, formal language, and — the part that decides the configuration — several revisions of the same text, where the wrong version answering is worse than no answer.
- Chunking
parent_child. A clause matches on its own wording, but a clause read without the section around it is how you get an answer that is technically present in the document and wrong in context.- Retrieval
- Hybrid. Clause and section numbers ("7.2(b)"), defined terms and party names are exact strings, and embeddings smooth those away — hybrid keeps a keyword route to them.
- Re-ranker
- Yes. This is the case it exists for: five near-identical revisions all score well on similarity, and only a stronger model reliably puts the right one first.
- top-K
- 5, or 8 if answers are being cut short. Bigger is not better — more chunks means more near-duplicates competing.
- The prompt
- Require the document name and effective date in the answer. Without that, a reader cannot tell which revision replied.
Product documentation and how-to guides#
Structured, headed, mostly distinct topics — the easiest case, and worth not over-tuning.
- Chunking
flat. Headed sections are already the right size; parent_child mostly adds tokens here.- Retrieval
- Hybrid, leaning semantic. Users ask in their words, but error codes and setting names must still hit exactly.
- Re-ranker
- Not at first. Distinct topics mean the first pass is usually right, and you would be paying a call per retrieval to reorder a list that was already correct.
- top-K
- 5.
- The prompt
- Ask for numbered steps when the question is "how do I", and require a link or document name so the reader can continue in the source.
Support tickets and FAQs#
Short, repetitive, and written in answer language while your users ask in problem language — "it says invalid credentials" versus "authentication troubleshooting".
- Chunking
qa. It embeds a generated question alongside the answer, which closes exactly that gap: keyword search also indexes the question, because the answer text often does not contain the words anyone would search for.- Retrieval
- Hybrid. Error strings and product names are quoted verbatim by users.
- Re-ranker
- Yes once the collection passes a few hundred entries, where near-duplicate tickets start crowding each other out.
- top-K
- 5–8. Several partial answers often combine into the right one here.
- The prompt
- Give it an escalation rule. A support agent that cannot answer should hand off, not improvise — see writing the system prompt.
A code repository#
Ingested from the GitHub source, so the collection is source files plus whatever docs live beside them.
- Chunking
flat. Code has its own structure and splitting it by prose rules helps nobody.- Retrieval
- Hybrid, weighted toward keyword. Identifiers are the query. Someone searching
resolveEmbedTargetwants that symbol, not something semantically adjacent to it, and this is the one collection type where the keyword side usually deserves to win. - Re-ranker
- Optional. Exact identifier matches are already decisive.
- top-K
- 8. A question about behaviour usually spans a definition and its call sites.
- The prompt
- Require file paths in the answer, and say that quoted code must come from retrieved chunks rather than memory — a model's recollection of a well-known library is confident and frequently not your version.
One collection holding several kinds at once#
Usually how collections actually end up, rather than a choice anyone made.
Split it before tuning it
Change one thing at a time, and know which need a re-index
Attaching a collection to an agent#
- 1
Create the collection and ingest into it
Keep unrelated subject matter in separate collections — a mixed collection retrieves measurably worse. - 2
Agent Builder → Knowledge → link it
Linking auto-enables thekb_searchtool. - 3
Add the grounding instruction to the system prompt
Without this the model happily falls back on general knowledge and you will not notice. - 4
Turn on Citation Check
Guardrails tab. It flags an answer that cites nothing when sources were available — see Guardrails.
Answer only from the provided sources. Cite them inline as [1], [2].
If the sources do not contain the answer, reply exactly:
"I don't have that in my documentation."
Never fill a gap with general knowledge.Retrieval happens automatically before each turn, with numbered citations inserted. The kb_search tool additionally lets the agent search on demand, mid-answer, when its first read wasn't enough.
Graph search#
Ordinary retrieval finds chunks resembling the question. Graph search also follows relationships extracted between entities across documents, answering questions plain retrieval structurally cannot — "which suppliers are affected by the clause in appendix B?", where no single chunk contains both halves.
- 1
Build the graph
Knowledge → Graph, on the collection. This is a separate, slower pass over the documents. - 2
Enable the tool
Agent Builder → Tools →kb_graph_search.
Careful
kb_graph_search without building the graph first returns nothing — and the agent will treat "nothing" as "no information exists" rather than as a configuration problem.Sharing and access#
Collections are private to their owner. An administrator can grant a user or group read-only access from Access control; shared collections show a Shared badge and cannot be edited by the recipient.
Because the grant is enforced in the database, an agent's retrieval inherits it automatically — there is no second permission to keep in sync.
Per-source access scopes#
Connected sources add a second, finer layer: within a collection someone can already see, which of its synced documents may they retrieve? Each connected source picks one of three scopes, enforced at retrieval — vector and keyword paths alike, before any reranking model sees the text:
| Scope | Who retrieves the documents |
|---|---|
| Everyone with this KB | Default. Documents behave exactly like uploads — collection visibility decides. Every pre-existing document works this way. |
| Only me | The connecting user, full stop — even when the collection itself is shared or granted. |
| Match source permissions | Sharing is mirrored from the provider per document: people shared on the original file (by email or domain, at Drive/SharePoint/Dropbox) retrieve it here; everyone else doesn't. A publicly-linked file stays public. |
- Owner always retrieves their own documents, whatever the scope — a restriction you configured cannot lock you out of your own data.
- Fails toward deny. A provider that exposes no sharing info (Notion's API does not; a Dropbox plan without member listing) leaves documents owner-only, and the sync stats count them so the choice is visible. Tenant-wide links ("anyone in the organisation") match nobody but the owner — tenant membership can't be verified from here, and a wrong deny is recoverable where a wrong allow is not.
- Public embeds are anonymous. An embedded assistant retrieves only default-scope documents (and provider-public ones) — never "Only me" or ACL-restricted material, even though the embed runs under its owner's account.
When an answer is wrong#
Work through these in order. The cause is nearly always one of them.
| # | Check | What it means |
|---|---|---|
| 1 | Does the document show a non-zero chunk count? | Zero means extraction produced nothing — almost always a scanned PDF. |
| 2 | Open the run in Traces: was anything retrieved? | Empty means the collection isn't linked to this agent, or the query embedded far from everything in it. |
| 3 | Are the retrieved chunks on-topic but not the right passage? | Chunking or phrasing. Try a reranker; consider re-uploading a better-structured source. |
| 4 | Retrieved correctly but ignored? | The system prompt doesn't require grounding, or another tool's result was more prominent. |
| 5 | Are knowledge sources listed under the answer? | They appear when the answer cites them, or when nothing else grounded it. If a web search answered instead, you will see links — that is correct, not a bug. |
Getting better results#
- Prefer structured documents. Headings give the splitter meaningful boundaries; an unbroken wall of text does not.
- Split by subject, not by department. One collection per coherent topic retrieves better than one collection of everything.
- Delete superseded versions. Three revisions of one policy retrieve interchangeably and the agent has no way to know which is current. This is the single most common cause of confidently outdated answers.
- Put numbers in tables. If the question is really arithmetic it belongs in Data Catalog — retrieval quotes prose, it does not compute.
- Name documents descriptively. The document name appears in the citation a reader sees, so
refund-policy-2026.pdfbeatsfinal_v3.pdf.
Use cases#
Index your own documentation site#
- 1
Add Source → Website
Give start URLs; optionally a sitemap URL, path prefixes to stay inside such as/docs, and a page cap (100 by default, 500 at most). No credential is needed. - 2
Sync
The crawler honours robots.txt, stays on the same site, prefers the sitemap and otherwise follows links breadth-first. A page version is the sitemap lastmod, else the ETag, else a content hash, so a scheduled sync re-fetches only what changed. A result like 5 documents indexed, 12 skipped by robots.txt is normal for a marketing site — narrow the prefixes to the documentation tree.
A Confluence space, code blocks intact#
- 1
Add Source → Confluence
Site URL, the space keys to sync, an API token — plus the account email on Confluence Cloud. Cloud and Data Center are told apart from the URL and authenticated accordingly. - 2
Pages arrive as text with their code macros preserved
That is what makes a runbook useful to an on-call agent. Restrict retrieval by sharing the knowledge base read-only with the on-call group.
Share it, and delete it safely#
Sharing is read-only and done from Admin → IAM → Access. Deleting a knowledge base asks first and names what goes with it — every document, chunk and connected source, and the agents wired to it lose their knowledge. There is no undo, which is why there is a dialog.