---
name: page-search
description: Find the pages of a PDF that answer a question with Page Search API by PageIndex, then send only those pages to an LLM instead of the whole document. Use for question answering, extraction or review over long PDFs such as filings, contracts, reports and manuals, in Python or JavaScript.
metadata:
  author: PageIndex
---

# Page Search API

Page Search API takes a PDF and up to 10 questions, and returns, for each
question, the pages that help answer it, ranked by relevance. Every page is
read, so tables, charts and scanned pages are found too. Send only those pages
to your LLM, not the whole PDF: answers stay as good, and far fewer tokens are spent.

## Connect

Get an API key from https://search.pageindex.ai/console/keys (sign in by email,
Google or GitHub) and keep it in the environment, never in source. Keys start with
`pis_live_`:

```bash
export PAGEINDEX_SEARCH_API_KEY="pis_live_..."
```

Python 3.9+:

```bash
pip install -U pageindex-search
```

JavaScript, Node 18+ (keep the key on the server; never ship it to a browser):

```bash
npm install @pageindex/search
```

## Upload and search

```python
from pageindex_search import PageSearch

client = PageSearch()  # reads PAGEINDEX_SEARCH_API_KEY

doc = client.documents.upload("report.pdf")  # returns once the document is ready
result = client.search(["What was revenue in 2024?", "Who is the CEO?"], document=doc)

for answer in result.results:
    print(answer.question, [(p.page, p.relevance) for p in answer.pages])  # pages of the one document
# What was revenue in 2024? [(37, 1.0), (40, 1.0), (77, 0.9)]
# Who is the CEO? [(5, 1.0)]

print(result.usage.cost_usd, result.usage.balance_usd)
```

```ts
import { PageSearch } from "@pageindex/search";

const client = new PageSearch(); // reads PAGEINDEX_SEARCH_API_KEY

const doc = await client.documents.upload("report.pdf");
const result = await client.search(["What was revenue in 2024?", "Who is the CEO?"], { document: doc });

for (const answer of result.results) {
  console.log(answer.question, answer.documents[0].pages.map((p) => [p.page, p.relevance]));
}
```

- `result.results` has one answer per question, in the order asked. Each
  answer's `documents` hold its `pages`, numbered from 1, most relevant first
  (in Python, `answer.pages` is the first document's).
- `relevance` runs from 0 to 1, to one decimal: the chance the page helps
  answer the question.
- By default each question lists the pages likely to help: more when the
  answer is spread over several sections, and none when the document probably
  does not answer it. Pass `top_k=5` (JavaScript: `{ topK: 5 }`) to get exactly
  that many pages, the most relevant, every time.
- Ask all the questions about one document in one search: ten questions take
  about as long as one and cost less than ten searches.
- Write questions as you would ask a person. A question may be up to 2,000 characters.
- `result.failed_pages` lists pages that could not be read; they are not charged.

## Send only those pages to the LLM

Ask for each page's text with `include_text=True` (`includeText: true` in
JavaScript), and give only those pages to the LLM:

```python
result = client.search("What was revenue in 2024?", document=doc, include_text=True)
answer = result.results[0]
context = "\n\n".join(f"[Page {p.page}]\n{p.text}" for p in answer.pages)
# Give `context` and answer.question to your LLM, and ask it to cite the page numbers.
```

Each page says how it is best read: `page.read` is `"text"`, or `"image"` for a
scan or a chart, whose text may be incomplete or empty. Send those pages as
images, from `client.documents.page_image(doc, page)` (PNG bytes;
`pageImage` in JavaScript), to a model that reads images. For small print, pass
`box=(x0, y0, x1, y1)`, fractions of the page from its top left (x across, y
down), from 0 to 1, for only that part of it, sharper.
If the pages listed do not answer, read the next most relevant from
`more_pages` (listed without `top_k`, without text) with
`client.documents.pages(doc, [...])`, which is free, before searching again.
If an answer has no pages, say the document does not seem to answer the
question rather than guessing.

## Agents

To let an LLM agent search and read the documents itself, give it
`client.tools()`: `list_collections` (free; to find a collection the question
names, then pass its `collection_id`), `list_documents` (free; its description lists the metadata's
keys and values, so the agent can write filters), `search_documents` (for when a
filter cannot pick the documents), `search_pages`, `read_pages`,
`view_pages` and `zoom_page` (part of a page, sharper, for small print). Pass `tools.definitions` to Claude or `tools.openai` to OpenAI,
and send `tools.run(name, input)` back as each tool's result
(`.anthropic()` for Claude). With the OpenAI Agents SDK
(`pip install "pageindex-search[agents]"`), pass `tools.openai_agents()` as the
agent's tools. Made for all documents, one setup serves every collection; give
`collection=` or `filter=` only to keep the agent to some documents.

To show where an answer is, add `CITATIONS` to the agent's instructions: it then cites each
sentence with `<cite doc="report.pdf" page="12"/>`. `CITATIONS_WITH_BOXES` adds
`box="x0,y0,x1,y1"`, the part of the page the answer is in; the agent looks at each page it cites,
so it takes a few seconds more. `client.citations(answer)` returns each citation's document, `document_id`,
page and box; `client.documents.page_image(c.document_id, c.page, box=c.box)` gives that part.

