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:

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

Artículos Relacionados

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.


← Volver al Índice de Documentación