# Search index > The compact search index of chips, boards, cases, models, motherboards and risers, its fields per kind, and how to search it with the open-source library. URL: https://gpuninja.com/docs/api/search Last updated: 2026-10-10 ## What it is The API has no search endpoint, and query parameters are ignored. Instead, `/api/v1/index` is one compact file listing every chip, board, chassis, model, motherboard and riser with the fields most searches need. Download it once, cache it, and search it locally. It is the file the website's buyer's guide and Solution Builder, both MCP servers and the desktop app all search. ```bash curl -s https://gpuninja.com/api/v1/index ``` The file is at most 1,000,000 bytes, and about 100 KB at launch. Like every API file it is cached for five minutes and revalidates with an `ETag`. ## The envelope ```json { "apiVersion": "v1", "kind": "search-index", "formatVersion": "1.0", "asOf": "2026-10-04T06:12:00Z", "datasetHash": "sha256:9f2c4e0d1b7a6c5e8f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e", "speedReference": { "benchmark": "llama-cpp-tg128", "model": "llama-3-1-8b-instruct-q4-k-m" }, "counts": { "chip": 1, "board": 1, "chassis": 0, "model": 0, "motherboard": 0, "riser": 0 }, "entries": [ { "slug": "ad102", "kind": "chip", "name": "NVIDIA GeForce RTX 4090", "chipSlug": null, "vendor": "NVIDIA", "vramGb": 24, "tdpW": 450, "lengthMm": null, "slotWidth": null, "lowestPriceUsd": null, "inStock": null, "status": "active", "pciVendorId": "10de", "pciDeviceIds": ["2684"], "socket": null, "pcieGen": null, "speedTokPerSec": null, "vramType": null, "powerConnectors": null, "recommendedPsuW": null, "widthMm": null }, { "slug": "asus-rog-strix-4090", "kind": "board", "name": "ASUS ROG Strix GeForce RTX 4090 OC", "chipSlug": "ad102", "vendor": "ASUS", "vramGb": 24, "tdpW": 450, "lengthMm": 357.6, "slotWidth": 3.5, "lowestPriceUsd": 1999.99, "inStock": true, "status": "active", "pciVendorId": null, "pciDeviceIds": [], "socket": null, "pcieGen": null, "speedTokPerSec": 128.4, "vramType": "GDDR6X", "powerConnectors": [{ "type": "12V-2x6", "count": 1 }], "recommendedPsuW": 1000, "widthMm": 149.3 } ] } ``` - `formatVersion` is `MAJOR.MINOR`. The major number matches the API version; a minor bump only adds properties. - `asOf` is the latest time any value of an indexed entity was retrieved or verified, or `null` for an empty catalogue. There is no build timestamp. - `datasetHash` changes if, and only if, some published value changed. Compare it with your cached copy to decide whether anything needs re-reading. - `speedReference` names what a board's `speedTokPerSec` measures: the llama.cpp text-generation benchmark `llama-cpp-tg128` on the model `llama-3-1-8b-instruct-q4-k-m`. It is fixed for v1. - `counts` gives the number of entries of each kind. - `entries` is sorted by kind (chip, board, chassis, model, motherboard, riser), then by slug. ## Fields per kind Every entry has every field. A field that doesn't apply to a kind is always `null`, or `[]` for `pciDeviceIds`. A field that does apply is also `null` when the value isn't recorded: a model with no recorded minimum VRAM, for example, stays in the index with `vramGb: null`. | Field | chip | board | chassis | model | motherboard | riser | | ---------------------- | ---------------------- | ---------------------------------------- | ------------ | --------------------- | ----------------- | --------------- | | `slug`, `kind`, `name` | yes | yes | yes | yes | yes | yes | | `chipSlug` | `null` | the board's chip | `null` | `null` | `null` | `null` | | `vendor` | vendor | vendor | vendor | the model's publisher | vendor | vendor | | `vramGb` | maximum VRAM | VRAM fitted | `null` | minimum VRAM required | `null` | `null` | | `tdpW` | reference TDP | board TDP | `null` | `null` | `null` | `null` | | `lengthMm` | `null` | card length | `null` | `null` | `null` | cable length | | `slotWidth` | `null` | card slot width | `null` | `null` | `null` | `null` | | `lowestPriceUsd` | `null` | lowest price | lowest price | `null` | `null` | `null` | | `inStock` | `null` | in stock | in stock | `null` | `null` | `null` | | `status` | status | status | status | always `active` | status | status | | `pciVendorId` | PCI vendor ID | `null` | `null` | `null` | `null` | `null` | | `pciDeviceIds` | PCI device IDs, sorted | `[]` | `[]` | `[]` | `[]` | `[]` | | `socket` | `null` | `null` | `null` | `null` | its socket's slug | `null` | | `pcieGen` | `null` | `null` | `null` | `null` | `null` | PCIe generation | | `speedTokPerSec` | `null` | tokens per second on the speed reference | `null` | `null` | `null` | `null` | | `vramType` | `null` | memory type, such as `GDDR7` | `null` | `null` | `null` | `null` | | `powerConnectors` | `null` | each connector's `type` and `count` | `null` | `null` | `null` | `null` | | `recommendedPsuW` | `null` | recommended power supply | `null` | `null` | `null` | `null` | | `widthMm` | `null` | the card's height from the slot | `null` | `null` | `null` | `null` | `lowestPriceUsd` and `inStock` follow the same rules as in the detail documents: discontinued boards and chassis have `null` and `false`. Case clearances are deliberately not in the index, so a "length at most" filter can never match a case. Ask fit questions of the compatibility documents (https://gpuninja.com/docs/api/compatibility) instead. A riser's `lengthMm` is a cable length, so filter by kind before using a length limit meant for cards. Platforms and benchmarks are not in the index. Neither are sockets: `/api/v1/sockets` lists them all. A board's `speedTokPerSec` comes from the board's own result for the speed reference, or, when it has none, from its chip's reference-design result; the most recent run counts. It is `null` when neither exists, and a discontinued board keeps its figure. `powerConnectors` keeps the order the card lists them in; `[]` means the card is powered by its slot alone, and `null` that its connectors aren't recorded. The detail document gives the same connectors as one display string. A board entry doesn't repeat its chip's vendor and name: read them from the chip entry whose `slug` is the board's `chipSlug`. Motherboard and riser entries carry only what the Solution Builder's part picker needs. A motherboard's PCIe slots, a riser's lanes, and both their offers are in their detail documents. PCI IDs are four lower-case hex digits with no `0x` prefix, as in `"10de"`. A device ID is unique only within its vendor, so match on both. ## Searching it yourself For simple questions, filter the entries directly. Treat `null` as "not recorded", and decide whether you want discontinued hardware: ```javascript const response = await fetch("https://gpuninja.com/api/v1/index"); const index = await response.json(); // Active graphics cards with at least 16 GB of VRAM, no longer than 320 mm. const boards = index.entries.filter( (entry) => entry.kind === "board" && entry.status === "active" && entry.vramGb !== null && entry.vramGb >= 16 && entry.lengthMm !== null && entry.lengthMm <= 320, ); ``` To answer "which cards fit this case" or "which cards can run this model", fetch that case's `fits` or that model's `runs-on` document and use its slugs as an allowlist over the index. ## The open-source query library GPU Ninja's own search code is open source under Apache 2.0, in the gpuninja-mcp repository (https://github.com/gpuninja/gpuninja-mcp) at `src/query/`, together with the type declarations it uses at `src/types/`. It is plain TypeScript with no dependencies and no I/O, and runs unchanged in browsers, Workers and Node. You fetch and parse the index yourself, then: ```typescript import { loadIndex, search } from "./src/query/index.ts"; const loaded = loadIndex(await (await fetch("https://gpuninja.com/api/v1/index")).json()); if (!loaded.ok) throw new Error(loaded.error.code); const result = search(loaded.value, { kind: "board", text: "rtx 4090", filter: { op: "range", field: "lengthMm", max: 340 }, limit: 20, }); if (result.ok) { for (const hit of result.value.items) console.log(hit.entry.name, hit.score); } ``` What the library does, so you can match it or simply use it: - One kind per query. `kind` is `chip`, `board`, `chassis`, `model`, `motherboard` or `riser`. - Discontinued entries are excluded by default. Set `includeDiscontinued: true` to include them. A filter that asks for `status` `discontinued` without it is rejected with `discontinued_excluded`, rather than silently returning nothing. - Filters are a small tree of `and`, `or` and `not` over `range` (inclusive bounds on a numeric field), `in` (equality on `slug`, `vendor`, `chipSlug`, `socket`, `status`, `vramType`, `powerConnectorType` or `chipVendor`, ignoring case, accents, symbols and surrounding spaces), `is` (on `inStock`), `has` (the field is not `null`) and `pci` (chips by PCI device ID, optionally with vendor ID, accepting forms such as `0x10DE`). A comparison against a `null` field is false, so `not` of it is true. A field that doesn't apply to the kind, such as `lengthMm` on a chassis query, is a validation error, not an empty result. - Board-only fields. Besides the index's own board fields, the library works out three per board when it loads the index: `pricePerVramGb` (`lowestPriceUsd` divided by `vramGb`), `chipVendor` (the vendor of the board's chip entry) and `powerConnectorType` (an `in` filter matches when any of the card's connectors has that type). They can be filtered, and `pricePerVramGb` sorted, but they never appear in a result's `entry`. A board with no value for a range filter never matches it. - Text matching splits text into runs of letters and runs of digits, so `rtx4090` finds "RTX 4090", after folding case and accents. It searches `name`, `vendor`, `chipSlug` and `slug`, weighted 4, 2, 2 and 1. Every word in the query must match something. A word matches exactly, or as a prefix when it is at least two characters long. All-letter words of five or more letters also tolerate one typo (an adjacent swap counts as one), and words of nine or more tolerate two. Numbers never match fuzzily: `4080` never finds a 4090. Two or more words found side by side, in order, in the name earn a phrase bonus, and a query equal to the whole name earns an exact-name bonus. Scores are whole numbers. - Ordering is by relevance then name when there is text, and by name otherwise. Every sort ends with the slug, so results are fully deterministic and never depend on the order of the index. You can give up to three sort keys from the fields sortable for the kind (below); `null` values always sort last, ascending or descending. Names compare by their folded form, by character code, never by locale. - Pagination uses an opaque `nextCursor`, which is `null` on the last page. A cursor records the last item's sort position, not an offset, so it stays valid across a deploy: you won't see an entry twice or miss one whose sort position didn't change. It also survives a change of `limit`. A cursor used with a different query, or minted before a change to the library's matching or ordering rules, is rejected with `cursor_mismatch`; start again from the first page. - Errors are values, not exceptions. `loadIndex` returns one `IndexError` with a `code` and a JSON Pointer `path`. `validateQuery` and `search` return every problem with the query (up to 20) as issues, each with a `code`, a `path` and a plain-English `message`. The limits and defaults, as the library defines them: | Search fact | Value | Meaning | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ | | `LIMITS.defaultLimit` | `20` | Results per page when you don't set `limit` | | `LIMITS.maxLimit` | `1000` | The largest `limit` you can ask for | | `LIMITS.maxTextLength` | `200` | Characters of `text` | | `LIMITS.maxTextTokens` | `16` | Distinct words in `text` | | `LIMITS.maxSortKeys` | `3` | Sort keys, not counting the final slug | | `LIMITS.maxFilterDepth` | `8` | Levels of nesting in a filter | | `LIMITS.maxFilterNodes` | `64` | Nodes in a filter | | `LIMITS.maxInValues` | `10000` | Values in one `in` node | | `LIMITS.maxIssues` | `20` | Issues returned for an invalid query | | `DEFAULT_SORT.withText` | `[{"by":"relevance","dir":"desc"},{"by":"name","dir":"asc"}]` | Order when the query has text | | `DEFAULT_SORT.withoutText` | `[{"by":"name","dir":"asc"}]` | Order otherwise | | `SORT_FIELDS.chip` | `["relevance","name","vramGb","tdpW"]` | Sortable fields for chips | | `SORT_FIELDS.board` | `["relevance","name","lowestPriceUsd","vramGb","tdpW","lengthMm","slotWidth","speedTokPerSec","pricePerVramGb","recommendedPsuW","widthMm","inStock"]` | Sortable fields for boards | | `SORT_FIELDS.chassis` | `["relevance","name","lowestPriceUsd","inStock"]` | Sortable fields for chassis | | `SORT_FIELDS.model` | `["relevance","name","vramGb"]` | Sortable fields for models | | `SORT_FIELDS.motherboard` | `["relevance","name"]` | Sortable fields for motherboards | | `SORT_FIELDS.riser` | `["relevance","name","lengthMm","pcieGen"]` | Sortable fields for risers | | `includeDiscontinued` | `false` | Discontinued entries are left out unless you ask | The library accepts any index with the same major `formatVersion`. A newer minor version loads, with unknown properties ignored. ## Or let an assistant search The MCP servers (https://gpuninja.com/docs/mcp) expose this same search as tools for AI assistants, with the same matching, filters and ordering. Their page sizes and cursors are their own, and are described there.