🎯 El Desafío de la Búsqueda Semántica
Imagina que tienes 413 artículos legales vectorizados con embeddings de 384 dimensiones. Ahora necesitas:
- Buscar entre millones de vectores en milisegundos
- Filtrar por metadatos complejos (libro, capítulo, artículo)
- Escalar para manejar más documentos sin degradar performance
- Desplegar tanto en desarrollo local como en producción cloud
El desafío real: encontrar una base de datos vectorial que sea rápida, escalable, open source y fácil de integrar con tu stack de Python.
📊 La Magnitud del Problema
Requisitos del Sistema RAG
- 413 vectores de 384 dimensiones cada uno
- Búsqueda semántica en < 100ms para experiencia en tiempo real
- Metadatos enriquecidos por cada documento
- Filtrado avanzado por libro, título, capítulo, número de artículo
- Alta disponibilidad en producción
- Persistencia de datos entre reinicios
Desafíos Técnicos Específicos
- 🔍 Velocidad de Búsqueda: Milisegundos importan en experiencia de usuario
- 📊 Filtrado Complejo: Necesidad de filtros AND/OR anidados
- 💾 Persistencia: Datos vectoriales deben sobrevivir reinicios
- 🔄 Escalabilidad: Del desarrollo local a producción cloud
- 🛡️ Seguridad: Autenticación para acceso remoto
💡 La Solución: Qdrant Vector Database
Qdrant es una base de datos vectorial open source escrita en Rust que ofrece:
- ⚡ Performance extrema: Búsqueda en < 50ms
- 🔍 Búsqueda semántica: Algoritmos HNSW optimizados
- 📊 Filtrado avanzado: Payloads con metadatos ricos
- 🐳 Fácil despliegue: Docker, Kubernetes, Cloud
- 🔒 Producción-ready: API keys, persistencia, backups
- 🐍 Python-first: Cliente nativo con excelente DX
¿Por Qué Qdrant vs Otras Opciones?
Característica | Qdrant | Pinecone | Weaviate | Milvus |
---|---|---|---|---|
Open Source | ✅ | ❌ | ✅ | ✅ |
Self-hosted | ✅ | ❌ | ✅ | ✅ |
Performance | ⚡⚡⚡ | ⚡⚡⚡ | ⚡⚡ | ⚡⚡ |
Facilidad Setup | ✅ | ✅ | ✅ | ⚠️ |
Python Client | ✅ | ✅ | ✅ | ✅ |
Costo | $0 (self-hosted) | $$$ | $0-$$ | $0 |
Metadatos | ✅ Rico | ✅ Básico | ✅ Rico | ✅ Rico |
gRPC | ✅ | ❌ | ✅ | ✅ |
Nuestra elección: Qdrant por su balance perfecto entre performance, facilidad de uso y control (open source + self-hosted).
🚀 Configuración Paso a Paso
1. Setup Local con Docker Compose
El desarrollo local requiere Qdrant ejecutándose en localhost. Usamos Docker Compose para simplicidad:
# services/vectordb/docker-compose.yml
services:
qdrant:
image: qdrant/qdrant:latest
container_name: qdrant
ports:
- 6333:6333 # HTTP API
- 6334:6334 # gRPC API (2-3x más rápido)
volumes:
- qdrant_storage:/qdrant/storage # Persistencia de datos
environment:
- QDRANT__SERVICE__HTTP_PORT=6333
- QDRANT__SERVICE__GRPC_PORT=6334
- QDRANT__LOG_LEVEL=INFO
# Para producción: descomentar y agregar API key
# - QDRANT__SERVICE__API_KEY=${QDRANT_API_KEY:-}
restart: always
networks:
- qdrant-network
volumes:
qdrant_storage:
driver: local
networks:
qdrant-network:
driver: bridge
Comandos de gestión:
# Navegar a la carpeta de Qdrant
cd services/vectordb
# Iniciar Qdrant
docker-compose up -d
# Verificar que está ejecutándose
curl http://localhost:6333/collections
# Ver logs
docker-compose logs -f qdrant
# Detener Qdrant
docker-compose down
# Detener y eliminar volúmenes (⚠️ borra datos)
docker-compose down -v
2. Configuración de Variables de Entorno
Las variables de Qdrant tienen nombres diferentes según dónde se usen:
Para Notebooks (exploración y desarrollo):
# .env (proyecto raíz)
# ==========================================================
# Configuración de Qdrant para Notebooks
# ==========================================================
QDRANT_URL=http://localhost:6333
QDRANT_API_KEY= # Vacío para desarrollo local
Para la API (FastAPI):
# .env (proyecto raíz)
# ==========================================================
# Configuración de Qdrant para API
# ==========================================================
API_QDRANT_URL=http://localhost:6333
API_QDRANT_API_KEY= # Vacío para desarrollo local
API_QDRANT_COLLECTION_NAME=labor_law_articles
API_QDRANT_GRPC_PORT=6334
API_QDRANT_PREFER_GRPC=True
💡 Nota: La API usa el prefijo
API_
para todas sus variables de entorno para evitar conflictos con otras partes del sistema.
Para producción (VM en GCP):
# Notebooks
QDRANT_URL=http://[IP_EXTERNA_VM]:6333
QDRANT_API_KEY=tu_clave_api_segura_aqui
# API
API_QDRANT_URL=http://[IP_EXTERNA_VM]:6333
API_QDRANT_API_KEY=tu_clave_api_segura_aqui
API_QDRANT_PREFER_GRPC=True
API_QDRANT_GRPC_PORT=6334
3. Cliente de Python - QdrantService
Creamos un servicio que encapsula todas las operaciones con Qdrant:
# src/lus_laboris_api/api/services/qdrant_service.py
import logging
import warnings
from typing import Any
import numpy as np
from qdrant_client import QdrantClient
from qdrant_client.http.models import Distance, Filter, PointStruct, VectorParams
from ..config import settings
logger = logging.getLogger(__name__)
# Suprimir warning de API key con HTTP en desarrollo local
warnings.filterwarnings("ignore", message="Api key is used with an insecure connection")
class QdrantService:
"""Service for Qdrant vector database operations"""
def __init__(self):
self.client = None
self.qdrant_url = settings.api_qdrant_url
self.qdrant_api_key = settings.api_qdrant_api_key
self.prefer_grpc = settings.api_qdrant_prefer_grpc
self.grpc_port = settings.api_qdrant_grpc_port
self._connect()
def _connect(self):
"""Connect to Qdrant with optimized settings (gRPC preferred)"""
try:
# Detectar si es conexión local o remota
is_local = "localhost" in self.qdrant_url or "127.0.0.1" in self.qdrant_url
# Configuración optimizada con gRPC
self.client = QdrantClient(
url=self.qdrant_url,
api_key=self.qdrant_api_key,
timeout=10.0, # Timeout de 10 segundos
prefer_grpc=self.prefer_grpc, # gRPC es 2-3x más rápido que HTTP
grpc_port=self.grpc_port, # Puerto gRPC (6334)
https=not is_local, # HTTPS solo para conexiones remotas
)
# Detectar tipo de conexión
has_grpc = hasattr(self.client, "grpc_points") or hasattr(
self.client, "grpc_collections"
)
connection_type = "gRPC" if has_grpc and self.prefer_grpc else "HTTP"
logger.info(f"Connected to Qdrant at {self.qdrant_url} using {connection_type}")
if connection_type == "gRPC":
logger.info(
f"gRPC port: {self.grpc_port} - Performance optimized (2-3x faster)"
)
except Exception as e:
logger.error(f"Failed to connect to Qdrant with gRPC: {e}")
logger.warning("Falling back to HTTP connection...")
# Fallback: intentar sin gRPC
try:
self.client = QdrantClient(
url=self.qdrant_url,
api_key=self.qdrant_api_key,
timeout=10.0,
prefer_grpc=False, # Forzar HTTP
)
logger.info(f"Connected to Qdrant at {self.qdrant_url} using HTTP (fallback)")
except Exception as e2:
logger.error(f"Failed to connect to Qdrant even with HTTP: {e2}")
raise ConnectionError(f"Failed to connect to Qdrant: {e2}")
Características del Cliente:
- ✅ Optimización gRPC: Usa gRPC cuando está disponible (2-3x más rápido)
- ✅ Fallback automático: Si gRPC falla, usa HTTP
- ✅ Detección de entorno: Configura HTTPS solo para producción
- ✅ Timeout configurado: Previene conexiones colgadas
- ✅ Logging detallado: Información de tipo de conexión
4. Creación de Colecciones
Una colección en Qdrant es como una tabla en SQL - contiene vectores del mismo tamaño con metadatos asociados.
def create_collection(
self,
collection_name: str,
vector_size: int,
distance: Distance = Distance.COSINE,
) -> bool:
"""Create a new collection"""
try:
# Verificar si ya existe
if self.collection_exists(collection_name):
logger.warning(f"Collection '{collection_name}' already exists")
return False
# Crear colección con configuración vectorial
self.client.create_collection(
collection_name=collection_name,
vectors_config=VectorParams(
size=vector_size, # Dimensiones del embedding (ej: 384, 768, 1024)
distance=distance, # Métrica de distancia
),
)
logger.info(f"Created collection: {collection_name} (size: {vector_size}, distance: {distance.value})")
return True
except Exception as e:
logger.error(f"Failed to create collection: {e}")
raise
Parámetros clave:
-
vector_size
: Debe coincidir con las dimensiones del modelo de embedding-
multilingual-e5-small
: 384 dimensiones -
gte-multilingual-base
: 768 dimensiones -
bge-m3
: 1024 dimensiones
-
-
distance
: Métrica para medir similitud-
Distance.COSINE
: Recomendado para embeddings normalizados (nuestro caso) -
Distance.EUCLIDEAN
: Distancia euclidiana (para vectores no normalizados) -
Distance.DOT
: Producto punto (para embeddings optimizados para dot product)
-
Ejemplo de uso:
# Crear colección para artículos legales
qdrant_service = QdrantService()
qdrant_service.create_collection(
collection_name="labor_law_articles",
vector_size=384, # multilingual-e5-small
distance=Distance.COSINE
)
5. Inserción de Documentos con Metadatos
Cada documento en Qdrant es un punto (point) que contiene:
- ID único: Identificador del documento
- Vector: Embedding del texto
- Payload: Metadatos enriquecidos (JSON)
def insert_documents(
self,
collection_name: str,
documents: list[dict[str, Any]],
vectors: np.ndarray,
) -> bool:
"""Insert documents with vectors and metadata"""
try:
if len(documents) != len(vectors):
raise ValueError("Number of documents must match number of vectors")
# Crear puntos (points) con vectores y payloads
points = []
for idx, (doc, vector) in enumerate(zip(documents, vectors)):
point = PointStruct(
id=idx, # ID único (puede ser UUID también)
vector=vector.tolist(), # Convertir numpy array a lista
payload={
# Metadatos obligatorios
"articulo_numero": doc["articulo_numero"],
"articulo": doc["articulo"], # Texto completo
# Metadatos de contexto legal
"libro": doc.get("libro"),
"libro_numero": doc.get("libro_numero"),
"titulo": doc.get("titulo"),
"capitulo": doc.get("capitulo"),
"capitulo_numero": doc.get("capitulo_numero"),
"capitulo_descripcion": doc.get("capitulo_descripcion"),
# Metadatos de análisis
"longitud_texto": len(doc["articulo"]),
"fuente": "codigo_trabajo_paraguay",
},
)
points.append(point)
# Insertar en lote (upsert sobrescribe si ya existe)
self.client.upsert(
collection_name=collection_name,
points=points,
)
logger.info(f"Inserted {len(points)} documents into {collection_name}")
return True
except Exception as e:
logger.error(f"Failed to insert documents: {e}")
raise
Ventajas de los Payloads:
- 🔍 Filtrado avanzado: Buscar solo en capítulos específicos
- 📊 Metadatos ricos: Información contextual para cada resultado
- 🎯 Ranking mejorado: Combinar similitud vectorial con filtros
- 📈 Analytics: Análisis de uso por libro/capítulo
Ejemplo de inserción:
# Cargar artículos procesados
with open('data/processed/codigo_trabajo_articulos.json') as f:
law_data = json.load(f)
articles = law_data['articulos']
# Generar embeddings
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('intfloat/multilingual-e5-small')
texts = [article['articulo'] for article in articles]
embeddings = model.encode(texts)
# Insertar en Qdrant
qdrant_service.insert_documents(
collection_name="labor_law_articles",
documents=articles,
vectors=embeddings
)
6. Búsqueda Semántica
La búsqueda vectorial es el corazón del sistema RAG:
def search_documents(
self,
collection_name: str,
query_vector: np.ndarray,
limit: int = 10,
score_threshold: float | None = None,
filter_conditions: Filter | None = None,
) -> list[dict[str, Any]]:
"""Search documents by vector similarity"""
try:
# Búsqueda vectorial con filtros opcionales
search_result = self.client.search(
collection_name=collection_name,
query_vector=query_vector.tolist(),
limit=limit,
score_threshold=score_threshold, # Umbral mínimo de similitud
query_filter=filter_conditions, # Filtros de metadatos
)
# Formatear resultados
results = []
for point in search_result:
result = {
"id": point.id,
"score": point.score, # Similitud (0-1 para cosine)
"payload": point.payload, # Metadatos completos
}
results.append(result)
logger.info(f"Found {len(results)} results in {collection_name}")
return results
except Exception as e:
logger.error(f"Failed to search documents: {e}")
raise
Ejemplo de búsqueda simple:
# Generar embedding de la consulta
query = "¿Cuántos días de vacaciones corresponden a un trabajador?"
query_embedding = model.encode([query])[0]
# Buscar documentos similares
results = qdrant_service.search_documents(
collection_name="labor_law_articles",
query_vector=query_embedding,
limit=5,
score_threshold=0.7 # Solo resultados con similitud > 0.7
)
# Mostrar resultados
for i, result in enumerate(results, 1):
print(f"{i}. Score: {result['score']:.3f}")
print(f" Artículo {result['payload']['articulo_numero']}: {result['payload']['articulo'][:100]}...")
print(f" Capítulo: {result['payload']['capitulo']}")
print()
Output:
1. Score: 0.912
Artículo 218: todo trabajador que cumpla un año de trabajo continuo al servicio del mismo empleador...
Capítulo: capitulo ii - de las vacaciones
2. Score: 0.876
Artículo 219: el trabajador perderá el derecho a las vacaciones cuando haya faltado más de quince...
Capítulo: capitulo ii - de las vacaciones
3. Score: 0.845
Artículo 220: durante las vacaciones el empleador abonará al trabajador la remuneración ordinaria...
Capítulo: capitulo ii - de las vacaciones
7. Filtrado Avanzado con Metadata
El poder real de Qdrant viene de combinar búsqueda vectorial con filtros de metadatos:
from qdrant_client.http.models import Filter, FieldCondition, MatchValue
# Buscar solo en el Libro Primero
filter_libro = Filter(
must=[
FieldCondition(
key="libro_numero",
match=MatchValue(value=1),
)
]
)
results = qdrant_service.search_documents(
collection_name="labor_law_articles",
query_vector=query_embedding,
limit=5,
filter_conditions=filter_libro
)
Filtros más complejos:
# Buscar en capítulos específicos del Libro Primero
filter_complex = Filter(
must=[
FieldCondition(key="libro_numero", match=MatchValue(value=1)),
FieldCondition(key="capitulo_numero", range=RangeCondition(gte=1, lte=5)),
]
)
# Excluir artículos cortos
filter_long_articles = Filter(
must=[
FieldCondition(key="longitud_texto", range=RangeCondition(gte=100)),
]
)
# Combinar múltiples condiciones
filter_advanced = Filter(
must=[
FieldCondition(key="libro_numero", match=MatchValue(value=1)),
],
should=[ # OR logic
FieldCondition(key="capitulo_numero", match=MatchValue(value=2)),
FieldCondition(key="capitulo_numero", match=MatchValue(value=5)),
]
)
8. Operaciones de Gestión
def collection_exists(self, collection_name: str) -> bool:
"""Check if collection exists"""
try:
collections = self.client.get_collections()
return any(col.name == collection_name for col in collections.collections)
except Exception as e:
logger.error(f"Failed to check collection existence: {e}")
return False
def get_collection_info(self, collection_name: str) -> dict[str, Any]:
"""Get collection information"""
try:
info = self.client.get_collection(collection_name)
return {
"name": collection_name,
"vectors_count": info.vectors_count,
"points_count": info.points_count,
"segments_count": info.segments_count,
"status": info.status,
"optimizer_status": info.optimizer_status.status,
"vector_size": info.config.params.vectors.size,
"distance": info.config.params.vectors.distance.value,
}
except Exception as e:
logger.error(f"Failed to get collection info: {e}")
raise
def delete_collection(self, collection_name: str) -> bool:
"""Delete a collection"""
try:
if not self.collection_exists(collection_name):
logger.warning(f"Collection '{collection_name}' does not exist")
return False
self.client.delete_collection(collection_name)
logger.info(f"Deleted collection: {collection_name}")
return True
except Exception as e:
logger.error(f"Failed to delete collection: {e}")
raise
def health_check(self) -> dict[str, str]:
"""Check Qdrant health status"""
try:
collections = self.client.get_collections()
# Detectar tipo de conexión
has_grpc = hasattr(self.client, "grpc_points")
connection_type = "gRPC" if has_grpc else "HTTP"
return {
"status": "healthy",
"url": self.qdrant_url,
"connection_type": connection_type,
"collections_count": len(collections.collections),
}
except Exception as e:
return {
"status": "unhealthy",
"error": str(e),
}
🚀 Despliegue en Producción (GCP Compute Engine)
Para producción, desplegamos Qdrant en una VM de GCP usando Terraform.
1. Infraestructura con Terraform
# terraform/modules/compute_engine/main.tf
resource "google_compute_instance" "qdrant_vm" {
name = var.vm_name
machine_type = var.machine_type # e2-medium (2 vCPU, 4GB RAM)
zone = var.zone
boot_disk {
initialize_params {
image = "ubuntu-os-cloud/ubuntu-2204-lts"
size = var.disk_size # 20GB
}
}
network_interface {
network = "default"
access_config {
# Ephemeral public IP
}
}
tags = ["qdrant-server", "http-server"]
# SPOT instance para optimización de costos
scheduling {
preemptible = true
automatic_restart = false
provisioning_model = "SPOT"
}
}
# Firewall para Qdrant
resource "google_compute_firewall" "qdrant_firewall" {
name = "${var.vm_name}-firewall"
network = "default"
allow {
protocol = "tcp"
ports = ["6333", "6334", "22"] # HTTP, gRPC, SSH
}
source_ranges = ["0.0.0.0/0"] # ⚠️ Restringir en producción real
target_tags = ["qdrant-server"]
}
Especificaciones de la VM:
- Machine type: e2-medium (2 vCPU, 4GB RAM)
- Disco: 20GB SSD persistente
- SO: Ubuntu 22.04 LTS
- Networking: IP pública efímera
- Costo: ~$15/mes con SPOT instances
2. Despliegue Automatizado con GitHub Actions
Workflow .github/workflows/deploy-qdrant.yml
automatiza el despliegue:
name: Deploy Qdrant to GCP VM
on:
workflow_dispatch: # Trigger manual
push:
paths:
- 'services/vectordb/**'
- 'terraform/modules/compute_engine/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GSA_KEY }}
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
- name: Get VM details
id: vm_info
run: |
VM_EXTERNAL_IP=$(gcloud compute instances describe ${VM_NAME} \
--zone=${GCP_ZONE} \
--format='get(networkInterfaces[0].accessConfigs[0].natIP)')
echo "vm_ip=${VM_EXTERNAL_IP}" >> $GITHUB_OUTPUT
- name: Install Docker on VM
run: |
gcloud compute ssh ${VM_NAME} --zone=${GCP_ZONE} --command="
# Instalar Docker si no existe
if ! command -v docker &> /dev/null; then
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
fi
# Instalar Docker Compose
sudo curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
"
- name: Deploy Qdrant
run: |
# Copiar docker-compose.yml
gcloud compute scp services/vectordb/docker-compose.yml ${VM_NAME}:~/docker-compose.yml --zone=${GCP_ZONE}
# Crear archivo .env con API key
gcloud compute ssh ${VM_NAME} --zone=${GCP_ZONE} --command="
echo 'QDRANT_API_KEY=${{ secrets.QDRANT_API_KEY }}' > ~/.env
# Iniciar Qdrant
docker-compose up -d
# Verificar health
sleep 10
curl -f http://localhost:6333/healthz || exit 1
"
- name: Verify deployment
run: |
curl -f http://${{ steps.vm_info.outputs.vm_ip }}:6333/collections
Proceso de despliegue:
- ✅ Autentica con GCP usando service account
- ✅ Obtiene IP externa de la VM
- ✅ Instala Docker y Docker Compose en la VM
- ✅ Copia configuración de Qdrant
- ✅ Crea
.env
con API key - ✅ Inicia Qdrant con Docker Compose
- ✅ Verifica health check
3. Configuración de Producción
# docker-compose.yml (producción en VM)
services:
qdrant:
image: qdrant/qdrant:latest
container_name: qdrant
ports:
- 6333:6333
- 6334:6334
volumes:
- /var/lib/qdrant:/qdrant/storage # Persistencia en disco de la VM
environment:
- QDRANT__SERVICE__HTTP_PORT=6333
- QDRANT__SERVICE__GRPC_PORT=6334
- QDRANT__LOG_LEVEL=INFO
- QDRANT__SERVICE__API_KEY=${QDRANT_API_KEY} # ⚠️ Obligatorio en producción
restart: always
🎯 Casos de Uso Reales
Para Desarrolladores:
"Necesito implementar búsqueda semántica en mi aplicación con datos locales"
Solución: Setup local con Docker Compose en 2 minutos
cd services/vectordb
docker-compose up -d
# Verificar
curl http://localhost:6333/collections
Para Científicos de Datos:
"Necesito experimentar con diferentes modelos de embedding"
Solución: Qdrant permite múltiples colecciones con diferentes dimensiones
# Probar modelo 1 (384 dims)
qdrant_service.create_collection("test_e5_small", vector_size=384)
# Probar modelo 2 (768 dims)
qdrant_service.create_collection("test_gte_base", vector_size=768)
# Comparar resultados
results_e5 = qdrant_service.search_documents("test_e5_small", query_embedding_384, limit=5)
results_gte = qdrant_service.search_documents("test_gte_base", query_embedding_768, limit=5)
Para DevOps:
"Necesito desplegar Qdrant en producción con alta disponibilidad"
Solución: Terraform + GitHub Actions para despliegue automatizado
# Despliegue completo con Terraform
cd terraform
terraform init
terraform apply
# Qdrant se despliega automáticamente en GCP VM
# Con persistencia, backups y monitoring
Para Equipos de Producto:
"Necesitamos filtrar búsquedas por categorías específicas"
Solución: Filtros de metadata flexibles
# Búsqueda solo en "Vacaciones"
filter_vacaciones = Filter(
must=[
FieldCondition(
key="capitulo_descripcion",
match=MatchText(text="vacaciones"),
)
]
)
results = qdrant_service.search_documents(
collection_name="labor_law_articles",
query_vector=query_embedding,
filter_conditions=filter_vacaciones,
limit=5
)
🚀 El Impacto Transformador
Antes de Qdrant:
- ⏱️ Búsqueda lenta: 500-1000ms con bases de datos relacionales + embeddings en memoria
- 📊 Filtrado limitado: SQL complejo para filtros de metadata
- 🔄 No escalable: Carga completa de vectores en memoria RAM
- 💾 Sin persistencia: Vectores regenerados en cada reinicio
Después de Qdrant:
- ⚡ Búsqueda ultrarrápida: 30-50ms con gRPC
- 📊 Filtrado rico: Combinación de similitud vectorial + filtros complejos
- 🔄 Escalable: Millones de vectores sin degradar performance
- 💾 Persistencia total: Datos sobreviven reinicios y updates
🔧 Características Técnicas Destacadas
Algoritmo HNSW (Hierarchical Navigable Small World)
- Indexación: O(log N) complejidad
- Búsqueda: O(log N) complejidad
- Precision/Recall: >95% con configuración óptima
- Memoria: Eficiente con large-scale datasets
Optimización gRPC
- Velocidad: 2-3x más rápido que HTTP REST
- Serialización: Protobuf vs JSON
- Streaming: Soporte para búsquedas batch
- Latencia: Reducción de 50% en promedio
Persistencia y Durabilidad
- WAL (Write-Ahead Log): Garantiza durabilidad
- Snapshots: Backups automáticos
- Compactación: Optimización automática de storage
- Recovery: Recuperación automática después de crashes
Seguridad
- API Keys: Autenticación por collection
- TLS/HTTPS: Encriptación en tránsito
- Network isolation: Firewall rules configurables
- Audit logs: Registro de todas las operaciones
📊 Métricas de Rendimiento
Búsqueda Vectorial:
- Latencia promedio (HTTP): 80-100ms
- Latencia promedio (gRPC): 30-50ms
- Throughput: >1000 qps con VM e2-medium
- Precision@5: 95%+ para consultas legales
Almacenamiento:
- 413 vectores de 384 dimensiones
- Tamaño en disco: ~500KB (vectores + metadata)
- RAM usage: ~200MB con índice HNSW
- Scaling: Lineal hasta millones de vectores
Operaciones:
- Inserción: 1000 vectores en ~2 segundos
- Actualización: Instantánea con upsert
- Eliminación: O(1) por ID
- Backup: Snapshots en ~5 segundos
💡 Lecciones Aprendidas
1. gRPC es Crítico para Performance
La diferencia entre HTTP (80ms) y gRPC (30ms) es perceptible para el usuario. Siempre configurar puerto gRPC y usar prefer_grpc=True
.
2. Metadata Payloads son Poder
Los payloads ricos permiten filtros complejos sin queries adicionales. Invertir tiempo en diseñar payloads completos vale la pena.
3. COSINE Distance para Embeddings
Para embeddings normalizados (mayoría de modelos de sentence-transformers), Distance.COSINE
es la mejor opción.
4. Persistencia Requiere Volúmenes
Siempre usar volúmenes Docker o storage persistente en VMs. Los datos en /qdrant/storage
deben sobrevivir reinicios.
5. API Keys en Producción No Negociables
Nunca exponer Qdrant sin autenticación en producción. Un atacante puede borrar colecciones o extraer datos sensibles.
📈 Comparación de Despliegues
Aspecto | Local (Docker) | GCP VM | Qdrant Cloud |
---|---|---|---|
Setup | 2 minutos | 10 minutos | 5 minutos |
Costo | $0 | ~$15/mes | ~$20/mes |
Performance | ⚡⚡⚡ | ⚡⚡⚡ | ⚡⚡⚡ |
Mantenimiento | Manual | Medio | Cero |
Escalabilidad | Limitada | Alta | Muy Alta |
Backups | Manual | Manual | Automáticos |
Monitoring | Logs | Logs + GCP | Dashboard |
Recomendado para | Desarrollo | Producción personal | Producción enterprise |
🎯 El Propósito Más Grande
Qdrant no es solo una base de datos - es el motor de búsqueda semántica que hace posible nuestro sistema RAG. Al proporcionar:
- Velocidad: Respuestas en tiempo real
- Precisión: Búsquedas semánticas exactas
- Flexibilidad: Filtros complejos de metadata
- Confiabilidad: Persistencia y alta disponibilidad
- Escalabilidad: Del desarrollo a producción sin fricción
Estamos democratizando el acceso a información legal con la misma experiencia de búsqueda que Google, pero especializada en derecho laboral paraguayo.
🔗 Recursos y Enlaces
Repositorio del Proyecto
- GitHub: lus-laboris-py
Documentación Técnica
-
QdrantService:
src/lus_laboris_api/api/services/qdrant_service.py
-
Docker Compose:
services/vectordb/docker-compose.yml
-
Terraform Module:
terraform/modules/compute_engine/
-
Guía de Qdrant:
docs/qdrant_guide.md
Recursos Externos
- Qdrant Docs: qdrant.tech/documentation
- Python Client: github.com/qdrant/qdrant-client
- Docker Hub: hub.docker.com/r/qdrant/qdrant
Top comments (0)