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

# Searching the index

> Text search, hard filters, similar-image expansion, file delivery, and the patterns that work — with the exact request and response shapes.

Four endpoints, all JSON. This page walks each one with real shapes, then the patterns agents get the most from.

<Note>
  Every response body below is verbatim from the live API on 2026-09-13, serving the full 10.2M-image index. `title` is `null` when the source's own title was a camera filename or similar junk; the file is still downloadable and the rights are complete.
</Note>

## 1. Text search

`POST /v1/search`. Describe what you need. Two retrieval lanes run in parallel — a keyword lane over the asset's normalized metadata and a visual lane over the image itself — and a multimodal reranker orders the merged candidates by looking at the actual pixels.

<CodeGroup>
  ```bash cURL theme={null}
  curl -s -X POST https://api.lightdrift.ai/v1/search \
    -H "X-API-Key: $LIGHTDRIFT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "query": "monarch butterfly on milkweed",
      "k": 2,
      "filters": { "commercial": true, "orientation": "landscape", "min_width": 1024 }
    }'
  ```

  ```python Python theme={null}
  import requests

  r = requests.post(
      "https://api.lightdrift.ai/v1/search",
      headers={"X-API-Key": LIGHTDRIFT_API_KEY},
      json={
          "query": "monarch butterfly on milkweed",
          "k": 2,
          "filters": {"commercial": True, "orientation": "landscape", "min_width": 1024},
      },
      timeout=180,
  )
  r.raise_for_status()
  for hit in r.json()["results"]:
      print(hit["score"], hit["title"], hit["rights"]["license"], hit["file"])
  ```

  ```javascript JavaScript theme={null}
  const r = await fetch("https://api.lightdrift.ai/v1/search", {
    method: "POST",
    headers: { "X-API-Key": process.env.LIGHTDRIFT_API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({
      query: "monarch butterfly on milkweed",
      k: 5,
      filters: { commercial: true, orientation: "landscape", min_width: 1024 },
    }),
  });
  const { query_id, results } = await r.json();
  ```
</CodeGroup>

Response:

```json theme={null}
{
  "query_id": "q_38c916f3f99742e6",
  "results": [
    {
      "asset_id": "govflickr:8412901414",
      "score": 0.7526,
      "title": null,
      "source": "govflickr",
      "width": 1024,
      "height": 768,
      "file": "https://api.lightdrift.ai/v1/asset/q_38c916f3f99742e6/govflickr:8412901414",
      "thumb": "https://api.lightdrift.ai/v1/asset/q_38c916f3f99742e6/govflickr:8412901414?v=thumb",
      "rights": {
        "license": "cc-by",
        "license_verbatim": "CC BY 2.0",
        "commercial": true,
        "attribution_required": true,
        "derivatives": true,
        "share_alike": false,
        "attribution": "\"govflickr:8412901414\" by USDA, govflickr, CC BY 4.0/3.0/2.0",
        "provenance_url": "https://www.flickr.com/photos/41284017@N08/8412901414",
        "basis": "as-declared by source; verify for critical use"
      }
    },
    {
      "asset_id": "govflickr:53305480173",
      "score": 0.6876,
      "title": "20190601-NRCS-MC-0003",
      "source": "govflickr",
      "width": 1024,
      "height": 839,
      "file": "https://api.lightdrift.ai/v1/asset/q_38c916f3f99742e6/govflickr:53305480173",
      "thumb": "https://api.lightdrift.ai/v1/asset/q_38c916f3f99742e6/govflickr:53305480173?v=thumb",
      "rights": {
        "license": "pdm",
        "license_verbatim": "Public Domain Mark",
        "commercial": true,
        "attribution_required": false,
        "derivatives": true,
        "share_alike": false,
        "attribution": null,
        "provenance_url": "https://www.flickr.com/photos/41284017@N08/53305480173",
        "basis": "as-declared by source; verify for critical use"
      }
    }
  ],
  "latency_ms": 2061,
  "timing_ms": {
    "embed": 644,
    "retrieve": 194,
    "rerank": 1221
  },
  "ranking": "rerank",
  "pool_size": 149,
  "reranked": 149
}
```

