# Open Library Search API Guide `/search.json` is the primary read surface: a Solr-backed work index with offset pagination, field-scoped queries, and server-side projection. This file documents the full parameter surface, result schema, sibling search endpoints, and the error model observed live. Key-graph basics (OL…M/W/A) live in [api-overview-and-key-graph.md](api-overview-and-key-graph.md); ISBN/edition/covers endpoints in [books-isbn-and-covers.md](books-isbn-and-covers.md). ## Endpoint and parameters ``` GET https://openlibrary.org/search.json?q=& ``` | Parameter | Behavior | |-----------|----------| | `q` | Solr query string; supports field scopes and Lucene syntax (below). | | `fields` | Comma-separated projection. `*` returns ~120 fields (docs warn it's "expensive, please use sparingly"). Special value `availability` adds an availability subdocument — **only if `ia` is also requested** (gotcha below). | | `sort` | One of the sort keys below; default is relevance. Invalid values cause an HTTP 500 (error model below). | | `limit` / `offset` | Offset pagination. `page`/`limit` also works (page starts at 1); if both `page` and `offset` are sent, **offset wins**. | | `lang` | Two-letter ISO 639-1 preference — "influences but doesn't exclude" results. To *exclude*, use `language:` inside `q`. | | `title`, `author` | Top-level scoped params equivalent to prefixing inside q (e.g. `/search.json?title=the+lord+of+the+rings`). | ### Sort keys All of these returned HTTP 200 in live probes (`q=harry potter&limit=1`); the authoritative enumeration lives in Open Library source (`openlibrary/plugins/worksearch/schemes/works.py`): `new`, `old`, `rating asc`, `rating desc` (bare `rating` = desc), `editions`, `title`, `scans`, `key` (sorts as a *string*, not numerically), `random`, `readinglog`, `already_read`, `want_to_read`, `currently_reading`, `ebook_access`. The existing CLI exposes `--sort {editions,new,old,rating,title}` plus empty for relevance — a safe subset. ## Pagination model: offsets only, no tokens There is no cursor or token concept anywhere on this API — clients compute the next offset from `numFound` and `start`: ```json {"numFound": 4045, "start": 0, "numFoundExact": true, "num_found": 4045, "documentation_url": "...", "q": "harry potter", "offset": null, "docs": [...]} ``` - `start` mirrors the effective zero-based offset of the first doc (`page=3&limit=10` → `start: 20`). - `numFoundExact: false` signals the count is approximate. - No documented cap on `limit` or `offset`: live probes honored `limit=2000` and `offset=11000`. The response simply clamps to the matched set. Stay modest anyway — the etiquette policy forbids using OL as a bulk backend. ## Result document schema Common `docs[]` fields: `key` (path form `/works/OL…W`), `title`, `author_name[]`, `author_key[]`, `first_publish_year`, `edition_count`, `cover_edition_key`, `cover_i`, `ia[]` (Internet Archive scan ids), `has_fulltext`, `public_scan_b`, `language[]`, `subject[]`, `publisher[]`, `publish_year[]`, `isbn[]`, `number_of_pages_median`, `ebook_access`, `ratings_average`, `ratings_count`, `readinglog_count`, `seed[]`. The docs state the schema "is not guaranteed to be stable, but most common fields … should be safe to depend on". Treat exotic fields as best-effort. ## Query syntax: field scopes and filters Verified live prefixes: | Prefix | Example | Notes | |--------|---------|-------| | `title:` | `q=title:flammable` | 551 hits | | `author:` | `q=author:solnit` | 129 hits | | `subject:` | `q=subject:"tennis rules"` | fuzzy containment (AND), not exact phrase | | `publisher:` | `q=publisher:harper` | 77,889 hits | | `isbn:` | `q=isbn:9780451524935` | ISBN-10 and ISBN-13 both resolve to the same single work | | `language:` | `q=language:fre` | excludes works without matching-language editions | Lucene extras from the official how-to ([search/howto](https://openlibrary.org/search/howto)): ranges (`first_publish_year:[1200 TO 1400]`, `publish_year:[* TO 1800]`), booleans `AND`/`OR`/`NOT`, negation `-subject_key:"apache_solr"`, prefix wildcards `ddc:200*`, normalized exact keys (`subject_key:`, `person_key:`, `place_key:`, `time_key:` — lowercase, spaces/slashes → underscores), availability filter `ebook_access:` with values `no_ebook`, `printdisabled`, `borrowable`, `public`, plus `has_fulltext:true`, `edition_count:N`, `readinglog_count:[25 TO *]`. ## The `fields=` projection and its availability gotcha Requesting fewer fields shrinks payloads dramatically. Live behavior: - `fields=key,title` returns exactly those keys per doc. - **`availability` is silently omitted unless `ia` is also requested** — verified live: - `fields=key,title,availability` → doc keys exactly `['key','title']` - `fields=key,title,ia,availability` → full availability subdocument present With `ia` included, each doc gains: ```json "availability": { "status": "borrow_available", "is_readable": false, "is_lendable": true, "is_printdisabled": true, "openlibrary_work": "OL82563W", "openlibrary_edition": "OL61057835M", ... } ``` `status` values include `borrow_available`, `borrow_unavailable`, `printdisabled`, `open` (readable), and absent/`error` when no scan exists. Bonus expansion: `fields=key,title,editions` nests a mini-result-set under each work (`numFound`/`start`/`docs[]` with edition fields); individual edition fields are requested as `editions.key`, `editions.ebook_access`, `editions.language`; `&editions.sort` overrides default boosting. ## Author search: `/search/authors.json` Same envelope (`numFound`/`start`/`docs[]`); author docs carry bare-form keys (`OL9937375A`) unlike book-search path keys: ```json {"name": "Mark Twain", "key": "OL9937375A", "birth_date": "30 November 1835", "death_date": "21 April 1910", "top_work": "Roughing It", "work_count": 2157, "top_subjects": ["Twain, mark, 1835-1910", ...]} ``` Supports Solr syntax in `q` too (e.g. `birth_date:1973`) plus `limit`/`offset`. Note `birth_date`/`death_date` may be null or free-text strings ("7 February 1812") — they are display strings, not typed dates. Batch-fetch trick documented on the Authors API page: partial author records via book search with `q=key:(/authors/OL11111A OR /authors/OL22222A)`; there is no batch endpoint for full author records. ## Subject browsing: `/subjects/.json` (plural!) The Subjects API ([dev/docs/api/subjects](https://openlibrary.org/dev/docs/api/subjects), marked experimental) browses works grouped by normalized subject: ``` GET https://openlibrary.org/subjects/pizza.json?limit=1 → {"key": "", "name": "pizza", "work_count": 519, "works": [{"key": "", "title": "Pete's a Pizza", "edition_count": 19, "authors": [{"name": "William Steig"}], "first_publish_year": 1998, "availability": {...}}, ...]} ``` - Path is **plural** `/subjects/.json`; singular `/subject/pizza.json` 404s (live-verified). - Names use underscores: `science_fiction`. - Params: `details=true` (adds related `subjects[]`/`authors[]`/`publishers[]` with counts plus `publishing_history`), `ebooks=true`, `published_in=1500-1600`, `limit`, `offset`. - Works here include `availability` by default, unlike `/search.json`. - Sibling collections exist for persons/places/times (`/persons/.json` etc.). ## Full-text inside-book search: `/search/inside.json` Searches OCR text across millions of scanned books; Elasticsearch-shaped response: ``` GET https://openlibrary.org/search/inside.json?q=%22library science%22 → hits.total, hits.hits[] with _id (ia identifier), _score, highlight.text[] ("{{{Library Science}}}" marks matches), fields.identifier (ia id), edition.key/title, availability ``` Default page size 20; `limit`/`offset` supported (live-verified). A separate, documented-but-experimental *per-book* inside search actually runs on archive.org data nodes (`https://ia800204.us.archive.org/fulltext/inside.php?item_id=...`) — see [dev/docs/api/search_inside](https://openlibrary.org/dev/docs/api/search_inside) if you need per-page match geometry; that host is outside this skill's CLI. ## Error model: silent empties vs hard failures Live-probed status codes — counterintuitive but consistent: | Request | Status | Body | |---------|--------|------| | missing or empty `q` | **200** | normal envelope, `numFound: 0`, `docs: []` | | malformed query `q=title:"unclosed` | **200** | `numFound: 0` — no error surfaced | | loosely-parseable garbage `q=(OR` | **200** | 568k loose matches | | invalid enum `sort=bogus` | **500** | plain text `Internal Server Error` (not JSON!) | | non-integer `limit=abc` | **422** | FastAPI validation JSON `{"detail":[{"type":"int_parsing",...}]}` | | negative `offset=-5` | **422** | FastAPI validation JSON `greater_than_equal` | | singular `/subject/pizza.json` | **404** | HTML error page | Design consequence: user-facing "no results" is usually **not** an error — treat empty `docs` as success. Conversely a bad `--sort` choice fails loudly as non-JSON 500, so clients should validate sort choices before sending (as the bundled CLI does). One environment caveat observed during research: responses can arrive with key fields masked to asterisks by anti-bot middleware depending on client reputation. Production responses carry real keys (the docs' own examples show them), but parsers should tolerate both `OL…W` and `/works/OL…W` shapes and not assume key presence. ## Sources - https://openlibrary.org/dev/docs/api/search — Search API parameters, fields= semantics, editions sub-query - https://openlibrary.org/search/howto — query syntax, field scopes, filter examples - https://openlibrary.org/developers/api — rate-limit etiquette applying to search traffic - https://openlibrary.org/dev/docs/api/authors — author batch-fetch via key:(…) search - https://openlibrary.org/dev/docs/api/subjects — Subjects API params (details/ebooks/published_in) - https://openlibrary.org/dev/docs/api/search_inside — experimental per-book inside search (archive.org hosted) - Live read-only probes against openlibrary.org (2026-08-26): sort keys, limit/offset ranges, fields=availability interaction, error status codes