lattice_embed/backfill/types.rs
1//! Backfill types: routing decisions, phases, and configuration.
2
3use serde::{Deserialize, Serialize};
4
5use crate::model::EmbeddingModel;
6
7/// Routing decision for an embedding request during migration.
8///
9/// Determines which model should handle an embedding operation based on
10/// the current migration state and the nature of the request.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum EmbeddingRoute {
13 /// Use the current (legacy) model -- no migration active or migration
14 /// incomplete for this operation type.
15 Legacy,
16 /// Use the new (target) model -- migration complete or sufficient
17 /// progress for query routing.
18 Target,
19 /// Embed with both models (dual-write) -- during active migration
20 /// for new documents to ensure both indexes stay current.
21 DualWrite,
22}
23
24/// Phase of the migration lifecycle for routing decisions.
25///
26/// Distinguishes between normal operation, active migration, and the
27/// post-cutover rollback window where dual-writing continues to enable
28/// safe rollback without data loss.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum RoutingPhase {
32 /// Normal operation, no migration active.
33 Stable,
34 /// Active migration in progress.
35 Migrating,
36 /// Post-cutover rollback window (still dual-writing).
37 ///
38 /// During this phase, queries use the target model but writes go to
39 /// both models. This enables instant rollback without losing atoms
40 /// created after cutover.
41 RollbackWindow,
42}
43
44/// Configuration for embedding request routing.
45///
46/// Separates query routing from write routing to enable true rollback.
47/// During the rollback window after cutover, queries use the new model
48/// but writes continue to both models, ensuring atoms created after
49/// cutover are present in both indexes.
50///
51/// # Example
52///
53/// ```rust
54/// use lattice_embed::backfill::{EmbeddingRoutingConfig, RoutingPhase};
55/// use lattice_embed::EmbeddingModel;
56///
57/// let config = EmbeddingRoutingConfig {
58/// query_model: EmbeddingModel::BgeBaseEnV15,
59/// write_models: vec![EmbeddingModel::BgeSmallEnV15, EmbeddingModel::BgeBaseEnV15],
60/// phase: RoutingPhase::RollbackWindow,
61/// migration_id: Some("mig-001".to_string()),
62/// };
63///
64/// assert!(config.write_models.len() > 1); // dual-write active
65/// ```
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct EmbeddingRoutingConfig {
68 /// Which model to query against.
69 pub query_model: EmbeddingModel,
70 /// Which models to write to (may be multiple for dual-write).
71 pub write_models: Vec<EmbeddingModel>,
72 /// Current routing phase.
73 pub phase: RoutingPhase,
74 /// Migration ID if in migration/rollback phase.
75 pub migration_id: Option<String>,
76}
77
78/// Configuration for the backfill coordinator.
79///
80/// Controls batch sizing, concurrency, dual-write behavior, and the
81/// threshold at which queries switch to the target model.
82///
83/// # Example
84///
85/// ```rust
86/// use lattice_embed::backfill::BackfillConfig;
87///
88/// let config = BackfillConfig {
89/// batch_size: 200,
90/// max_concurrent: 8,
91/// dual_write: true,
92/// target_query_threshold: 0.9,
93/// rollback_window_secs: 3600, // 1 hour
94/// };
95///
96/// assert_eq!(config.batch_size, 200);
97/// ```
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct BackfillConfig {
100 /// Batch size for backfill operations.
101 pub batch_size: usize,
102 /// Maximum concurrent backfill batches.
103 pub max_concurrent: usize,
104 /// Whether new documents should be dual-written during migration.
105 pub dual_write: bool,
106 /// Minimum progress fraction before allowing target-only queries `[0.0, 1.0]`.
107 ///
108 /// When the migration progress reaches this threshold, query routing
109 /// switches from legacy to target embeddings.
110 pub target_query_threshold: f64,
111 /// Duration in seconds to continue dual-writing after cutover.
112 ///
113 /// During the rollback window, queries use the new target model but
114 /// writes continue to both models. This ensures atoms created after
115 /// cutover exist in both indexes, enabling instant rollback without
116 /// data loss.
117 ///
118 /// Default: 86400 seconds (24 hours).
119 pub rollback_window_secs: u64,
120}
121
122impl Default for BackfillConfig {
123 fn default() -> Self {
124 Self {
125 batch_size: 100,
126 max_concurrent: 4,
127 dual_write: true,
128 target_query_threshold: 0.8,
129 rollback_window_secs: 86400, // 24 hours
130 }
131 }
132}