Global Search

March 20, 2026 · View on GitHub

Version: 1.0 Updated: 2026-03-20


1. Overview

Chorus Global Search provides unified search across 6 entity types: Task, Idea, Proposal, Document, Project, and Project Group. Users and AI agents can quickly locate any entity via the REST API, MCP tool, or the Cmd+K command palette UI.

2. Architecture

┌─────────────────────────────────────────────────┐
│  Frontend (global-search.tsx)                    │
│  Cmd+K Dialog · Filter Tabs · Keyboard Nav      │
│  Scope Selector (Global / Group / Project)       │
└──────────────────┬──────────────────────────────┘
                   │ GET /api/search?q=...&scope=...

┌─────────────────────────────────────────────────┐
│  REST API (api/search/route.ts)                  │
│  Auth · Param Validation · Scope Resolution      │
└──────────────────┬──────────────────────────────┘


┌─────────────────────────────────────────────────┐
│  Search Service (search.service.ts)              │
│  6 parallel Prisma queries · Snippet Generation  │
│  Unified Sort · Scope Filtering · Counts         │
└──────────────────┬──────────────────────────────┘

    ┌──────────────┼──────────────┐
    ▼              ▼              ▼
┌────────┐  ┌──────────┐  ┌───────────┐
│  Task  │  │   Idea   │  │ Proposal  │  ...
│  table │  │   table  │  │   table   │
└────────┘  └──────────┘  └───────────┘

┌─────────────────────────────────────────────────┐
│  MCP Tool (chorus_search)                        │
│  Registered in public.ts · All roles             │
│  Same search.service.search() backend            │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│  OpenClaw Plugin (common-tools.ts)               │
│  Proxy tool → mcpClient.callTool()               │
└─────────────────────────────────────────────────┘

3. Searchable Entities & Fields

EntitySearch FieldsStatus FieldScope Filtering
Tasktitle, descriptionstatusprojectUuid
Ideatitle, contentstatusprojectUuid
Proposaltitle, descriptionstatusprojectUuid
Documenttitle, contenttype (as status)projectUuid
Projectname, description"active" (fixed)uuid directly
Project Groupname, description"active" (fixed)uuid directly

4. Search Strategy

4.1 Current: ILIKE Substring Matching

All queries use Prisma's contains with mode: "insensitive", which translates to PostgreSQL ILIKE '%keyword%'.

where: {
  companyUuid,
  OR: [
    { title: { contains: query, mode: "insensitive" } },
    { description: { contains: query, mode: "insensitive" } },
  ],
}

Pros: Simple, no extra dependencies, no schema changes. Cons: No relevance ranking, no CJK word segmentation, performance degrades on large datasets.

When needed, migrate to to_tsvector / to_tsquery with GIN indexes for:

  • Relevance ranking (ts_rank)
  • Match highlighting (ts_headline)
  • Multi-language support (English + Chinese via zhparser)

5. Scope System

Three scope levels control the search boundary:

ScopeBehaviorParameter
globalAll entities in the companyNone
groupAll projects within a Project GroupscopeUuid = group UUID
projectSingle projectscopeUuid = project UUID

Scope Resolution

global  → no projectUuid filter
group   → resolveGroupProjects(groupUuid) → projectUuid IN [...]
project → projectUuid = scopeUuid

For group scope, the service first queries all project UUIDs belonging to the group, then uses projectUuid: { in: [...] } for filtering. Project and Project Group entities have their own scope logic (e.g., project scope returns only that project if it matches).

Scope Intelligence (UI)

The frontend automatically selects the default scope based on the current page:

