Alumni — Search & Recommendations#
Phase 2 ships a single global search box and per-user recommendation rails on the home page. Both are built on the same search_document index and the same audience-tag filter so what you see in search and what you see in recommendations honour the same scoping rules.
Who this is for: every authenticated user. This guide is filed under alumni because chapter/batch affinity makes the recommendations more useful for alumni out of the box; the surface itself is universal.
Where to find it#
- Global search box: top of the
/appshell — submit goes to/app/search?q=.... - Recommendation rails: home page
/app, three rails — People you might know, Groups for you, Events you might like. (Marketplace rail also lives on/app/marketplace.) - Per-card explanation: every recommended card has a Why? tooltip that surfaces the structured reasons that fed the score.
What’s indexed#
Seven surfaces (constants in backend/internal/search/types.go:34):
directory— people search.feed— Phase 1 social feed posts.jobs— alumni job board entries.groups— interest groups + alumni chapters.events— calendar events (chapter + campus).announcements— CMS announcements.marketplace— partner deals.
The index is populated transactionally from the relevant mutating RPC — for example, when a group is created, the Reconciler.ReconcileGroup writes the search row inside the same transaction (backend/internal/search/reconciler.go:57). There is no async lag.
Audience scoping#
Every search hit carries an audience_tags text[] array. Your effective tag set is computed at query time from your principal: role tags (alumni, students, etc.), batch (batch:2018), chapter (chapter:<uuid>), institute (institute). The query applies a Postgres && $N::text[] overlap filter — if the document and your tag set share at least one tag, the document is visible.
This means:
- A private group post is invisible to someone outside the group, even if it matches the keyword.
- A
batch:2024-only deal does not appear for an alumnus from 2018. - Anonymous (logged-out) users do not have access to the search surface at all.
Ranking#
Title hits outrank body hits — the indexer applies setweight(title, 'A') || setweight(body, 'B') so a query that matches both ranks the title hit higher. Beyond that, the ordering is (rank, id) keyset-paginated.
Recommendations#
The Stage 19 recommender (backend/internal/recommender/recommender.go) runs as a background job and produces up to 20 candidates per (user, surface). It uses:
- Collaborative-light signals: co-membership in groups, co-attendance at events, co-application to jobs.
- Content-based signals: skill/interest overlap; same batch/department for directory; chapter affinity for events/announcements.
There is no ML model in V2. The score is a transparent weighted sum and reasons_json is populated per candidate so the Why? tooltip can render “Because you joined the CS ‘18 Bangalore chapter”.
Five surfaces are scored: groups, events, directory, jobs, announcements. Marketplace recommendations come from the same recommender but use deal views and redemption history as the dominant signal.
Recording a signal#
The webapp emits a click signal whenever you open a card from a search result or a recommendation rail. The endpoint is POST /v1/search/signals; it is rate-limited and idempotent on the client-supplied signal_id for 5 seconds. Server-side joins/applies/reactions are recorded automatically; you do not need to do anything for those.
Common issues#
- “Search returns nothing for a group I just created” — the index is transactional; the group should be searchable the instant the create RPC commits. If it’s not, check the backend logs for a reconciler error on
search_document. - “Why am I getting recommendations for a chapter I already left?” — leaving a chapter flips your membership state to
leftbut the recommender re-scores users on a schedule, not in real time. Wait for the next batch run, or click Hide on the rail card to suppress it locally. - “I see a card but the Why? tooltip is empty” —
reasons_jsonis best-effort. For some surfaces (notably announcements with broad audience scoping) there isn’t a rich rationale to surface; the score still stands. - “Search is slow” — p95 target is < 250 ms at 180k indexed users. If you are seeing seconds of latency, check the backend logs for tsvector index health on
search_document.