# Knowledge bases and search A knowledge base is the unit everything else hangs off. Files belong to one, RBAC grants are made against one, and both the index and search are scoped to one. Folders sit inside a knowledge base and are optional. **Before you start.** You need an API key from the [quickstart](/quickstart). The index and search only report content for files that have been [parsed](/parsing) — an unparsed document appears in the index with `latest_parse_job: null` and produces no search hits. Two endpoints do most of the work on this page: [`GET /v1/knowledge-bases/{kbId}/index`](/api/knowledge-base-corpus#list-indexed-files-for-a-knowledgebase) tells you the state of every document, and [`POST /v1/knowledge-bases/{kbId}/search`](/api/knowledge-base-corpus#search-grounding-evidence-elements) finds the evidence to cite. ## Knowledge bases ```bash curl -s -X POST "$DOCAI_API/v1/knowledge-bases" \ -H "x-api-key: $DOCAI_API_KEY" \ -H "content-type: application/json" \ -d '{ "name": "Contracts", "description": "Signed MSAs" }' ``` Names are unique per organisation, compared case-insensitively. A clash returns `409` with `duplicate_knowledge_base_name`, one of the stable [error codes](/errors). | Call | Notes | |---|---| | [`POST /v1/knowledge-bases`](/api/knowledge-bases#create-knowledgebase) | `name` required; `description` and `metadata` optional | | [`GET /v1/knowledge-bases`](/api/knowledge-bases#list-knowledgebases) | Returns `{ "knowledge_bases": [...] }`, newest first, filtered to what your key or role can reach | | [`PATCH /v1/knowledge-bases/{kbId}`](/api/knowledge-bases#rename-knowledgebase) | Rename. `name` is required even when you only want to change `description` | | [`DELETE /v1/knowledge-bases/{kbId}?confirm_permanent=true`](/api/knowledge-bases#permanently-delete-knowledgebase-and-all-files) | Permanent, and it takes every file with it | ### Delete is permanent There is no soft delete for a knowledge base. The call removes every file in it — active and trashed alike — deletes their managed filesystem roots, and then removes the folders, imports, RBAC grants and jobs attached to the knowledge base. Nothing can be restored afterwards. Without `confirm_permanent=true` the request fails with `400` and the code `confirmation_required`. That query parameter is the only safety catch. The response reports exactly what went: ```json { "deleted": true, "permanent": true, "knowledge_base_id": "6f3a…", "knowledge_base_name": "Contracts", "files_deleted": 42, "filesystem_roots_removed": ["…/files/8c21…"], "database": { "artifacts": 84, "credit_reservations": 0, "evidence_index_segments": 5310, "extraction_results": 12, "files": 42, "file_versions": 44, "jobs": 96, "page_versions": 612, "knowledge_bases": 1, "folders": 3, "imports": 2, "import_items": 51, "rbac_grants": 4, "knowledge_base_jobs": 0, "knowledge_base_job_credit_reservations": 0 } } ``` The per-file counts are summed across the deleted files; the remaining keys are the knowledge-base-level rows. ## Folders Folders live inside one knowledge base and nest through `parent_folder_id`. ```bash curl -s -X POST "$DOCAI_API/v1/knowledge-bases/$KB_ID/folders" \ -H "x-api-key: $DOCAI_API_KEY" \ -H "content-type: application/json" \ -d '{ "name": "2026", "parent_folder_id": null }' ``` [`POST /v1/knowledge-bases/{kbId}/folders`](/api/knowledge-bases#create-folder-inside-knowledgebase) is the only folder operation on the public API. To see which folder a file is in, read the index — every entry carries `folder_id` and `folder_name`. To read one folder, pass `?folder_id=` to the index. To move a file between folders, use [`PATCH /v1/files/{fileId}/move`](/api/files#move-file-to-another-knowledgebase-folder). ## The index `GET /v1/knowledge-bases/{kbId}/index` returns every file in the knowledge base, with its parse and [extraction](/extraction) state and links to its artifacts, in one request. ```bash curl -s "$DOCAI_API/v1/knowledge-bases/$KB_ID/index" \ -H "x-api-key: $DOCAI_API_KEY" ``` ```json { "knowledge_base": { "id": "6f3a…", "name": "Contracts" }, "files": [ { "file_id": "8c21…", "filename": "msa-2026.pdf", "content_type": "application/pdf", "folder_id": "b70e…", "folder_name": "2026", "current_version_id": "1f44…", "version_number": 1, "storage_path": "…/versions/1f44…", "sha256": "9ad3…", "byte_size": 482113, "page_count": 18, "metadata": {}, "created_at": "2026-08-01T09:12:44.000Z", "updated_at": "2026-08-01T09:14:02.000Z", "latest_parse_job": { "job_id": "44b1…", "status": "completed", "stage": "completed", "percent": 100, "error": null, "created_at": "2026-08-01T09:12:50.000Z", "completed_at": "2026-08-01T09:14:02.000Z" }, "latest_extract_job": null, "parse_artifacts": { "manifest_id": "c9f0…", "created_at": "2026-08-01T09:14:02.000Z", "links": { "result_md": "/v1/artifacts/c9f0…/result_md", "grounding_json": "/v1/artifacts/c9f0…/grounding_json", "workbook_json": null, "tables_json": null, "pages": "/v1/artifacts/c9f0…/pages", "imgs": "/v1/artifacts/c9f0…/imgs" } }, "extract_artifacts": null } ] } ``` `latest_parse_job` and `latest_extract_job` are `null` when the file has never been through that stage. Inside `parse_artifacts.links`, an individual link is `null` when that artifact does not exist for this document — `workbook_json` and `tables_json` only appear for spreadsheets, `pages` and `imgs` only when the parse produced page images or figures. `extract_artifacts.links` holds a single `result_json`. Every link is a path, not an absolute URL. Prefix it with your API base and send the same `x-api-key` header. `storage_path` is an opaque bundle key. Do not build file paths out of it — fetch artifacts through the links. ### Start here, not with the file list An agent connected to a knowledge base should begin at the index, which is the same advice the [agent guidance](/skill) gives its tools. Listing files and then fetching each file, its jobs and its artifacts is three round trips per document for information the index already returned in one, and the per-file walk gives you less — job status and artifact links do not appear on the file record. Polling the index is also how you watch a bulk parse finish. One call shows which documents are `completed`, which are still `running`, and which failed and why. ## Search `POST /v1/knowledge-bases/{kbId}/search` is lexical Postgres full-text search over the Evidence Index — the segments derived from parse artifacts. It calls no model. It therefore spends no [credits](/quickstart) and returns in the time a database query takes. ```bash curl -s -X POST "$DOCAI_API/v1/knowledge-bases/$KB_ID/search" \ -H "x-api-key: $DOCAI_API_KEY" \ -H "content-type: application/json" \ -d '{ "query": "termination for convenience", "limit": 10, "table_only": false }' ``` | Field | Type | Notes | |---|---|---| | `query` | string | Up to 4000 characters. A blank query applies the filters with no text matching | | `limit` | integer | Defaults to 30, capped at 80 | | `file_ids` | array of uuid | Restrict to specific documents | | `folder_id` | uuid | Restrict to one folder | | `years` | array of integer | Matches segments tagged with any of these years | | `source_type` | string | For example `pdf` | | `table_only` | boolean | Only segments the parser identified as tables | The snake_case fields are also accepted in camelCase: `fileIds`, `folderId`, `sourceType`, `tableOnly`. A malformed body returns `400` with `invalid_request_body`. Matching is term-based, not semantic. The query is lowercased and split into tokens, common filler words are dropped, and a small synonym list is applied, so `bills` also matches `invoice` and `charges`. Table segments get a small ranking boost. Only segments from a file's current version are searched, and deleted files are excluded. ### The response ```json { "knowledge_base_id": "6f3a…", "index_version": "evidence-v1", "status": "fresh", "snapshot": { }, "hits": [ { "id": "e11c…", "docId": "8c21…", "fileId": "8c21…", "fileVersionId": "1f44…", "artifactManifestId": "c9f0…", "segmentId": "p7_e12", "docName": "msa-2026.pdf", "title": "text", "markdown": "Either party may terminate for convenience on 30 days notice…", "score": 0.0731, "sourceType": "pdf", "coordinates": { "pageNumber": 7, "xmin": 72.4, "ymin": 331.8, "xmax": 523.1, "ymax": 402.6, "sourceType": "pdf" }, "tableMetadata": { "isTable": false, "headers": [], "rowLabels": [], "noteRefs": [] }, "noteRefs": [], "years": [2026], "quarters": [], "neighbors": { "previousSegmentId": "p7_e11", "nextSegmentId": "p7_e13", "noteSegmentIds": [] } } ] } ``` | Field | What it is | |---|---| | `fileId` | The document. `docId` is the same value, kept for older clients | | `fileVersionId`, `artifactManifestId` | The exact version and parse run the text came from | | `segmentId` | The element inside that parse artifact — the citation anchor | | `markdown` | The segment text as parsed | | `score` | Postgres `ts_rank_cd` against the query. Comparable within one response, not across responses | | `coordinates` | `pageNumber` and the `xmin`/`ymin`/`xmax`/`ymax` box for PDFs; for spreadsheets, `sheetIndex`, `sheetName`, `startRow`, `endRow`, `startCol`, `endCol` and `rangeA1` | | `tableMetadata` | `isTable`, plus `headers`, `rowLabels` and `noteRefs` when the segment is a table | | `noteRefs` | Footnote markers found in this segment | | `neighbors` | The segments either side, and the segment ids of any notes this table points at | `neighbors` is what you widen a hit with. A single row of a table rarely carries its own context; fetch `previousSegmentId`, `nextSegmentId` or `noteSegmentIds` from the parse artifact when the hit alone is too narrow. ## Index freshness Search reads a derived index, rebuilt from the latest parse artifacts — the artifacts are the source of truth, the index never is. A completed parse enqueues an incremental index update for that file on its own, and a parsed-bundle import does the same for what it imported, so the index follows the corpus without your involvement. Right after a parse or import completes, search may briefly lag the newest content while that update runs. Every search response embeds the index snapshot it answered from, so a client that cares can see exactly what state it searched. ## Citing a hit A hit already contains everything a citation needs: `fileId`, `segmentId` and `coordinates`, pinned to `fileVersionId` and `artifactManifestId`. To get from there back to the source: - **Full segment list for the document** — [`GET /v1/files/{fileId}/segments`](/api/knowledge-base-corpus#read-segment-map-data-from-latest-parse-artifacts) returns the segments of the newest parse, so you can pull a neighbour by id. - **The parsed markdown** — the index gives you `parse_artifacts.links.result_md`, or fetch [`GET /v1/artifacts/{manifestId}/result_md`](/api/knowledge-base-corpus#download-or-list-an-artefact-by-generated-link) with the `artifactManifestId` from the hit. Using the manifest id from the hit rather than the newest one keeps the citation pointing at the text you actually searched. ## Importing existing documents Bringing an already-parsed corpus into a knowledge base — from a previous DocAI deployment or another system — is a **DocAI web application** feature: Document Library → Import corpus, with Connect folder for a directory that keeps changing. There is no integration API for it. From the API, the path for existing documents is the ordinary one: upload each file with [`POST /v1/files`](/api/files), then parse the set in one call with [bulk parse](/parsing#bulk-parse). Parsing is charged per page. ## Where to next - [Parsing](/parsing) — how a document becomes the artifacts the index lists and search reads. - [Extraction](/extraction) — pull the same fields out of every document a knowledge base holds. - [MCP server](/mcp) — the index and search as tools your coding agent can call directly.