Skip to main content

Install

pip install raily-ai
Python 3.10+. The import name is raily:
from raily import Raily, AsyncRaily, RailyError, RailyAuthError

Quickstart

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.

Raily(...)

client = Raily(
    api_key="rly_...",      # required
    endpoint="https://...", # required — your endpoint URL
    timeout=60.0,           # optional, seconds
)
ParameterTypeDefaultDescription
api_keystrThe API key for your endpoint.
endpointstrYour endpoint URL.
timeoutfloat60.0Per-request timeout, in seconds.
The client holds no session state — construct it once and reuse it.

search(query, limit=3)

results = client.search("your search query", limit=5)
ParameterTypeDefaultDescription
querystrWhat to search for.
limitint3Max results to return (1–10). Fewer may come back.
explainboolFalseReturn 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

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:
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:
client = Raily(api_key="rly_...", endpoint="https://...", debug=True)
# logs to the "raily" logger when a search returns nothing

SearchHit

FieldTypeDescription
scorefloat | NoneRelevance score (higher is better).
fieldsdict[str, str]Your source’s display fields as a flat {name: value} map (see below).
textstr | NoneA rendered text summary, when available.
image_urlstr | NoneThe result’s image, for image sources (otherwise None).
is_relevantbool | NoneFalse for an exploratory hit shown below the relevance threshold.
source_collectionstr | NoneThe source the result came from.
idstr | NoneResult id, when the source provides one.
rawdictThe 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:
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:
for name, value in client.search("your search query", limit=1)[0].fields.items():
    print(name, "=", value)
Need the original structured payload (render types, individual items)? It’s on hit.raw.

Errors

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}")
ExceptionWhenWhat to do
RailyAuthErrorKey missing, invalid, expired, or for a different endpoint.Check RAILY_API_KEY and that it was issued for this endpoint.
RailyErrorNetwork, 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:
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:
queries = ["batteries", "climate", "elections"]
results = await asyncio.gather(*(client.search(q, limit=3) for q in queries))

Recipe: a search endpoint with FastAPI

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

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 scores above the relevance threshold. Search a specific topic ("battery recycling", not "articles").
  2. A freshness filter. Queries that imply recency ("latest", "news") apply a published_date cutoff — on an older corpus that can remove everything. Drop the recency words or widen the range.
  3. The corpus is empty. A brand-new source may have no documents indexed yet. Search a term you know is in the data; if that’s also empty, check ingestion/embedding.
Use explain=True to see which it is:
res = client.search("articles", explain=True)
print(res.info.count, res.info.message, res.info.note)
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.
Raise the timeout (seconds) and retry. Reuse one client rather than constructing one per request.