Every response also carries `timing_ms` (embed, retrieve, rerank in milliseconds), `mode` (what you asked for), `ranking` (what actually ordered the results: the mode, or `none (degraded)` if the reranker was unavailable), `pool_size` (distinct candidates from both lanes) and `reranked` (how many the reranker scored). Use them to tune the fields below per query type instead of guessing.

Measured quality per mode, 200 hard eval queries judged by a vision model on the top-5 (2026-09-13):

| mode     | satisfied\@5 | relevant\@1 | precision\@5 | typical latency |
| -------- | ------------ | ----------- | ------------ | --------------- |
| `none`   | 60.5%        | 41.5%       | 33.0%        | 0.3–0.6 s       |
| `visual` | 70.5%        | 60.0%       | 47.6%        | 1.0–1.6 s warm  |

Measured trade-offs of the reranker parameters, 60 hard queries each, same judge (2026-09-13, three queries in flight so latencies are higher than a lone query):

| setting                           | satisfied\@5 | relevant\@1 | precision\@5 | latency p50 |
| --------------------------------- | ------------ | ----------- | ------------ | ----------- |
| default (150 candidates, text on) | 71.7%        | 66.7%       | 48.3%        | 4.7 s       |
| `rerank_text: false`              | 68.3%        | 61.7%       | 45.0%        | 3.6 s       |
| `rerank_k: 60`                    | 71.7%        | 61.7%       | 45.3%        | 1.7 s       |
| `rerank_k: 40`                    | 65.0%        | 58.3%       | 43.3%        | 1.5 s       |

Sixty queries per row means differences under about 6 points are noise. `rerank_k: 60` is the notable one: same satisfaction as the default at a third of the latency.

The parameters are independent of each other. `mode` picks the reranker; `k` picks how many results come back; `ann_k` and `bm25_k` shape the candidate pool (`ann_ef` is accepted for compatibility and currently ignored); `rerank_k` and `rerank_text` bound the reranker's work. `GET /v1/health` reports the server's current defaults under `defaults` and the available modes under `modes`.

Rough cost model, measured 2026-09-14: retrieval \~0.1 s regardless of pool depth up to a few hundred; the visual reranker \~8 ms per candidate on its GPU plus the thumbnail fetch (0.2–0.6 s for 150 candidates, less for fewer); `rerank_text: false` cuts reranker tokens by about 60%.

### Request fields

| Field            | Type               | Default  | Notes                                                                                                                                                                                                                                                                                                     |
| ---------------- | ------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `query`          | string             | required | Natural language. `q` is accepted as an alias.                                                                                                                                                                                                                                                            |
| `k`              | integer 1–100      | 10       | Results to return. `top_k` is accepted as an alias.                                                                                                                                                                                                                                                       |
| `ann_k`          | integer 1–1000     | 100      | Visual-lane candidate depth. Raise for aesthetic or "vibe" queries where the words don't name the subject.                                                                                                                                                                                                |
| `bm25_k`         | integer 0–1000     | 50       | Keyword-lane candidate depth. Raise for precise subjects, species, places and names. `0` disables the keyword lane.                                                                                                                                                                                       |
| `ann_ef`         | integer 64–4096    | 2048     | Accepted for compatibility; has no effect on the current retrieval store.                                                                                                                                                                                                                                 |
| `mode`           | `none` \| `visual` | `visual` | **Which reranker runs, and nothing else.** `none` returns lane order (best rank in either lane) with no GPU call, about 0.5–0.7 s end to end. `visual` runs the multimodal reranker over the candidates. A text-only reranker (`text`) is coming. `rerank: false` is a deprecated alias for `mode: none`. |
| `rerank_k`       | integer 1–500      | 60       | Rerank only the best N candidates in lane order. The reranker costs \~3.5 ms per candidate, so 150 → 60 saves \~0.3 s. Measured: cutting to 60+30 lost 17% of top-10 results on our eval, so measure for your queries.                                                                                    |
| `rerank_text`    | boolean            | true     | `false` makes the reranker judge pixels only, ignoring titles and metadata. Faster, and immune to noisy metadata.                                                                                                                                                                                         |
| `explain`        | boolean            | false    | Attach `lanes` to every result: its rank and score in each lane plus the rerank score. Always on when `rerank` is `false`.                                                                                                                                                                                |
| `filters`        | object             | `{}`     | Hard constraints, below.                                                                                                                                                                                                                                                                                  |
| `client_session` | string             | —        | Optional. Link related queries from one agent session.                                                                                                                                                                                                                                                    |

