Módulo 5: Landscape de Vector Databases para AI Engineers
Cápsula 08: Proyecto - Decision Tree para Elegir Vector DB
Descripción de la cápsula
Cerrarás el módulo diseñando e implementando un árbol de decisión programático para seleccionar vector database en proyectos RAG. No es solo un diagrama estático: construirás un motor en Python que recibe requisitos concretos y genera una recomendación con justificación, alternativas y trade-offs explícitos.
El output alimenta directamente el Módulo 6 (matriz de scoring).
Tiempo estimado: 35-45 minutos
🎯 Objetivo del proyecto
- Implementar un decision tree como código Python con 6 dimensiones de decisión.
- Producir recomendación principal + alternativa con justificación.
- Validar contra 5 escenarios reales y generar visualización Mermaid.
📋 Especificaciones del proyecto
Requisitos funcionales
- Recibir requisitos del proyecto y evaluar 6 dimensiones: volumen, operación, presupuesto, latencia, compliance y equipo.
- Producir recomendación principal con score + alternativa con condiciones de cuándo preferirla.
- Justificar por qué se descartaron las demás opciones.
Success Criteria
- ✅ Cubre 5 proveedores: ChromaDB, Pinecone, Weaviate, Qdrant, Milvus
- ✅ 5 escenarios validados con resultado coherente
- ✅ Justificación con trade-offs técnicos y financieros
- ✅ Visualización Mermaid generada
🧠 Contexto antes de empezar
Este proyecto sintetiza las cápsulas 02-07: panorama de proveedores (perfiles), managed vs self-hosted (nodo clave), features para RAG (criterios), costos/trade-offs (presupuesto), cuándo elegir cada opción (lógica) y anti-patrones (validación). Si algún concepto no está claro, revisa la cápsula correspondiente antes de continuar.
💻 Implementación paso a paso
Paso 1: Definir el modelo de requisitos
El primer paso es estructurar las entradas que el árbol necesita para tomar una decisión. Cada campo representa una dimensión que impacta la elección.
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
class OperationalModel(Enum):
MANAGED = "managed"
SELF_HOSTED = "self_hosted"
FLEXIBLE = "flexible" # sin preferencia fuerte
class ComplianceLevel(Enum):
NONE = "none"
BASIC = "basic" # GDPR genérico, datos no sensibles
STRICT = "strict" # residencia de datos, auditoría, regulación financiera/salud
class TeamCapability(Enum):
MINIMAL = "minimal" # 1-2 devs, sin DevOps
MODERATE = "moderate" # equipo pequeño con algo de infra
STRONG = "strong" # equipo con DevOps o plataforma dedicada
class ProjectStage(Enum):
POC = "poc"
VALIDATION = "validation"
PRODUCTION = "production"
SCALE = "scale"
@dataclass
class ProjectRequirements:
"""Requisitos del proyecto para selección de vector DB."""
vector_count: int # vectores actuales o proyectados a 6 meses
operational_preference: OperationalModel # managed vs self-hosted
monthly_budget_usd: float # presupuesto mensual para vector DB
target_latency_p95_ms: float # latencia objetivo p95 en ms
compliance: ComplianceLevel # nivel de compliance requerido
team_capability: TeamCapability # capacidad operativa del equipo
project_stage: ProjectStage # etapa actual del producto
needs_hybrid_search: bool = False # ¿requiere búsqueda híbrida?
needs_multi_tenancy: bool = False # ¿requiere aislamiento multi-tenant?
project_name: str = "unnamed" # nombre del proyecto (para reportes)
def validate(self) -> list[str]:
"""Valida consistencia de los requisitos."""
warnings = []
if self.vector_count < 0:
warnings.append("vector_count no puede ser negativo")
if self.monthly_budget_usd < 0:
warnings.append("monthly_budget_usd no puede ser negativo")
if self.target_latency_p95_ms <= 0:
warnings.append("target_latency_p95_ms debe ser positivo")
if (self.compliance == ComplianceLevel.STRICT
and self.operational_preference == OperationalModel.MANAGED):
warnings.append("ALERTA: compliance estricto + managed puede ser contradictorio")
if self.vector_count > 5_000_000 and self.team_capability == TeamCapability.MINIMAL:
warnings.append("ALERTA: escala >5M con equipo mínimo es riesgo operativo")
if self.project_stage == ProjectStage.POC and self.monthly_budget_usd > 500:
warnings.append("INFO: presupuesto alto para PoC — considera opción gratuita")
return warnings
¿Por qué este diseño? Los Enum fuerzan inputs válidos, validate() detecta combinaciones contradictorias antes de ejecutar el árbol, y cada campo mapea a una dimensión cubierta en cápsulas 02-06.
Paso 2: Definir el perfil de cada vector DB
Antes de construir el árbol, necesitas un modelo de cada proveedor. Esto centraliza la información y facilita actualizar el árbol cuando el mercado cambie.
@dataclass
class VectorDBProfile:
"""Perfil de un proveedor de vector database."""
name: str
max_scale_comfort: int # vectores donde opera cómodamente
supports_managed: bool
supports_self_hosted: bool
supports_hybrid_search: bool
supports_multi_tenancy: bool
compliance_ready: bool # ¿soporta compliance estricto de forma nativa?
min_monthly_cost_usd: float # costo mínimo operativo estimado
operational_complexity: int # 1 (simple) a 5 (requiere equipo dedicado)
typical_latency_p95_ms: float # latencia típica p95 en producción
best_for: list[str] = field(default_factory=list)
risks: list[str] = field(default_factory=list)
VECTOR_DB_PROFILES = {
"ChromaDB": VectorDBProfile(
name="ChromaDB",
max_scale_comfort=500_000,
supports_managed=False, supports_self_hosted=True,
supports_hybrid_search=False, supports_multi_tenancy=False,
compliance_ready=False,
min_monthly_cost_usd=0, operational_complexity=1, typical_latency_p95_ms=5.0,
best_for=["PoC y prototipos", "aprendizaje", "desarrollo local"],
risks=["no pensado para escala productiva", "sin managed", "features limitadas"],
),
"Pinecone": VectorDBProfile(
name="Pinecone",
max_scale_comfort=50_000_000,
supports_managed=True, supports_self_hosted=False,
supports_hybrid_search=True, supports_multi_tenancy=True,
compliance_ready=True,
min_monthly_cost_usd=70, operational_complexity=1, typical_latency_p95_ms=10.0,
best_for=["producción managed sin DevOps", "SLA rápido", "time-to-market"],
risks=["vendor lock-in fuerte", "costo escala con volumen", "sin self-hosted"],
),
"Weaviate": VectorDBProfile(
name="Weaviate",
max_scale_comfort=100_000_000,
supports_managed=True, supports_self_hosted=True,
supports_hybrid_search=True, supports_multi_tenancy=True,
compliance_ready=True,
min_monthly_cost_usd=25, operational_complexity=3, typical_latency_p95_ms=8.0,
best_for=["hybrid search avanzado", "flexibilidad managed+self-hosted", "ecosistema amplio"],
risks=["curva de aprendizaje mayor", "complejidad operativa self-hosted"],
),
"Qdrant": VectorDBProfile(
name="Qdrant",
max_scale_comfort=100_000_000,
supports_managed=True, supports_self_hosted=True,
supports_hybrid_search=True, supports_multi_tenancy=True,
compliance_ready=True,
min_monthly_cost_usd=25, operational_complexity=2, typical_latency_p95_ms=5.0,
best_for=["performance y eficiencia", "control self-hosted con buena UX"],
risks=["ecosistema más joven", "self-hosted requiere disciplina operativa"],
),
"Milvus": VectorDBProfile(
name="Milvus",
max_scale_comfort=1_000_000_000,
supports_managed=True, supports_self_hosted=True,
supports_hybrid_search=True, supports_multi_tenancy=True,
compliance_ready=True,
min_monthly_cost_usd=100, operational_complexity=5, typical_latency_p95_ms=12.0,
best_for=["escalas enterprise masivas (>10M)", "equipo de plataforma dedicado"],
risks=["overkill para proyectos pequeños", "complejidad operativa alta"],
),
}
Nota: estos valores son estimaciones pedagógicas. En un proyecto real, valida contra benchmarks propios y pricing actualizado.
Paso 3: Construir el motor de decisión
Cada nodo evalúa una dimensión y puntúa candidatos. La estructura es deliberadamente legible: otro engineer debe poder seguir la lógica sin documentación adicional.
@dataclass
class Recommendation:
"""Resultado del decision tree."""
primary: str # nombre de la DB recomendada
primary_score: float # score normalizado 0-100
primary_justification: list[str] # razones de la recomendación
alternative: str # segunda opción
alternative_score: float
alternative_justification: list[str]
eliminated: dict[str, list[str]] # {db_name: [razones de eliminación]}
warnings: list[str] # alertas del proceso
confidence: str # "high", "medium", "low"
def evaluate_decision_tree(reqs: ProjectRequirements) -> Recommendation:
"""
Evalúa requisitos contra perfiles de vector DB.
Retorna recomendación estructurada con justificación.
"""
warnings = reqs.validate()
scores: dict[str, float] = {}
reasons: dict[str, list[str]] = {}
eliminations: dict[str, list[str]] = {}
for db_name, profile in VECTOR_DB_PROFILES.items():
score = 0.0
db_reasons = []
db_eliminations = []
# ── Nodo 1: Escala ──
if reqs.vector_count <= profile.max_scale_comfort:
scale_ratio = reqs.vector_count / profile.max_scale_comfort
if scale_ratio < 0.3:
score += 20
db_reasons.append(f"escala cómoda ({reqs.vector_count:,} vectores)")
elif scale_ratio < 0.7:
score += 15
db_reasons.append("escala dentro de rango operativo")
else:
score += 8
db_reasons.append("escala cercana al límite cómodo")
else:
score -= 30
db_eliminations.append(
f"escala ({reqs.vector_count:,}) excede zona cómoda "
f"({profile.max_scale_comfort:,})"
)
# ── Nodo 2: Modelo operativo ──
if reqs.operational_preference == OperationalModel.MANAGED:
if profile.supports_managed:
score += 20
db_reasons.append("soporta modelo managed")
else:
score -= 25
db_eliminations.append("no ofrece opción managed")
elif reqs.operational_preference == OperationalModel.SELF_HOSTED:
if profile.supports_self_hosted:
score += 20
db_reasons.append("soporta self-hosted")
else:
score -= 25
db_eliminations.append("no ofrece opción self-hosted")
else: # FLEXIBLE
if profile.supports_managed and profile.supports_self_hosted:
score += 15
db_reasons.append("flexible: soporta ambos modelos")
elif profile.supports_managed or profile.supports_self_hosted:
score += 8
db_reasons.append("soporta al menos un modelo operativo")
# ── Nodo 3: Presupuesto ──
if reqs.monthly_budget_usd >= profile.min_monthly_cost_usd:
budget_headroom = reqs.monthly_budget_usd / max(profile.min_monthly_cost_usd, 1)
if budget_headroom > 3:
score += 15
db_reasons.append("presupuesto holgado para esta opción")
else:
score += 10
db_reasons.append("presupuesto suficiente")
else:
score -= 20
db_eliminations.append(
f"presupuesto (${reqs.monthly_budget_usd:.0f}) menor que "
f"costo mínimo (${profile.min_monthly_cost_usd:.0f})"
)
# ── Nodo 4: Latencia ──
if profile.typical_latency_p95_ms <= reqs.target_latency_p95_ms:
latency_margin = reqs.target_latency_p95_ms - profile.typical_latency_p95_ms
if latency_margin > 10:
score += 15
db_reasons.append("latencia con margen amplio")
else:
score += 10
db_reasons.append("latencia dentro de objetivo")
else:
score -= 15
db_eliminations.append(
f"latencia típica ({profile.typical_latency_p95_ms}ms) "
f"excede objetivo ({reqs.target_latency_p95_ms}ms)"
)
# ── Nodo 5: Compliance ──
if reqs.compliance == ComplianceLevel.STRICT:
if profile.compliance_ready and profile.supports_self_hosted:
score += 20
db_reasons.append("soporta compliance estricto con self-hosted")
elif profile.compliance_ready:
score += 10
db_reasons.append("compliance ready (verificar residencia de datos)")
else:
score -= 20
db_eliminations.append("no cumple requisitos de compliance estricto")
elif reqs.compliance == ComplianceLevel.BASIC:
if profile.compliance_ready:
score += 10
db_reasons.append("compliance básico cubierto")
else:
score += 3
# ── Nodo 6: Capacidad del equipo ──
complexity_gap = profile.operational_complexity - _team_to_complexity(reqs.team_capability)
if complexity_gap <= 0:
score += 15
db_reasons.append("complejidad operativa manejable por el equipo")
elif complexity_gap == 1:
score += 5
db_reasons.append("complejidad operativa al límite del equipo")
else:
score -= 15
db_eliminations.append(
f"complejidad operativa ({profile.operational_complexity}/5) "
f"excede capacidad del equipo"
)
# ── Bonus: Features especiales ──
if reqs.needs_hybrid_search:
if profile.supports_hybrid_search:
score += 10
db_reasons.append("soporta hybrid search")
else:
score -= 10
db_eliminations.append("no soporta hybrid search requerido")
if reqs.needs_multi_tenancy:
if profile.supports_multi_tenancy:
score += 10
db_reasons.append("soporta multi-tenancy")
else:
score -= 10
db_eliminations.append("no soporta multi-tenancy requerido")
# ── Bonus: Etapa del proyecto ──
if reqs.project_stage == ProjectStage.POC:
if profile.operational_complexity <= 2:
score += 10
db_reasons.append("baja fricción para PoC")
elif reqs.project_stage == ProjectStage.SCALE:
if profile.max_scale_comfort > 10_000_000:
score += 10
db_reasons.append("preparado para escala")
scores[db_name] = score
reasons[db_name] = db_reasons
if db_eliminations:
eliminations[db_name] = db_eliminations
# ── Ranking final ──
sorted_dbs = sorted(scores.items(), key=lambda x: x[1], reverse=True)
primary_name = sorted_dbs[0][0]
primary_score = sorted_dbs[0][1]
alternative_name = sorted_dbs[1][0]
alternative_score = sorted_dbs[1][1]
score_gap = primary_score - alternative_score
if score_gap > 30:
confidence = "high"
elif score_gap > 15:
confidence = "medium"
else:
confidence = "low"
eliminated_others = {
name: eliminations.get(name, ["puntuó más bajo que las opciones principales"])
for name, _ in sorted_dbs[2:]
}
return Recommendation(
primary=primary_name,
primary_score=primary_score,
primary_justification=reasons[primary_name],
alternative=alternative_name,
alternative_score=alternative_score,
alternative_justification=reasons[alternative_name],
eliminated=eliminated_others,
warnings=warnings,
confidence=confidence,
)
def _team_to_complexity(capability: TeamCapability) -> int:
"""Mapea capacidad del equipo a nivel de complejidad que puede manejar."""
return {
TeamCapability.MINIMAL: 1,
TeamCapability.MODERATE: 3,
TeamCapability.STRONG: 5,
}[capability]
Decisiones de diseño: scoring aditivo (cada nodo suma/resta puntos), eliminación suave (penalizaciones en vez de hard-filters), y confianza basada en la diferencia de score entre las dos mejores opciones.
Paso 4: Generar el reporte de recomendación
El reporte debe ser legible sin contexto adicional.
def generate_report(reqs: ProjectRequirements, rec: Recommendation) -> str:
"""Genera reporte legible de la recomendación."""
lines = []
lines.append("=" * 70)
lines.append(f" DECISION TREE REPORT: {reqs.project_name}")
lines.append("=" * 70)
lines.append("\n📋 INPUTS")
lines.append("-" * 40)
inputs = [
("Vectores", f"{reqs.vector_count:,}"), ("Modelo op.", reqs.operational_preference.value),
("Budget", f"${reqs.monthly_budget_usd:,.0f}/mes"), ("Latencia p95", f"{reqs.target_latency_p95_ms:.0f}ms"),
("Compliance", reqs.compliance.value), ("Equipo", reqs.team_capability.value),
("Etapa", reqs.project_stage.value),
("Hybrid search", "sí" if reqs.needs_hybrid_search else "no"),
("Multi-tenancy", "sí" if reqs.needs_multi_tenancy else "no"),
]
for label, val in inputs:
lines.append(f" {label:<20} {val}")
if rec.warnings:
lines.append(f"\n⚠️ ALERTAS ({len(rec.warnings)})")
lines.append("-" * 40)
for w in rec.warnings:
lines.append(f" ⚠ {w}")
lines.append(f"\n✅ RECOMENDACIÓN PRINCIPAL")
lines.append("-" * 40)
lines.append(f" → {rec.primary} (score: {rec.primary_score:.0f}, confianza: {rec.confidence})")
for r in rec.primary_justification:
lines.append(f" • {r}")
lines.append(f"\n🔄 ALTERNATIVA")
lines.append("-" * 40)
lines.append(f" → {rec.alternative} (score: {rec.alternative_score:.0f})")
for r in rec.alternative_justification:
lines.append(f" • {r}")
lines.append(f"\n Preferir {rec.alternative} sobre {rec.primary} si:")
_print_switch_conditions(lines, rec.primary, rec.alternative)
lines.append(f"\n❌ OPCIONES DESCARTADAS")
lines.append("-" * 40)
for db_name, elim_reasons in rec.eliminated.items():
lines.append(f" {db_name}:")
for r in elim_reasons:
lines.append(f" ✗ {r}")
lines.append(f"\n📌 PRÓXIMOS PASOS")
lines.append("-" * 40)
if rec.confidence == "low":
lines.append(" 1. Opciones muy cercanas — PoC comparativo recomendado.")
lines.append(" 2. Define métricas de éxito antes de la prueba.")
elif rec.confidence == "medium":
lines.append(f" 1. Valida {rec.primary} con PoC de 1-2 semanas.")
lines.append(f" 2. Ten {rec.alternative} como plan B documentado.")
else:
lines.append(f" 1. Procede con {rec.primary} para la fase actual.")
lines.append(f" 2. Reevalúa en 3 meses o al cambiar escala 3x.")
lines.append("\n" + "=" * 70)
return "\n".join(lines)
def _print_switch_conditions(lines: list[str], primary: str, alternative: str):
"""Genera condiciones para preferir la alternativa."""
switch_map = {
("ChromaDB", "Pinecone"): ["necesitas SLA en producción", "sin capacidad de mantener infra propia"],
("Pinecone", "Weaviate"): ["hybrid search es requisito fuerte", "necesitas opción self-hosted futura"],
("Pinecone", "Qdrant"): ["buscas mejor ratio performance/costo", "quieres flexibilidad operativa"],
("Weaviate", "Qdrant"): ["priorizas simplicidad sobre features avanzadas", "latencia p95 es crítica"],
("Qdrant", "Weaviate"): ["necesitas hybrid search más maduro", "quieres ecosistema más amplio"],
("Milvus", "Weaviate"): ["la escala es menor a 10M vectores", "prefieres menor complejidad operativa"],
}
conditions = switch_map.get((primary, alternative), [
"cambian requisitos de escala o compliance",
"el equipo adquiere capacidad operativa diferente",
])
for condition in conditions:
lines.append(f" • {condition}")
Paso 5: Ejecutar escenarios de validación
Define al menos 5 escenarios con combinaciones distintas de inputs.
def run_validation_scenarios() -> list[dict]:
"""Ejecuta 5 escenarios de validación y retorna resultados."""
scenarios = [
{"name": "MVP Startup - PoC rápido",
"description": "Startup temprana, equipo pequeño, sin presupuesto",
"requirements": ProjectRequirements(
project_name="MVP Startup", vector_count=50_000,
operational_preference=OperationalModel.MANAGED, monthly_budget_usd=0,
target_latency_p95_ms=50, compliance=ComplianceLevel.NONE,
team_capability=TeamCapability.MINIMAL, project_stage=ProjectStage.POC),
"expected_primary": "ChromaDB"},
{"name": "SaaS en crecimiento",
"description": "Producto validado, necesita SLA, equipo moderado",
"requirements": ProjectRequirements(
project_name="SaaS Growth", vector_count=2_000_000,
operational_preference=OperationalModel.MANAGED, monthly_budget_usd=500,
target_latency_p95_ms=15, compliance=ComplianceLevel.BASIC,
team_capability=TeamCapability.MODERATE, project_stage=ProjectStage.PRODUCTION,
needs_multi_tenancy=True),
"expected_primary": "Pinecone"},
{"name": "Enterprise - Compliance estricto",
"description": "Regulación financiera, residencia de datos, equipo fuerte",
"requirements": ProjectRequirements(
project_name="Enterprise FinTech", vector_count=10_000_000,
operational_preference=OperationalModel.SELF_HOSTED, monthly_budget_usd=2000,
target_latency_p95_ms=20, compliance=ComplianceLevel.STRICT,
team_capability=TeamCapability.STRONG, project_stage=ProjectStage.SCALE,
needs_hybrid_search=True, needs_multi_tenancy=True),
"expected_primary": "Weaviate"},
{"name": "Equipo técnico - Performance first",
"description": "Equipo con DevOps, prioriza latencia y control",
"requirements": ProjectRequirements(
project_name="Performance Team", vector_count=5_000_000,
operational_preference=OperationalModel.FLEXIBLE, monthly_budget_usd=800,
target_latency_p95_ms=10, compliance=ComplianceLevel.BASIC,
team_capability=TeamCapability.STRONG, project_stage=ProjectStage.PRODUCTION,
needs_hybrid_search=True),
"expected_primary": "Qdrant"},
{"name": "Mega-escala enterprise global",
"description": "Plataforma global, >100M vectores, equipo de plataforma",
"requirements": ProjectRequirements(
project_name="Global Platform", vector_count=200_000_000,
operational_preference=OperationalModel.SELF_HOSTED, monthly_budget_usd=5000,
target_latency_p95_ms=25, compliance=ComplianceLevel.STRICT,
team_capability=TeamCapability.STRONG, project_stage=ProjectStage.SCALE,
needs_hybrid_search=True, needs_multi_tenancy=True),
"expected_primary": "Milvus"},
]
results = []
for scenario in scenarios:
reqs = scenario["requirements"]
rec = evaluate_decision_tree(reqs)
match = rec.primary == scenario["expected_primary"]
results.append({
"name": scenario["name"],
"description": scenario["description"],
"expected": scenario["expected_primary"],
"actual": rec.primary,
"score": rec.primary_score,
"confidence": rec.confidence,
"match": match,
})
return results
Paso 6: Generar diagrama Mermaid
def generate_mermaid_diagram() -> str:
"""Genera diagrama Mermaid del decision tree."""
diagram = """```mermaid
flowchart TD
START([🚀 Inicio: Selección de Vector DB]) --> N1
N1{📊 Volumen > 10M vectores?}
N1 -- Sí --> N1_HIGH{🏢 Equipo de plataforma?}
N1 -- No --> N2
N1_HIGH -- Sí --> MILVUS[🟣 Milvus]
N1_HIGH -- No --> N1_WARN[⚠️ Reforzar equipo o<br/>reducir scope]
N2{☁️ Preferencia operativa?}
N2 -- Managed --> N3_M
N2 -- Self-hosted --> N3_S
N2 -- Flexible --> N3_F
N3_M{💰 Budget > $70/mes?}
N3_M -- Sí --> N4_M{🔒 Compliance estricto?}
N3_M -- No --> CHROMADB_POC[🟢 ChromaDB<br/>PoC / desarrollo]
N4_M -- Sí --> PINECONE_CHECK[Verificar residencia<br/>de datos Pinecone]
N4_M -- No --> N5_M{🔍 Hybrid search?}
N5_M -- Sí --> WEAVIATE_M[🔵 Weaviate Cloud]
N5_M -- No --> PINECONE[🟡 Pinecone]
N3_S{🔒 Compliance estricto?}
N3_S -- Sí --> N4_S{⚡ Latencia < 10ms?}
N3_S -- No --> N4_S2{🔍 Hybrid search?}
N4_S -- Sí --> QDRANT_SH[🔴 Qdrant self-hosted]
N4_S -- No --> WEAVIATE_SH[🔵 Weaviate self-hosted]
N4_S2 -- Sí --> WEAVIATE_SH
N4_S2 -- No --> QDRANT_SH
N3_F{📊 Volumen > 1M?}
N3_F -- Sí --> N4_F{⚡ Latencia < 10ms?}
N3_F -- No --> N4_F2{💰 Budget bajo?}
N4_F -- Sí --> QDRANT_F[🔴 Qdrant]
N4_F -- No --> WEAVIATE_F[🔵 Weaviate]
N4_F2 -- Sí --> CHROMADB[🟢 ChromaDB]
N4_F2 -- No --> QDRANT_F
style MILVUS fill:#9b59b6,color:#fff
style PINECONE fill:#f1c40f,color:#000
style CHROMADB fill:#2ecc71,color:#fff
style CHROMADB_POC fill:#2ecc71,color:#fff
```"""
return diagram
Paso 7: Script principal completo
Ahora junta todo en un flujo ejecutable.
def main():
"""Ejecuta el decision tree completo con validación."""
print("=" * 70)
print(" VECTOR DB DECISION TREE - Módulo 5 Proyecto Final")
print("=" * 70)
# ── Paso A: Ejecutar un escenario individual ──
print("\n" + "─" * 70)
print(" PARTE 1: Escenario individual")
print("─" * 70)
my_project = ProjectRequirements(
project_name="Mi Proyecto RAG",
vector_count=800_000,
operational_preference=OperationalModel.MANAGED,
monthly_budget_usd=300,
target_latency_p95_ms=20,
compliance=ComplianceLevel.BASIC,
team_capability=TeamCapability.MODERATE,
project_stage=ProjectStage.PRODUCTION,
needs_hybrid_search=False,
needs_multi_tenancy=True,
)
rec = evaluate_decision_tree(my_project)
report = generate_report(my_project, rec)
print(report)
# ── Paso B: Validación de escenarios ──
print("\n" + "─" * 70)
print(" PARTE 2: Validación de escenarios")
print("─" * 70)
results = run_validation_scenarios()
print(f"\n{'Escenario':<35} {'Esperado':<12} {'Actual':<12} {'Score':>6} {'Conf':>8} {'OK':>4}")
print("-" * 80)
for r in results:
status = "✅" if r["match"] else "⚠️"
print(
f"{r['name']:<35} {r['expected']:<12} {r['actual']:<12} "
f"{r['score']:>6.0f} {r['confidence']:>8} {status:>4}"
)
passed = sum(1 for r in results if r["match"])
total = len(results)
print(f"\nResultado: {passed}/{total} escenarios validados")
if passed < total:
print("\n⚠️ Escenarios con discrepancia (revisa si tienen sentido):")
for r in results:
if not r["match"]:
print(f" • {r['name']}: esperado {r['expected']}, obtuvo {r['actual']}")
# ── Paso C: Diagrama Mermaid ──
print("\n" + "─" * 70)
print(" PARTE 3: Diagrama Mermaid")
print("─" * 70)
print("\nCopia en un archivo .md o en https://mermaid.live/\n")
print(generate_mermaid_diagram())
# ── Resumen final ──
print("\n" + "=" * 70)
print(f" PROYECTO COMPLETADO")
print(f" ✅ {len(VECTOR_DB_PROFILES)} proveedores | {passed}/{total} escenarios | Mermaid listo")
print(f" → Siguiente: Módulo 6 (matriz de decisión con scoring formal)")
print("=" * 70)
if __name__ == "__main__":
main()
📊 Output esperado
Al ejecutar el script, verás un reporte con esta estructura:
✅ RECOMENDACIÓN PRINCIPAL
→ Pinecone (score: 80, confianza: medium)
Justificación:
• escala cómoda (800,000 vectores)
• soporta modelo managed
• presupuesto suficiente
• soporta multi-tenancy
🔄 ALTERNATIVA
→ Qdrant (score: 68)
❌ OPCIONES DESCARTADAS
ChromaDB: ✗ no ofrece opción managed
Milvus: ✗ complejidad operativa excede capacidad del equipo
VALIDACIÓN DE ESCENARIOS
Escenario Esperado Actual OK
MVP Startup - PoC rápido ChromaDB ChromaDB ✅
SaaS en crecimiento Pinecone Pinecone ✅
Enterprise - Compliance estricto Weaviate Weaviate ✅
Equipo técnico - Performance first Qdrant Qdrant ✅
Mega-escala enterprise global Milvus Milvus ✅
Resultado: 5/5 escenarios validados
🔧 Troubleshooting del proyecto
"Mi árbol siempre recomienda la misma DB"
Revisa los pesos de cada nodo. Es probable que un solo criterio (como escala o modelo operativo) esté dominando el score. Ajusta los puntos para que ningún nodo individual represente más del 25% del score máximo posible. También verifica que tus escenarios de prueba tengan inputs realmente distintos — si todos tienen team_capability=MINIMAL, naturalmente convergerán.
"El score de dos opciones es casi idéntico"
Esto es información, no un error. Cuando la confianza es "low", el árbol te está diciendo que necesitas más datos para decidir. Opciones:
- Agrega un criterio diferenciador (ej. soporte de la comunidad, documentación).
- Ejecuta un PoC comparativo con las dos finalistas.
- Usa el Módulo 6 (matriz de scoring) para un análisis más granular.
"No sé qué valores poner en los inputs"
Empieza con lo que sabes y marca lo que estás estimando. Valores típicos: PoC (10k-100k vectores, $0, latencia flexible), producción temprana (100k-2M, $100-500, p95 <30ms), escala (>2M, >$500, p95 <15ms).
"Quiero agregar un proveedor que no está en la lista"
Crea un nuevo VectorDBProfile y agrégalo a VECTOR_DB_PROFILES. El motor lo evaluará automáticamente contra todos los nodos existentes.
"La recomendación no coincide con lo que yo habría elegido"
Buena señal. Revisa qué criterio valora el árbol que tú no, o viceversa. Los desacuerdos revelan suposiciones implícitas que vale la pena explicitar. Ajusta pesos según tu contexto y documenta por qué.
🏋️ Ejercicios post-proyecto
Ejercicio 1: Agregar dimensión de vendor lock-in
Extiende el árbol con un criterio de portabilidad que penalice opciones con alto lock-in.
Pistas:
- Agrega un campo
portability_concern: boolaProjectRequirements. - Agrega un campo
lock_in_risk: int(1-5) aVectorDBProfile. - Crea un nuevo nodo en
evaluate_decision_tree.
Ver solución
# En ProjectRequirements, agregar:
portability_concern: bool = False
# En VectorDBProfile, agregar:
lock_in_risk: int = 1 # 1=bajo, 5=alto
# Actualizar perfiles:
# ChromaDB: lock_in_risk=1 (open source, estándar)
# Pinecone: lock_in_risk=5 (API propietaria, sin self-hosted)
# Weaviate: lock_in_risk=2 (open source, API estándar)
# Qdrant: lock_in_risk=2 (open source, gRPC estándar)
# Milvus: lock_in_risk=2 (open source, API estándar)
# En evaluate_decision_tree, agregar nodo:
# ── Nodo 7: Vendor lock-in ──
if reqs.portability_concern:
if profile.lock_in_risk <= 2:
score += 10
db_reasons.append("bajo riesgo de vendor lock-in")
elif profile.lock_in_risk >= 4:
score -= 15
db_eliminations.append(
f"alto riesgo de lock-in ({profile.lock_in_risk}/5) "
f"con requisito de portabilidad"
)
Ejercicio 2: Generar comparación lado a lado
Crea una función que tome dos ProjectRequirements distintos y muestre cómo cambia la recomendación entre escenarios.
Pistas:
- La función recibe dos objetos
ProjectRequirements. - Ejecuta
evaluate_decision_treepara cada uno. - Imprime tabla comparativa con diffs resaltados.
Ver solución
def compare_scenarios(reqs_a: ProjectRequirements, reqs_b: ProjectRequirements) -> str:
"""Compara recomendaciones entre dos escenarios."""
rec_a = evaluate_decision_tree(reqs_a)
rec_b = evaluate_decision_tree(reqs_b)
lines = []
lines.append("=" * 70)
lines.append(" COMPARACIÓN DE ESCENARIOS")
lines.append("=" * 70)
lines.append(f"\n{'Dimensión':<25} {'Escenario A':<22} {'Escenario B':<22}")
lines.append("-" * 70)
lines.append(f"{'Proyecto':<25} {reqs_a.project_name:<22} {reqs_b.project_name:<22}")
lines.append(f"{'Vectores':<25} {reqs_a.vector_count:<22,} {reqs_b.vector_count:<22,}")
lines.append(f"{'Budget':<25} {'$'+str(int(reqs_a.monthly_budget_usd)):<22} {'$'+str(int(reqs_b.monthly_budget_usd)):<22}")
lines.append(f"{'Modelo op.':<25} {reqs_a.operational_preference.value:<22} {reqs_b.operational_preference.value:<22}")
lines.append(f"\n{'RESULTADO':<25}")
lines.append("-" * 70)
marker_a = " ←" if rec_a.primary != rec_b.primary else ""
marker_b = " ←" if rec_a.primary != rec_b.primary else ""
lines.append(f"{'Recomendación':<25} {rec_a.primary + marker_a:<22} {rec_b.primary + marker_b:<22}")
lines.append(f"{'Score':<25} {rec_a.primary_score:<22.0f} {rec_b.primary_score:<22.0f}")
lines.append(f"{'Confianza':<25} {rec_a.confidence:<22} {rec_b.confidence:<22}")
lines.append(f"{'Alternativa':<25} {rec_a.alternative:<22} {rec_b.alternative:<22}")
if rec_a.primary != rec_b.primary:
lines.append(f"\n💡 La recomendación cambió de {rec_a.primary} a {rec_b.primary}.")
lines.append(" Revisa qué input causó el cambio para entender la sensibilidad.")
return "\n".join(lines)
# Uso: compare_scenarios(reqs_mvp, reqs_produccion)
Ejercicio 3: Sensibilidad — ¿qué input cambia más la decisión?
Crea una función que tome un ProjectRequirements base y varíe un input a la vez para identificar cuáles son los más sensibles (los que más cambian el ranking).
Pistas:
- Usa
dataclasses.replace()para clonar requisitos con un campo cambiado. - Itera sobre variaciones predefinidas de cada campo.
- Compara si cambia
primaryo si cambiaconfidence.
Ver solución
from dataclasses import replace
def sensitivity_analysis(base_reqs: ProjectRequirements) -> str:
"""Analiza qué inputs son más sensibles al cambio de recomendación."""
base_rec = evaluate_decision_tree(base_reqs)
lines = []
lines.append("=" * 70)
lines.append(f" ANÁLISIS DE SENSIBILIDAD: {base_reqs.project_name}")
lines.append(f" Recomendación base: {base_rec.primary} (score: {base_rec.primary_score:.0f})")
lines.append("=" * 70)
variations = [
("vector_count", [10_000, 500_000, 2_000_000, 10_000_000, 100_000_000]),
("monthly_budget_usd", [0, 50, 200, 500, 2000]),
("target_latency_p95_ms", [5, 10, 20, 50, 100]),
("operational_preference", list(OperationalModel)),
("compliance", list(ComplianceLevel)),
("team_capability", list(TeamCapability)),
]
for field_name, values in variations:
lines.append(f"\n📊 Variando: {field_name}")
lines.append(f" {'Valor':<25} {'Recomendación':<15} {'Score':>6} {'Cambió?':>8}")
lines.append(" " + "-" * 58)
for val in values:
try:
varied_reqs = replace(base_reqs, **{field_name: val})
varied_rec = evaluate_decision_tree(varied_reqs)
changed = "⚠️ SÍ" if varied_rec.primary != base_rec.primary else " no"
display_val = f"{val:,}" if isinstance(val, (int, float)) else val.value
lines.append(
f" {str(display_val):<25} {varied_rec.primary:<15} "
f"{varied_rec.primary_score:>6.0f} {changed:>8}"
)
except (TypeError, ValueError):
continue
return "\n".join(lines)
# Uso: print(sensitivity_analysis(mis_requisitos_base))
El output mostrará una tabla por cada dimensión variada. Los campos marcados con "SÍ" en la columna "Cambió?" son los inputs más sensibles — los que debes estimar con mayor cuidado.
✅ Checklist de completitud
Verifica todos estos puntos antes de dar el proyecto por terminado:
Estructura del código:
-
ProjectRequirementstiene las 6 dimensiones obligatorias. -
VectorDBProfilecubre los 5 proveedores con datos coherentes. -
evaluate_decision_tree()produce score numérico yRecommendationcompleta.
Calidad de la decisión:
- El árbol no depende de un solo criterio para decidir.
- Justificación explica por qué sí (primaria) y por qué no (descartadas).
- La alternativa incluye condiciones de cuándo preferirla.
Validación:
- Mínimo 5 escenarios ejecutados (PoC, producción, enterprise).
- Discrepancias entre esperado y actual están explicadas.
Output:
- Reporte legible sin contexto adicional, Mermaid renderiza correctamente.
- Scores numéricos compatibles con la matriz del Módulo 6.
Resumen
- Construiste un motor de decisión programático que transforma requisitos en recomendación justificada.
- Cada nodo del árbol evalúa una dimensión concreta: escala, operación, presupuesto, latencia, compliance y capacidad del equipo.
- El sistema produce recomendación principal + alternativa, explicando por qué se descartaron las demás opciones.
- Validaste con 5 escenarios distintos que cubren desde PoC hasta enterprise global.
- El diagrama Mermaid permite comunicar la lógica a stakeholders no técnicos.
- La confianza del resultado ("high", "medium", "low") indica cuándo necesitas más análisis antes de decidir.
- Este proyecto cierra el Módulo 5 y alimenta directamente el Módulo 6 donde formalizarás la decisión con matriz de scoring.
- Tienes ejercicios de extensión (lock-in, comparación, sensibilidad) para profundizar el análisis.
📚 Recursos adicionales
- Mermaid.js — Documentación oficial de diagramas
- ChromaDB — Documentación
- Pinecone — Documentación
- Weaviate — Documentación para developers
- Qdrant — Documentación técnica
- Milvus — Documentación
- Martin Fowler — Trade-offs en decisiones de arquitectura
- Python dataclasses — Documentación oficial
Tiempo estimado: 35-45 minutos
Siguiente módulo: ../../module-06-decision-matrix/es/01-introduccion-modulo.md