Skip to content

Instantly share code, notes, and snippets.

@ASRagab
Last active March 11, 2026 20:55
Show Gist options
  • Select an option

  • Save ASRagab/9b0dc9a679a27c5e66091e70e2c6df1f to your computer and use it in GitHub Desktop.

Select an option

Save ASRagab/9b0dc9a679a27c5e66091e70e2c6df1f to your computer and use it in GitHub Desktop.
Embedding Feature Store Migration - Visual Explainer
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Embedding Feature Store Migration: Optimized to Bigtable</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Fira+Code:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--font-body: 'DM Sans', system-ui, sans-serif;
--font-mono: 'Fira Code', 'SF Mono', Consolas, monospace;
--bg: #f7f8fa;
--surface: #ffffff;
--surface2: #f0f2f5;
--surface-elevated: #ffffff;
--border: rgba(0, 0, 0, 0.07);
--border-bright: rgba(0, 0, 0, 0.14);
--text: #1a1d23;
--text-dim: #6b7280;
--red: #be123c;
--red-dim: rgba(190, 18, 60, 0.08);
--green: #059669;
--green-dim: rgba(5, 150, 105, 0.08);
--blue: #0369a1;
--blue-dim: rgba(3, 105, 161, 0.08);
--amber: #b45309;
--amber-dim: rgba(180, 83, 9, 0.08);
--slate: #475569;
--slate-dim: rgba(71, 85, 105, 0.08);
--teal: #0f766e;
--teal-dim: rgba(15, 118, 110, 0.08);
--gold: #92400e;
--gold-dim: rgba(146, 64, 14, 0.08);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f1117;
--surface: #181b23;
--surface2: #1e2130;
--surface-elevated: #22263a;
--border: rgba(255, 255, 255, 0.06);
--border-bright: rgba(255, 255, 255, 0.12);
--text: #e2e5eb;
--text-dim: #8b919e;
--red: #fb7185;
--red-dim: rgba(251, 113, 133, 0.1);
--green: #34d399;
--green-dim: rgba(52, 211, 153, 0.1);
--blue: #38bdf8;
--blue-dim: rgba(56, 189, 248, 0.1);
--amber: #fbbf24;
--amber-dim: rgba(251, 191, 36, 0.1);
--slate: #94a3b8;
--slate-dim: rgba(148, 163, 184, 0.08);
--teal: #5eead4;
--teal-dim: rgba(94, 234, 212, 0.1);
--gold: #fbbf24;
--gold-dim: rgba(251, 191, 36, 0.1);
}
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
background-image:
radial-gradient(ellipse at 15% 5%, var(--blue-dim) 0%, transparent 45%),
radial-gradient(ellipse at 85% 95%, var(--green-dim) 0%, transparent 35%);
color: var(--text);
font-family: var(--font-body);
padding: 40px;
min-height: 100vh;
line-height: 1.5;
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeScale {
from { opacity: 0; transform: scale(0.92); }
to { opacity: 1; transform: scale(1); }
}
.anim { animation: fadeUp 0.45s ease-out both; animation-delay: calc(var(--i, 0) * 0.06s); }
.anim-scale { animation: fadeScale 0.4s ease-out both; animation-delay: calc(var(--i, 0) * 0.06s); }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-delay: 0ms !important;
transition-duration: 0.01ms !important;
}
}
/* ===== NAV ===== */
.wrap {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: 180px 1fr;
gap: 0 40px;
}
.main { min-width: 0; }
.toc {
position: sticky;
top: 24px;
align-self: start;
padding: 14px 0;
grid-row: 1 / -1;
max-height: calc(100dvh - 48px);
overflow-y: auto;
}
.toc::-webkit-scrollbar { width: 3px; }
.toc::-webkit-scrollbar-thumb { background: var(--surface-elevated); border-radius: 2px; }
.toc-title {
font-family: var(--font-mono);
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 2px;
color: var(--text-dim);
padding: 0 0 10px;
margin-bottom: 8px;
border-bottom: 1px solid var(--border);
}
.toc a {
display: block;
font-size: 11px;
color: var(--text-dim);
text-decoration: none;
padding: 5px 8px;
border-radius: 5px;
border-left: 2px solid transparent;
transition: all 0.15s;
line-height: 1.4;
margin-bottom: 1px;
}
.toc a:hover { color: var(--text); background: var(--surface2); }
.toc a.active { color: var(--text); border-left-color: var(--blue); background: var(--blue-dim); }
@media (max-width: 1000px) {
.wrap { grid-template-columns: 1fr; padding-top: 0; }
body { padding: 20px; padding-top: 0; }
.toc {
position: sticky; top: 0; z-index: 200;
max-height: none; display: flex; gap: 4px;
align-items: center; overflow-x: auto;
-webkit-overflow-scrolling: touch;
background: var(--bg); border-bottom: 1px solid var(--border);
padding: 10px 0; margin: 0 -20px; padding-left: 20px; padding-right: 20px;
grid-row: auto;
}
.toc::-webkit-scrollbar { display: none; }
.toc-title { display: none; }
.toc a { white-space: nowrap; flex-shrink: 0; border-left: none; border-bottom: 2px solid transparent; border-radius: 4px 4px 0 0; padding: 6px 10px; font-size: 10px; }
.toc a.active { border-left: none; border-bottom-color: var(--blue); background: var(--surface); }
.main { padding-top: 20px; }
.sec-head { scroll-margin-top: 52px; }
}
/* ===== TYPOGRAPHY ===== */
h1 {
font-size: 36px;
font-weight: 700;
letter-spacing: -0.8px;
margin-bottom: 6px;
text-wrap: balance;
line-height: 1.15;
}
.subtitle {
color: var(--text-dim);
font-size: 13px;
font-family: var(--font-mono);
margin-bottom: 32px;
}
.sec-head {
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
padding: 20px 0 14px;
display: flex;
align-items: center;
gap: 8px;
}
.sec-head .dot {
width: 8px; height: 8px;
border-radius: 50%;
display: inline-block;
}
h2 {
font-size: 20px;
font-weight: 700;
letter-spacing: -0.3px;
margin-bottom: 16px;
}
h3 {
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
}
/* ===== CARDS ===== */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px 24px;
margin-bottom: 16px;
}
.card--hero {
background: var(--surface-elevated);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
padding: 28px 32px;
}
.card--recessed {
background: var(--surface2);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.04);
}
.card--red { border-color: var(--red); border-left: 4px solid var(--red); }
.card--green { border-color: var(--green-dim); border-left: 4px solid var(--green); }
.card--blue { border-color: var(--blue-dim); border-left: 4px solid var(--blue); }
.card--amber { border-color: var(--amber-dim); border-left: 4px solid var(--amber); }
.card--teal { border-color: var(--teal-dim); border-left: 4px solid var(--teal); }
/* ===== HERO DEPRECATION BANNER ===== */
.deprecation-banner {
display: grid;
grid-template-columns: 1fr auto;
gap: 24px;
align-items: start;
}
.deprecation-banner .timeline-kpis {
display: flex;
flex-direction: column;
gap: 12px;
}
.kpi {
text-align: center;
padding: 12px 20px;
border-radius: 10px;
min-width: 140px;
}
.kpi--red { background: var(--red-dim); border: 1px solid color-mix(in srgb, var(--red) 30%, transparent); }
.kpi--amber { background: var(--amber-dim); border: 1px solid color-mix(in srgb, var(--amber) 30%, transparent); }
.kpi--green { background: var(--green-dim); border: 1px solid color-mix(in srgb, var(--green) 30%, transparent); }
.kpi .kpi-val {
font-family: var(--font-mono);
font-size: 22px;
font-weight: 700;
line-height: 1.2;
}
.kpi--red .kpi-val { color: var(--red); }
.kpi--amber .kpi-val { color: var(--amber); }
.kpi--green .kpi-val { color: var(--green); }
.kpi .kpi-label {
font-size: 10px;
font-family: var(--font-mono);
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 2px;
}
.deprecation-text { font-size: 14px; line-height: 1.7; }
.deprecation-text strong { color: var(--red); }
.deprecation-text .tag {
display: inline-block;
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
vertical-align: middle;
}
.tag--red { background: var(--red-dim); color: var(--red); }
.tag--amber { background: var(--amber-dim); color: var(--amber); }
.tag--green { background: var(--green-dim); color: var(--green); }
/* ===== THREE STORES GRID ===== */
.stores-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 14px;
margin-bottom: 16px;
}
.store-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 18px 20px;
position: relative;
overflow: hidden;
}
.store-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 3px;
}
.store-card--safe::before { background: var(--green); }
.store-card--danger::before { background: var(--red); }
.store-card .store-name {
font-family: var(--font-mono);
font-size: 12px;
font-weight: 600;
margin-bottom: 6px;
}
.store-card--safe .store-name { color: var(--green); }
.store-card--danger .store-name { color: var(--red); }
.store-card .store-type {
font-family: var(--font-mono);
font-size: 10px;
padding: 2px 7px;
border-radius: 4px;
display: inline-block;
margin-bottom: 8px;
}
.store-card--safe .store-type { background: var(--green-dim); color: var(--green); }
.store-card--danger .store-type { background: var(--red-dim); color: var(--red); }
.store-card .store-detail {
font-size: 12px;
color: var(--text-dim);
line-height: 1.6;
}
.store-card .store-detail strong { color: var(--text); font-weight: 500; }
/* ===== INNER GRID ===== */
.inner-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.inner-card {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 16px;
}
.inner-card .title {
font-weight: 600;
font-size: 13px;
margin-bottom: 4px;
}
.inner-card .desc {
color: var(--text-dim);
font-size: 12px;
line-height: 1.6;
}
.inner-card code {
font-family: var(--font-mono);
font-size: 10px;
background: var(--blue-dim);
color: var(--blue);
padding: 1px 5px;
border-radius: 3px;
}
/* ===== FLOW ARROW ===== */
.flow-arrow {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 11px;
padding: 4px 0;
}
.flow-arrow svg {
width: 18px; height: 18px;
fill: none;
stroke: var(--border-bright);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
/* ===== DATA FLOW PIPELINE ===== */
.pipeline {
display: flex;
gap: 0;
align-items: stretch;
overflow-x: auto;
padding-bottom: 6px;
}
.pipeline-step {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 14px;
min-width: 110px;
flex-shrink: 0;
text-align: center;
}
.pipeline-step .step-num {
font-family: var(--font-mono);
font-size: 9px;
font-weight: 600;
margin-bottom: 4px;
}
.pipeline-step .step-name {
font-size: 12px;
font-weight: 600;
margin-bottom: 3px;
}
.pipeline-step .step-detail {
font-size: 10px;
color: var(--text-dim);
line-height: 1.4;
}
.pipeline-step--blue { border-color: var(--blue-dim); }
.pipeline-step--blue .step-num { color: var(--blue); }
.pipeline-step--green { border-color: var(--green-dim); }
.pipeline-step--green .step-num { color: var(--green); }
.pipeline-step--amber { border-color: var(--amber-dim); }
.pipeline-step--amber .step-num { color: var(--amber); }
.pipeline-step--teal { border-color: var(--teal-dim); }
.pipeline-step--teal .step-num { color: var(--teal); }
.pipeline-step--red { border-color: var(--red-dim); }
.pipeline-step--red .step-num { color: var(--red); }
.pipeline-arrow {
display: flex;
align-items: center;
padding: 0 3px;
color: var(--border-bright);
font-size: 16px;
flex-shrink: 0;
}
/* ===== PHASE CARDS ===== */
.phase-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.phase-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 22px 24px;
position: relative;
}
.phase-card .phase-num {
font-family: var(--font-mono);
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 6px;
}
.phase-card .phase-title {
font-size: 16px;
font-weight: 700;
margin-bottom: 4px;
letter-spacing: -0.2px;
}
.phase-card .phase-repo {
font-family: var(--font-mono);
font-size: 10px;
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
margin-bottom: 10px;
}
.phase-card .phase-files {
font-size: 11px;
color: var(--text-dim);
line-height: 1.8;
list-style: none;
margin-bottom: 10px;
}
.phase-card .phase-files li {
padding-left: 14px;
position: relative;
}
.phase-card .phase-files li::before {
content: '';
width: 5px; height: 5px;
border-radius: 50%;
position: absolute;
left: 0; top: 7px;
}
.phase-card .phase-desc {
font-size: 12px;
color: var(--text-dim);
line-height: 1.6;
}
.phase-card .phase-desc strong { color: var(--text); }
.phase-card--blue .phase-num { color: var(--blue); }
.phase-card--blue { border-top: 3px solid var(--blue); }
.phase-card--blue .phase-repo { background: var(--blue-dim); color: var(--blue); }
.phase-card--blue .phase-files li::before { background: var(--blue); }
.phase-card--teal .phase-num { color: var(--teal); }
.phase-card--teal { border-top: 3px solid var(--teal); }
.phase-card--teal .phase-repo { background: var(--teal-dim); color: var(--teal); }
.phase-card--teal .phase-files li::before { background: var(--teal); }
.phase-card--amber .phase-num { color: var(--amber); }
.phase-card--amber { border-top: 3px solid var(--amber); }
.phase-card--amber .phase-repo { background: var(--amber-dim); color: var(--amber); }
.phase-card--amber .phase-files li::before { background: var(--amber); }
.phase-card--red .phase-num { color: var(--red); }
.phase-card--red { border-top: 3px solid var(--red); }
.phase-card--red .phase-repo { background: var(--red-dim); color: var(--red); }
.phase-card--red .phase-files li::before { background: var(--red); }
/* ===== TIMELINE ===== */
.timeline {
position: relative;
padding-left: 32px;
margin: 16px 0;
}
.timeline::before {
content: '';
position: absolute;
left: 10px; top: 0; bottom: 0;
width: 2px;
background: linear-gradient(to bottom, var(--red), var(--amber), var(--green));
border-radius: 1px;
}
.tl-item {
position: relative;
margin-bottom: 20px;
padding: 14px 18px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
}
.tl-item::before {
content: '';
position: absolute;
left: -26px; top: 18px;
width: 10px; height: 10px;
border-radius: 50%;
border: 2px solid var(--bg);
}
.tl-item--past::before { background: var(--red); }
.tl-item--soon::before { background: var(--amber); }
.tl-item--future::before { background: var(--green); }
.tl-item--past { border-left-color: var(--red); }
.tl-item--soon { border-left-color: var(--amber); }
.tl-item--future { border-left-color: var(--green); }
.tl-date {
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
margin-bottom: 3px;
}
.tl-item--past .tl-date { color: var(--red); }
.tl-item--soon .tl-date { color: var(--amber); }
.tl-item--future .tl-date { color: var(--green); }
.tl-desc { font-size: 13px; color: var(--text-dim); }
.tl-desc strong { color: var(--text); }
/* ===== RISK TABLE ===== */
.risk-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.risk-table thead th {
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-dim);
text-align: left;
padding: 10px 14px;
border-bottom: 2px solid var(--border-bright);
position: sticky;
top: 0;
background: var(--surface);
}
.risk-table tbody td {
padding: 12px 14px;
border-bottom: 1px solid var(--border);
vertical-align: top;
line-height: 1.5;
}
.risk-table tbody tr:nth-child(even) td {
background: var(--surface2);
}
.risk-table .severity {
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
}
.severity--high { background: var(--red-dim); color: var(--red); }
.severity--med { background: var(--amber-dim); color: var(--amber); }
.severity--low { background: var(--green-dim); color: var(--green); }
/* ===== STAKEHOLDER TABLE ===== */
.stakeholder-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.stakeholder-table thead th {
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-dim);
text-align: left;
padding: 10px 14px;
border-bottom: 2px solid var(--border-bright);
position: sticky;
top: 0;
background: var(--surface);
}
.stakeholder-table tbody td {
padding: 10px 14px;
border-bottom: 1px solid var(--border);
vertical-align: top;
}
.stakeholder-table tbody tr:nth-child(even) td {
background: var(--surface2);
}
.stakeholder-table .role-tag {
font-family: var(--font-mono);
font-size: 10px;
padding: 2px 7px;
border-radius: 3px;
display: inline-block;
}
.role-tag--owner { background: var(--blue-dim); color: var(--blue); }
.role-tag--impl { background: var(--teal-dim); color: var(--teal); }
.role-tag--consumer { background: var(--amber-dim); color: var(--amber); }
.role-tag--infra { background: var(--slate-dim); color: var(--slate); }
/* ===== FILE REF LIST ===== */
.file-list {
list-style: none;
font-size: 12px;
line-height: 2;
}
.file-list li {
padding-left: 14px;
position: relative;
overflow-wrap: break-word;
}
.file-list li::before {
content: '';
width: 5px; height: 5px;
border-radius: 50%;
position: absolute;
left: 0; top: 10px;
}
.file-list code {
font-family: var(--font-mono);
font-size: 10px;
padding: 1px 5px;
border-radius: 3px;
}
.file-list--blue li::before { background: var(--blue); }
.file-list--blue code { background: var(--blue-dim); color: var(--blue); }
.file-list--teal li::before { background: var(--teal); }
.file-list--teal code { background: var(--teal-dim); color: var(--teal); }
.file-list--amber li::before { background: var(--amber); }
.file-list--amber code { background: var(--amber-dim); color: var(--amber); }
/* ===== LEGEND ===== */
.legend {
display: flex;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--text-dim);
font-family: var(--font-mono);
}
.legend-swatch {
width: 12px; height: 12px;
border-radius: 3px;
}
/* ===== MERMAID ===== */
.mermaid-wrap {
position: relative;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
overflow: hidden;
margin-bottom: 16px;
}
.mermaid-inner {
overflow: auto;
cursor: grab;
transform-origin: 0 0;
}
.mermaid-inner.dragging { cursor: grabbing; }
.mermaid-zoom {
position: absolute;
top: 10px;
right: 10px;
display: flex;
gap: 4px;
z-index: 10;
}
.mermaid-zoom button {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 6px;
width: 28px; height: 28px;
font-family: var(--font-mono);
font-size: 14px;
color: var(--text-dim);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.mermaid-zoom button:hover { color: var(--text); background: var(--surface-elevated); }
.mermaid .nodeLabel { color: var(--text) !important; }
.mermaid .edgeLabel { color: var(--text-dim) !important; background-color: var(--bg) !important; }
.mermaid .edgeLabel rect { fill: var(--bg) !important; }
.mermaid .node rect, .mermaid .node circle, .mermaid .node polygon { stroke-width: 1.5px; }
.mermaid .edge-pattern-solid { stroke-width: 1.5px; }
.mermaid .edgeLabel { font-family: var(--font-mono) !important; font-size: 12px !important; }
.mermaid .nodeLabel { font-family: var(--font-body) !important; font-size: 14px !important; }
/* ===== CALLOUT ===== */
.callout {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 0 8px 8px 0;
padding: 14px 18px;
font-size: 13px;
line-height: 1.6;
color: var(--text-dim);
margin-bottom: 16px;
}
.callout strong { color: var(--text); font-weight: 600; }
.callout code { font-family: var(--font-mono); font-size: 10px; background: var(--blue-dim); color: var(--blue); padding: 1px 5px; border-radius: 3px; }
.callout--red { border-left: 3px solid var(--red); }
.callout--blue { border-left: 3px solid var(--blue); }
.callout--amber { border-left: 3px solid var(--amber); }
.callout--green { border-left: 3px solid var(--green); }
/* ===== RESPONSIVE ===== */
@media (max-width: 768px) {
.stores-grid { grid-template-columns: 1fr; }
.phase-grid { grid-template-columns: 1fr; }
.inner-grid { grid-template-columns: 1fr; }
.deprecation-banner { grid-template-columns: 1fr; }
.deprecation-banner .timeline-kpis { flex-direction: row; flex-wrap: wrap; }
.pipeline { flex-wrap: wrap; gap: 6px; }
.pipeline-arrow { display: none; }
}
/* ===== DETAILS/SUMMARY ===== */
details {
margin-bottom: 16px;
}
details summary {
cursor: pointer;
font-family: var(--font-mono);
font-size: 12px;
font-weight: 500;
color: var(--text-dim);
padding: 8px 0;
list-style: none;
display: flex;
align-items: center;
gap: 6px;
}
details summary::-webkit-details-marker { display: none; }
details summary::before {
content: '+';
font-size: 14px;
width: 18px; height: 18px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: transform 0.2s;
}
details[open] summary::before {
content: '-';
}
</style>
</head>
<body>
<div class="wrap">
<nav class="toc" id="toc">
<div class="toc-title">Migration Guide</div>
<a href="#s1">1. The Problem</a>
<a href="#s2">2. Current State</a>
<a href="#s3">3. Data Flow</a>
<a href="#s4">4. Migration Phases</a>
<a href="#s5">5. Timeline</a>
<a href="#s6">6. Risks</a>
<a href="#s7">7. Stakeholders</a>
<a href="#s8">8. Key Files</a>
</nav>
<div class="main">
<h1 class="anim" style="--i:0">Embedding Feature Store Migration</h1>
<p class="subtitle anim" style="--i:1">vertex ai optimized online serving &rarr; bigtable &mdash; DAMP-242</p>
<!-- ===== SECTION 1: THE PROBLEM ===== -->
<div id="s1" class="sec-head anim" style="--i:2; color: var(--red);">
<span class="dot" style="background: var(--red);"></span> 1 &mdash; The Problem
</div>
<div class="card card--hero card--red anim" style="--i:3">
<div class="deprecation-banner">
<div class="deprecation-text">
<h2 style="margin-bottom: 12px;">Google is Sunsetting Optimized Online Serving</h2>
<p>
Vertex AI Feature Store <strong>Optimized Online Serving</strong> has been
<span class="tag tag--red">deprecated</span> as of February 17, 2026.
No new features will be added after <span class="tag tag--amber">May 17, 2026</span>,
and the API will be <strong>fully removed</strong> on
<span class="tag tag--red">February 17, 2027</span>.
</p>
<p style="margin-top: 10px;">
Handshake has <strong>one</strong> optimized online store: <code style="font-family: var(--font-mono); font-size: 11px; background: var(--red-dim); color: var(--red); padding: 1px 6px; border-radius: 3px;">online_feature_store_student_embeddings</code>.
It serves all embedding models (GNNz, TwoTower, RGCN, content_mpnet) to Job Search,
Content/Feed, and Promotion Service via Private Service Connect at 2-3ms latency.
</p>
<p style="margin-top: 10px;">
The other two stores (<code style="font-family: var(--font-mono); font-size: 11px; background: var(--green-dim); color: var(--green); padding: 1px 6px; border-radius: 3px;">tender_ostrich</code> and
<code style="font-family: var(--font-mono); font-size: 11px; background: var(--green-dim); color: var(--green); padding: 1px 6px; border-radius: 3px;">sassy_jay</code>)
are already on Bigtable and are <strong style="color: var(--green);">not affected</strong>.
</p>
</div>
<div class="timeline-kpis">
<div class="kpi kpi--red anim-scale" style="--i:4">
<div class="kpi-val">DONE</div>
<div class="kpi-label">Deprecated</div>
</div>
<div class="kpi kpi--amber anim-scale" style="--i:5">
<div class="kpi-val">~2 mo</div>
<div class="kpi-label">Feature Freeze</div>
</div>
<div class="kpi kpi--green anim-scale" style="--i:6">
<div class="kpi-val">~11 mo</div>
<div class="kpi-label">Until Sunset</div>
</div>
</div>
</div>
</div>
<!-- ===== SECTION 2: CURRENT STATE ===== -->
<div id="s2" class="sec-head anim" style="--i:7; color: var(--blue);">
<span class="dot" style="background: var(--blue);"></span> 2 &mdash; Current Architecture
</div>
<div class="legend anim" style="--i:8">
<div class="legend-item"><div class="legend-swatch" style="background: var(--green-dim); border: 1px solid var(--green);"></div>Bigtable (safe)</div>
<div class="legend-item"><div class="legend-swatch" style="background: var(--red-dim); border: 1px solid var(--red);"></div>Optimized (deprecated)</div>
</div>
<div class="stores-grid anim" style="--i:9">
<div class="store-card store-card--safe">
<div class="store-name">tender_ostrich</div>
<div class="store-type">Bigtable</div>
<div class="store-detail">
<strong>Autoscale:</strong> 4-6 nodes (prod)<br>
<strong>Serves:</strong> student features, job features, user features<br>
<strong>Consumers:</strong> ranking services
</div>
</div>
<div class="store-card store-card--safe">
<div class="store-name">sassy_jay</div>
<div class="store-type">Bigtable</div>
<div class="store-detail">
<strong>Autoscale:</strong> 1-2 nodes<br>
<strong>Serves:</strong> employer activity, search traffic, AIRA<br>
<strong>Consumers:</strong> ranking services
</div>
</div>
<div class="store-card store-card--danger">
<div class="store-name">student_embeddings</div>
<div class="store-type">Optimized + PSC</div>
<div class="store-detail">
<strong>Latency:</strong> 2-3ms via gRPC/PSC<br>
<strong>Serves:</strong> ~42 feature views, 6-day retention<br>
<strong>Models:</strong> GNNz, TwoTower, RGCN, content_mpnet<br>
<strong>Consumers:</strong> Job Search, Content, Feed, Promo
</div>
</div>
</div>
<div class="callout callout--blue anim" style="--i:10">
<strong>Why PSC matters:</strong> The optimized store uses a <code>google_compute_forwarding_rule</code> to a
Private Service Connect endpoint. Corgi connects via hardcoded IPs
(<code>10.126.0.130:10002</code> prod, <code>10.154.0.105:10002</code> staging)
using insecure gRPC. Bigtable stores use the standard Vertex API endpoint
(<code>us-central1-aiplatform.googleapis.com:443</code>) instead &mdash; no PSC needed.
</div>
<div class="mermaid-wrap anim" style="--i:11">
<div class="mermaid-zoom">
<button onclick="zoomDiagram('arch', 1)">+</button>
<button onclick="zoomDiagram('arch', -1)">-</button>
<button onclick="zoomDiagram('arch', 0)">R</button>
</div>
<div class="mermaid-inner" id="arch">
<svg id="mermaid-svg-1" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1076.08px; background-color: transparent;" viewBox="0 0 1076.078125 420" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#mermaid-svg-1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1 .error-icon{fill:#552222;}#mermaid-svg-1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1 .marker.cross{stroke:#333333;}#mermaid-svg-1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1 p{margin:0;}#mermaid-svg-1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1 .cluster-label text{fill:#333;}#mermaid-svg-1 .cluster-label span{color:#333;}#mermaid-svg-1 .cluster-label span p{background-color:transparent;}#mermaid-svg-1 .label text,#mermaid-svg-1 span{fill:#333;color:#333;}#mermaid-svg-1 .node rect,#mermaid-svg-1 .node circle,#mermaid-svg-1 .node ellipse,#mermaid-svg-1 .node polygon,#mermaid-svg-1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1 .rough-node .label text,#mermaid-svg-1 .node .label text,#mermaid-svg-1 .image-shape .label,#mermaid-svg-1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1 .rough-node .label,#mermaid-svg-1 .node .label,#mermaid-svg-1 .image-shape .label,#mermaid-svg-1 .icon-shape .label{text-align:center;}#mermaid-svg-1 .node.clickable{cursor:pointer;}#mermaid-svg-1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1 .arrowheadPath{fill:#333333;}#mermaid-svg-1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1 .cluster text{fill:#333;}#mermaid-svg-1 .cluster span{color:#333;}#mermaid-svg-1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-1 .icon-shape,#mermaid-svg-1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1 .icon-shape p,#mermaid-svg-1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1 .icon-shape .label rect,#mermaid-svg-1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="my-svg_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"><g class="cluster" id="Services" data-look="classic"><rect style="" x="848.21875" y="22" width="219.859375" height="388"/><g class="cluster-label" transform="translate(929.0625, 22)"><foreignObject width="58.171875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5;"><span class="nodeLabel"><p>Services</p></span></div></foreignObject></g></g><g class="cluster" id="Corgi" data-look="classic"><rect style="" x="530.890625" y="8" width="267.328125" height="404"/><g class="cluster-label" transform="translate(646.0703125, 8)"><foreignObject width="36.96875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5;"><span class="nodeLabel"><p>Corgi</p></span></div></foreignObject></g></g><g class="cluster" id="Terraform" data-look="classic"><rect style="" x="8" y="8" width="472.890625" height="404"/><g class="cluster-label" transform="translate(209.0078125, 8)"><foreignObject width="70.875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5;"><span class="nodeLabel"><p>Terraform</p></span></div></foreignObject></g></g></g><g class="edgePaths"><path d="M455.891,82L460.057,82C464.224,82,472.557,82,480.891,82C489.224,82,497.557,82,505.891,82C514.224,82,522.557,82,530.227,82.314C537.896,82.629,544.901,83.258,548.404,83.572L551.907,83.887" id="L_TO_V2_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_TO_V2_0" data-points="W3sieCI6NDU1Ljg5MDYyNSwieSI6ODJ9LHsieCI6NDgwLjg5MDYyNSwieSI6ODJ9LHsieCI6NTA1Ljg5MDYyNSwieSI6ODJ9LHsieCI6NTMwLjg5MDYyNSwieSI6ODJ9LHsieCI6NTU1Ljg5MDYyNSwieSI6ODQuMjQ0NDMyNzU0NjkwNTF9XQ==" marker-end="url(#mermaid-svg-1_flowchart-v2-pointEnd)"/><path d="M436.57,210L443.957,210C451.344,210,466.117,210,477.671,210C489.224,210,497.557,210,505.891,210C514.224,210,522.557,210,541.008,197.604C559.459,185.207,588.027,160.415,602.311,148.018L616.595,135.622" id="L_SJ_V2_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SJ_V2_0" data-points="W3sieCI6NDM2LjU3MDMxMjUsInkiOjIxMH0seyJ4Ijo0ODAuODkwNjI1LCJ5IjoyMTB9LHsieCI6NTA1Ljg5MDYyNSwieSI6MjEwfSx7IngiOjUzMC44OTA2MjUsInkiOjIxMH0seyJ4Ijo2MTkuNjE1OTA3ODY2Mzc5MywieSI6MTMzfV0=" marker-end="url(#mermaid-svg-1_flowchart-v2-pointEnd)"/><path d="M240.969,338L245.135,338C249.302,338,257.635,338,266.18,338C274.724,338,283.479,338,287.857,338L292.234,338" id="L_SE_PSC_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SE_PSC_0" data-points="W3sieCI6MjQwLjk2ODc1LCJ5IjozMzh9LHsieCI6MjY1Ljk2ODc1LCJ5IjozMzh9LHsieCI6Mjk2LjIzNDM3NSwieSI6MzM4fV0=" marker-end="url(#mermaid-svg-1_flowchart-v2-pointEnd)"/><path d="M450.625,338L455.669,338C460.714,338,470.802,338,480.013,338C489.224,338,497.557,338,505.891,338C514.224,338,522.557,338,531.844,338C541.13,338,551.37,338,556.49,338L561.609,338" id="L_PSC_EMB_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_PSC_EMB_0" data-points="W3sieCI6NDUwLjYyNSwieSI6MzM4fSx7IngiOjQ4MC44OTA2MjUsInkiOjMzOH0seyJ4Ijo1MDUuODkwNjI1LCJ5IjozMzh9LHsieCI6NTMwLjg5MDYyNSwieSI6MzM4fSx7IngiOjU2NS42MDkzNzUsInkiOjMzOH1d" marker-end="url(#mermaid-svg-1_flowchart-v2-pointEnd)"/><path d="M773.219,84.244L777.385,83.87C781.552,83.496,789.885,82.748,798.219,82.374C806.552,82,814.885,82,823.219,82C831.552,82,839.885,82,850.236,82.113C860.586,82.225,872.954,82.45,879.137,82.563L885.321,82.675" id="L_V2_JS_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_V2_JS_0" data-points="W3sieCI6NzczLjIxODc1LCJ5Ijo4NC4yNDQ0MzI3NTQ2OTA1MX0seyJ4Ijo3OTguMjE4NzUsInkiOjgyfSx7IngiOjgyMy4yMTg3NSwieSI6ODJ9LHsieCI6ODQ4LjIxODc1LCJ5Ijo4Mn0seyJ4Ijo4ODkuMzIwMzEyNSwieSI6ODIuNzQ3Nzc5MTIwMTc2MjV9XQ==" marker-end="url(#mermaid-svg-1_flowchart-v2-pointEnd)"/><path d="M702.329,133L718.311,149.5C734.292,166,766.256,199,786.404,215.5C806.552,232,814.885,232,823.219,232C831.552,232,839.885,232,847.556,232.382C855.227,232.765,862.234,233.53,865.738,233.912L869.242,234.295" id="L_V2_CT_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_V2_CT_0" data-points="W3sieCI6NzAyLjMyOTMxMzg1ODY5NTYsInkiOjEzM30seyJ4Ijo3OTguMjE4NzUsInkiOjIzMn0seyJ4Ijo4MjMuMjE4NzUsInkiOjIzMn0seyJ4Ijo4NDguMjE4NzUsInkiOjIzMn0seyJ4Ijo4NzMuMjE4NzUsInkiOjIzNC43MjkwMTcxMjc0MjUyfV0=" marker-end="url(#mermaid-svg-1_flowchart-v2-pointEnd)"/><path d="M697.971,299L714.679,279.5C731.387,260,764.803,221,785.677,201.5C806.552,182,814.885,182,823.219,182C831.552,182,839.885,182,856.828,170.61C873.771,159.221,899.324,136.441,912.1,125.051L924.876,113.662" id="L_EMB_JS_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_EMB_JS_0" data-points="W3sieCI6Njk3Ljk3MDcwMzEyNSwieSI6Mjk5fSx7IngiOjc5OC4yMTg3NSwieSI6MTgyfSx7IngiOjgyMy4yMTg3NSwieSI6MTgyfSx7IngiOjg0OC4yMTg3NSwieSI6MTgyfSx7IngiOjkyNy44NjE2ODY4NjIyNDQ5LCJ5IjoxMTF9XQ==" marker-end="url(#mermaid-svg-1_flowchart-v2-pointEnd)"/><path d="M763.5,299.507L769.286,297.256C775.073,295.005,786.646,290.502,796.599,288.251C806.552,286,814.885,286,823.219,286C831.552,286,839.885,286,849.973,283.738C860.06,281.476,871.901,276.952,877.822,274.69L883.743,272.428" id="L_EMB_CT_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_EMB_CT_0" data-points="W3sieCI6NzYzLjUsInkiOjI5OS41MDY4MDkyODE2NjQ2M30seyJ4Ijo3OTguMjE4NzUsInkiOjI4Nn0seyJ4Ijo4MjMuMjE4NzUsInkiOjI4Nn0seyJ4Ijo4NDguMjE4NzUsInkiOjI4Nn0seyJ4Ijo4ODcuNDc5MzUyNjc4NTcxNCwieSI6MjcxfV0=" marker-end="url(#mermaid-svg-1_flowchart-v2-pointEnd)"/><path d="M763.5,345.403L769.286,345.835C775.073,346.268,786.646,347.134,796.599,347.567C806.552,348,814.885,348,823.219,348C831.552,348,839.885,348,848.26,348C856.635,348,865.052,348,869.26,348L873.469,348" id="L_EMB_PS_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_EMB_PS_0" data-points="W3sieCI6NzYzLjUsInkiOjM0NS40MDI1MzY2NzY2MDI5NX0seyJ4Ijo3OTguMjE4NzUsInkiOjM0OH0seyJ4Ijo4MjMuMjE4NzUsInkiOjM0OH0seyJ4Ijo4NDguMjE4NzUsInkiOjM0OH0seyJ4Ijo4NzcuNDY4NzUsInkiOjM0OH1d" marker-end="url(#mermaid-svg-1_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_TO_V2_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_SJ_V2_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_SE_PSC_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_PSC_EMB_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_V2_JS_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_V2_CT_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_EMB_JS_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_EMB_CT_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_EMB_PS_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="flowchart-TO-0" transform="translate(373.4296875, 82)"><rect class="basic label-container" style="" x="-82.4609375" y="-39" width="164.921875" height="78"/><g class="label" style="" transform="translate(-52.4609375, -24)"><rect/><foreignObject width="104.921875" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>tender_ostrich<br />Bigtable</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-SJ-1" transform="translate(373.4296875, 210)"><rect class="basic label-container" style="" x="-63.140625" y="-39" width="126.28125" height="78"/><g class="label" style="" transform="translate(-33.140625, -24)"><rect/><foreignObject width="66.28125" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>sassy_jay<br />Bigtable</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-SE-2" transform="translate(136.984375, 338)"><rect class="basic label-container" style="" x="-103.984375" y="-39" width="207.96875" height="78"/><g class="label" style="" transform="translate(-73.984375, -24)"><rect/><foreignObject width="147.96875" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>student_embeddings<br />OPTIMIZED</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-PSC-3" transform="translate(373.4296875, 338)"><rect class="basic label-container" style="" x="-77.1953125" y="-39" width="154.390625" height="78"/><g class="label" style="" transform="translate(-47.1953125, -24)"><rect/><foreignObject width="94.390625" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>PSC Endpoint<br />10.126.0.130</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-V2-4" transform="translate(664.5546875, 94)"><rect class="basic label-container" style="" x="-108.6640625" y="-39" width="217.328125" height="78"/><g class="label" style="" transform="translate(-78.6640625, -24)"><rect/><foreignObject width="157.328125" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>VertexFeatureStoreV2<br />Standard API</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-EMB-5" transform="translate(664.5546875, 338)"><rect class="basic label-container" style="" x="-98.9453125" y="-39" width="197.890625" height="78"/><g class="label" style="" transform="translate(-68.9453125, -24)"><rect/><foreignObject width="137.890625" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>EmbeddingProvider<br />gRPC to PSC</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-JS-6" transform="translate(958.1484375, 84)"><rect class="basic label-container" style="" x="-68.828125" y="-27" width="137.65625" height="54"/><g class="label" style="" transform="translate(-38.828125, -12)"><rect/><foreignObject width="77.65625" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Job Search</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-CT-7" transform="translate(958.1484375, 244)"><rect class="basic label-container" style="" x="-84.9296875" y="-27" width="169.859375" height="54"/><g class="label" style="" transform="translate(-54.9296875, -12)"><rect/><foreignObject width="109.859375" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Content / Feed</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-PS-8" transform="translate(958.1484375, 348)"><rect class="basic label-container" style="" x="-80.6796875" y="-27" width="161.359375" height="54"/><g class="label" style="" transform="translate(-50.6796875, -12)"><rect/><foreignObject width="101.359375" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Promotion Svc</p></span></div></foreignObject></g></g></g></g></g></svg>
</div>
</div>
<!-- ===== SECTION 3: DATA FLOW ===== -->
<div id="s3" class="sec-head anim" style="--i:12; color: var(--teal);">
<span class="dot" style="background: var(--teal);"></span> 3 &mdash; Embedding Data Flow
</div>
<div class="card card--teal anim" style="--i:13">
<h3 style="margin-bottom: 14px;">Daily Embedding Publishing Pipeline</h3>
<div class="pipeline">
<div class="pipeline-step pipeline-step--blue">
<div class="step-num">STEP 1</div>
<div class="step-name">GCS</div>
<div class="step-detail">Embeddings<br>from Anyscale</div>
</div>
<div class="pipeline-arrow">&rarr;</div>
<div class="pipeline-step pipeline-step--blue">
<div class="step-num">STEP 2</div>
<div class="step-name">Spanner</div>
<div class="step-detail">Query for<br>unpublished</div>
</div>
<div class="pipeline-arrow">&rarr;</div>
<div class="pipeline-step pipeline-step--teal">
<div class="step-num">STEP 3</div>
<div class="step-name">BigQuery</div>
<div class="step-detail">Load &amp; flatten<br>to BQ table</div>
</div>
<div class="pipeline-arrow">&rarr;</div>
<div class="pipeline-step pipeline-step--amber">
<div class="step-num">STEP 4</div>
<div class="step-name">Feature View</div>
<div class="step-detail">Create in<br>online store</div>
</div>
<div class="pipeline-arrow">&rarr;</div>
<div class="pipeline-step pipeline-step--green">
<div class="step-num">STEP 5</div>
<div class="step-name">Sync</div>
<div class="step-detail">BQ &rarr; Online<br>Store</div>
</div>
<div class="pipeline-arrow">&rarr;</div>
<div class="pipeline-step pipeline-step--blue">
<div class="step-num">STEP 6</div>
<div class="step-name">Spanner</div>
<div class="step-detail">Update<br>manifest</div>
</div>
<div class="pipeline-arrow">&rarr;</div>
<div class="pipeline-step pipeline-step--red">
<div class="step-num">STEP 7</div>
<div class="step-name">Cleanup</div>
<div class="step-detail">Delete old<br>views &gt; 6d</div>
</div>
</div>
<div style="margin-top: 16px;">
<h3>DAGs that use this pipeline</h3>
<div class="inner-grid" style="margin-top: 8px;">
<div class="inner-card">
<div class="title">twotower_embedding_generation</div>
<div class="desc">TwoTower_v2_7day, TwoTower_v3 &mdash; daily 3:00 UTC</div>
</div>
<div class="inner-card">
<div class="title">gnn_embedding_generation</div>
<div class="desc">RGCN_v2, GNNz_v1, GNNz_v2 &mdash; daily 23:00 UTC</div>
</div>
<div class="inner-card">
<div class="title">content_semantic_embedding_gen</div>
<div class="desc">content_mpnet &mdash; daily</div>
</div>
<div class="inner-card">
<div class="title">dummy_embedding_generation</div>
<div class="desc">All models (staging only) &mdash; daily 01:00 UTC</div>
</div>
</div>
</div>
</div>
<div class="callout callout--amber anim" style="--i:14">
<strong>Spanner is untouched during migration.</strong> The <code>vertex_v2_student_embedding_versions</code> and
<code>gcs_embedding_versions</code> tables in the <code>job_search</code> database track
feature view names, not which store backs them. Clients query Spanner to resolve versions &mdash;
the storage backend is opaque. This is why dual-write works without Spanner changes.
</div>
<!-- ===== SECTION 4: MIGRATION PHASES ===== -->
<div id="s4" class="sec-head anim" style="--i:15; color: var(--blue);">
<span class="dot" style="background: var(--blue);"></span> 4 &mdash; Migration Phases
</div>
<div class="phase-grid">
<div class="phase-card phase-card--blue anim" style="--i:16">
<div class="phase-num">Phase 1</div>
<div class="phase-title">Provision Bigtable Store</div>
<span class="phase-repo">terraform</span>
<ul class="phase-files">
<li><code>online_feature_stores.tf</code> &mdash; add new <code>google_vertex_ai_feature_online_store</code> with bigtable config</li>
<li>Follow <code>tender_ostrich</code> pattern: autoscaling 4-6 nodes</li>
<li>No PSC endpoint needed</li>
<li>Update locals/outputs for downstream</li>
</ul>
<div class="phase-desc">
Create a new Bigtable-backed online store alongside the existing optimized one.
Same module, same file, same pattern as the two existing Bigtable stores.
</div>
</div>
<div class="phase-card phase-card--teal anim" style="--i:17">
<div class="phase-num">Phase 2</div>
<div class="phase-title">Dual Write (6-7 days)</div>
<span class="phase-repo">data-platform</span>
<ul class="phase-files">
<li><code>embeddings_to_online_feature_store.py</code> &mdash; add second store target</li>
<li>All embedding DAGs automatically pick up the change</li>
<li>Run for minimum 6 days (full retention window)</li>
<li>Spanner manifests unchanged</li>
</ul>
<div class="phase-desc">
Modify the reusable <code>gcs_embeddings_to_online_feature_store</code> utility to create
feature views and sync to <strong>both</strong> the optimized and new Bigtable stores.
Same feature view names in both stores.
</div>
</div>
<div class="phase-card phase-card--amber anim" style="--i:18">
<div class="phase-num">Phase 3</div>
<div class="phase-title">Switch Corgi Client</div>
<span class="phase-repo">corgi</span>
<ul class="phase-files">
<li><code>feature_store_v2_embeddings.go</code> &mdash; switch from PSC gRPC to standard Vertex API</li>
<li>Point at new Bigtable store name</li>
<li>Keep <code>EmbeddingProvider</code> interface unchanged</li>
<li>Services bump corgi version to adopt</li>
</ul>
<div class="phase-desc">
Replace the hardcoded PSC endpoint connections with the standard Vertex AI API
(<code>us-central1-aiplatform.googleapis.com:443</code>). The <code>EmbeddingProvider</code>
interface stays the same &mdash; consumers just upgrade their corgi dependency.
<strong>Latency goes from 2-3ms to ~30ms</strong> (accepted per team discussion).
</div>
</div>
<div class="phase-card phase-card--red anim" style="--i:19">
<div class="phase-num">Phase 4</div>
<div class="phase-title">Cleanup</div>
<span class="phase-repo">all repos</span>
<ul class="phase-files">
<li><code>terraform</code> &mdash; delete optimized store, PSC endpoint, forwarding rule, IP</li>
<li><code>data-platform</code> &mdash; remove optimized store from dual-write config</li>
<li><code>corgi</code> &mdash; remove hardcoded PSC endpoint map</li>
<li>Update Datadog monitors if needed</li>
</ul>
<div class="phase-desc">
Once all consumers are on the Bigtable store and stable, tear down the deprecated
optimized infrastructure. Delete <code>online_feature_store_endpoints.tf</code> resources
and the optimized store resource from <code>online_feature_stores.tf</code>.
</div>
</div>
</div>
<div class="mermaid-wrap anim" style="--i:20">
<div class="mermaid-zoom">
<button onclick="zoomDiagram('future', 1)">+</button>
<button onclick="zoomDiagram('future', -1)">-</button>
<button onclick="zoomDiagram('future', 0)">R</button>
</div>
<div class="mermaid-inner" id="future">
<svg id="mermaid-svg-2" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 861.156px; background-color: transparent;" viewBox="0 0 861.15625 420" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#mermaid-svg-2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2 .error-icon{fill:#552222;}#mermaid-svg-2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2 .marker.cross{stroke:#333333;}#mermaid-svg-2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2 p{margin:0;}#mermaid-svg-2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2 .cluster-label text{fill:#333;}#mermaid-svg-2 .cluster-label span{color:#333;}#mermaid-svg-2 .cluster-label span p{background-color:transparent;}#mermaid-svg-2 .label text,#mermaid-svg-2 span{fill:#333;color:#333;}#mermaid-svg-2 .node rect,#mermaid-svg-2 .node circle,#mermaid-svg-2 .node ellipse,#mermaid-svg-2 .node polygon,#mermaid-svg-2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2 .rough-node .label text,#mermaid-svg-2 .node .label text,#mermaid-svg-2 .image-shape .label,#mermaid-svg-2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2 .rough-node .label,#mermaid-svg-2 .node .label,#mermaid-svg-2 .image-shape .label,#mermaid-svg-2 .icon-shape .label{text-align:center;}#mermaid-svg-2 .node.clickable{cursor:pointer;}#mermaid-svg-2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2 .arrowheadPath{fill:#333333;}#mermaid-svg-2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2 .cluster text{fill:#333;}#mermaid-svg-2 .cluster span{color:#333;}#mermaid-svg-2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-2 .icon-shape,#mermaid-svg-2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2 .icon-shape p,#mermaid-svg-2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2 .icon-shape .label rect,#mermaid-svg-2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="my-svg_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"><g class="cluster" id="subGraph2" data-look="classic"><rect style="" x="633.296875" y="22" width="219.859375" height="388"/><g class="cluster-label" transform="translate(693.8515625, 22)"><foreignObject width="98.75" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5;"><span class="nodeLabel"><p>Services After</p></span></div></foreignObject></g></g><g class="cluster" id="subGraph1" data-look="classic"><rect style="" x="315.96875" y="8" width="267.328125" height="404"/><g class="cluster-label" transform="translate(410.859375, 8)"><foreignObject width="77.546875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5;"><span class="nodeLabel"><p>Corgi After</p></span></div></foreignObject></g></g><g class="cluster" id="subGraph0" data-look="classic"><rect style="" x="8" y="8" width="257.96875" height="404"/><g class="cluster-label" transform="translate(82.8515625, 8)"><foreignObject width="108.265625" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5;"><span class="nodeLabel"><p>After Migration</p></span></div></foreignObject></g></g></g><g class="edgePaths"><path d="M219.445,82L227.199,82C234.953,82,250.461,82,262.382,82C274.302,82,282.635,82,290.969,82C299.302,82,307.635,82,315.305,82.314C322.974,82.629,329.979,83.258,333.482,83.572L336.985,83.887" id="L_TO2_V2B_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_TO2_V2B_0" data-points="W3sieCI6MjE5LjQ0NTMxMjUsInkiOjgyfSx7IngiOjI2NS45Njg3NSwieSI6ODJ9LHsieCI6MjkwLjk2ODc1LCJ5Ijo4Mn0seyJ4IjozMTUuOTY4NzUsInkiOjgyfSx7IngiOjM0MC45Njg3NSwieSI6ODQuMjQ0NDMyNzU0NjkwNTF9XQ==" marker-end="url(#mermaid-svg-2_flowchart-v2-pointEnd)"/><path d="M200.125,210L211.099,210C222.073,210,244.021,210,259.161,210C274.302,210,282.635,210,290.969,210C299.302,210,307.635,210,326.086,197.604C344.537,185.207,373.105,160.415,387.389,148.018L401.673,135.622" id="L_SJ2_V2B_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SJ2_V2B_0" data-points="W3sieCI6MjAwLjEyNSwieSI6MjEwfSx7IngiOjI2NS45Njg3NSwieSI6MjEwfSx7IngiOjI5MC45Njg3NSwieSI6MjEwfSx7IngiOjMxNS45Njg3NSwieSI6MjEwfSx7IngiOjQwNC42OTQwMzI4NjYzNzkzLCJ5IjoxMzN9XQ==" marker-end="url(#mermaid-svg-2_flowchart-v2-pointEnd)"/><path d="M240.969,338L245.135,338C249.302,338,257.635,338,265.969,338C274.302,338,282.635,338,290.969,338C299.302,338,307.635,338,316.922,338C326.208,338,336.448,338,341.568,338L346.688,338" id="L_SE2_EMBB_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_SE2_EMBB_0" data-points="W3sieCI6MjQwLjk2ODc1LCJ5IjozMzh9LHsieCI6MjY1Ljk2ODc1LCJ5IjozMzh9LHsieCI6MjkwLjk2ODc1LCJ5IjozMzh9LHsieCI6MzE1Ljk2ODc1LCJ5IjozMzh9LHsieCI6MzUwLjY4NzUsInkiOjMzOH1d" marker-end="url(#mermaid-svg-2_flowchart-v2-pointEnd)"/><path d="M558.297,84.244L562.464,83.87C566.63,83.496,574.964,82.748,583.297,82.374C591.63,82,599.964,82,608.297,82C616.63,82,624.964,82,635.314,82.113C645.664,82.225,658.032,82.45,664.215,82.563L670.399,82.675" id="L_V2B_JS2_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_V2B_JS2_0" data-points="W3sieCI6NTU4LjI5Njg3NSwieSI6ODQuMjQ0NDMyNzU0NjkwNTF9LHsieCI6NTgzLjI5Njg3NSwieSI6ODJ9LHsieCI6NjA4LjI5Njg3NSwieSI6ODJ9LHsieCI6NjMzLjI5Njg3NSwieSI6ODJ9LHsieCI6Njc0LjM5ODQzNzUsInkiOjgyLjc0Nzc3OTEyMDE3NjI1fV0=" marker-end="url(#mermaid-svg-2_flowchart-v2-pointEnd)"/><path d="M487.407,133L503.389,149.5C519.371,166,551.334,199,571.482,215.5C591.63,232,599.964,232,608.297,232C616.63,232,624.964,232,632.634,232.382C640.305,232.765,647.313,233.53,650.817,233.912L654.32,234.295" id="L_V2B_CT2_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_V2B_CT2_0" data-points="W3sieCI6NDg3LjQwNzQzODg1ODY5NTYsInkiOjEzM30seyJ4Ijo1ODMuMjk2ODc1LCJ5IjoyMzJ9LHsieCI6NjA4LjI5Njg3NSwieSI6MjMyfSx7IngiOjYzMy4yOTY4NzUsInkiOjIzMn0seyJ4Ijo2NTguMjk2ODc1LCJ5IjoyMzQuNzI5MDE3MTI3NDI1Mn1d" marker-end="url(#mermaid-svg-2_flowchart-v2-pointEnd)"/><path d="M483.049,299L499.757,279.5C516.465,260,549.881,221,570.756,201.5C591.63,182,599.964,182,608.297,182C616.63,182,624.964,182,641.906,170.61C658.849,159.221,684.402,136.441,697.178,125.051L709.954,113.662" id="L_EMBB_JS2_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_EMBB_JS2_0" data-points="W3sieCI6NDgzLjA0ODgyODEyNSwieSI6Mjk5fSx7IngiOjU4My4yOTY4NzUsInkiOjE4Mn0seyJ4Ijo2MDguMjk2ODc1LCJ5IjoxODJ9LHsieCI6NjMzLjI5Njg3NSwieSI6MTgyfSx7IngiOjcxMi45Mzk4MTE4NjIyNDQ5LCJ5IjoxMTF9XQ==" marker-end="url(#mermaid-svg-2_flowchart-v2-pointEnd)"/><path d="M548.578,299.507L554.365,297.256C560.151,295.005,571.724,290.502,581.677,288.251C591.63,286,599.964,286,608.297,286C616.63,286,624.964,286,635.051,283.738C645.138,281.476,656.98,276.952,662.9,274.69L668.821,272.428" id="L_EMBB_CT2_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_EMBB_CT2_0" data-points="W3sieCI6NTQ4LjU3ODEyNSwieSI6Mjk5LjUwNjgwOTI4MTY2NDYzfSx7IngiOjU4My4yOTY4NzUsInkiOjI4Nn0seyJ4Ijo2MDguMjk2ODc1LCJ5IjoyODZ9LHsieCI6NjMzLjI5Njg3NSwieSI6Mjg2fSx7IngiOjY3Mi41NTc0Nzc2Nzg1NzE0LCJ5IjoyNzF9XQ==" marker-end="url(#mermaid-svg-2_flowchart-v2-pointEnd)"/><path d="M548.578,345.403L554.365,345.835C560.151,346.268,571.724,347.134,581.677,347.567C591.63,348,599.964,348,608.297,348C616.63,348,624.964,348,633.339,348C641.714,348,650.13,348,654.339,348L658.547,348" id="L_EMBB_PS2_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_EMBB_PS2_0" data-points="W3sieCI6NTQ4LjU3ODEyNSwieSI6MzQ1LjQwMjUzNjY3NjYwMjk1fSx7IngiOjU4My4yOTY4NzUsInkiOjM0OH0seyJ4Ijo2MDguMjk2ODc1LCJ5IjozNDh9LHsieCI6NjMzLjI5Njg3NSwieSI6MzQ4fSx7IngiOjY2Mi41NDY4NzUsInkiOjM0OH1d" marker-end="url(#mermaid-svg-2_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_TO2_V2B_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_SJ2_V2B_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_SE2_EMBB_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_V2B_JS2_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_V2B_CT2_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_EMBB_JS2_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_EMBB_CT2_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_EMBB_PS2_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="flowchart-TO2-0" transform="translate(136.984375, 82)"><rect class="basic label-container" style="" x="-82.4609375" y="-39" width="164.921875" height="78"/><g class="label" style="" transform="translate(-52.4609375, -24)"><rect/><foreignObject width="104.921875" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>tender_ostrich<br />Bigtable</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-SJ2-1" transform="translate(136.984375, 210)"><rect class="basic label-container" style="" x="-63.140625" y="-39" width="126.28125" height="78"/><g class="label" style="" transform="translate(-33.140625, -24)"><rect/><foreignObject width="66.28125" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>sassy_jay<br />Bigtable</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-SE2-2" transform="translate(136.984375, 338)"><rect class="basic label-container" style="" x="-103.984375" y="-39" width="207.96875" height="78"/><g class="label" style="" transform="translate(-73.984375, -24)"><rect/><foreignObject width="147.96875" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>student_embeddings<br />Bigtable NEW</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-V2B-3" transform="translate(449.6328125, 94)"><rect class="basic label-container" style="" x="-108.6640625" y="-39" width="217.328125" height="78"/><g class="label" style="" transform="translate(-78.6640625, -24)"><rect/><foreignObject width="157.328125" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>VertexFeatureStoreV2<br />Standard API</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-EMBB-4" transform="translate(449.6328125, 338)"><rect class="basic label-container" style="" x="-98.9453125" y="-39" width="197.890625" height="78"/><g class="label" style="" transform="translate(-68.9453125, -24)"><rect/><foreignObject width="137.890625" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>EmbeddingProvider<br />Standard API</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-JS2-5" transform="translate(743.2265625, 84)"><rect class="basic label-container" style="" x="-68.828125" y="-27" width="137.65625" height="54"/><g class="label" style="" transform="translate(-38.828125, -12)"><rect/><foreignObject width="77.65625" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Job Search</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-CT2-6" transform="translate(743.2265625, 244)"><rect class="basic label-container" style="" x="-84.9296875" y="-27" width="169.859375" height="54"/><g class="label" style="" transform="translate(-54.9296875, -12)"><rect/><foreignObject width="109.859375" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Content / Feed</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-PS2-7" transform="translate(743.2265625, 348)"><rect class="basic label-container" style="" x="-80.6796875" y="-27" width="161.359375" height="54"/><g class="label" style="" transform="translate(-50.6796875, -12)"><rect/><foreignObject width="101.359375" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Promotion Svc</p></span></div></foreignObject></g></g></g></g></g></svg>
</div>
</div>
<div class="callout callout--green anim" style="--i:21">
<strong>End state:</strong> All three online stores are Bigtable-backed. No PSC endpoints.
All clients use the standard Vertex AI API. Uniform infrastructure, lower cost, simpler operations.
</div>
<!-- ===== SECTION 5: TIMELINE ===== -->
<div id="s5" class="sec-head anim" style="--i:22; color: var(--amber);">
<span class="dot" style="background: var(--amber);"></span> 5 &mdash; Timeline
</div>
<div class="timeline anim" style="--i:23">
<div class="tl-item tl-item--past">
<div class="tl-date">February 17, 2026</div>
<div class="tl-desc"><strong>Optimized Online Serving deprecated.</strong> Google announced deprecation. DAMP-242 created.</div>
</div>
<div class="tl-item tl-item--past">
<div class="tl-date">March 9, 2026</div>
<div class="tl-desc"><strong>Migration squad kickoff.</strong> Ahmad, Noah, Jacob aligned on approach: provision Bigtable store, dual-write, switch client.</div>
</div>
<div class="tl-item tl-item--soon">
<div class="tl-date">March-April 2026</div>
<div class="tl-desc"><strong>Phase 1-2:</strong> Terraform provisioning + DAG dual-write. Noah creates strategy ticket. Jacob starts Corgi client work.</div>
</div>
<div class="tl-item tl-item--soon">
<div class="tl-date">May 17, 2026</div>
<div class="tl-desc"><strong>Feature freeze.</strong> No new features added to optimized serving. Critical patches only.</div>
</div>
<div class="tl-item tl-item--future">
<div class="tl-date">Q2-Q3 2026 (target)</div>
<div class="tl-desc"><strong>Phase 3-4:</strong> Switch all consumers to Bigtable store. Cleanup optimized infrastructure. Well before sunset.</div>
</div>
<div class="tl-item tl-item--future">
<div class="tl-date">February 17, 2027</div>
<div class="tl-desc"><strong>Full sunset.</strong> Optimized Online Serving APIs removed. Must be migrated before this date.</div>
</div>
</div>
<!-- ===== SECTION 6: RISKS ===== -->
<div id="s6" class="sec-head anim" style="--i:24; color: var(--red);">
<span class="dot" style="background: var(--red);"></span> 6 &mdash; Risks
</div>
<div class="card anim" style="--i:25">
<div style="overflow-x: auto;">
<table class="risk-table">
<thead>
<tr>
<th style="width: 80px;">Severity</th>
<th style="width: 160px;">Risk</th>
<th>Detail</th>
<th style="width: 220px;">Mitigation</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="severity severity--med">Medium</span></td>
<td><strong>Latency regression</strong></td>
<td>Optimized = 2-3ms. Bigtable = ~30ms (10x slower). All embedding consumers will see higher p50/p95.</td>
<td>Team accepted this tradeoff. Monitor via Datadog corgi-client P95 monitors. Can tune Bigtable nodes if needed.</td>
</tr>
<tr>
<td><span class="severity severity--high">High</span></td>
<td><strong>Feature view cap (50/store)</strong></td>
<td>Previously hit 50 feature view limit on optimized store. 7 models x 6 days = 42 views. With dual-write, new store will also accumulate.</td>
<td>Cleanup step in pipeline already deletes views older than retention. Verify cleanup runs against new store too.</td>
</tr>
<tr>
<td><span class="severity severity--high">High</span></td>
<td><strong>PSC endpoint atomicity</strong></td>
<td>Corgi has hardcoded IPs (10.126.0.130:10002). Client switch and store switch must be coordinated.</td>
<td>Dual-write period ensures both stores have identical data. Switch Corgi first, then cleanup PSC. Never remove PSC before Corgi is off it.</td>
</tr>
<tr>
<td><span class="severity severity--med">Medium</span></td>
<td><strong>Dual-write window</strong></td>
<td>Must run dual-write for full 6-day retention window before switching consumers. Cutting short = missing embeddings.</td>
<td>Minimum 7 days dual-write. Verify all models have views in new store before switching. Check via Vertex API or Spanner query.</td>
</tr>
<tr>
<td><span class="severity severity--low">Low</span></td>
<td><strong>Bigtable cost</strong></td>
<td>New Bigtable store adds nodes (4-6 in prod). During dual-write, running both stores temporarily.</td>
<td>Bigtable is significantly cheaper than optimized. Net cost savings after cleanup. Dual-write period cost is minimal (1-2 weeks).</td>
</tr>
<tr>
<td><span class="severity severity--low">Low</span></td>
<td><strong>Idempotency issues</strong></td>
<td>Past incidents: 409 ALREADY_EXISTS errors when feature views are created twice (ML-618).</td>
<td>Already fixed &mdash; operator now skips to sync if view exists. Verify same fix applies to new store code path.</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ===== SECTION 7: STAKEHOLDERS ===== -->
<div id="s7" class="sec-head anim" style="--i:26; color: var(--slate);">
<span class="dot" style="background: var(--slate);"></span> 7 &mdash; Who Needs to Know
</div>
<div class="card anim" style="--i:27">
<div style="overflow-x: auto;">
<table class="stakeholder-table">
<thead>
<tr>
<th style="width: 180px;">Person / Team</th>
<th style="width: 80px;">Role</th>
<th>Why They Care</th>
<th style="width: 200px;">Action Needed</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Noah Carniglia</strong><br><span style="font-size: 11px; color: var(--text-dim);">ML Infra Platform</span></td>
<td><span class="role-tag role-tag--owner">Owner</span></td>
<td>Built the original optimized stores, PSC endpoints, embedding operator, and Corgi client. Primary SME.</td>
<td>Create strategy ticket. Review all PRs. Guide Terraform provisioning.</td>
</tr>
<tr>
<td><strong>Jacob Rodriguez</strong><br><span style="font-size: 11px; color: var(--text-dim);">ML Infra Platform</span></td>
<td><span class="role-tag role-tag--impl">Implementer</span></td>
<td>Leading Corgi client update from PSC gRPC to standard Vertex API.</td>
<td>Update <code style="font-family: var(--font-mono); font-size: 10px;">feature_store_v2_embeddings.go</code>. Test locally + staging.</td>
</tr>
<tr>
<td><strong>Job Search team</strong><br><span style="font-size: 11px; color: var(--text-dim);">Core Relevance</span></td>
<td><span class="role-tag role-tag--consumer">Consumer</span></td>
<td>Primary consumer of GNNz and TwoTower embeddings. Will see latency change (2-3ms to ~30ms).</td>
<td>Bump corgi dependency. Monitor latency post-switch. Adjust timeouts if needed.</td>
</tr>
<tr>
<td><strong>Content / Feed team</strong><br><span style="font-size: 11px; color: var(--text-dim);">Feed &amp; Content AI</span></td>
<td><span class="role-tag role-tag--consumer">Consumer</span></td>
<td>Consumer of content_mpnet embeddings. Also migrated to v2 operator recently.</td>
<td>Bump corgi dependency. Monitor latency post-switch.</td>
</tr>
<tr>
<td><strong>Promotion Service</strong><br><span style="font-size: 11px; color: var(--text-dim);">Recruiting ML</span></td>
<td><span class="role-tag role-tag--consumer">Consumer</span></td>
<td>Consumer of embeddings for promotion ranking.</td>
<td>Bump corgi dependency. Monitor latency post-switch.</td>
</tr>
<tr>
<td><strong>Data Platform team</strong></td>
<td><span class="role-tag role-tag--impl">Implementer</span></td>
<td>DAG changes for dual-write. Owns the embedding publishing infrastructure.</td>
<td>Review dual-write PR. Validate DAG runs in staging. Monitor cleanup task.</td>
</tr>
<tr>
<td><strong>Cloud Engineering</strong></td>
<td><span class="role-tag role-tag--infra">Infra</span></td>
<td>PSC endpoint and networking cleanup. VPC forwarding rules.</td>
<td>Review Terraform PR for PSC decommission. Confirm no other services use the endpoint.</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ===== SECTION 8: KEY FILES ===== -->
<div id="s8" class="sec-head anim" style="--i:28; color: var(--teal);">
<span class="dot" style="background: var(--teal);"></span> 8 &mdash; Key Files by Repo
</div>
<div class="card card--blue anim" style="--i:29">
<h3 style="color: var(--blue); margin-bottom: 10px;">terraform</h3>
<ul class="file-list file-list--blue">
<li><code>stacks/data/modules/ml-platform/online_feature_stores.tf</code> &mdash; All 3 online store definitions + feature groups + feature views</li>
<li><code>stacks/data/modules/ml-platform/online_feature_store_endpoints.tf</code> &mdash; PSC endpoint, forwarding rule, internal IP (to be deleted)</li>
<li><code>stacks/iam/vertex_ai_access.tf</code> &mdash; IAM bindings for Vertex AI access</li>
<li><code>stacks/data/modules/datadog_monitors/json/vertex/</code> &mdash; Corgi + GIL client monitors (fetch errors, P95 latency)</li>
</ul>
</div>
<div class="card card--teal anim" style="--i:30">
<h3 style="color: var(--teal); margin-bottom: 10px;">corgi</h3>
<ul class="file-list file-list--teal">
<li><code>feature_store_v2_embeddings.go</code> &mdash; EmbeddingProvider using PSC gRPC (hardcoded IPs). THE critical file to change.</li>
<li><code>feature_store_v2.go</code> &mdash; Standard V2 client (reference for Bigtable API pattern)</li>
<li><code>feature_store_v2_batch.go</code> &mdash; Batch streaming client</li>
<li><code>configuration.go</code> &mdash; FeatureStoreConfigV2 struct</li>
<li><code>gax.go</code> &mdash; Retry configuration</li>
</ul>
</div>
<div class="card card--amber anim" style="--i:31">
<h3 style="color: var(--amber); margin-bottom: 10px;">data-platform</h3>
<ul class="file-list file-list--amber">
<li><code>handshake/airflow/dag_utils/embeddings_to_online_feature_store.py</code> &mdash; Core reusable utility. Modify for dual-write.</li>
<li><code>handshake/relevance/new_vertex/feature_view_syncer.py</code> &mdash; Feature view creation + sync orchestration</li>
<li><code>handshake/relevance/new_vertex/feature_store_client.py</code> &mdash; REST client for Vertex AI Feature Store API</li>
<li><code>handshake/relevance/new_vertex/embeddings_job_utils.py</code> &mdash; Spanner queries, job config, cleanup</li>
<li><code>airflow/core_relevance/twotower_embedding_generation.py</code> &mdash; TwoTower DAG</li>
<li><code>airflow/jobs_digest/gnn_embedding_generation.py</code> &mdash; GNN DAG</li>
<li><code>airflow/core_relevance/content_semantic_embedding_generation.py</code> &mdash; Content DAG</li>
</ul>
</div>
<details class="anim" style="--i:32">
<summary>Spanner Tables (reference)</summary>
<div class="card card--recessed" style="margin-top: 8px;">
<div class="inner-grid">
<div class="inner-card">
<div class="title">gcs_embedding_versions</div>
<div class="desc">Tracks all embedding batches produced to GCS. Columns: model_name, model_id, batch_embedding_ts, model_gcs_location</div>
</div>
<div class="inner-card">
<div class="title">vertex_v2_student_embedding_versions</div>
<div class="desc">Tracks embeddings published to Vertex (non-legacy). Columns: model_name, batch_embedding_ts, vertex_feature_view, ingestion_ts</div>
</div>
</div>
<p style="font-size: 12px; color: var(--text-dim); margin-top: 12px;">
Both tables live in the <strong>job_search</strong> Spanner database. Neither table references the online store name &mdash;
only feature view names. This is why the migration is transparent to Spanner.
</p>
</div>
</details>
</div><!-- /main -->
</div><!-- /wrap -->
<!-- Mermaid pre-rendered as inline SVG -->
<script>
// Scroll spy
(function() {
const toc = document.getElementById('toc');
const links = toc.querySelectorAll('a');
const sections = [];
links.forEach(link => {
const id = link.getAttribute('href').slice(1);
const el = document.getElementById(id);
if (el) sections.push({ id, el, link });
});
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
links.forEach(l => l.classList.remove('active'));
const match = sections.find(s => s.el === entry.target);
if (match) {
match.link.classList.add('active');
if (window.innerWidth <= 1000) {
match.link.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
}
}
}
});
}, { rootMargin: '-10% 0px -80% 0px' });
sections.forEach(s => observer.observe(s.el));
links.forEach(link => {
link.addEventListener('click', e => {
e.preventDefault();
const id = link.getAttribute('href').slice(1);
const el = document.getElementById(id);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
history.replaceState(null, '', '#' + id);
}
});
});
})();
// Zoom controls for Mermaid diagrams
const zoomStates = {};
function zoomDiagram(id, dir) {
if (!zoomStates[id]) zoomStates[id] = { scale: 1, x: 0, y: 0 };
const s = zoomStates[id];
if (dir === 0) { s.scale = 1; s.x = 0; s.y = 0; }
else { s.scale = Math.max(0.5, Math.min(3, s.scale + dir * 0.25)); }
const el = document.getElementById(id);
const svg = el.querySelector('svg');
if (svg) { svg.style.transform = `scale(${s.scale})`; svg.style.transformOrigin = '0 0'; }
}
// Drag to pan on Mermaid diagrams
document.querySelectorAll('.mermaid-inner').forEach(wrap => {
let isDragging = false, startX, startY, scrollLeft, scrollTop;
wrap.addEventListener('mousedown', e => {
isDragging = true; wrap.classList.add('dragging');
startX = e.pageX - wrap.offsetLeft; startY = e.pageY - wrap.offsetTop;
scrollLeft = wrap.scrollLeft; scrollTop = wrap.scrollTop;
});
wrap.addEventListener('mouseleave', () => { isDragging = false; wrap.classList.remove('dragging'); });
wrap.addEventListener('mouseup', () => { isDragging = false; wrap.classList.remove('dragging'); });
wrap.addEventListener('mousemove', e => {
if (!isDragging) return; e.preventDefault();
wrap.scrollLeft = scrollLeft - (e.pageX - wrap.offsetLeft - startX);
wrap.scrollTop = scrollTop - (e.pageY - wrap.offsetTop - startY);
});
// Ctrl/Cmd + scroll to zoom
wrap.addEventListener('wheel', e => {
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
zoomDiagram(wrap.id, e.deltaY < 0 ? 1 : -1);
}
}, { passive: false });
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment