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

# Search

> GET /search — keyword, semantic, and hybrid search over individual messages, with anchors for jumping to conversation context.

`GET /search` searches individual messages across all your conversations and returns snippet-level hits. Every result carries a `conversation_id` + `message_index` anchor, so you can jump straight to the surrounding turns with [`GET /conversations/:id/context`](/data-api/conversations#get-conversationsidcontext).

```
GET https://moda.dev/api/v1/data/search
```

## Query parameters

<ParamField query="q" type="string" required>
  The search query, 1–500 characters. Leading and trailing whitespace is trimmed; a whitespace-only query returns `400`.
</ParamField>

<ParamField query="mode" type="string" default="hybrid">
  `keyword`, `semantic`, or `hybrid`. Keyword matches literal substrings; semantic matches by meaning; hybrid fuses both. See [Search modes](#search-modes-and-degradation).
</ParamField>

<ParamField query="user_id" type="string">
  Restrict results to conversations from one end user. Maximum 200 characters.
</ParamField>

<ParamField query="time_range" type="string" default="all">
  One of `all`, `1h`, `24h`, `3d`, `7d`, `30d`, `90d`.
</ParamField>

<ParamField query="limit" type="integer" default="20">
  Maximum results to return, 1–100. There is no `offset` — pagination is limit-only.
</ParamField>

<ParamField query="include_tool_io" type="string" default="false">
  `true` or `false`. When `true`, matches inside tool call inputs and tool results are included in the result list.
</ParamField>

## Search modes and degradation

The response always reports which mode **actually ran** (`search_mode`) and why, if it differs from what you requested (`degrade_reason`). A `semantic` or `hybrid` request degrades transparently to `keyword` rather than failing:

| `degrade_reason`        | Meaning                                                                                                                                                   | Retry?                                |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
| `none`                  | The requested mode ran.                                                                                                                                   | —                                     |
| `semantic_unavailable`  | Semantic search was temporarily unavailable (for example, the query embedding timed out). Results were served by keyword search.                          | Retrying the same query may succeed.  |
| `below_relevance_floor` | Semantic search ran, but no hit cleared the relevance floor — the corpus has no conceptual matches for this query. Results were served by keyword search. | Retrying will not change the outcome. |

<Warning>
  Scores use a different scale per mode and are **not comparable across modes**: in `keyword` mode the score is an integer count of substring matches; in `semantic` mode it is a similarity value; in `hybrid` mode it is a small rank-fusion fraction. Branch on `search_mode` before applying any threshold, and use scores only to order results within a single response. Scores are rounded to 4 decimals.
</Warning>

## Response fields

<ResponseField name="query" type="string">
  The query as searched (trimmed).
</ResponseField>

<ResponseField name="search_mode" type="string">
  The mode that actually ran: `keyword`, `semantic`, or `hybrid`. May differ from the requested `mode`.
</ResponseField>

<ResponseField name="degrade_reason" type="string">
  `none`, `semantic_unavailable`, or `below_relevance_floor`.
</ResponseField>

<ResponseField name="results" type="array">
  Ranked hits. Each hit has `conversation_id`, `message_index`, `role` (`user` or `assistant`), `timestamp`, `snippet` (the matched text with surrounding context), `score`, `cluster_id` and `cluster_name` (the conversation's use-case cluster, or `null`), and `conversation_summary` (a one-line summary of the conversation, or `null`). Hits that resolve to a specific content block additionally carry `unit_id`, `content_block_index`, `block_type`, `tool_name`, and `chunk_index`.
</ResponseField>

<ResponseField name="totals" type="object">
  Exact counts of distinct conversations containing a literal (case-insensitive) match for the query — independent of the ranked page. `matched_conversations` covers the requested window; `prior_window_matched_conversations` covers the equal-length window immediately before it (`null` when `time_range=all`). Either can be `null` if the count query timed out — a missing number, never a fabricated zero.
</ResponseField>

<ResponseField name="pagination" type="object">
  `limit` (requested page size), `returned` (hits in this response), and `has_more` (`true` when the ranked candidate pool held more hits than the page). There is no offset; to see different results, narrow `q`, `time_range`, or `user_id`.
</ResponseField>

## Example

```bash theme={"dark"}
curl "https://moda.dev/api/v1/data/search?q=refund%20policy&mode=hybrid&time_range=7d&limit=2" \
  -H "x-api-key: YOUR_MODA_API_KEY"
```

```json theme={"dark"}
{
  "query": "refund policy",
  "search_mode": "hybrid",
  "degrade_reason": "none",
  "results": [
    {
      "conversation_id": "conv_9f2c41d08ab37e51",
      "message_index": 6,
      "role": "user",
      "timestamp": "2026-08-14T09:31:22",
      "snippet": "…I already asked about the refund policy twice and the bot keeps linking the same page…",
      "score": 0.0328,
      "cluster_id": "node_3",
      "cluster_name": "Billing and refunds",
      "conversation_summary": "User asks how to request a refund for an annual plan; agent explains the policy and opens a ticket."
    },
    {
      "conversation_id": "conv_5d1a7be2c90f4438",
      "message_index": 12,
      "role": "assistant",
      "timestamp": "2026-08-13T18:04:57",
      "snippet": "Our refund policy allows cancellation within 14 days of purchase…",
      "score": 0.0312,
      "cluster_id": null,
      "cluster_name": null,
      "conversation_summary": null,
      "unit_id": "3e8b1c5d9f2a70648e0c3b7a1d5f2946c8a0e3d71f5b2c69a4e8d0b3f7c1a25e",
      "content_block_index": 0,
      "block_type": "text",
      "chunk_index": 0
    }
  ],
  "totals": {
    "matched_conversations": 41,
    "prior_window_matched_conversations": 28
  },
  "pagination": { "limit": 2, "returned": 2, "has_more": true }
}
```

To read the turns around the first hit:

```bash theme={"dark"}
curl "https://moda.dev/api/v1/data/conversations/conv_9f2c41d08ab37e51/context?msg_index=6&window=2" \
  -H "x-api-key: YOUR_MODA_API_KEY"
```

## Next steps

* [Conversations](/data-api/conversations) — fetch windowed context around any search hit.
* [Signals](/data-api/signals) — if you are hunting failures, the frustration and tool-failure endpoints already embed context.
* [CLI reference](/cli/reference) — `moda search` wraps this endpoint with the same modes.
