feat: platform-wide server-side full-text search across courses, lessons, wiki and forum - #1094
Merged
Anuoluwapo25 merged 6 commits intoAug 26, 2026
Conversation
…um and profiles Generated tsvector columns weighted title > summary > body with GIN indexes; being generated columns they can never drift from the source rows, which is stronger than a trigger or reindex path.
- websearch_to_tsquery bound as a parameter so quotes/&/|/! never reach SQL as text; query capped at 200 chars, limit at 50, own rate limiter - typed UNION across courses, lessons, wiki, forum and public profiles, weighted by ts_rank_cd with ts_headline snippets using <mark> markers - visibility enforced in SQL: published courses only (lessons inherit via join), published wiki pages, profiles with a display name - keyset pagination on (negated rank, type, id) — stable, no dup/gap
15 cases including the issue's required proofs: title match outranks body match by assertion, unpublished content filters appear in the SQL, pages share no duplicates, and websearch operators plus oversized queries never error.
- dropdown now queries the server (debounce + abort kept), groups results by type, supports arrows/enter/escape with combobox ARIA roles, renders snippets via split-on-mark instead of dangerouslySetInnerHTML, and keeps clearable recent searches in localStorage - enter opens the dedicated results page with type filter tabs and load-more cursor pagination
Six vitest cases covering debounce-gated fetching, two-char minimum, request abort on retype, arrow/enter navigation, recent-search storage and the empty state. Search strings added to en/es/fr/sw with the pseudo-locale regenerated; npm run i18n:scan passes clean.
|
@Anuoluwapo25 hi! This is ready for your review whenever you get a chance. It closes #1079 — Postgres full-text search across courses, lessons, wiki and forum with weighted ranking, a rate-limited |
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1079
Summary of Changes
Search now runs on the server. A Postgres full-text index covers courses, lessons, wiki pages, forum threads and public scholar profiles; a new
GET /api/searchendpoint returns ranked typed results with snippets and cursor pagination; the frontend dropdown queries it (keeping its debounce and abort behaviour) and a dedicated/searchpage handles full result sets with type filters.What Changed
Search index (
server/src/db/migrations/033_platform_search.sql+ undo)tsvectorcolumns weighted title (A) > summary/description/bio (B/C) with GIN indexes on all five content tablesAPI (
server/src/controllers/search.controller.ts,routes/search.routes.ts)websearch_to_tsquerybound as a parameter — quotes,&,|,!follow Postgres websearch syntax and never reach SQL as textts_rank_cd, snippets viats_headlinewith<mark>markers the frontend splits on(negated rank, type, id)— stable order, no duplicates or gaps between pagespublished_at IS NOT NULLfor courses (lessons inherit through the join),is_published = TRUEfor wiki, profiles require a display name. One honest scope note: forum threads have no moderation state in this schema yet (flagged_contentcovers comments/proposals only), so there is nothing to filter against there — the moment that exists, one WHERE clause in the forum branch picks it up.docs/openapi.yamlFrontend
GlobalSearch.tsxrewritten against the API — debounce and abort-on-retype kept, results grouped by type, arrows/enter/escape navigation with proper combobox ARIA roles, snippets highlighted by splitting on<mark>(nodangerouslySetInnerHTML), clearable recent searches in localStorage/searchpage for full result sets: type filter tabs and load-more cursor paginationIcon.SearchMdfrom the design system doesn't exist in its export setTesting / Local Verification
npm run i18n:scanclean