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

# search_notes

> Deep keyword search returning matched notes with their primary documents

## Overview

`search_notes` is the deep tier of note discovery. It takes a **required keyword** and returns matching notes hydrated with their primary documents (one-pager, custom). Use this when the LLM needs to **understand the context behind a topic** — its decisions, action items, or conclusions — not just see which notes match.

For lightweight metadata-only listing (e.g., "which notes are in folder X"), use [`list_notes`](./list-notes) instead.

**Primary Use Cases:**

* "What did the team decide about OKR Q2?" — search by topic, read documents inline.
* "Where is the source note for this decision?" — keyword finds the note, documents give the context.
* "Show me everything we wrote about 네이버" — Korean keyword search with content.

**Key Features:**

* Keyword required; results ordered by full-text relevance with `createdAt` desc as tiebreaker.
* Each matched note carries its primary documents inline (one-pager, custom).
* HTML stripped from document content; documents larger than 5,000 chars are truncated and flagged.
* Graceful degradation when the search index is unavailable.

<Warning>
  **Heavier than `list_notes`.** Each result includes document content (\~1,500 tokens per note). Default page size is `50`, max `200`. If you only need metadata, use [`list_notes`](./list-notes).
</Warning>

## Parameters

| Parameter              | Type   | Required | Description                                                                                                                                                                                                          |
| ---------------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `keyword`              | string | **Yes**  | Search keyword (≥1 char). Full-text matched against title and paragraph content.                                                                                                                                     |
| `pagination.cursor`    | string | Optional | Reserved for future use; the deep-search endpoint currently emits `null`.                                                                                                                                            |
| `pagination.size`      | number | Optional | Results per page (default `50`, max `200`).                                                                                                                                                                          |
| `workspaceGuid`        | string | Optional | Workspace GUID (from `list_workspaces`). Omit to search across every workspace you can access — a workspace-scoped key automatically searches its own workspace; pass one to scope the search to a single workspace. |
| `filter.folderId`      | string | Optional | Restrict to a folder (recursive — includes descendant folders).                                                                                                                                                      |
| `filter.createdAtFrom` | string | Optional | ISO 8601 datetime, inclusive lower bound on `createdAt`.                                                                                                                                                             |
| `filter.createdAtTo`   | string | Optional | ISO 8601 datetime, exclusive upper bound on `createdAt`.                                                                                                                                                             |

### keyword (required)

The search term. Tokenized server-side; Korean text is morphologically analyzed (e.g., `네이버` → `[네, 버]`). Empty or whitespace-only keywords return `400`.