### Filters

Every filter is a hard constraint applied before ranking; omitted filters do not constrain.

| Filter                     | Type                                  | Meaning                                                                                                                                                                                                                                      |
| -------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `commercial`               | boolean                               | Only assets whose declared license permits commercial use. **Set this if your output is commercial.**                                                                                                                                        |
| `attribution_required`     | boolean                               | Only assets that do (`true`) or do not (`false`) require attribution.                                                                                                                                                                        |
| `derivatives`              | boolean                               | Only assets whose license permits derivative works.                                                                                                                                                                                          |
| `license_id`               | string\[]                             | Restrict to these licenses: `cc0`, `pdm`, `us-gov-pd`, `cc-by`, `cc-by-sa`, `cc-by-nd`, `cc-by-nc`, `cc-by-nc-sa`, `cc-by-nc-nd`, `mit`, `apache-2.0`, `isc`, `ofl`.                                                                         |
| `source`                   | string\[]                             | Restrict to these sources: `inat`, `yfcc`, `flickr`, `wikimedia`, `wm_quality`, `smithsonian`, `nasa`, `govflickr`, …                                                                                                                        |
| `min_width` / `min_height` | integer                               | Minimum pixel dimensions of the full-resolution file.                                                                                                                                                                                        |
| `orientation`              | `landscape` \| `portrait` \| `square` | Aspect class.                                                                                                                                                                                                                                |
| `format`                   | string\[]                             | e.g. `["jpeg", "png"]`.                                                                                                                                                                                                                      |
| `year_min` / `year_max`    | integer                               | Capture or creation year, where the source declares one.                                                                                                                                                                                     |
| `monochrome`               | boolean                               | Only (or never) monochrome images.                                                                                                                                                                                                           |
| `colors`                   | string\[]                             | Dominant named colours: `black`, `white`, `gray`, `red`, `orange`, `yellow`, `green`, `teal`, `blue`, `navy`, `purple`, `pink`, `brown`, `beige`, `cream`. Present only where an image has a clear dominant palette (roughly 40% of assets). |
| `ai_generated`             | boolean                               | Exclude (`false`) or select (`true`) assets the source marks as AI-generated.                                                                                                                                                                |
| `nsfw_max`                 | number 0–1                            | Maximum permitted NSFW score. Default `0.2`; assets without a score pass.                                                                                                                                                                    |

## 2. Similar images

`POST /v1/similar`. Expand from any asset you already have — usually the best hit from a search. Same filters, same response envelope.

```bash theme={null}
curl -s -X POST https://api.lightdrift.ai/v1/similar \
  -H "X-API-Key: $LIGHTDRIFT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "asset_id": "govflickr:8412901414", "k": 2, "filters": { "commercial": true } }'
```

Response:

```json theme={null}
{
  "query_id": "q_61754b7b029a425b",
  "results": [
    {
      "asset_id": "govflickr:12837419013",
      "score": 0.9907,
      "title": null,
      "source": "govflickr",
      "width": 640,
      "height": 480,
      "file": "https://api.lightdrift.ai/v1/asset/q_61754b7b029a425b/govflickr:12837419013",
      "thumb": "https://api.lightdrift.ai/v1/asset/q_61754b7b029a425b/govflickr:12837419013?v=thumb",
      "rights": {
        "license": "cc-by",
        "license_verbatim": "CC BY 2.0",
        "commercial": true,
        "attribution_required": true,
        "derivatives": true,
        "share_alike": false,
        "attribution": "\"govflickr:12837419013\" by USDA, govflickr, CC BY 4.0/3.0/2.0",
        "provenance_url": "https://www.flickr.com/photos/41284017@N08/12837419013",
        "basis": "as-declared by source; verify for critical use"
      }
    },
    {
      "asset_id": "inat:48513215",
      "score": 0.9578,
      "title": "Danaus plexippus",
      "source": "inat",
      "width": 867,
      "height": 1024,
      "file": "https://api.lightdrift.ai/v1/asset/q_61754b7b029a425b/inat:48513215",
      "thumb": "https://api.lightdrift.ai/v1/asset/q_61754b7b029a425b/inat:48513215?v=thumb",
      "rights": {
        "license": "cc-by",
        "license_verbatim": "CC-BY",
        "commercial": true,
        "attribution_required": true,
        "derivatives": true,
        "share_alike": false,
        "attribution": "\"Danaus plexippus\" by Shirley Zundell, inat, CC BY 4.0/3.0/2.0",
        "provenance_url": "https://www.inaturalist.org/photos/48513215",
        "basis": "as-declared by source; verify for critical use"
      }
    }
  ],
  "latency_ms": 61,
  "timing_ms": {
    "retrieve": 61
  },
  "ranking": "lanes",
  "pool_size": 99,
  "reranked": 0
}
```

Returns `404 {"detail": "asset has no embedding"}` for an `asset_id` that isn't in the index.

## 3. One asset's metadata and rights

`GET /v1/asset/{asset_id}`. The same rights object as a search result plus `format`, without running a search. Use it to re-check rights before you publish, or to resolve an id you stored earlier.

```bash theme={null}
curl -s https://api.lightdrift.ai/v1/asset/govflickr:8412901414 \
  -H "X-API-Key: $LIGHTDRIFT_API_KEY"
```

```json theme={null}
{
  "asset_id": "govflickr:8412901414",
  "title": "d2664-1",
  "source": "govflickr",
  "width": 1024,
  "height": 768,
  "format": "JPEG",
  "file": "https://api.lightdrift.ai/v1/asset/direct/govflickr:8412901414",
  "thumb": "https://api.lightdrift.ai/v1/asset/direct/govflickr:8412901414?v=thumb",
  "rights": {
    "license": "cc-by",
    "license_verbatim": "CC BY 2.0",
    "commercial": true,
    "attribution_required": true,
    "derivatives": true,
    "share_alike": false,
    "attribution": "\"d2664-1\" by USDA, govflickr, CC BY 4.0/3.0/2.0",
    "provenance_url": "https://www.flickr.com/photos/41284017@N08/8412901414",
    "basis": "as-declared by source; verify for critical use"
  }
}
```

## 4. Getting the file

`file` and `thumb` are **tracked URLs**. A `GET` on either answers `302 Found` with a signed download link that is valid for one hour. Follow redirects; don't store the signed link, store `asset_id` and the tracked URL.

```bash theme={null}
# full resolution
curl -L -o monarch.jpg "https://api.lightdrift.ai/v1/asset/q_38c916f3f99742e6/govflickr:8412901414"
# 512px preview
curl -L -o monarch_thumb.jpg "https://api.lightdrift.ai/v1/asset/q_38c916f3f99742e6/govflickr:8412901414?v=thumb"
```

The `query_id` segment is how a download is attributed to the query that produced it. URLs from `/v1/asset/{asset_id}` use `direct` in that position instead.

## 5. Warming the GPU services

The embedding and reranking models run on GPUs that shut down after an hour without requests. The first request after that restarts them from a memory snapshot, about 7 seconds, occasionally two to three minutes during beta when no snapshot exists yet for the machine handed to us. `GET /v1/warm` (with your key) starts both warming in the background and reports their state; call it when an agent session begins and poll every 10 s until `ready` is true.

```bash theme={null}
curl -s https://api.lightdrift.ai/v1/warm -H "X-API-Key: $LIGHTDRIFT_API_KEY"
```

```json theme={null}
{"embed": {"state": "warm", "took_s": 161.2, "checked": "2026-09-13T19:05:25Z"}, "rerank": {"state": "warming", "for_s": 42.0}, "ready": false, "hint": "poll every 10 s until ready; a cold start takes 2.5-4 min"}
```

`state` is one of `warm`, `warming`, `error`, `unknown`. A search sent while a service is warming waits for it rather than failing.

## 6. Health

`GET /v1/health` needs no key and reports the serving stack plus the last-known state of the GPU services (`upstream`), so you can pin a result to the exact configuration that produced it.

```json theme={null}
{"ok": true, "retrieval": "tpuf", "stack": {"index": "assets_serving", "embedder": "Qwen/Qwen3-VL-Embedding-2B", "reranker": "Qwen3-VL-Reranker-2B@modal-v2-keys", "ef_search": 2048, "api": "v1.1", "modes": ["none", "visual"], "config_hash": "a9dc401d3397"}, "defaults": {"k": 10, "ann_k": 100, "bm25_k": 50, "ann_ef": 2048, "rerank_k": 60, "rerank_text": true, "mode": "visual", "explain": false}, "tiers": [{"name": "promo", "min_purchased_usd": 0, "rpm": 30, "rpd": 1000, "concurrent": 2}, {"name": "starter", "min_purchased_usd": 10, "rpm": 60, "rpd": 5000, "concurrent": 4}, {"name": "growth", "min_purchased_usd": 100, "rpm": 300, "rpd": 25000, "concurrent": 10}, {"name": "scale", "min_purchased_usd": 1000, "rpm": 1000, "rpd": 100000, "concurrent": 25}], "upstream": {"embed": "warm", "rerank": "warm"}}
```

## Errors

| Status | Body                                                                  | When                                                                                                     |
| ------ | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `401`  | `{"detail": "invalid or missing API key"}`                            | No `X-API-Key` (or `Authorization: Bearer …`) header, or an unknown key.                                 |
| `404`  | `{"detail": "unknown asset"}`                                         | `/v1/asset/…` for an id not in the corpus.                                                               |
| `404`  | `{"detail": "asset has no embedding"}`                                | `/v1/similar` for an id without a vector.                                                                |
| `422`  | Pydantic detail                                                       | A field out of range, e.g. `k: 500`.                                                                     |
| `402`  | `{"detail": "insufficient credit"}`                                   | The account's prepaid balance is below one search (\$0.02). Top up in the dashboard.                     |
| `429`  | `{"detail": "rate limit exceeded: 30 per minute on the promo tier"}`  | Over the key's per-minute or per-day limit. `Retry-After` says how many seconds until the window resets. |
| `429`  | `{"detail": "too many concurrent requests for this account (max 2)"}` | More searches in flight at once than the tier allows. Wait for one to finish; `Retry-After: 1`.          |
| `502`  | `{"detail": "search failed: …"}`                                      | An upstream (embedder or reranker) failed for the whole request budget. Safe to retry.                   |
| `503`  | `{"detail": "auth database unavailable, retry shortly"}`              | The key store is briefly unreachable. Retry with backoff.                                                |

A `200` may also carry `"degraded": "reranker unavailable; lane-order results"`. The results are valid and in retrieval order; `score` is `null`. Treat it as a soft signal, not an error.

## Rate limits and tiers

Limits are per key and come from the account's tier, which is set by the total credit you have **purchased** (welcome credit does not count, and spending never lowers a tier). Every authenticated response carries the current numbers:

| Header                  | Meaning                                                |
| ----------------------- | ------------------------------------------------------ |
| `X-RateLimit-Limit`     | e.g. `60/min, 5000/day`                                |
| `X-RateLimit-Remaining` | requests left in the tighter of the two windows        |
| `X-RateLimit-Reset`     | seconds until the minute window resets                 |
| `X-RateLimit-Tier`      | `promo`, `starter`, `growth`, `scale`, or `enterprise` |
| `X-Concurrency-Limit`   | searches you may have in flight at once                |

