Skip to content

Search Match Engine

Search runs entirely on the client against the Dexie cache, so it works offline (Product Brief §1.1). The engine lives in src/lib/search/.

The modules of the engine and how they connect:

flowchart LR
  Search["search(...)<br/>(search.ts)"] --> Matchable["isMatchable(query)<br/>(normalize.ts)"]
  Search --> Match["match(...)<br/>(match.ts)"]
  Match --> Fold["fold(s)<br/>(normalize.ts)"]
  Match --> Snippet["snippet(text, query)<br/>(snippet.ts)"]
  Match --> Cache[("Dexie cache<br/>(TaskRow + StepRow)")]
  Match --> Result["SearchResult[]<br/>(MatchOutcome)"]
  • normalize.tsfold(s) normalizes text (case/diacritics) for accent-insensitive matching; isMatchable(query) gates whether a query is searchable (e.g. non-empty after folding).
  • match.tsmatch(...) produces a MatchOutcome over the match fields MatchField = 'title' | 'notes' | 'step', with MatchOptions/SearchResult types.
  • search.tssearch(...) is the top-level entry that runs the matcher across the cached Task set (title, notes, and child Step titles) and returns ranked SearchResults.
  • snippet.tssnippet(text, query, max = 80) builds a highlighted excerpt around the match for result display.

Search spans the Tasks a user can see (owned + member Lists), matching across a Task’s title, its notes, and its Steps’ titles. Because it reads the local cache, results are instant and available offline; there is no server search endpoint on the hot path.

The match/scoring pipeline inside match(...), with field precedence and the cap branch:

flowchart TD
  Q{isMatchable<br/>query?} -->|no| Empty["empty MatchOutcome"]
  Q -->|yes| P1["Pass 1: fold title then notes<br/>per Task (one entry each)"]
  P1 --> P2{Task already<br/>title/notes match?}
  P2 -->|no| Step["Pass 2: fold Step title<br/>attribute to parent Task"]
  P2 -->|yes| Skip["skip Step (precedence)"]
  Step --> Sort["sort: tier then<br/>updatedAt desc then id asc"]
  Skip --> Sort
  Sort --> Cap{count > cap?}
  Cap -->|yes| Trunc["slice to cap, truncated = true"]
  Cap -->|no| Full["return all results"]

Related: Offline Outbox & Optimistic Writes (the cache it searches), Create-Form & Search Triggers (UI entry, deferred guide).