Metadata-Version: 2.5
Name: pageindex-search
Version: 0.7.1
Summary: Python client for Page Search API by PageIndex: find the pages of a PDF that answer your questions.
Project-URL: Homepage, https://search.pageindex.ai
Project-URL: Documentation, https://search.pageindex.ai/docs
Project-URL: Repository, https://github.com/VectifyAI/pageindex-search-python
Project-URL: Issues, https://github.com/VectifyAI/pageindex-search-python/issues
Author-email: PageIndex <support@pageindex.ai>
License-Expression: MIT
License-File: LICENSE
Keywords: llm,pageindex,pdf,rag,retrieval,search
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.9
Requires-Dist: httpx>=0.24
Provides-Extra: agents
Requires-Dist: openai-agents>=0.24; (python_version >= '3.10') and extra == 'agents'
Description-Content-Type: text/markdown

# pageindex-search

Python client for [Page Search API](https://search.pageindex.ai) by PageIndex. Upload a PDF, ask up to 10
questions at once, and get back the pages that answer each one, ranked by relevance. Every page is read.

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

Create an API key in the [console](https://search.pageindex.ai/console/keys) and set it:

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

## Quick start

```python
from pageindex_search import PageSearch

client = PageSearch()

doc = client.documents.upload("3M_2018_10K.pdf")
result = client.search(["What was capital expenditure in 2018?", "Who is the CEO?"], document=doc)

for answer in result.results:
    print(answer.question)
    for page in answer.pages:
        print(f"  page {page.page}: {page.relevance}")

print(f"cost ${result.usage.cost_usd}, credit left ${result.usage.balance_usd}")
```

Upload a document once and search it as often as you like; uploading the same file again returns the same
document. Documents are kept until you delete them:

```python
for doc in client.documents.list():  # every document, newest first, a page at a time
    print(doc.id, doc.filename, doc.pages)

client.search(["What was revenue in 2018?"], document="doc_60a07bcef3094b68806070f2")  # search one again by its id
client.documents.delete("doc_60a07bcef3094b68806070f2")
```

With many documents, search across them: the documents likely to answer are found first, then their pages.

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

To find only which documents answer, use `client.documents.search`.

Group documents into collections, and search within one:

```python
reports = client.collections.create("Annual reports")
client.documents.upload("3M_2018_10K.pdf", collection=reports)  # or later: client.collections.add(reports, [doc])
result = client.search("What was capital expenditure in 2018?", collection=reports)
```

A document may be in many collections; deleting a collection leaves its documents.

Give documents your own metadata (strings, numbers and booleans) and filter by it:

```python
doc = client.documents.upload("3M_2018_10K.pdf", metadata={"ticker": "MMM", "year": 2018})
client.documents.update(doc, metadata={"audited": True})          # None removes a key
recent = client.documents.list(filter={"year": {"$gte": 2020}})
result = client.search("Who is the CEO?", filter={"ticker": {"$in": ["MMM", "AAPL"]}})
```

Filters take `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$and` and `$or`.

Read pages: their text, or a page as a picture. Each page a search lists says how it is best read (`page.read`:
`"text"`, or `"image"` for a scan or a chart, whose text may be incomplete); `include_text=True` lists it with its
text.

```python
texts = client.documents.pages(doc, [38, 39])   # each with .text and .read
png = client.documents.page_image(doc, 38)       # PNG bytes
part = client.documents.page_image(doc, 38, box=(0, 0.3, 1, 0.6))  # a band of it, sharper
```

## Agents

`client.tools()` gives an LLM agent six tools over your documents: `list_documents` (free: documents whose metadata
meets a filter, with your metadata's keys and values in its description, so the agent can write filters),
`search_documents` (which documents answer a question, for when a filter cannot pick them), `search_pages` (with each
page's text),
`read_pages` (text, with a picture of any page that is one), `view_pages` (pages as pictures) and `zoom_page`
(part of a page, sharper, for small print).

```python
tools = client.tools(collection=reports)  # or filter=..., or neither for all your documents

response = anthropic.messages.create(model=..., tools=tools.definitions, messages=messages)
for block in response.content:
    if block.type == "tool_use":
        content = tools.run(block.name, block.input).anthropic()  # the tool_result content
```

For OpenAI, pass `tools.openai`; `tools.run()` gives `.text` and `.images`, each image with `.data_url`. With the
OpenAI Agents SDK (`pip install "pageindex-search[agents]"`), `tools.openai_agents()` gives the tools ready to use:

```python
from agents import Agent, Runner

agent = Agent(name="Reports", instructions="Answer from the documents.", tools=tools.openai_agents())
print(Runner.run_sync(agent, "What was capital expenditure in 2018?").final_output)
```

## Reference

| | |
| --- | --- |
| `PageSearch(api_key=None, base_url=None, timeout=120, max_retries=2)` | The key defaults to `PAGEINDEX_SEARCH_API_KEY`. |
| `documents.upload(file, collection=None, metadata=None, wait=True)` | A path, bytes or an open file. Returns a `Document`, ready to search unless `wait=False`. |
| `search(questions, document=None, documents=None, collection=None, filter=None, max_documents=None, document_top_k=None, top_k=None, include_text=False, idempotency_key=None)` | Up to 10 questions. For each, the pages that help, under the documents they are in: of one document, or across documents (those named, a collection's, those meeting `filter`, or all), whose most relevant are opened: those likely to answer, at most `max_documents` (default 3), or exactly `document_top_k`. Each document lists the pages likely to help, or exactly `top_k`, each with how it is best read, and with `include_text`, its text; without `top_k`, `more_pages` lists the next most relevant, without text. A search retried after a timeout is not charged twice. |
| `documents.search(questions, document_ids=None, collection=None, filter=None, top_k=None, idempotency_key=None)` | Up to 10 questions of your documents (all of them, up to 1,000, those named, or a collection's). Each answer lists the documents likely to hold it, or exactly `top_k`. Each document is priced at the document search price. |
| `documents.list(limit=20, offset=0, collection=None, filter=None)` | Your documents, newest first, or a collection's, or those whose metadata meets `filter`: `limit` of them after the first `offset`. Loop over it for every document; `.data`, `.has_more` and `.total` (how many in all) are one page. |
| `documents.get(id)`, `documents.delete(document)` | One document; delete one. |
| `documents.pages(document, pages)`, `documents.page_image(document, page, box=None)` | The text of up to 20 pages, each with how it is best read; a page as PNG bytes, or with `box` (x0, y0, x1, y1 as fractions of the page from its top left) part of it, sharper. |
| `tools(collection=None, filter=None)` | Tools for an LLM agent: `.definitions` (Anthropic), `.openai`, `.openai_agents()` (OpenAI Agents SDK), and `.run(name, input)`. |
| `documents.update(document, metadata=)` | Set metadata keys; `None` removes one. |
| `collections.schema(collection)`, `documents.schema(collection=None, filter=None)` | The metadata keys the documents have: each key's types, how many documents have it, its least and greatest numbers and commonest strings. Counted as they are now; nothing is enforced. |
| `collections.create(name)`, `.list()`, `.get(id)`, `.rename(collection, name)`, `.delete(collection)` | Named groups of your documents. Deleting one leaves its documents. |
| `collections.add(collection, documents)`, `.remove(collection, document)` | Put documents in a collection, or take one out. `documents.upload(..., collection=)` puts a new one in. |
| `usage(start=None, end=None)` | Uploads, searches and cost by day, your credit and stored pages. |

In 0.4, `search(document, questions)` became `search(questions, document=document)`, and each answer lists
documents, each with its pages (`answer.pages` is the first document's). Before 0.2 these were `upload()`, `document()`, `documents()`, `iter_documents()` and `delete()` on the client;
they still work, with a warning, for now.

Errors raise `PageSearchError`, with the API's `status`, `code`, `message` and `request_id`.

See the [API reference](https://search.pageindex.ai/docs/api) for every field and the
[pricing](https://search.pageindex.ai/pricing).
