Search: global-as-you-type with ranked name matching #292

Closed
opened 2026-08-20 19:41:10 +00:00 by Cordy · 2 comments
Owner

Dogfood finding (2026-08-20, Nikola): "the search bar can so far only search within the current folder." Scouted reality: a global search already existsGET /api/v1/search?q= (internal/api/search.go) does a bounded BFS from / over the scoped store (exactly the caller's visible tree: Personal + member spaces), case-insensitive substring, caps 200 results / 500 dirs / depth 12. But the UI fires it only on Enter; typing runs filterRows which just hides rows of the current listing. The global capability is invisible.

Target UX: typing in the search box (≥2 chars) IS the global search — debounced live results across everything the user can access, ranked intelligently. Enter no longer required (still works). Escape restores the previous view (existing behavior).

Frontend

  • Debounce input ~300ms; issue /api/v1/search, render into the existing search view (renderRows(items, {showPath:true})).
  • Stale-response guard: abort/sequence-number the in-flight request so a slow earlier query can never overwrite newer results.
  • <2 chars: restore the current listing (no more local-only filter mode — the server search covers the current folder anyway).

Backend ranking ("intelligent")
Replace the plain path sort with match-quality ranking, all case-insensitive on the NAME:

  1. exact name match (incl. name-without-extension)
  2. name starts with query
  3. a word boundary in the name starts with the query (-, _, ., space, digit/letter transitions)
  4. substring — earlier position ranks higher
    Tiebreaks: shallower path first, then name alphabetically. Multi-token queries (split on whitespace): every token must match the name; rank by the best token's class.

Bounds unchanged (no index; the backend stays the source of truth). truncated already reported and surfaced.

Mockup-first per house process.

**Dogfood finding (2026-08-20, Nikola):** "the search bar can so far only search within the current folder." Scouted reality: a **global search already exists** — `GET /api/v1/search?q=` (internal/api/search.go) does a bounded BFS from `/` over the scoped store (exactly the caller's visible tree: Personal + member spaces), case-insensitive substring, caps 200 results / 500 dirs / depth 12. But the UI fires it **only on Enter**; typing runs `filterRows` which just hides rows of the current listing. The global capability is invisible. **Target UX:** typing in the search box (≥2 chars) IS the global search — debounced live results across everything the user can access, ranked intelligently. Enter no longer required (still works). Escape restores the previous view (existing behavior). **Frontend** - Debounce input ~300ms; issue `/api/v1/search`, render into the existing `search` view (`renderRows(items, {showPath:true})`). - Stale-response guard: abort/sequence-number the in-flight request so a slow earlier query can never overwrite newer results. - <2 chars: restore the current listing (no more local-only filter mode — the server search covers the current folder anyway). **Backend ranking ("intelligent")** Replace the plain path sort with match-quality ranking, all case-insensitive on the NAME: 1. exact name match (incl. name-without-extension) 2. name starts with query 3. a word boundary in the name starts with the query (`-`, `_`, `.`, space, digit/letter transitions) 4. substring — earlier position ranks higher Tiebreaks: shallower path first, then name alphabetically. Multi-token queries (split on whitespace): every token must match the name; rank by the best token's class. **Bounds unchanged** (no index; the backend stays the source of truth). `truncated` already reported and surfaced. Mockup-first per house process.
Author
Owner

Shipped as v0.6.56 (PR #294), live on both dogfoods.

What changed:

  • Typing 2+ characters in the search box now runs the global search (everything you have access to — Personal + spaces), debounced at 300ms, with stale responses discarded. Under 2 characters falls back to the old local row filter; Escape clears and returns to your folder; Enter still forces an immediate search.
  • Results are ranked: exact name (extension ignored) → starts-with → word start (q3-fall-…) → plain contains by position in the name; ties broken by shallower path, then name. Multi-word queries need every word to match.

Dogfood checklist:

  • Type 2+ chars anywhere — results from other folders/spaces appear without pressing Enter (after ~0.3s)
  • Type fast / delete back — no flicker from stale results; going below 2 chars restores the folder listing
  • A file named exactly like the query (with or without extension) ranks above prefix matches, which rank above mid-word hits (e.g. search "fall": fall.pdf > fall-report… > q3-fall… > waterfall…)
  • Multi-word query ("fall report") only returns names containing both
  • Escape returns to the folder you were in; Enter still works
  • Results respect access: nothing from paths you can't browse
  • Clicking a result row opens/downloads as before (paths shown under names)
Shipped as **v0.6.56** (PR #294), live on both dogfoods. **What changed:** - Typing 2+ characters in the search box now runs the **global** search (everything you have access to — Personal + spaces), debounced at 300ms, with stale responses discarded. Under 2 characters falls back to the old local row filter; Escape clears and returns to your folder; Enter still forces an immediate search. - Results are ranked: exact name (extension ignored) → starts-with → word start (`q3-fall-…`) → plain contains by position in the name; ties broken by shallower path, then name. Multi-word queries need every word to match. **Dogfood checklist:** - [ ] Type 2+ chars anywhere — results from other folders/spaces appear without pressing Enter (after ~0.3s) - [ ] Type fast / delete back — no flicker from stale results; going below 2 chars restores the folder listing - [ ] A file named exactly like the query (with or without extension) ranks above prefix matches, which rank above mid-word hits (e.g. search "fall": `fall.pdf` > `fall-report…` > `q3-fall…` > `waterfall…`) - [ ] Multi-word query ("fall report") only returns names containing both - [ ] Escape returns to the folder you were in; Enter still works - [ ] Results respect access: nothing from paths you can't browse - [ ] Clicking a result row opens/downloads as before (paths shown under names)
Author
Owner

Dogfood verified by Nikola on both instances (v0.6.56) — search-as-you-type with ranked global results works as designed. Closing.

Solution summary: the global search engine already existed (bounded BFS over the caller's scoped store) but only fired on Enter. The fix wired typing directly to it (300ms debounce, sequence guard against stale responses, Escape restore) and added ranked matching in internal/api/search.go: matchClass (exact incl. sans-extension → prefix → word-boundary → substring-by-position) and rankName (multi-token: all must match, worst class scores), sorted rank → path depth → name. Covered by TestMatchClassOrdering / TestRankNameMultiToken.

Dogfood verified by Nikola on both instances (v0.6.56) — search-as-you-type with ranked global results works as designed. Closing. **Solution summary:** the global search engine already existed (bounded BFS over the caller's scoped store) but only fired on Enter. The fix wired typing directly to it (300ms debounce, sequence guard against stale responses, Escape restore) and added ranked matching in `internal/api/search.go`: `matchClass` (exact incl. sans-extension → prefix → word-boundary → substring-by-position) and `rankName` (multi-token: all must match, worst class scores), sorted rank → path depth → name. Covered by `TestMatchClassOrdering` / `TestRankNameMultiToken`.
Cordy closed this issue 2026-08-20 20:36:03 +00:00
Sign in to join this conversation.
No labels
No project
No assignees
1 participant
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: Cordy/Cairn#292
No description provided.