The same tools are an MCP server at `https://api.search.pageindex.ai/mcp`
(header `Authorization: Bearer <key>`; it sees all your documents, and
`?collection=col_...` or `?filter=<JSON>` keeps it to some). With OpenAI's Responses API, give it
as a tool of type `mcp` with that URL and header; the model then gets each tool's text only, not
the pictures of pages, so for scans, charts and maps run the tools yourself with `client.tools()`
instead. Locally, for Claude Desktop,
`pageindex-search-mcp` (from `pip install "pageindex-search[mcp]"`) serves them
over stdio. See https://search.pageindex.ai/docs/agent-tools and
https://search.pageindex.ai/docs/mcp.

## Documents

Upload a document once and search it as often as you like. Store `doc.id` and
search by it later; there is no need to upload again:

```python
client.search(["What was capital expenditure?"], document="doc_60a07bcef3094b68806070f2")

for doc in client.documents.list():  # every document, newest first
    print(doc.id, doc.filename, doc.pages, doc.status)

client.documents.get("doc_60a07bcef3094b68806070f2")
client.documents.delete("doc_60a07bcef3094b68806070f2")
```

- A PDF may be up to 100 MB and 5,000 pages. `upload` takes a path, bytes or
  an open file (JavaScript: a path, `Uint8Array`, `ArrayBuffer` or `Blob`).
- Uploading the same file again, to the same collection (or to none), returns the same document, with `duplicate` set.
- Documents are kept until you delete them. Uploads are free; storage is free
  up to 10,000 pages, then billed from your credit.
- `upload(..., wait=False)` returns at once, maybe still `processing`;
  `client.documents.wait(doc)` waits for it. A search of a document still
  processing waits for it too, up to a minute.

## Many documents

When the user has many documents and does not know which one answers, leave
out the document: the documents likely to answer are found first, then the
most relevant (3, or `max_documents` up to 10) are opened and their pages
searched. `document_top_k` opens exactly that many instead, however likely:

```python
result = client.search("What was capital expenditure in 2018?")  # every document
for doc in result.results[0].documents:  # most relevant first
    print(doc.filename, doc.relevance, [p.page for p in doc.pages])
```

JavaScript: `client.search(questions, { documentIds, collection, filter, maxDocuments })`.
It looks at every document of the user's, up to 1,000, those in `documents`
(JavaScript: `documentIds`), those in a collection, or those meeting a filter;
each document searched is priced as a document, plus the pages of those opened.
To find only which documents answer, use `client.documents.search`.

Group documents into collections to search within one:

```python
reports = client.collections.create("Annual reports")
client.documents.upload("report.pdf", collection=reports)
client.collections.add(reports, [doc])  # or move existing documents in
client.search("What was revenue?", collection=reports)
```

A document is in at most one collection, as a file in a folder: adding it to one
moves it there, and deleting a collection deletes its documents. The same file
uploaded into another collection is another document there.

Documents can carry metadata (strings, numbers and booleans, at most 16 keys)
to list and search by:

```python
client.documents.upload("report.pdf", metadata={"ticker": "MMM", "year": 2018})
client.documents.list(filter={"year": {"$gte": 2020}})
client.search("Who is the CEO?", filter={"ticker": {"$in": ["MMM", "AAPL"]}})
```

Filters take `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$and`
and `$or`; compare dates stored as numbers, such as 20240301.

Metadata is not declared ahead. Before writing a filter, see which keys exist,
their types and values: `client.collections.schema(collection)` or
`client.documents.schema(filter=...)` (JavaScript: `documents.schema({ filter })`).

## Cost

A search costs the document's pages times a per-page price that falls with
each extra question in the same search, so ask a document's questions together;
a search of documents is priced per document. See
https://search.pageindex.ai/pricing.
Every response reports `usage.cost_usd` and the credit left in
`usage.balance_usd`. `client.usage()` returns cost by day. A search the client
retries is not charged twice.

## Handle failures

Errors raise (JavaScript: reject with) `PageSearchError`, with `status`, `code`,
`message` and `request_id` (`requestId`). The codes to handle:

- `invalid_api_key` (401): the key is missing, wrong or revoked.
- `insufficient_balance` (402): add credit at https://search.pageindex.ai/console/billing.
- `document_not_found` (404), `document_failed`: check the id, or upload the file again.
- `document_exists` (409): the collection a document is moved to already has the same file.
- `encrypted_pdf`, `unreadable_pdf`, `not_a_pdf`, `file_too_large`, `too_many_pages`: the file cannot be used as it is.
- `invalid_request` (422): no questions, more than 10, or one over 2,000 characters.
- `rate_limited`, `too_many_at_once` (429): wait the seconds in `Retry-After`
  and try again. One key may make 20 requests a second and have 10 searches
  and 5 uploads under way at once.
- `search_unavailable` (503): try again.

The client already retries 429 and 5xx responses a couple of times. Preserve
errors rather than inventing answers.

Validate the integration with one small search. If no API key is available,
say that the search was not tested.

## Further documentation

- [Overview](https://search.pageindex.ai/docs)
- [Quick start](https://search.pageindex.ai/docs/quick-start)
- [Concepts](https://search.pageindex.ai/docs/concepts)
- [Search pages](https://search.pageindex.ai/docs/search-pages)
- [Organize documents](https://search.pageindex.ai/docs/organize)
- [Search documents](https://search.pageindex.ai/docs/search-documents)
- [Agent tools](https://search.pageindex.ai/docs/agent-tools)
- [MCP](https://search.pageindex.ai/docs/mcp)
- [API reference](https://search.pageindex.ai/docs/api)
- [Pricing](https://search.pageindex.ai/pricing)
- [Benchmark](https://github.com/VectifyAI/page-search-benchmark)
