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 /app shell — 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 left but 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_json is 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.