Skip to content

Drawer search: loading feedback + relevance ordering everywhere#33

Merged
sysread merged 1 commit into
mainfrom
claude/add-search-loading-feedback-4YUPk
May 12, 2026
Merged

Drawer search: loading feedback + relevance ordering everywhere#33
sysread merged 1 commit into
mainfrom
claude/add-search-loading-feedback-4YUPk

Conversation

@sysread
Copy link
Copy Markdown
Owner

@sysread sysread commented May 12, 2026

SYNOPSIS

Every drawer-side search now shows a Scanner spinner while
the embed+query round-trip is in flight and ranks results by
closest match
. Chat, wiki, journal, recipes - one shape.

PURPOSE

Currently the four drawer searches disagree on feedback and
order
:

  • chat: spinner only on first-result-empty; stale results stay during subsequent searches
  • wiki: spinner only on first load; an active query is forced back to alphabetical, throwing away the cosine ranking
  • journal: substring only over in-memory entries; no spinner
  • recipes: substring only over in-memory titles; no spinner; no embedding pipeline at all on the server side

Bad because the user can't tell working from frozen between
keystrokes, and the closest-meaning match doesn't surface.

DESCRIPTION

Three layers.

(1) How it works today.

  • Chat search runs embedsearchThreads (exact-title hits then
    semantic by similarity); the Scanner is gated on searchResults.length === 0 so subsequent searches read as frozen.
  • Wiki sidebar runs embedsearchWikiArticles (semantic merged
    with ILIKE) but re-sorts the result list alphabetically at the
    view layer
    , and the loading text only fires on first mount.
  • Journal sidebar filters journal.entries in-memory by substring
    over content/mood/topics. No async, no spinner.
  • Recipe sidebar filters cookbook.recipes in-memory by title
    substring
    . Schema has no embedding column on recipes (the
    comment in schema.sql:889 says "personal cookbook is small, ILIKE
    is fast enough").

(2) What this PR changes (same names, same order):

  • Chat: Scanner now replaces any prior result list whenever
    searchBusy is true, not only when results are empty. Ordering
    untouched.
  • Wiki: alphabetical sort runs only when query is empty; an
    active query passes the server's cosine order through. Scanner
    replaces the listing for every in-flight search
    , not just the
    initial load.
  • Journal: rewritten to embed + searchJournalEntries (the same
    function the journal_search tool uses). Per-entry hits are
    aggregated by date keeping the best per-date similarity as
    the date's rank
    . Scanner during in-flight; empty-query browse
    mode unchanged.
  • Recipes: added the full embedding pipeline on the server side
    (no prior infra existed). New columns on public.recipes
    (embedding vector(2048) + claim cols), clear_recipe_embedding_on_change
    trigger, three RPCs (claim_next_pending_recipe,
    save_recipe_embedding_if_claimed, search_recipes_by_embedding),
    one new worker source (src/lib/embeddings/sources/recipes.ts)
    registered in worker.ts, one new service method
    (SupabaseService.searchRecipes) that merges semantic+ILIKE the
    same way searchWikiArticles does. Sidebar consumes it. Sort
    picker hides during a search since relevance is the active
    sort then. recipe_list / recipe_search tools are
    untouched
    - still ILIKE-on-title; the new pipeline only feeds
    the human drawer search.

(3) How this fixes PURPOSE.

Every search surface now uses the same idiom: type → debounce →
spinner replaces list → ranked results arrive. The closest-meaning
match is at the top in all four tabs.

Notes:

  • recipes ship graceful-degrades before the schema sync lands -
    searchRecipes falls back to ILIKE-only when the new RPC isn't
    present yet, matching the wiki/journal "no Venice client" path.
  • the schema change is idempotent (every statement is add column if not exists / drop ... if exists / create or replace) so
    mise run sync is safe to re-run.
  • the existing "No embedding column" comment block on recipes
    is updated rather than deleted - the rationale for the original
    decision is still relevant context (the LLM tool path still uses
    ILIKE; the new pipeline is for the human drawer).
  • the journal sidebar's previous substring-on-three-fields
    matching is replaced with the server-side semantic+ILIKE merge,
    which is a behavioural change (a query that matched a topic
    substring but not content might rank differently now). The
    trade-off was accepted as part of choosing the "Add semantic
    search everywhere" option.
  • docs: docs/user/search.md was a stub (H1 + placeholders); now
    fleshed out per-tab. docs/dev/cookbook.md and docs/dev/embeddings.md
    updated to mention the recipes source.

Test plan

  • open each drawer tab, confirm Scanner replaces the list while
    typing and results appear in relevance order
  • empty-query browse mode unchanged in all four tabs (wiki
    alphabetical; recipes by 'updated' or 'rating'; journal newest-day
    first; chat by Recent/Older/Archived buckets)
  • mise run sync applies the recipe-embedding schema cleanly
  • background embeddings worker picks up the recipes source on
    next sweep
  • recipe-list LLM tool still ILIKE-on-title (unchanged)
  • mise run check green

Generated by Claude Code

Every drawer-side search now gives the same kind of feedback while
embeddings are being computed and ranks results by closest match.
Chat already did this; the wiki, journal, and recipe sidebars now
behave the same way.

Wiki: the sidebar used to override the server-returned similarity
order with an alphabetical sort and only showed a "Loading wiki..."
string on the very first fetch. Subsequent searches read as frozen.
The sort now switches to alphabetical only when the query is empty;
an active query keeps the cosine-ordered result list intact and a
Scanner replaces the listing for the duration of any in-flight
search.

Journal: the sidebar was substring-only over the eagerly-loaded
`journal.entries` list with no spinner. It now embeds the query
via Venice and calls supabase.searchJournalEntries (the same
function the assistant's journal_search tool uses), aggregates
matching entries by date keeping the best per-date similarity as
the date's rank, and shows the Scanner while the round-trip is in
flight. Empty-query browse mode is unchanged.

Recipes: the cookbook originally had no embedding pipeline at all
on the rationale that ILIKE-on-title is enough for a single-user
cookbook. That's true for the LLM tool path, but the drawer is a
human surface where "fluffy potato side" should find "Mashed
Potatoes". Added the standard column + claim/save/search RPC trio
to public.recipes, a clear_recipe_embedding_on_change trigger, the
sources/recipes.ts worker adapter, and a searchRecipes() method on
SupabaseService that merges semantic and ILIKE hits the same way
searchWikiArticles does. The recipe_list / recipe_search tools
still use the ILIKE path; the new pipeline only feeds the sidebar.

Chat: the search box already ranked exact title hits ahead of
semantic ones and ordered semantic hits by cosine similarity. The
Scanner only appeared on a first-result-empty search though, so
subsequent queries kept showing stale results until they returned.
Aligned with the other three so the spinner replaces any prior
result list whenever a new search is in flight.

Docs: fleshed out docs/user/search.md (was a stub) so the new
behaviour is documented per-tab, and updated docs/dev/cookbook.md
and docs/dev/embeddings.md to mention the recipes source.
@sysread sysread merged commit 425884e into main May 12, 2026
1 check passed
@sysread sysread deleted the claude/add-search-loading-feedback-4YUPk branch May 12, 2026 16:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants