# Search API

Source: https://mostlyright.md/docs/reference/search/

Configure the public tables your application can search. Your users enter a query; the server searches that scope and returns one result list. Search strategy is handled by each table's search engine.

**Multi-table search is experimental** (caution)
Searching more than one table is experimental. Each table plans and executes its search independently and incurs its own usage. Results are interleaved by rank; relevance is not calibrated across tables, and there is no shared cross-table plan. Start with a single table unless you need to combine sources. Start with the single-table example below.


## Get a table ID and key

Open a dataset, choose **Use → Search API**, and select a table. Your workspace key searches every table your workspace owns, whether the dataset is private, shared by link or public, so there is nothing to publish first. For someone else's dataset, it must be public with row access enabled; list its table IDs with `mr-data public tables DATASET_SLUG --json`. Replace the illustrative ID below with your table's ID. The table must have search ready.

Create an `mr_use_` key in [Settings → Access](/dashboard/settings/access/). Keep it on your server. A workspace `mr_live_` key and a CLI device credential are different credentials; do not substitute them in this request.

For a runnable server example, see [JavaScript and TypeScript](/docs/guides/javascript/).

## HTTP API

```http
POST https://mostlyright.md/api/v2/public/search
x-api-key: mr_use_…
content-type: application/json
```

```json
{
  "query": "vegetarian pasta",
  "tables": [
    "11111111-1111-4111-8111-111111111111"
  ],
  "limit": 20
}
```

Use one to five distinct public table IDs with search enabled. `query` accepts up to 512 characters; an empty query browses the configured tables. `limit` caps the combined result at 1–100 rows and defaults to 20. Finished searches use `settled: true` by default.

The backend searches the configured scope with bounded concurrency. It preserves each table's ranking and interleaves results by rank, breaking ties in the order you configured the tables. Scores from unrelated tables are not treated as a shared relevance scale. Each row carries its source table and immutable table version:

```json
{
  "schema_version": "public-search.v1",
  "request_id": "33333333-3333-4333-8333-333333333333",
  "query": "vegetarian pasta",
  "rows": [
    {
      "table_id": "11111111-1111-4111-8111-111111111111",
      "table_version_id": "44444444-4444-4444-8444-444444444444",
      "row_id": 42,
      "fields": { "title": "Spinach pasta" }
    }
  ],
  "tables": [
    {
      "table_id": "11111111-1111-4111-8111-111111111111",
      "status": "ok",
      "returned_rows": 1
    }
  ],
  "partial": false
}
```

`partial: true` means at least one table answered and another was unavailable. Inspect `tables` to decide whether to display the available rows or ask the user to try again. A valid empty result is successful. If every table is unavailable, the API returns 503. Authentication, permission, allowance and rate-limit refusals remain HTTP errors, including when another table has already answered.

## Search one table

To search one table, send `POST /api/v2/public/tables/{tableId}/search` with the same key. The body takes `query` and optionally `settled`, `plan`, `cursor`, `like`, `mode`, `page_size` (1–100, default 20), `count` and `columns`. The answer holds `rows` (`row_id`, `fields`), the applied `plan` and `next_cursor`. For the next page, send the same `query` with that `plan` and with `next_cursor` as `cursor`. An empty `query` browses the table in stored order.

```http
POST https://mostlyright.md/api/v2/public/tables/11111111-1111-4111-8111-111111111111/search
x-api-key: mr_use_…
content-type: application/json

{"query": "vegetarian pasta"}
```

A public table also answers without a key at `POST /api/public/tables/{tableId}/search` with the same body and `content-type: application/json`. Without a key, each address gets 30 searches a minute and 600 a day, and all keyless callers together get 300 a minute and 10,000 a day. The table's publisher must allow download. A browser page on another site cannot call it, so search from your server. With a key, search shares the key's query limit of 60 a minute and the workspace's 5,000 a day.

A table whose search is still being prepared answers 503 `search_unavailable` with reason `preparing` and a `Retry-After` header. After a publisher activates search this can last minutes, and longer on a large table. Retry once after that wait.

## Server JavaScript and TypeScript

The `@mostlyright/search` package in the Cloud repository provides a server client with no runtime dependencies:

```ts
import { createSearchClient } from "@mostlyright/search";

const search = createSearchClient({
  apiKey: process.env.MOSTLYRIGHT_API_KEY!,
  tables: [
    "11111111-1111-4111-8111-111111111111",
  ],
});

const result = await search.search({ query: "vegetarian pasta", limit: 20 });
for (const row of result.rows) {
  console.log(row.table_id, row.fields);
}
```

Table scope is fixed when the client is created. Each call supplies the query, optional result limit and optional `AbortSignal`. Model names and search modes are not client options.

The package is available from the Cloud source workspace; registry publication is a separate release. Build and pack it for a local installation:

```sh
pnpm --filter @mostlyright/search build
pnpm --filter @mostlyright/search pack
```

Install the resulting tarball in your server application. The HTTP endpoint can also be called directly with native `fetch`.

### A browser search box

Keep the key and configured table list on your server. Add a same-origin route protected by your app's authentication and per-user request limits. That route accepts a query, calls the server client, and returns its result. It must take table IDs from server configuration, rather than from the browser request.

The browser calls your route:

```ts
const response = await fetch("/api/search", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ query: input.value }),
  signal: controller.signal,
});
if (!response.ok) throw new Error("Search could not be completed.");
const result = await response.json();
```

The server client refuses browser execution. Never place an `mr_use_` key in a browser bundle or a public environment variable.

## Usage, errors and cancellation

Every table search uses the existing per-table limits and usage accounting. One unified call can run up to five table searches. Each source can return up to `limit` rows before the combined result is trimmed, so accounting can include rows that do not appear in the combined list. Completed table requests remain counted if another table subsequently causes the overall call to be refused.

The client exposes typed `SearchError` failures, including HTTP status, request ID and retry delay when supplied. It does not retry automatically. Forward an `AbortSignal` to cancel queued work and propagate cancellation to active searches. Cancellation does not reverse work already accepted or usage already recorded. The backend also bounds the overall request deadline.

This endpoint returns the first combined results. For table-specific pagination and additional controls, use the existing table search endpoint in the [Public Dataset API](/docs/reference/public-dataset-api/). Browser search tokens and local typing indexes are separate SDK work.
