# How-to: search across everything

**Goal:** find records by what they say, not by walking list endpoints —
one query over people, events, documents, invoices, communications and
workflow definitions, with highlighted matches and typeahead suggestions.

Assumes the [Quickstart](/docs/quickstart.html) (`$LYDIAN`, `$TENANT`, `$KEY`
exported). Examples that touch documents or communications also send
`X-Acting-Person: $ME` — a person id in your tenant.

There is one full-text index (embedded Lucene) and every record kind is in
it, written on every create or update and cleared on delete. Each kind's
base fields map onto four analyzed fields so a single query shape serves
them all:

| field | person | event | document | invoice | communication | workflow |
|---|---|---|---|---|---|---|
| **title** | full name | name | title | name (or number) | subject | name |
| **description** | — | description | description | memo | — | description |
| **attrValues** | attribute values (text only — reference ids are skipped) | ← | ← | ← | ← | — |
| **content** | email | participant names | extracted text | number, issuer, bill-to, recipient + team names, line descriptions | body, sender + recipient names | trigger + step kinds |

So "meetings with Alice" and "invoices for Globex" are queries, not joins.

---

## 1. Search one kind

Every kind has a `…/search` next to its list endpoint, returning the full
entity, its relevance score and (optionally) highlights:

```bash
curl -s -H "X-API-Key: $KEY" \
  "$LYDIAN/tenants/$TENANT/people/search?q=marlowe" | jq 'map({name: (.person.firstName + " " + .person.lastName), score})'
```

```json
[{"name": "Alice Marlowe", "score": 2.81}]
```

The hit wraps the entity under a kind-named key — `hit.person`, `hit.event`,
`hit.document`, `hit.invoice`, `hit.communication`, `hit.definition` — never a
flattened summary. The per-kind endpoints:

```
GET /people/search?q=                 GET /invoices/search?q=
GET /events/search?q=                 GET /communications/search?q=      (X-Acting-Person)
GET /documents/search?q=  (X-Acting-Person)   GET /workflow-definitions/search?q=
```

All take `limit` (default 20, capped at 100) and `highlight` (default false).

## 2. Search across kinds

```bash
curl -s -H "X-API-Key: $KEY" -H "X-Acting-Person: $ME" \
  "$LYDIAN/tenants/$TENANT/search?q=zephyr" | jq 'map({kind, id, title, score})'
```

```json
[
  {"kind": "DOCUMENT", "id": "0198c3…", "title": "Zephyr spec",       "score": 3.02},
  {"kind": "EVENT",    "id": "0198c2…", "title": "Zephyr planning",   "score": 2.77},
  {"kind": "INVOICE",  "id": "0198c4…", "title": "INV-000012",        "score": 1.40},
  {"kind": "PERSON",   "id": "0198c0…", "title": "Zephyr Lumen",      "score": 1.12}
]
```

`/search` returns **lightweight hits** — `{kind, id, score, title,
highlights}` — fetch the record through its own endpoint when you need it.
Narrow with `kinds`, a comma-separated subset of `person, event, document,
invoice, communication, workflow`. A hit's `kind` comes back as the enum
name (`PERSON`, `DOCUMENT`, …) like every enum on the platform, while `kinds=`
accepts either spelling case-insensitively:

```bash
curl -s -H "X-API-Key: $KEY" \
  "$LYDIAN/tenants/$TENANT/search?q=zephyr&kinds=person,event" | jq 'map(.kind)'
```

An unknown kind is a `400 INVALID_REQUEST`.

## 3. Who sees what

Access rules are applied **after scoring, per kind, exactly as on the kind's
own endpoints**:

- **Documents** — only those the acting person may `READ`.
- **Communications** — only those the acting person sent or received.
- People, events, invoices, workflow definitions — no per-record access
  model; anyone who can reach the tenant sees them.

That means the identity you present shapes the result. With
`X-Acting-Person` (or an end-user bearer token, which binds it for you) the
two identity-bound kinds are filtered to that person. **Without one**, an
unrestricted `/search` silently omits documents and communications, and
asking for them explicitly (`kinds=document`) is a `401 UNAUTHENTICATED` —
the same answer `/documents/search` gives.

## 4. Highlight the matches

`highlight=true` adds a `highlights` map per hit: field → fragments, matches
wrapped in `<mark>`, at most two fragments per field, and only for fields
that actually matched.

```bash
curl -s -H "X-API-Key: $KEY" -H "X-Acting-Person: $ME" \
  "$LYDIAN/tenants/$TENANT/documents/search?q=migration&highlight=true" | jq '.[0].highlights'
```

```json
{
  "title":   ["<mark>Migration</mark> runbook"],
  "content": ["Step two: run the <mark>migration</mark> script and verify."]
}
```

The fragments are the stored text, so render them as HTML only if you trust
your own tenant's content — or strip everything but `<mark>`. Highlighting
re-analyzes each hit's stored text, so leave it off for typeahead.

## 5. Suggest terms while typing

`/search/suggest` completes the **last word** of `q` from the vocabulary that
actually occurs in your tenant's records, ranked by how many entries contain
each term:

```bash
curl -s -H "X-API-Key: $KEY" \
  "$LYDIAN/tenants/$TENANT/search/suggest?q=marg" | jq
```

```json
[
  {"term": "marguerite", "count": 3},
  {"term": "margaux",    "count": 1},
  {"term": "margin",     "count": 1}
]
```

`kinds` scopes the vocabulary the same way it scopes a search; `limit` caps
at 50. Purely numeric tokens (ids, amounts, dates) are never suggested, and
another tenant's words can never appear — the suggestion terms are stored
per tenant. Pair it with a debounced input and feed the chosen term back into
`q`.

## 6. Query syntax

Lucene classic syntax, parsed over all four fields with the title weighted
highest:

| you type | it means |
|---|---|
| `piano lesson` | either word (OR) |
| `"piano lesson"` | the phrase |
| `+piano -lesson` | must contain / must not contain |
| `pian*` | prefix |
| `title:piano` | one field only (`title`, `description`, `attrValues`, `content`) |

Matching is case-insensitive on whole words. An unparseable query (an
unbalanced quote, a leading `*`) is a `400 INVALID_REQUEST`; a blank one too.

## 7. Index data that predates search

Records created before the platform-wide index existed are not in it. Rebuild
once per tenant:

```bash
curl -s -X POST -H "X-API-Key: $KEY" \
  "$LYDIAN/tenants/$TENANT/search/reindex" | jq
```

```json
{"PERSON": 42, "EVENT": 318, "INVOICE": 27, "COMMUNICATION": 105, "WORKFLOW": 3, "DOCUMENT": 61}
```

It rewrites every kind from the source of truth and re-extracts every
document's text from object storage, so it takes a while on a large tenant.
Admin plane only: API key or admin token — end-user bearer tokens may search
and suggest but not rebuild.

---

## Next

- [Manage a document](/docs/manage-a-document.html) — the roles that decide
  which documents a search may return.
- [Generate an invoice from events](/docs/invoice-from-events.html) — the
  invoice fields (name, issuer, team members) that the invoice index covers.