| Tier       | Purchased credit          | Per minute | Per day | Concurrent |
| ---------- | ------------------------- | ---------- | ------- | ---------- |
| promo      | \$0 (welcome credit only) | 30         | 1,000   | 2          |
| starter    | \$10+                     | 60         | 5,000   | 4          |
| growth     | \$100+                    | 300        | 25,000  | 10         |
| scale      | \$1,000+                  | 1,000      | 100,000 | 25         |
| enterprise | negotiated                | per key    | per key | per key    |

A `429` names the limit you hit and carries `Retry-After`. Pace on `X-RateLimit-Remaining` rather than retrying blindly. `GET /v1/health` lists the tier table the server is running.

## Latency, and what to expect

Measured 2026-09-14 on the full 10.2M-image index, through the public API, on queries the system had never seen:

* **Typical new query, reranked**: 1.8 to 2.3 seconds end to end. About 0.02 s to embed, 0.1 s to retrieve both lanes, and 1.5 to 1.9 s to fetch 150 thumbnails and score them on the GPU. `timing_ms` reports `embed`, `retrieve`, `rerank` (which includes the thumbnail fetch) and `hop` (the single round trip to the GPU service).
* **Retrieval variance**: the retrieval store keeps our index warm on one of its nodes; occasionally a request is routed to a node that has not loaded it yet, and retrieval runs 0.5 to 1.3 s for a stretch of queries until that node warms. Bounded, and it does not affect results.
* **Cold thumbnails**: when most of the 150 candidates are images nobody has fetched recently, the fetch can add several seconds (worst seen: 8 s).
* **Cold start**: the GPU service shuts down after an hour without requests and restores from a memory snapshot. **Beta note:** the first search after that can take 15 to 40 seconds; every later search is normal. Calling `/v1/warm` at session start (section 5) moves this out of your first real query. Give the first call a 120-second client timeout and do not retry inside that window; a search sent while the service is restoring waits rather than fails.
* **Tuning**: `mode: "none"` skips the reranker (about 0.3 s end to end, lane-order results, noticeably lower quality). The default `rerank_k` is 60 (judged equal to scoring all 150 candidates, at half the time); raise it toward 150 for a deeper rerank, or lower it for speed. `rerank_text: false` shaves a little more; `ann_ef` currently has no effect. `k` itself barely matters.

## Patterns that work

<AccordionGroup>
  <Accordion title="Commercial pipeline: filter, then carry attribution through">
    Set `filters.commercial: true` on every request rather than inspecting results. When `rights.attribution_required` is `true`, copy `rights.attribution` into the artifact you produce — it is already formatted.
  </Accordion>

  <Accordion title="Take control of the pipeline">
    Agents can drive each stage. A cheap first pass with `mode: "none"` and `explain: true` returns lane order with every candidate's lane ranks in \~0.6 s; if the top results look right, stop there. If not, re-query with `mode: "visual"`, or widen the pool (`ann_k: 300, bm25_k: 100, rerank_k: 150`) for a hard query. `timing_ms` on every response shows where the time went.
  </Accordion>

  <Accordion title="Precise subjects: raise the keyword lane">
    For a species, a place, a named object, or anything with an exact term, `bm25_k: 200` gives the keyword lane more candidates to hand the reranker. The visual lane alone can miss exact facts a photo doesn't show.
  </Accordion>

  <Accordion title="Aesthetic or mood queries: raise the visual lane">
    "moody coastal fog at dawn" has no keyword to match. `ann_k: 300, bm25_k: 0` leans entirely on the image embedding.
  </Accordion>

  <Accordion title="Find more like the one that worked">
    Take the best `asset_id` from a search and call `/v1/similar` with the same filters. This is usually better than rephrasing the query.
  </Accordion>

  <Accordion title="Noncommercial is in the corpus on purpose">
    Assets under `cc-by-nc*` licenses are indexed and come back with `commercial: false`. If your use is noncommercial you get the larger corpus by leaving the filter unset; if it isn't, set it and they never appear.
  </Accordion>
</AccordionGroup>