Keyword search accepts both user-scoped and workspace-scoped API keys. Omit `workspaceGuid` to search across every workspace you can access — a workspace-scoped key automatically searches its own workspace. Pass an explicit `workspaceGuid` (see [Parameters](#parameters)) to scope the search to a specific workspace.

### filter.folderId (optional)

Same semantics as `list_notes` — recursive folder scope, authorized server-side.

### pagination (optional)

`{ cursor, size }`. Default `size: 50`, max `200`. The cap reflects the higher per-result cost (each note carries its documents).

## Response Format

### Success Response

```json theme={"system"}
{
  "notes": [
    {
      "noteGuid": "abc-123-def",
      "title": "OKR Q2 Planning",
      "webUrl": "https://platform.tiro.ooo/notes/abc-123-def",
      "createdAt": "2026-04-15T10:00:00Z",
      "updatedAt": "2026-04-15T11:30:00Z",
      "recordingDurationSeconds": 3625,
      "sourceType": "live-voice",
      "participants": [
        { "name": "Alice Kim", "email": "alice@example.com" }
      ],
      "matchedSnippets": null,
      "documents": [
        {
          "documentGuid": "7821",
          "templateId": 12,
          "templateTitle": "One Pager",
          "content": "## 결정 사항\n- 자동 ingest 우선...\n## Action Items\n- ...",
          "truncated": false,
          "updatedAt": "2026-04-15T11:30:00Z"
        }
      ]
    }
  ],
  "nextCursor": null,
  "degraded": false,
  "degradedReason": null
}
```

**Field Descriptions:**

| Field                               | Type              | Description                                                                                                                                     |
| ----------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `notes[]`                           | array             | Matched notes, ordered by full-text relevance with `createdAt` desc as tiebreaker.                                                              |
| `notes[].matchedSnippets`           | string\[] \| null | Reserved for highlight snippets. Currently `null` — distinguishes "highlights not yet implemented" from "searched, no highlights matched".      |
| `notes[].documents[]`               | array             | Note's primary documents. Empty array when the note has no documents.                                                                           |
| `notes[].documents[].documentGuid`  | string            | Stable document identifier.                                                                                                                     |
| `notes[].documents[].templateId`    | number \| null    | Numeric template id — the only stable discriminator.                                                                                            |
| `notes[].documents[].templateTitle` | string            | Display label (e.g., `"One Pager"`, `"회의록"`). **Locale-sensitive and user-editable for custom templates** — do NOT discriminate on this string. |
| `notes[].documents[].content`       | string            | HTML-stripped, markdown-friendly text. Capped at 5,000 chars; see `truncated`.                                                                  |
| `notes[].documents[].truncated`     | boolean           | `true` when content was truncated. Fetch the full document via [`get_note(include: ['documents'])`](./get-note) if you need the rest.           |
| `nextCursor`                        | string \| null    | Reserved for future use; currently always `null`.                                                                                               |
| `degraded`                          | boolean           | `true` when the search index was unavailable or failed.                                                                                         |
| `degradedReason`                    | string \| null    | When `degraded=true`: `"search_index_unavailable"` or `"search_index_degraded"`.                                                                |

<Warning>
  **Don't discriminate on `templateTitle`.** It's a display label, not an enum. For boolean checks ("is this a one-pager?") use `templateId` against a known managed-template id, or surface both fields and let your UI choose by label.
</Warning>

### Degraded Response

When the search index is unavailable, the response sets `degraded=true` and returns an empty `notes` array:

```json theme={"system"}
{
  "notes": [],
  "nextCursor": null,
  "degraded": true,
  "degradedReason": "search_index_unavailable"
}
```

`degradedReason` is one of:

* `search_index_unavailable` — the search index is disabled in this environment.
* `search_index_degraded` — the search index threw an error mid-query.

Surface this to the LLM/user so retries can be deferred or the result interpreted appropriately.

## Usage Examples

### Example 1: Topical search

**Request:**

```json theme={"system"}
{
  "keyword": "OKR"
}
```

Returns the 10 most relevant notes mentioning OKR, each with its primary documents inline.

### Example 2: Folder-scoped Korean keyword

**Request:**

```json theme={"system"}
{
  "keyword": "네이버",
  "filter": {
    "folderId": "455765"
  },
  "pagination": {
    "size": 5
  }
}
```

Searches `네이버` only inside folder `455765` (and descendants). Korean morphological analysis runs server-side.

### Example 3: Date-bounded deep search

**Request:**

```json theme={"system"}
{
  "keyword": "release",
  "filter": {
    "createdAtFrom": "2026-04-01T00:00:00Z",
    "createdAtTo": "2026-05-01T00:00:00Z"
  }
}
```

## Best Practices

<AccordionGroup>
  <Accordion title="Start with list_notes, switch to search_notes when content is needed">
    Use [`list_notes`](./list-notes) first to find which notes match. If the LLM still needs document content to answer, switch to `search_notes` for the same keyword. This is the cheapest progressive-disclosure path.
  </Accordion>

  <Accordion title="Use folderId to narrow the search universe">
    A folder-scoped search is faster and returns better-ranked results because the relevance space is smaller. Pair with `list_notes(filter.folderId)` to find folder candidates first.
  </Accordion>

  <Accordion title="Don't loop on `nextCursor`">
    The deep-search endpoint currently emits `nextCursor: null`. If 30 results aren't enough, refine the keyword (more specific) or scope by folderId/dateRange instead of paginating.
  </Accordion>

  <Accordion title="Watch the `degraded` flag">
    A `degraded=true` response is honest — the search index couldn't run. Don't treat the empty `notes` array as "no matches"; instead, surface the degradation to the user/LLM so retries can be timed appropriately.
  </Accordion>
</AccordionGroup>

## Common Errors

### Empty keyword

<Error>
  ```json theme={"system"}
  {
    "error": "keyword must not be blank",
    "code": "BAD_REQUEST",
    "statusCode": 400
  }
  ```
</Error>

## Token Usage

**Per Note (with documents):** \~1,500 tokens average. Truncated documents cap at \~1,500 tokens regardless of source size.

<Tip>
  **Compared to `get_note_transcript`** (\~3,000–5,000 tokens per hour of meeting), `search_notes` returns curated documents that are typically 10× more compact and cover the same key decisions. Reach for transcripts only when you need exact spoken words.
</Tip>
