4.6 KiB
Design: Briefing UX & Reason Labels
Overview
This feature adds a ReasonLabel system to tidalDB's query pipeline. Each ranked result carries structured reason codes explaining why it was surfaced and what factors dominated its score. The design integrates into the existing scoring pipeline with minimal overhead by tagging candidates at each stage rather than computing reasons post-hoc.
Architecture
New Types
tidal/src/ranking/reason.rs (NEW)
This module defines the core types:
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ReasonCode {
FollowedCreator, TrendingGlobal, TrendingInCategory, TrendingInCohort,
PreferenceMatch, SocialProof, HighQuality, HiddenGem, NewFromFollowed,
Rising, Controversial, ExplorationBudget, TopInWindow, SemanticMatch,
TextRelevance, CoEngagement, SessionContext, CohortPopular, SavedSearchMatch,
}
pub struct ReasonLabel {
pub code: ReasonCode,
pub context: HashMap<String, String>,
pub weight: f64,
}
Integration Points
1. ScoredCandidate (existing type, modified)
Add a reasons: Vec<ReasonLabel> field to ScoredCandidate in ranking/executor/context.rs.
2. RetrieveResult and SearchResultItem (existing types, modified)
Both gain a pub reasons: Vec<ReasonLabel> field. This is the public API surface.
3. Pipeline Integration
RETRIEVE Pipeline:
Stage 1 (Candidate Gen) --> tag source strategy reasons
Stage 2 (Filters) --> no reason changes
Stage 3 (Scoring) --> tag dominant signal reasons + populate signal_snapshot
Stage 4 (Diversity) --> no reason changes (reasons travel with candidates)
Stage 5 (Assembly) --> select top reasons, attach to results
SEARCH Pipeline:
Stage 0 (Scope) --> tag scope reasons (TrendingGlobal, etc.)
Stage 1 (BM25+ANN) --> tag retrieval method (TextRelevance, SemanticMatch)
Stage 2 (Filters) --> no reason changes
Stage 3 (Scoring) --> tag dominant signal reasons + populate signal_snapshot
Stage 4 (Diversity) --> no reason changes
Stage 5 (Assembly) --> select top reasons, attach to results
Detailed Stage Logic
Stage 1: Candidate Generation Reasons
| Candidate Strategy | Reason Code | Context Fields |
|---|---|---|
Relationship (following) |
FollowedCreator |
creator_id |
SignalRanked with trending profile |
TrendingGlobal |
-- |
SignalRanked with category filter |
TrendingInCategory |
category |
SignalRanked with cohort |
TrendingInCohort |
cohort |
Scan (default) |
(no reason from stage 1) | -- |
EmbeddingSimilarity |
PreferenceMatch |
-- |
Stage 3: Signal Scoring Reasons
In ProfileExecutor::score(), after scoring each candidate:
- Populate
signal_snapshot: Record boost signal values. - Tag dominant reason: Map sort mode to reason code, detect social proof and quality gates.
- Merge with Stage 1 reasons: Append to candidate's existing reason list.
Stage 5: Assembly / Reason Selection
- Sort by weight descending.
- Apply dominance threshold (drop < 0.10).
- Take top 3.
- Attach to result.
Performance Budget
- Memory: ~72 KB for 200 candidates at 3 reasons each. Negligible.
- CPU: O(n * k) where n = candidate count, k bounded by ~5.
- No additional signal reads: reasons derived from already-computed values.
File Changes
| File | Change |
|---|---|
tidal/src/ranking/reason.rs |
NEW -- ReasonCode, ReasonLabel, threshold constants |
tidal/src/ranking/mod.rs |
Add pub mod reason; and re-export types |
tidal/src/ranking/executor/context.rs |
Add reasons: Vec<ReasonLabel> to ScoredCandidate |
tidal/src/ranking/executor/mod.rs |
Populate signal_snapshot and tag scoring reasons |
tidal/src/query/executor/pipeline.rs |
Tag candidate-gen reasons, select top reasons in assembly |
tidal/src/query/search/executor/pipeline.rs |
Tag search-method reasons, select top reasons in assembly |
tidal/src/query/retrieve/types.rs |
Add reasons: Vec<ReasonLabel> to RetrieveResult |
tidal/src/query/search/types.rs |
Add reasons: Vec<ReasonLabel> to SearchResultItem |
tidal/src/lib.rs |
Re-export ReasonCode, ReasonLabel |
tidal/tests/p1_reason_labels.rs |
NEW -- integration tests |
Alternatives Considered
Post-hoc reason inference
Rejected: Cannot distinguish candidate source strategies or exploration-injected items.
String-based reason codes
Rejected: No compile-time exhaustiveness checking, risk of typos.
Separate "explain" query mode
Rejected: Overhead is negligible (< 5%), always-on is simpler.