Current PageDefault Scope
/projects (global)Global
/project-groups/{uuid}Group (with group name)
/projects/{uuid}/*This Project (with project name)

Users can manually switch scope via the dropdown in the search header.

6. API Design

6.1 REST API

GET /api/search?q=keyword&scope=global&scopeUuid=xxx&types=task,idea&limit=20
ParameterTypeRequiredDefaultDescription
qstringYes-Search query
scopeenumNoglobalglobal | group | project
scopeUuidstringNo-Required when scope is group or project
typesstringNoallComma-separated: task,idea,proposal,document,project,project_group
limitnumberNo20Max results (1-100)

Response:

{
  "success": true,
  "data": {
    "results": [
      {
        "entityType": "task",
        "uuid": "...",
        "title": "...",
        "snippet": "...matched context...",
        "status": "in_progress",
        "projectUuid": "...",
        "projectName": "...",
        "updatedAt": "2026-03-20T..."
      }
    ],
    "counts": {
      "tasks": 3, "ideas": 2, "proposals": 1,
      "documents": 4, "projects": 1, "projectGroups": 1
    }
  }
}

6.2 MCP Tool

chorus_search({
  query: "keyword",
  scope?: "global" | "group" | "project",
  scopeUuid?: "uuid",
  entityTypes?: ["task", "idea", "proposal", "document", "project", "project_group"]
})

Registered in public.ts — available to all agent roles (PM, Developer, Admin).

7. Search Execution

7.1 Parallel Query

All 6 entity type queries execute in parallel via Promise.all(). Each query returns both results (with take: limit) and a count (total matches).

const searchPromises = [
  searchTasks(companyUuid, query, projectUuids, limit),
  searchIdeas(companyUuid, query, projectUuids, limit),
  searchProposals(companyUuid, query, projectUuids, limit),
  searchDocuments(companyUuid, query, projectUuids, limit),
  searchProjects(companyUuid, query, projectUuids, limit),
  searchProjectGroups(companyUuid, query, groupUuid, limit),
];
const searchResults = await Promise.all(searchPromises);

7.2 Result Merging & Sorting

Results from all entity types are merged into a single array, sorted by updatedAt descending, then truncated to the requested limit.

7.3 Snippet Generation

For each result, a ~100 character snippet is extracted around the first match position:

  1. Find the match position in the text (case-insensitive)
  2. Calculate a window centered on the match
  3. Adjust start to a word boundary if possible
  4. Add ellipsis markers (...) when truncated

If the query doesn't match in the description/content, the beginning of the text is returned instead.

8. Frontend Design

8.1 Cmd+K Command Palette

The search UI is a Dialog-based command palette, inspired by Linear and Notion:

  • Trigger: Sidebar button (desktop) / Header icon (mobile) / Cmd+K keyboard shortcut
  • Search Header: Input field + Scope selector dropdown
  • Filter Tabs: All / Tasks / Ideas / Proposals / Documents / Projects (server-side filtering)
  • Results List: Scrollable list with keyboard navigation
  • Footer: Keyboard hints (desktop only)

8.2 Keyboard Navigation

KeyAction
Cmd+K / Ctrl+KOpen search dialog
EscClose dialog
/ Navigate results
EnterOpen selected result

Selected result is visually highlighted and auto-scrolls into view.

8.3 Filter Tabs (Server-Side)

Clicking a filter tab sends a new API request with types=<selected_type>, returning up to 20 results of that specific type. This ensures the user sees the full Top 20 of each type, not just what happened to be in the mixed Top 20.

8.4 Responsive Design

AspectDesktopMobile
TriggerSidebar button with text + ⌘K badgeHeader icon button
Dialog positiontop: 20%top: 1rem
Dialog widthmax-w: 600px100vw - 2rem
Footer hintsVisibleHidden
Filter tabsInlineHorizontally scrollable

8.5 Navigation

Clicking a result navigates to the entity's detail page:

Entity TypeRoute
Task/projects/{projectUuid}/tasks/{taskUuid}
Idea/projects/{projectUuid}/ideas/{ideaUuid}
Proposal/projects/{projectUuid}/proposals/{proposalUuid}
Document/projects/{projectUuid}/documents/{documentUuid}
Project/projects/{projectUuid}/dashboard
Project Group/projects

9. File Map

FilePurpose
src/services/search.service.tsCore search logic, types, snippet generation
src/services/__tests__/search.service.test.ts16 unit tests (90%+ branch coverage)
src/app/api/search/route.tsREST API endpoint
src/components/global-search.tsxCmd+K dialog component
src/app/(dashboard)/layout.tsxSearch trigger integration
src/mcp/tools/public.tschorus_search MCP tool
packages/openclaw-plugin/src/tools/common-tools.tsOpenClaw proxy tool
messages/en.json / messages/zh.jsoni18n keys under search.*
docs/design.penUI mockup (frame: "Chorus - Global Search")

10. Multi-tenancy

All search queries are scoped by companyUuid from the authenticated user's AuthContext. No cross-tenant data leakage is possible — every Prisma where clause includes companyUuid as a mandatory filter.