feat: backend-powered search with keyword/semantic/hybrid modes #16
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/pal-e-docs-app!16
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "14-feat-backend-powered-search-full-text-se"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds full-text and semantic search to the pal-e-app frontend, backed by the pal-e-docs
/notes/searchAPI endpoint. Users can search across all notes using keyword, semantic, or hybrid mode, with results showing title, note_type badge, project, headline snippet, and rank score.Changes
src/lib/api.ts: AddedSearchResultinterface andsearchNotes()function callingGET /notes/searchwith query params (q, mode, alpha, note_type, project, limit)src/routes/search/+page.server.ts: New server load function that reads URL search params and callssearchNotes(), returning results + query statesrc/routes/search/+page.svelte: Full search page with input, mode toggle (keyword/semantic/hybrid), filter pills, results list with type badges and headline snippets, empty state with search tipssrc/routes/+layout.svelte: Added nav bar search input (right side) with keyboard shortcuts (/andCmd+Kto focus), submits to/search?q=...src/routes/projects/[slug]/+page.svelte: Fixed pre-existing build error: inlinedCOLUMNSconstant instead of importing from$lib/api(which pulled in$env/dynamic/privateinto client-side code)Test Plan
/search-- should show empty state with search tips/notes/{slug}/from any page (not in an input) -- nav search focusesCmd+Kfrom any page -- nav search focuses/search?q=...npm run buildpassesReview Checklist
Related
plan-2026-03-13-pal-e-frontendSelf-Review
Build:
npm run buildpasses.npm run checkpasses with 0 errors (1 a11y warning for intentionalautofocuson search page).Files changed (5):
src/lib/api.tsSearchResulttype +searchNotes()functionsrc/routes/search/+page.server.tssearchNotes()src/routes/search/+page.sveltesrc/routes/+layout.svelte/andCmd+Kkeyboard shortcutssrc/routes/projects/[slug]/+page.svelteCOLUMNSconstant)Findings:
projects/[slug]/+page.sveltereferencesdata.boardwhich the server load does not return. This is a pre-existing type error (not introduced by this PR) and is guarded by{#if board}so it renders gracefully. Tracked separately.navSearchModein layout is not user-configurable from the nav bar (defaults tohybrid). This is intentional -- the full mode toggle lives on/searchpage. Nav search is a quick shortcut.Verdict: Ready for merge.
PR #16 Review
BLOCKERS
1. Missing loading indicator (AC5)
Acceptance criterion #5 requires: "Loading indicator during semantic search." There is no loading state in the code -- no
$statevariable tracking request progress, no spinner, no "Searching..." text. Semantic search can be noticeably slow (embedding generation + vector similarity), so this is a real UX gap, not just a cosmetic miss. The+page.svelteneeds a loading state that activates betweendoSearch()navigation and result render.2. Mode toggle does not auto-update results (AC4)
src/routes/search/+page.sveltelines 81-91 (in diff): The mode toggle buttons only setmode = min local state. They do not calldoSearch(). AC4 says "Mode toggle updates results and persists in URL" -- currently, toggling mode does nothing visible until the user manually clicks Search again. EitherdoSearch()should be called on mode change (when a query exists), or this acceptance criterion needs to be revisited.NITS
1. Unchecked mode cast --
src/routes/search/+page.server.ts:7This casts any arbitrary string to the union type without validation. A URL like
?q=test&mode=evilwould passevilthrough to the backend. The backend likely validates, so this is not a security issue, but a defensive check (e.g., falling back tohybridfor unrecognized values) would be cleaner:2. Unused
navSearchModevariable --src/routes/+layout.svelte:12navSearchModeis declared and used inhandleNavSearch()to set the URL mode param, but there is no UI element in the nav bar that lets users change it. It will always be'hybrid', making the conditionalif (navSearchMode !== 'hybrid')dead code. Either add a mode selector to the nav or remove this variable and the conditional.3.
COLUMNS/COLUMN_COLORSduplicationThese constants now exist in three places:
src/lib/api.ts,src/routes/projects/[slug]/+page.svelte, andsrc/routes/boards/[slug]/+page.svelte. The PR description explains why the inline was necessary (avoiding$env/dynamic/privatein client code), which is valid. But these should be extracted to a shared client-safe module (e.g.,$lib/constants.ts) to avoid triple-maintenance. Not blocking, but a cleanup target.4.
COLUMNSstill exported fromapi.tsAfter inlining
COLUMNSin the project page, nothing importsCOLUMNSfrom$lib/apianymore (confirmed by grep). The export is now dead code. Low priority but should be cleaned up.SOP COMPLIANCE
14-feat-backend-powered-search-full-text-se-> Issue #14)plan-2026-03-13-pal-e-frontend)+page.server.ts)$lib/sanitize.ts)#0a0a14bg,#e94560accent, gray-300 text)typeColor()used for note_type badgesVERDICT: NOT APPROVED
Two blockers must be resolved: the missing loading indicator (AC5) and the mode toggle not updating results (AC4). The nits are non-blocking suggestions for cleanup.
PR #16 Re-Review
Re-review after blocker fixes from first QA pass.
BLOCKER FIXES VERIFIED
1. Loading indicator (was BLOCKER) -- FIXED.
+page.svelteline 6 importsnavigatingfrom$app/stores, and lines 123-127 use{#if $navigating}to render a CSS spinner and "Searching..." text. This is the idiomatic SvelteKit pattern for showing loading state during server-load navigations. The spinner appears whilegoto()triggers the server load, and disappears when data arrives.2. Mode toggle auto-search (was BLOCKER) -- FIXED.
+page.sveltelines 86-89: the mode buttononclicknow setsmode = mthen conditionally callsdoSearch()when there is an active query. This triggersgoto()with updated URL params, causing a server-side re-fetch with the new mode. When no query is present, it correctly only updates the visual toggle without firing a search.Additional fixes observed:
+page.server.ts(lines 7-12): validatesrawModeagainstvalidModesarray, defaults to'hybrid'for invalid values. Good defensive input handling.COLUMNSun-exported inapi.ts(line 18):exportkeyword removed, preventing client-side code from importing a module that references$env/dynamic/private.COLUMNSandCOLUMN_COLORSinlined inprojects/[slug]/+page.svelte(lines 7-26): eliminates the build error that would have occurred from the server-only import.ACCEPTANCE CRITERIA AUDIT
/search?q=deploy+recoveryshows ranked results with headline snippets -- YES.+page.server.tsreadsqfrom URL params, callssearchNotes(). Results rendered withresult.rank.toFixed(3)(line 159) andrenderHeadline()using@html(lines 161-164). Headlines sanitized via DOMPurify ($lib/sanitize).Nav bar search input navigates to
/search?q=myqueryon Enter -- YES.+layout.svelteline 17:goto(/search?q=${encodeURIComponent(q)})fires on form submit. Query is properly URI-encoded./orCmd+Kfocuses search input -- YES.+layout.sveltelines 22-36:svelte:windowonkeydownhandler checks for/(when not in input/textarea/contentEditable) andCmd+K/Ctrl+K(unconditionally). Both callnavSearchInput?.focus().Mode toggle updates results and persists in URL -- YES. Mode buttons call
doSearch()which builds URL params and navigates viagoto().+page.server.tsreadsmodefrom URL params.$effecton lines 16-21 syncs local state from server data on navigation. Default modehybridis omitted from URL params (line 28:mode !== 'hybrid'), keeping URLs clean.Loading indicator during search -- YES. Verified as described in blocker fix #1 above.
Empty state with search tips -- YES. Lines 174-204: when
data.query.qis falsy, renders search mode descriptions and keyboard shortcut tips. No-results state (lines 169-173) also present with "Try different keywords" guidance.Click result navigates to
/notes/{slug}-- YES. Line 137:href="/notes/{result.slug}". The/notes/[slug]route exists on main (+page.svelteand+page.server.ts).note_typebadges use correcttypeColor()colors -- YES. Lines 142-154: both the dot indicator and pill badge usetypeColor(result.note_type)from$lib/colors. ThetypeColor()function covers all 16 note types with a#666fallback for unknowns/nulls.BLOCKERS
None.
NITS
Headline sanitization order --
renderHeadline()(lines 36-40) converts**word**to<strong>tags before sanitizing. This works correctly because DOMPurify allows<strong>by default, but the comment says "Convert ... then sanitize" which reads like sanitization happens second (it does). Just noting for clarity -- no functional issue.Duplicate COLUMNS definition --
COLUMNSis now defined in bothapi.ts(line 18, used bygetBoardWithItems) andprojects/[slug]/+page.svelte(line 7). This is the correct fix for the build error, but creates a maintenance risk if columns change. Consider extracting to a shared constants file that does not import$env. Non-blocking.Nav search does not inherit mode -- When using the nav bar search, it navigates to
/search?q=...without a mode param, so it always defaults tohybrid. If a user was on the search page withkeywordmode selected and uses the nav bar, their mode preference is lost. Very minor UX consideration.SOP COMPLIANCE
14-feat-backend-powered-search-full-text-sereferences issue #14)plan-2026-03-13-pal-e-frontend)Closes #14present in Related sectionVERDICT: APPROVED