Skip to main content

Search a library of PDFs

By the end of this tutorial you will have a knowledge collection built from your own PDFs that answers a question with the exact passages that match it.

Four steps:

  1. Create the collection.
  2. Upload a PDF into it.
  3. Wait for it to be indexed.
  4. Query it.

Every step is one API call, shown for all three clients. The ids in the responses are examples — copy the ones your own calls return.

Prerequisites

  1. A credential. A nat_sk_… API key (or a session JWT from Auth) exported as NATURALI_TOKEN, and your client set up — the CLI, the SDK or plain curl:

    export NATURALI_TOKEN=nat_sk_...
    export NATURALI_API=https://api.naturali.ai/v1 # curl examples only
  2. A project. Create a provider builds one — do it first if you haven't, and arrive with its id exported:

    export PROJECT=proj_V1StGXR8Z5jdHi6B
    export NATURALI_PROJECT=$PROJECT # lets the CLI omit --project-id
  3. A PDF with a text layer, in the working directory as manual.pdf. Any born-digital PDF works — one exported from a word processor, a datasheet, a contract. A scanned PDF is a picture of text with nothing to extract; that one needs a converter.

1. Create the collection

A collection is the named library your documents live in, and the unit retrieval is scoped to.

naturali create-knowledge-collection \
--project-id "$PROJECT" \
--name manuals \
--description 'Product manuals the support agent works from.'
{
"id": "kcol_V1StGXR8Z5jdHi6B",
"project_id": "proj_V1StGXR8Z5jdHi6B",
"name": "manuals",
"description": "Product manuals the support agent works from.",
"document_count": 0
}

Export the id — every later step needs it:

export COLLECTION=kcol_V1StGXR8Z5jdHi6B

2. Upload a PDF into it

Send the bytes base64-encoded, with content_type: application/pdf. That media type is what tells the platform to parse it as a PDF rather than treat it as text.

The default page chunk strategy makes one chunk per page, so a retrieved passage can cite the page it came from.

naturali create-knowledge-document \
--project-id "$PROJECT" \
--collection-id "$COLLECTION" \
--filename manual.pdf \
--content-type application/pdf \
--file "$(base64 -w0 manual.pdf)"
{
"id": "doc_V1StGXR8Z5jdHi6B",
"collection_id": "kcol_V1StGXR8Z5jdHi6B",
"filename": "manual.pdf",
"content_type": "application/pdf",
"status": "pending",
"error": null,
"chunk_count": null,
"content": null
}
export DOCUMENT=doc_V1StGXR8Z5jdHi6B
Files are capped at 8 MiB

The limit is on the decoded bytes, not the base64 you send. A larger file comes back 413 file_too_large with its actual size; split it, or drop the scanned images it is carrying.

Denser pages? Use size chunks

One chunk per page is too coarse when a page holds a lot of unrelated facts — the whole page becomes one embedding and retrieval blurs. size splits the text into fixed-width character windows instead. The trade-off is that windows are not page-aligned, so they carry no page number:

naturali create-knowledge-document \
--project-id "$PROJECT" \
--collection-id "$COLLECTION" \
--filename manual.pdf \
--content-type application/pdf \
--chunk-strategy size \
--chunk-size 800 \
--chunk-overlap 150 \
--file "$(base64 -w0 manual.pdf)"

Start with page and switch only if step 4 retrieves poorly.

3. Wait for it to be indexed

Ingestion — extract, chunk, embed — runs in the background, so the document came back pending. Read it until status is indexed; chunk_count then tells you how many passages it produced.

naturali get-knowledge-document \
--project-id "$PROJECT" \
--collection-id "$COLLECTION" \
--document-id "$DOCUMENT"
{
"id": "doc_V1StGXR8Z5jdHi6B",
"status": "indexed",
"chunk_count": 12,
"error": null,
"content": "X1000 Printer Quick Guide\nThe paper tray holds 250 sheets. …"
}

Rather than poll, subscribe a webhook to knowledge.document_ingested and knowledge.ingest_failed — the same transition, pushed.

A status of failed names the reason in error. The usual cause for a PDF is no text layer at all, which is the scanned-PDF fallback case; fix the cause and retry with POST /v1/projects/{project_id}/knowledge/collections/{collection_id}/documents/{document_id}:reingest.

4. Query it

Ask the collection a question. Retrieval runs at the chunk level, so each result names the document it came from and how strongly it matched — which is what lets an answer cite its source.

naturali query-knowledge-collection \
--project-id "$PROJECT" \
--collection-id "$COLLECTION" \
--query 'How many sheets does the paper tray hold?' \
--top-k 3
{
"data": [
{
"document_id": "doc_V1StGXR8Z5jdHi6B",
"chunk_id": "dchunk_V1StGXR8Z5jdHi6B",
"score": 0.78,
"content": "X1000 Printer Quick Guide\nThe paper tray holds 250 sheets. …"
}
]
}

The passage that answers the question comes back with its document_id and score. That is the library working: add more PDFs to the same collection with step 2 and they join the same index — no reconfiguration, no re-chunking of what is already there.

If the top hit is off, raise top_k to see what else was close, then re-ingest with size chunking (step 2) if whole pages are the problem.

What's next

  • Ingest images and audio — register a converter so scanned PDFs, screenshots and recordings join the same collection.
  • Knowledge — chunking, converters and the ingestion lifecycle in full.
  • Webhooks — get the indexed / failed transition pushed instead of polling for it.