Add a central search feature to search for a card #159

Closed
opened 2026-08-26 19:26:40 +00:00 by Grandiras · 2 comments
Owner

Make this semantically aware, so maybe use a simple embedding model and make the DB vector-like. This way, cards can also be found by their context (take title and description into account). Should be a centralized "omni-search", so user-based and cross-board.

Followup-Idea: Use @ to search for cards by a user, date: for date-based search (allow things like date:September), and tag: for tag-based search. Add more if you see fit. Add a simple teaching tip inside the default flyout (so before a search has been performed, but after the search box has been focused. Design it in a way that fits on desktop (on the nav bar in the center) and mobile (separate page? modal? flyout?)

Make this semantically aware, so maybe use a simple embedding model and make the DB vector-like. This way, cards can also be found by their context (take title and description into account). Should be a centralized "omni-search", so user-based and cross-board. Followup-Idea: Use @ to search for cards by a user, date: for date-based search (allow things like date:September), and tag: for tag-based search. Add more if you see fit. Add a simple teaching tip inside the default flyout (so before a search has been performed, but after the search box has been focused. Design it in a way that fits on desktop (on the nav bar in the center) and mobile (separate page? modal? flyout?)
Collaborator

🤖 On it — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 On it — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/386); the reply will link the full session transcript. <!-- claude-run:comment --> 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-159/run-386.md) — updating every ~5 minutes while the task runs.
Collaborator

Pushed claude/issue-159.

What it does

One box for every board, in the middle of the header — the magnifier below 960px, and the middle of the phone's bottom bar. Type a word and it finds the card on any board you can see.

The search box open in the header, showing cards from two boards found by a word that is only in their descriptions

Context comes from the card's surroundings. A word is looked for in five places at once — title, tags, the names of the people on it, the board and column it lives in, and the description — and counts wherever it lands. So urgent deploy finds the card tagged Urgent whose description says time to deploy, with neither word in its title. Half-typed words, typos (encrytpion → Rotate the encryption key; transpositions cost one edit, not four) and both languages' plurals are handled by the matching rules rather than a stemmer.

Four operators, taught in a flyout the moment the box is focused and before anything is typed:

Typed Finds
@alice somebody on the card — assignee or author, by handle or display name
tag:design wearing that tag
date:September due then. Also today/tomorrow/week/month/overdue/none, and 2027, 2027-02, 2027-02-14
board:Roadmap on that board

Repeating one operator widens (tag:bug tag:urgent = either), two different ones narrow. Both languages' spellings work whichever language is on screen (datum:überfällig on an English page) — operators are syntax, so switching language shouldn't cost you your muscle memory. Month names come out of LocaleService.Translations, so a third locale brings its own. / and Ctrl/⌘-K open it; ↑↓ and Enter walk the results; a result is a real <a> to /board/{id}?card={id}.

On a phone it is a full-screen mode rather than a sheet — it covers the bottom bar, which is the card dialog's rule, and pays for that with a way out on screen.

The phone's full-screen search, showing the teaching tip before anything has been typed

The one thing I did not build

The issue asked for an embedding model and a vector-like DB. I deliberately didn't, and docs/search.md carries the reasoning: every searchable column is AES-encrypted with non-deterministic ciphertext, so there is nothing for pgvector to index. A plaintext embedding beside a ciphertext column is close enough to the content that it undoes the encryption for the one table anybody would attack; an encrypted one can only be read by decrypting all of it — which is exactly the in-memory pass SearchService already does, plus ~90 MB of model in the image. What ranking quality that costs is bought back by the five-field matching above. I filed the decision as an issue with the measurement that would change it.

SearchService reads and decrypts the viewer's cards once per search session, scores them against every keystroke after that, and drops the snapshot when the box closes — a plaintext copy of a member's whole board set isn't a thing to hold for the life of a circuit. Two collection includes and no comments, on CardService.GetColumnsAsync's rule.

