UI reference

Search (MicroSearch)

Platform-level command palette for searching within the app whose tab is open. The platform owns the endpoint, the access checks, the matching and the ranking; each app owns only what it searches and who may see it.

JS: public/js/microsearch.js CSS: public/js/microsearch.css Backend: core/search.py (registry and matcher), handlers/search.py (dispatcher) Trigger: Ctrl+K / Cmd+K, or click the navbar search bar


How it works

  1. The user opens the palette and types. MicroSearch debounces (200ms) and sends POST /search with {app, q, types}. A reply that arrives after a newer search was sent is discarded.
  2. The dispatcher refuses an app the member cannot open (the same check as opening the app), then loads the member's permissions for that app.
  3. Each searchable type may name permission keys; a type the member holds none of is never searched.
  4. The app's handler queries its own tables inside its own visibility rules, through the shared trigram matcher.
  5. The dispatcher ranks: under All, every result best match first; with chips chosen, chip order, best match first within each.

Endpoints

POST /search   {"app": "tasks", "q": "PL-417", "types": ["task"], "filters": {...}}
GET  /search?app=tasks&q=PL-417&types=task

Both run the same code. GET passes any other query parameter through as a filter (the Files picker sends type, owner, after, before, min_size, max_size). The response is the standard collection envelope (many()): results are in data.

Result fields

Field Type Description
type str One of the app's registered searchable types.
id str Short identifier shown as a badge before the title. Can be empty.
title str Primary text.
subtitle str Secondary text (context, status, parent).
url str Navigation URL when the result is selected.
score float Match strength. Results arrive already ranked.

Matching

core.search.Match builds the WHERE clause and score for a set of columns:

  • Substrings and IDs: each query word must appear in the record (PL-41 finds PL-417).
  • Typos: a word that does not appear still matches a word in the record within 1 edit (4 or 5 letters) or 2 edits (6 or more), so fshes and ifshes both find fishes. Candidates come from the pg_trgm index, edit distance from fuzzystrmatch.
  • Ranking: a word found in a name column outranks one found in a detail column; an exact match on the first name column (an ID, a code) ranks highest, then a prefix.

Every searched column set has a trigram index built on exactly the expression core.search.document() produces, in the app's own migration.


Implementing search for an app

1. Write the handler

from core.search import Search, search_rows

async def api_search(req: Request, ctx: Context, search: Search) -> list:
    if not search.wants("document"):
        return []
    rows = await search_rows(
        ctx.db.raw, search, source="paper_documents", columns="id, title", names=["title"],
        scope="workspace_id = $1 AND NOT is_deleted AND (visibility = $2 OR id = ANY($3::text[]))",
        params=[ctx.db.workspace_id, VISIBILITY_WORKSPACE, list(await accessible_doc_ids(ctx))],
    )
    return [{"type": "document", "id": r["id"], "title": r["title"], "score": r["score"],
             "url": f"/app/papers/doc/{r['id']}"} for r in rows]

scope is the app's own visibility rule, exactly as its list endpoint applies it. For text that lives outside a table (a resolved name, a mailbox subject), score it with rank_texts(), the same matcher.

2. Register it, in chip order

from core.search import Searchable, register

register("papers", api_search, [Searchable("document")])
register("crm", crm_search, [Searchable("pipeline", (DEALS_VIEW,)), Searchable("deal", (DEALS_VIEW,))])

A type's keys are the ones its list endpoint requires; holding any one is enough. A platform search that checks its own caller passes gate= instead of being checked as an app.

3. Index what it searches

CREATE INDEX IF NOT EXISTS paper_documents_search_trgm ON paper_documents
    USING gin ((lower(coalesce(title, ''))) gin_trgm_ops);

4. Include the search init partial

templates/partials/search_init.html, included in index.html inside {% block body %}:

<div x-data="{}" x-init="$nextTick(() => {
  MicroSearch.init({
    app: 'papers',
    apiBase: '{{ ctx.api_url | default("http://api.local.localhost:8000") }}',
    searchables: [
      { type: 'document', label: __('Documents'), icon: 'description', color: '#3B82F6' },
    ],
  });
})"></div>

The searchables order must match the registration order.


UI anatomy

┌─────────────────────────────────────────────────┐
│ 🔍  Search Tasks...                        Esc  │  ← input row
├─────────────────────────────────────────────────┤
│ All  ● Tasks  ● Boards  ● Schedules  ...        │  ← legend (All plus type filters)
├─────────────────────────────────────────────────┤
│ ● PL-417  Upgrade production Amebo to 0.9.4     │  ← best match first
│           Product Launch · Done                 │
│ ◆ PL      Product Launch                        │
│           406 tasks · 300 done                  │
├─────────────────────────────────────────────────┤
│ ↑↓ navigate    Enter open    Esc close          │  ← footer hints
└─────────────────────────────────────────────────┘

Legend filtering

All is active by default and searches every type the member may see, best match first. Clicking a type chip narrows to the chosen types; clicking All clears the choice. An app with one type shows no All chip.


CSS classes

Class Element Purpose
.ms-overlay div Fixed full-screen backdrop with blur. Click outside palette to close.
.ms-palette div The command palette container. Max-width 620px, centered.
.ms-input-row div Search input row with icon and Esc badge.
.ms-input input The search text input.
.ms-kbd kbd Keyboard shortcut badge.
.ms-legend div Row of filterable type chips.
.ms-legend-chip button All, or one type filter. .active when selected.
.ms-dot span 8px colored circle. Used in legend chips and results.
.ms-results div Scrollable results container (hidden scrollbar).
.ms-result a Single result row.
.ms-result-selected .ms-result Keyboard-selected result (highlighted).
.ms-result-id span ID badge before the title.
.ms-result-subtitle div Secondary line of text.
.ms-empty div Empty/loading state message.
.ms-footer div Keyboard hint bar at the bottom.
html.ms-open html Added when palette is open. Prevents body scroll.

Keyboard shortcuts

Shortcut Action
Ctrl+K / Cmd+K Open/close the search palette
Esc Close the palette
↑ / ↓ Navigate results
Enter Open the selected result