Arquitectura del Servicio de Búsqueda: Aplicación Flask Independiente con Valkey
Este artículo explica cómo nuestro servicio de búsqueda se ejecuta como una aplicación Flask independiente en un servidor separado, utilizando Valkey (fork de Redis) para búsqueda vectorial de alto rendimiento, almacenamiento en caché y autocompletado.
El Problema: Rendimiento y Escalabilidad de la Búsqueda
Las operaciones de búsqueda son computacionalmente costosas:
-
Extracción de filtros: Coincidir más de 2500 frases contra cada consulta
-
Búsquedas relacionadas: Calcular similitud entre 65K consultas
-
Autocompletado: Coincidencia por prefijo en 65K consultas
-
Filtrado de productos: Filtrar más de 5K productos por múltiples criterios
Ejecutar estas operaciones en el servidor web principal causa:
-
Cargas de página lentas: La búsqueda bloquea otras solicitudes
-
Presión en la memoria: Los embeddings ocupan una huella de memoria masiva
-
Contención de CPU: El cálculo de similitud es intensivo en CPU
-
Dificultad de escalado: No se puede escalar la búsqueda de forma independiente
Necesitamos un servicio de búsqueda dedicado que pueda escalar de forma independiente.
La Solución: Servicio de Búsqueda Independiente
Ejecutamos una aplicación Flask separada en un servidor dedicado:
Servidor Web Principal
↓ Llamadas a API HTTP
Servicio de Búsqueda
↓ Consultas a Valkey
Servidor Valkey
Esta arquitectura proporciona:
-
Escalado independiente: Escalar el servicio de búsqueda sin afectar el servidor web principal
-
Aislamiento de recursos: Las operaciones de búsqueda no impactan el servidor web principal
-
Almacenamiento en caché: Valkey almacena en caché los resultados para consultas repetidas rápidas
-
Alta disponibilidad: El servicio de búsqueda puede reiniciarse sin afectar el servidor web principal
Componentes del Servicio de Búsqueda
1. API de Extracción de Filtros
-
Endpoint:
/api/extract_filters -
Propósito: Extraer filtros estructurados de consultas en lenguaje natural
-
Ejemplo: ``` GET /api/extract_filters?q=mini+pc+16gb+ram
Respuesta: { "Form Factor": "Mini PC", "Main Memory": "16" } ```
Implementación:
-
Cargar mapeos de frase a filtro desde Valkey o JSON
-
Coincidir frases usando regex de límite de palabra
-
Devolver filtros estructurados
Almacenamiento en caché: Los mapeos de frases se almacenan en caché en Valkey (TTL de 30 días)
2. API de Búsquedas Relacionadas
-
Endpoint:
/api/related -
Propósito: Encontrar consultas semánticamente similares usando búsqueda vectorial
-
Ejemplo: ``` POST /api/related { "query": "mini pc", "limit": 10 }
Respuesta: { "related": [ {"query": "small computer", "similarity": 0.92}, {"query": "compact desktop", "similarity": 0.89}, {"query": "mini pc 8gb", "similarity": 0.87} ] } ```
Implementación:
-
Incrustar la consulta usando all-mpnet-base-v2
-
Consultar RediSearch de Valkey para vecinos más cercanos
-
Devolver los N mejores resultados ordenados por similitud
Almacenamiento en caché: Los resultados se almacenan en caché en Valkey (TTL de 7 días)
3. API de Autocompletado
-
Endpoint:
/api/autocomplete -
Propósito: Sugerir consultas mientras el usuario escribe
-
Ejemplo: ``` GET /api/autocomplete?q=mini+p&limit=5
Respuesta: { "suggestions": [ "mini pc", "mini pc 16gb", "mini pc 8gb ram", "mini pc fanless", "mini pc windows 11" ] } ```
Implementación:
-
Consultar RediSearch de Valkey con coincidencia por prefijo
-
Clasificar por popularidad (puntuación de impresiones + clics)
-
Devolver las N mejores sugerencias
Almacenamiento en caché: El índice de autocompletado está en Valkey (actualizado diariamente)
4. API de Consultas Populares
-
Endpoint:
/api/popular -
Propósito: Obtener las consultas más populares
-
Ejemplo: ``` GET /api/popular?limit=10
Respuesta: { "queries": [ "mini pc", "thin client", "industrial pc", "all in one pc" ] } ```
Implementación:
-
Cargar consultas desde Valkey o JSON
-
Ordenar por puntuación de tráfico (impresiones + clics)
-
Devolver las N mejores consultas
Almacenamiento en caché: Las consultas populares se almacenan en caché en Valkey (TTL de 30 días)
Integración con Valkey
Valkey es un fork de Redis que proporciona:
-
Búsqueda vectorial: Módulo RediSearch para búsqueda por similitud
-
Almacenamiento en caché: Almacén clave-valor en memoria rápido
-
Autocompletado: Coincidencia por prefijo con conjuntos ordenados
-
Persistencia: AOF (Archivo de solo adición) para durabilidad
Búsqueda Vectorial con RediSearch
Usamos el módulo RediSearch de Valkey para búsqueda por similitud vectorial:
Creación del índice:
client.ft("queries_idx").create_index([
VectorField("embedding", "FLAT", {
"TYPE": "FLOAT32",
"DIM": 768,
"DISTANCE_METRIC": "COSINE"
}),
TextField("query"),
NumericField("score")
])
Búsqueda Vectorial:
query_embedding = model.encode(query)
results = client.ft("queries_idx").search(
Query("*=>[KNN 10 @embedding $vec AS score]")
.sort_by("score")
.return_fields("query", "score")
.dialect(2),
query_params={"vec": query_embedding.tobytes()}
)
Esto devuelve los 10 vecinos más cercanos por similitud coseno.
Estrategia de Almacenamiento en Caché
Almacenamos en caché múltiples tipos de datos en Valkey:
Mapeos de Frases (TTL de 30 días):
client.setex(
"seo:phrase_mappings",
30 * 24 * 3600,
json.dumps(phrase_mappings)
)
Búsquedas Relacionadas (TTL de 7 días):
cache_key = f"related:{query_hash}"
client.setex(cache_key, 7 * 24 * 3600, json.dumps(results))
Consultas Populares (TTL de 30 días):
client.setex(
"seo:popular_queries",
30 * 24 * 3600,
json.dumps(popular_queries)
)
Índice de Autocompletado (actualizado diariamente):
for query, score in queries:
client.zadd("autocomplete:mini", {query: score})
Autocompletado con Conjuntos Ordenados
Usamos conjuntos ordenados de Valkey para autocompletado:
Estructura del Índice:
autocomplete:m → ["mini pc": 5000, "mini computer": 3000]
autocomplete:mi → ["mini pc": 5000, "mini computer": 3000]
autocomplete:min → ["mini pc": 5000, "mini computer": 3000]
autocomplete:mini → ["mini pc": 5000, "mini computer": 3000]
Búsqueda por Prefijo:
prefix = "mini"
results = client.zrevrange(f"autocomplete:{prefix}", 0, 9, withscores=True)
Esto devuelve las 10 mejores consultas que comienzan con "mini", ordenadas por puntuación.
Comunicación de la API
El servidor web principal llama al servicio de búsqueda a través de HTTP:
Extracción de Filtros
from app.shared.filter_service import extract_filters_from_query
filters = extract_filters_from_query("mini pc 16gb ram")
# Internamente llama: GET SEARCH_SERVICE_URL/api/extract_filters?q=...
Búsquedas Relacionadas
import requests
response = requests.post(
"SEARCH_SERVICE_URL/api/related",
json={"query": "mini pc", "limit": 10},
timeout=2
)
related = response.json()["related"]
Autocompletado
response = requests.get(
"SEARCH_SERVICE_URL/api/autocomplete",
params={"q": "mini p", "limit": 5},
timeout=1
)
suggestions = response.json()["suggestions"]
Manejo de Errores y Alternativas
El servidor web principal maneja las fallas del servicio de búsqueda de manera elegante:
try:
filters = extract_filters_from_query(query)
except Exception as e:
logger.error(f"Search service failed: {e}")
filters = {} # Alternativa: filtros vacíos
Esto asegura que el servidor web principal continúe funcionando incluso si el servicio de búsqueda está caído.
### Configuración de Red
Todos los servidores están en una red privada:
- **Servidor web principal**: Puede acceder al servicio de búsqueda y a Valkey
- **Servicio de búsqueda**: Puede acceder a Valkey
- **Valkey**: Solo accesible desde el servidor web principal y el servicio de búsqueda
No hay acceso externo al servicio de búsqueda o a Valkey.
## Integración con el Pipeline de SEO
El servicio de búsqueda se integra con el pipeline de SEO:
### Paso 11: Migrar a Valkey
El pipeline de SEO carga datos en Valkey:
```python
# Cargar embeddings de consultas
for query, embedding in zip(queries, embeddings):
client.hset(f"query:{query_hash}", mapping={
"query": query,
"embedding": embedding.tobytes(),
"score": score
})
# Crear índice de RediSearch
client.ft("queries_idx").create_index([...])
Ver Migración a Valkey para más detalles.
Registro de Consultas
El servicio de búsqueda registra consultas para el pipeline de SEO:
log_entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"query": query,
"filters_extracted": filters,
"results_count": len(results)
}
with open(SEO_LIVE_QUERIES_LOG, "a") as f:
f.write(json.dumps(log_entry) + "\n")
Estos registros alimentan el Paso 1d: Obtener Consultas en Vivo.
Referencias
Conceptos Técnicos
-
Valkey - Sitio web oficial
-
Redis - Sitio web oficial (fork de Valkey)
-
RediSearch - Módulo de búsqueda vectorial
-
Similitud Coseno - Wikipedia
Artículos Relacionados
-
Extracción de Filtros - Cómo se extraen los filtros
-
Generación de Búsquedas Relacionadas - Cómo se generan las búsquedas relacionadas
-
Estrategia de Embeddings - Cómo se generan los embeddings
-
Descripción General del Pipeline de SEO - Arquitectura completa del pipeline
-
Migración a Valkey - Cargar datos en Valkey
Resumen
Nuestro servicio de búsqueda se ejecuta como una aplicación Flask independiente en un servidor separado:
Arquitectura:
-
Aplicación Flask independiente en servidor dedicado
-
Valkey (fork de Redis) para almacenamiento en caché y búsqueda vectorial
-
API HTTP para comunicación con el servidor web principal
APIs:
-
/api/extract_filters- Extraer filtros de consultas -
/api/related- Encontrar consultas similares (búsqueda vectorial) -
/api/autocomplete- Sugerir consultas (coincidencia por prefijo) -
/api/popular- Obtener consultas populares
Características de Valkey:
-
Búsqueda vectorial (módulo RediSearch)
-
Almacenamiento en caché (TTL de 30 días para mapeos de frases)
-
Autocompletado (conjuntos ordenados)
-
Persistencia (AOF)
Beneficios:
-
Escalado independiente
-
Aislamiento de recursos
-
Alto rendimiento (caché de Valkey)
-
Tolerancia a fallos (degradación elegante)
Esta arquitectura permite una búsqueda rápida y escalable manteniendo al servidor web principal receptivo.