Searching a timeline
Search in the timeline toolbar (or / outside a text field) opens a search dialog for the open timeline. Type words from a title or notes. Every word must match. Results list moments and durations, 25 per page, with each one's time or extent and a preview of its notes. A moment also matches through the titles and notes of its nested entries, such as stacks. The view does not move while you search. Choosing a result closes the dialog and moves the view to it: a moment is centred at the current zoom; a duration is fitted with a margin on both sides. The chosen moment or band pulses briefly. Nothing else opens; click it to edit.
Matching depends on where the timeline lives:
| Timeline | How it matches | Order |
|---|---|---|
| Server (PostgreSQL) | Full-text search with the language-neutral simple configuration. Each word matches the start of a word (harb finds "harbour"). | Rank (titles weigh more than notes), then time |
Desktop .och file | Each word occurs anywhere in the title or notes. Case is ignored for ASCII letters only. | Title matches first, then time |
| In-memory (browser drafts, offline HTML) | Each word occurs anywhere in the title or notes, ignoring case. | Title matches first, then time |
Indexed (server and desktop) timelines search the saved version. Unsaved edits replace their saved matches on the current page and are flagged in the result count, which is then approximate. Comparisons have no search.
Storage and API
PostgreSQL keeps one oc_entity_search row per event (moment or duration) with a stored, weighted tsvector. Sparse saves update the rows of changed entities; whole-document saves rebuild them with the rational index, and npm run migrate builds them once for existing timelines (oc_timelines.search_version). A GIN index on the vectors (oc_entity_search_document) finds a search's matches without reading the rest of the timeline. A search's cost therefore follows how many entities match, not how large the timeline is. At a million moments, a search for one moment's number took 4–13 ms, on a fork as on its source; without the index, every search read all million rows (0.6–0.9 s). A search matching many entities reads each of them to rank and count them: one matching 7% of a million moments takes about 0.7 s (0.9 s on a fork). Only the page's results are read whole. The index is on the text alone, and each match's timeline is checked on its row; a small timeline may read its own rows by the table's primary key instead. A fork searches what it reads through to the same way, a level at a time, including the versions its source has changed since (their table, oc_entity_search_retired, has the same index). The index adds about 15% to creating a timeline, nothing measurable to saves, and about a tenth to the search rows' size.
POST /api/timelines/:id/query accepts {"kind": "search", "text": "harbor", "page": 1} (at most 200 characters; pages 1–4,000; "only": "moment" leaves out durations, as the duration endpoint selector does) and returns {results: [{kind, id, first, last, title, snippet}], total, page}. The desktop uses the same request through its native query. Search terms are lowercased words of letters and digits (at most eight), so punctuation and query operators are never interpreted as syntax.