> ## Documentation Index
> Fetch the complete documentation index at: https://docs.raily.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK

> Search your data from Python with a typed client.

## Install

```bash theme={null}
pip install raily-ai
```

Python 3.10+. The import name is `raily`:

```python theme={null}
from raily import Raily, AsyncRaily, RailyError, RailyAuthError
```

## Quickstart

```python theme={null}
import os
from raily import Raily

client = Raily(
    api_key=os.environ["RAILY_API_KEY"],
    endpoint=os.environ["RAILY_ENDPOINT"],
)

for hit in client.search("your search query", limit=5):
    print(f"{hit.score:.2f}  {hit.fields.get('title', '(untitled)')}")
```

Get the endpoint URL and key from your endpoint's **API access** panel — see the
[SDK quickstart](/connect/sdk).

## `Raily(...)`

```python theme={null}
client = Raily(
    api_key="rly_...",      # required
    endpoint="https://...", # required — your endpoint URL
    timeout=60.0,           # optional, seconds
)
```

| Parameter  | Type    | Default | Description                      |
| ---------- | ------- | ------- | -------------------------------- |
| `api_key`  | `str`   | —       | The API key for your endpoint.   |
| `endpoint` | `str`   | —       | Your endpoint URL.               |
| `timeout`  | `float` | `60.0`  | Per-request timeout, in seconds. |

The client holds no session state — construct it once and reuse it.

## `search(query, limit=3)`

```python theme={null}
results = client.search("your search query", limit=5)
```

| Parameter | Type   | Default | Description                                                                      |
| --------- | ------ | ------- | -------------------------------------------------------------------------------- |
| `query`   | `str`  | —       | What to search for.                                                              |
| `limit`   | `int`  | `3`     | Max results to return (up to 10; some endpoints cap lower). Fewer may come back. |
| `explain` | `bool` | `False` | Return a `SearchResult` (`hits` + `info`) instead of a bare list.                |

Returns a `list[SearchHit]` — or a `SearchResult` when `explain=True` (see
[Debugging empty results](#debugging-empty-results)).

### Debugging empty results

A search can legitimately return nothing — and a bare `[]` doesn't say why. Pass
`explain=True` to get the endpoint's own message plus a hint:

```python theme={null}
res = client.search("articles", explain=True)
print(res.info.count)    # 0
print(res.info.message)  # the endpoint's summary, e.g. "No results found"
print(res.info.note)     # why, and what to try (only set when there are no hits)
for hit in res:          # SearchResult is iterable over hits
    ...
```

Or have the client log a one-line hint on every empty result during development:

```python theme={null}
client = Raily(api_key="rly_...", endpoint="https://...", debug=True)
# logs to the "raily" logger when a search returns nothing
```

## SearchHit

| Field               | Type             | Description                                                                       |
| ------------------- | ---------------- | --------------------------------------------------------------------------------- |
| `score`             | `float \| None`  | Relevance score (higher is better).                                               |
| `fields`            | `dict[str, str]` | Your source's display fields as a flat `{name: value}` map (see below).           |
| `text`              | `str \| None`    | A plain-text summary of the result, when available.                               |
| `image_url`         | `str \| None`    | The result's image, for image sources (otherwise `None`).                         |
| `is_relevant`       | `bool \| None`   | `False` for a lower-confidence result returned when there are few strong matches. |
| `source_collection` | `str \| None`    | The source the result came from.                                                  |
| `id`                | `str \| None`    | Result id, when the source provides one.                                          |
| `raw`               | `dict`           | The full original result, for anything not surfaced above.                        |

### Working with `fields`

`fields` is a flat `{name: value}` map of the display fields configured on your source —
e.g. `title`, `text`, `author`, `published_date`. Read a value by name:

```python theme={null}
for hit in client.search("your search query", limit=5):
    print(hit.fields.get("title"), "—", hit.fields.get("published_date"))
```

A field with multiple values is joined with `", "`. To see what your source exposes, print
the whole map once:

```python theme={null}
for name, value in client.search("your search query", limit=1)[0].fields.items():
    print(name, "=", value)
```

Need the full original result (every field, untrimmed)? It's on `hit.raw`.

## Errors

```python theme={null}
from raily import Raily, RailyError, RailyAuthError

try:
    hits = client.search("your search query")
except RailyAuthError:
    ...  # key missing, invalid, expired, or not valid for this endpoint
except RailyError as exc:
    ...  # network / server / timeout
    print(f"Search failed: {exc}")
```

| Exception        | When                                                        | What to do                                                      |
| ---------------- | ----------------------------------------------------------- | --------------------------------------------------------------- |
| `RailyAuthError` | Key missing, invalid, expired, or for a different endpoint. | Check `RAILY_API_KEY` and that it was issued for this endpoint. |
| `RailyError`     | Network, server, or timeout.                                | Retry with backoff; raise `timeout` for slow networks.          |

`RailyAuthError` is a subclass of `RailyError`, so catch it first.

## Async

`AsyncRaily` has the same surface with `await`:

```python theme={null}
import asyncio
from raily import AsyncRaily

async def main():
    client = AsyncRaily(api_key="rly_...", endpoint="https://...")
    hits = await client.search("your search query", limit=5)
    print(len(hits))

asyncio.run(main())
```

Run many searches concurrently:

```python theme={null}
queries = ["batteries", "climate", "elections"]
results = await asyncio.gather(*(client.search(q, limit=3) for q in queries))
```

## Recipe: a search endpoint with FastAPI

```python theme={null}
import os
from fastapi import FastAPI
from raily import AsyncRaily, RailyError

app = FastAPI()
raily = AsyncRaily(
    api_key=os.environ["RAILY_API_KEY"],
    endpoint=os.environ["RAILY_ENDPOINT"],
)

@app.get("/search")
async def search(q: str, limit: int = 5):
    try:
        hits = await raily.search(q, limit=limit)
    except RailyError:
        return {"results": []}
    return {
        "results": [
            {"score": h.score, "fields": h.fields, "source": h.source_collection}
            for h in hits
        ]
    }
```

## Troubleshooting

<AccordionGroup>
  <Accordion title="No results came back (empty list)">
    An empty list is a valid answer, not an error. The usual causes, in order:

    1. **The query is too generic.** Category words like `"articles"`, `"news"`, or `"posts"`
       carry no topical signal, so nothing matches strongly enough to return. Search a
       *specific topic* (`"battery recycling"`, not `"articles"`).
    2. **A freshness filter.** Queries that imply recency (`"latest"`, `"news"`) apply a
       `published_date` cutoff — if your source's data is older, that can remove everything.
       Drop the recency words or widen the range.
    3. **Your source has no data yet.** A brand-new source may not have finished indexing yet.
       Search a term you *know* is in the data; if that's also empty, confirm the source has
       finished indexing in the Raily app.

    Use `explain=True` to see which it is:

    ```python theme={null}
    res = client.search("articles", explain=True)
    print(res.info.count, res.info.message, res.info.note)
    ```
  </Accordion>

  <Accordion title="“API key was rejected for this endpoint” (RailyAuthError)">
    The key is missing, wrong, expired, or was issued for a different endpoint. A key only
    works against the endpoint it was created on. Re-check `RAILY_API_KEY` and `RAILY_ENDPOINT`.
  </Accordion>

  <Accordion title="Searches are slow or time out">
    Raise the `timeout` (seconds) and retry. Reuse one client rather than constructing one
    per request.
  </Accordion>
</AccordionGroup>
