6.3 KiB
Question Selection Algorithm
This document explains how questions are selected and ordered for users in the swipe interface.
Overview
Question selection happens in two stages:
- Database Layer -
get_recommended_questions()PostgreSQL function filters available questions - Client Layer -
swipe.tsxapplies additional filtering and sorting with bias
Stage 1: Database Filtering
The get_recommended_questions(target_pack_id) function (apps/supabase/migrations/20251227172500_role_specific_questions.sql) returns questions the user hasn't answered yet.
Pack Selection Logic
IF target_pack_id provided:
→ Use only that specific pack
ELSE IF couple has any couple_packs entries:
→ Use only packs where enabled = true
ELSE (new couple, no preferences set):
→ Use all public packs (is_public = true)
Question Filtering
For each question in active packs:
- Excluded: Questions the current user has already answered
- Included: All unanswered questions from active packs
Returned Fields
| Field | Description |
|---|---|
id |
Question UUID |
text |
Display text (see two-part logic below) |
partner_text |
Original partner text (for reference) |
is_two_part |
true if question has different text for second responder |
pack_id |
Parent pack UUID |
intensity |
1-5 rating |
partner_answered |
true if partner has already responded |
Two-Part Questions
Some questions have role-specific text (partner_text column). The function handles this:
IF partner_text EXISTS AND partner has answered:
→ Return partner_text as display text
ELSE:
→ Return original text
This enables questions like:
- First person sees: "Would you like your partner to wake you up with oral?"
- Second person sees: "Would you wake your partner up with oral?"
Stage 2: Client Sorting & Filtering
Located in apps/mobile/app/(app)/swipe.tsx, lines 57-95.
Skip Filtering
Before sorting, questions are filtered against locally-skipped questions:
const filtered = data.filter(q => !skippedIds.has(q.id));
Skip behavior (src/lib/skippedQuestions.ts):
- Swipe down = skip question
- Skipped questions hidden for 24 hours
- Stored in SecureStore (native) or localStorage (web)
- No server-side record - purely client-side convenience
Sorting Algorithm with Bias
Questions are sorted by a computed score:
const sorted = filtered.sort((a, b) => {
let scoreA = Math.random(); // Base: 0.0 - 1.0
let scoreB = Math.random();
// Apply bias bonuses
if (a.partner_answered && a.is_two_part) scoreA += 1.5;
else if (a.partner_answered) scoreA += 0.7;
else if (a.is_two_part) scoreA += 0.4;
// Same for scoreB...
return scoreB - scoreA; // Descending order
});
Priority Tiers
| Priority | Condition | Score Range | Rationale |
|---|---|---|---|
| 1st | partner_answered + is_two_part |
1.5 - 2.5 | Completes two-part question, immediate match potential |
| 2nd | partner_answered only |
0.7 - 1.7 | High match potential, partner waiting |
| 3rd | is_two_part only |
0.4 - 1.4 | Initiates two-part sequence |
| 4th | Neither | 0.0 - 1.0 | Standard questions, fully randomized |
Why This Bias?
-
Engagement: Questions partner already answered can immediately create matches, providing instant gratification
-
Two-part completion: When partner answered a two-part question first, completing it quickly maintains context and creates better matches
-
Two-part initiation: Slightly prioritizes two-part questions to create more engaging interactions
-
Randomization within tiers: The random base prevents predictable ordering while maintaining priority bands
Visual Flow
┌─────────────────────────────────────────────────────────────┐
│ get_recommended_questions() │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Pack Filter │ → │ User Answered │ → │ Two-Part Text │ │
│ │ │ │ Exclusion │ │ Resolution │ │
│ └─────────────┘ └──────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Client (swipe.tsx) │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Skip Filter │ → │ Bias Scoring │ → │ Sort │ │
│ │ (24 hour) │ │ │ │ (Descending) │ │
│ └─────────────┘ └──────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
Questions shown to user
Edge Cases
| Scenario | Behavior |
|---|---|
| No packs enabled | Shows "No packs enabled" prompt |
| All questions answered | Shows "All caught up!" with refresh button |
| No partner paired | Shows pairing prompt |
| Pack disabled mid-session | Client filters out questions from disabled packs |
Configuration
| Setting | Value | Location |
|---|---|---|
| Skip duration | 24 hours | src/lib/skippedQuestions.ts:6 |
| Two-part + answered bias | +1.5 | swipe.tsx:78 |
| Answered-only bias | +0.7 | swipe.tsx:79 |
| Two-part-only bias | +0.4 | swipe.tsx:80 |