Verified

  • dotnet build and dotnet test green — 444 tests, 27 new across SearchQueryTests / CardSearchTests (operators, both languages' date words, month disambiguation, field weights, fuzzy budgets, umlaut folding).
  • Ran the app for real against Postgres and drove it with Playwright: new tests/e2e/search.spec.js (2 tests) passes, as do filter, mobile, board, boards, calendar, changelog, install, prefs, icons. Every query in the table above was executed against a live two-board deployment.
  • Four specs failed in that environment — board.spec.js "edit a card", two in calendar.spec.js, one in boards.spec.js. All four reproduce with my changes stashed on an untouched main, so they are environmental/pre-existing; the board.spec.js one I filed, since it fails 2 runs in 6 on a clean tree.
  • Changelog bullet in both locales; docs/search.md is new and in CLAUDE.md's table, with orientation, ui, frontend and live-updates updated to point at it.

One gap I recorded rather than fixed: design/Ponente.free has no frame for the search box (Lunacy isn't reachable from CI), so every header in the canvas is now the one without it. That is written into docs/frontend.md's "still approximate" list.


⏱ 58m 23s · 179 turns · tokens: 315.8k in / 119.8k out (+39408.6k cache-read)
📜 Full session transcript
→ Opened PR: #162
→ Filed follow-up issue #163: Search: revisit semantic ranking once the corpus outgrows the in-memory pass
→ Filed follow-up issue #164: Search does not index card comments
→ Filed follow-up issue #165: Flaky e2e: "edit a card, verify changes persist" fails ~1 run in 3 on Escape

Pushed **`claude/issue-159`**. ## What it does One box for every board, in the middle of the header — the magnifier below 960px, and the middle of the phone's bottom bar. Type a word and it finds the card on any board you can see. ![The search box open in the header, showing cards from two boards found by a word that is only in their descriptions](https://git.grandiras.net/attachments/9db8e059-c664-4892-9fad-93752f7aeb0b) **Context comes from the card's surroundings.** A word is looked for in five places at once — title, tags, the names of the people on it, the board and column it lives in, and the description — and counts wherever it lands. So `urgent deploy` finds the card tagged *Urgent* whose description says *time to deploy*, with neither word in its title. Half-typed words, typos (`encrytpion` → *Rotate the encryption key*; transpositions cost one edit, not four) and both languages' plurals are handled by the matching rules rather than a stemmer. **Four operators**, taught in a flyout the moment the box is focused and before anything is typed: | Typed | Finds | |---|---| | `@alice` | somebody on the card — assignee **or** author, by handle or display name | | `tag:design` | wearing that tag | | `date:September` | due then. Also `today`/`tomorrow`/`week`/`month`/`overdue`/`none`, and `2027`, `2027-02`, `2027-02-14` | | `board:Roadmap` | on that board | Repeating one operator widens (`tag:bug tag:urgent` = either), two different ones narrow. Both languages' spellings work whichever language is on screen (`datum:überfällig` on an English page) — operators are syntax, so switching language shouldn't cost you your muscle memory. Month names come out of `LocaleService.Translations`, so a third locale brings its own. `/` and Ctrl/⌘-K open it; ↑↓ and Enter walk the results; a result is a real `<a>` to `/board/{id}?card={id}`. On a phone it is a full-screen mode rather than a sheet — it covers the bottom bar, which is the card dialog's rule, and pays for that with a way out on screen. ![The phone's full-screen search, showing the teaching tip before anything has been typed](https://git.grandiras.net/attachments/d236558b-8566-496b-85d1-6f6d40b70860) ## The one thing I did not build The issue asked for an embedding model and a vector-like DB. **I deliberately didn't**, and `docs/search.md` carries the reasoning: every searchable column is AES-encrypted with non-deterministic ciphertext, so there is nothing for `pgvector` to index. A plaintext embedding beside a ciphertext column is close enough to the content that it undoes the encryption for the one table anybody would attack; an encrypted one can only be read by decrypting all of it — which is exactly the in-memory pass `SearchService` already does, plus ~90 MB of model in the image. What ranking quality that costs is bought back by the five-field matching above. I filed the decision as an issue with the measurement that would change it. `SearchService` reads and decrypts the viewer's cards **once per search session**, scores them against every keystroke after that, and drops the snapshot when the box closes — a plaintext copy of a member's whole board set isn't a thing to hold for the life of a circuit. Two collection includes and no comments, on `CardService.GetColumnsAsync`'s rule. ## Verified - `dotnet build` and `dotnet test` green — 444 tests, 27 new across `SearchQueryTests` / `CardSearchTests` (operators, both languages' date words, month disambiguation, field weights, fuzzy budgets, umlaut folding). - Ran the app for real against Postgres and drove it with Playwright: new `tests/e2e/search.spec.js` (2 tests) passes, as do `filter`, `mobile`, `board`, `boards`, `calendar`, `changelog`, `install`, `prefs`, `icons`. Every query in the table above was executed against a live two-board deployment. - Four specs failed in that environment — `board.spec.js` "edit a card", two in `calendar.spec.js`, one in `boards.spec.js`. **All four reproduce with my changes stashed on an untouched `main`**, so they are environmental/pre-existing; the `board.spec.js` one I filed, since it fails 2 runs in 6 on a clean tree. - Changelog bullet in both locales; `docs/search.md` is new and in `CLAUDE.md`'s table, with `orientation`, `ui`, `frontend` and `live-updates` updated to point at it. One gap I recorded rather than fixed: `design/Ponente.free` has no frame for the search box (Lunacy isn't reachable from CI), so every header in the canvas is now the one without it. That is written into `docs/frontend.md`'s "still approximate" list. --- ⏱ 58m 23s · 179 turns · tokens: 315.8k in / 119.8k out (+39408.6k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-159/run-386.md) → Opened PR: https://git.grandiras.net/Grandiras/Ponente/pulls/162 → Filed follow-up issue [#163](https://git.grandiras.net/Grandiras/Ponente/issues/163): Search: revisit semantic ranking once the corpus outgrows the in-memory pass → Filed follow-up issue [#164](https://git.grandiras.net/Grandiras/Ponente/issues/164): Search does not index card comments → Filed follow-up issue [#165](https://git.grandiras.net/Grandiras/Ponente/issues/165): Flaky e2e: "edit a card, verify changes persist" fails ~1 run in 3 on Escape
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Grandiras/Ponente#159
No description provided.