Señales cruzadas: validación con búsquedas reales y matching de proveedores
Este artículo técnico aborda la problemática de identificar tendencias de productos basadas únicamente en la oferta del marketplace, que no siempre reflejan la demanda real de los consumidores. Para solucionarlo, se implementa un sistema que combina datos de Google Trends con información del marketplace, utilizando Reciprocal Rank Fusion para crear un ranking unificado. Además, se realiza scraping de sitios de proveedores mayoristas y se aplica matching semántico mediante embeddings de Vertex AI para estimar márgenes. El lector aprenderá sobre la integración de múltiples fuentes de datos, técn
En el post anterior el dashboard ya mostraba tendencias del marketplace y productos asociados. Pero había un problema conceptual: las tendencias del marketplace reflejan lo que los vendedores publican, no necesariamente lo que los consumidores buscan. Un keyword puede estar en el top 10 del marketplace porque hay mucha oferta, no porque haya mucha demanda.
Necesitaba una segunda señal independiente: volumen de búsqueda real. Y de paso, quería saber si los productos trending tienen proveedores mayoristas disponibles, para estimar márgenes.
Google Trends como señal de demanda
Google Trends no da volúmenes absolutos — da índices relativos entre 0 y 100 comparando keywords dentro de la misma consulta. El truco es que solo puedes comparar 5 keywords por request, y los índices de requests distintos no son comparables entre sí.
La solución clásica: usar un keyword ancla. Incluyo el mismo keyword de referencia en cada batch, y normalizo todos los índices respecto a él:
get_interest = function(keywords, category_id, anchor_keyword) {
# Dividir en batches de 4 + 1 ancla
batches <- split(keywords, ceiling(seq_along(keywords) / 4))
for (batch in batches) {
query_kws <- c(anchor_keyword, batch)
result <- gtrendsR::gtrends(
keyword = query_kws,
geo = "CL",
time = "now 7-d"
)
# Normalizar: (keyword_index / anchor_index) * 100
}
}
Configuré la búsqueda para Chile (geo="CL") y ventana de 7 días. El resultado es un gt_index normalizado para cada keyword que es comparable entre batches.
Si gtrendsR falla (a veces Google bloquea requests consecutivos), hay un fallback automático a pytrends via reticulate — R llamando a Python llamando a Google. No es elegante, pero funciona.
Reciprocal Rank Fusion: un ranking que combina dos fuentes
Con dos rankings independientes — el del marketplace (rank_ml) y el de Google Trends (rank_gt) — necesitaba fusionarlos en uno solo. Implementé Reciprocal Rank Fusion (RRF), un algoritmo de information retrieval que es simple y robusto:
RRF_score(keyword) = 1/(k + rank_ml) + 1/(k + rank_gt)
El parámetro k (default 60) controla cuánto peso tienen las posiciones altas vs bajas. Un k alto democratiza el ranking; uno bajo amplifica las diferencias entre top y bottom.
El endpoint Django computa esto on-the-fly:
class FusedTrendsView(APIView):
def get(self, request):
k = int(request.query_params.get("k", 60))
# JOIN trends + google_trends por keyword
for kw in merged:
kw["rrf_score"] = 1/(k + kw["rank_ml"]) + 1/(k + kw["rank_gt"])
kw["delta"] = kw["rank_ml"] - kw["rank_gt"]
# Ordenar por rrf_score descendente
El delta es revelador: si un keyword está en posición 5 del marketplace pero posición 40 en Google, el delta es -35 — la gente busca mucho menos de lo que el marketplace sugiere. Si el delta es positivo, el keyword tiene demanda real que el marketplace subestima.
En el frontend, los deltas extremos se colorean: rojo para keywords sobreestimados por el marketplace, verde para los subestimados. Es la métrica más útil del dashboard.
Scraping de proveedores + matching semántico
La tercera pieza del puzzle: proveedores mayoristas. De nada sirve identificar un producto trending si no puedo comprarlo al por mayor.
Construí scrapers para sitios de distribución mayorista. Cada scraper hereda de BaseScraper y tiene la misma interfaz:
class BaseScraper(ABC):
@abstractmethod
def scrape(self, keywords, max_results=50):
"""Retorna lista de SupplierProduct."""
pass
Uno usa una API REST (WooCommerce), otro hace scraping HTML con BeautifulSoup. Los productos extraídos se guardan en una tabla Django normal (no managed=False, porque estos datos los gestiona el backend, no R).
El matching entre productos del marketplace y productos de proveedores usa embeddings de Vertex AI (text-embedding-005, 768 dimensiones). Genero embeddings del nombre+marca de cada producto y computo similitud coseno:
def match_products(threshold=0.7, top_k=5):
# Para cada producto del marketplace:
# 1. Buscar los top_k proveedores más similares
# 2. Si similarity > threshold, crear ProductMatch
# 3. Calcular margen: (precio_marketplace - precio_proveedor) / precio_marketplace
El resultado es una tabla de matches con score de similitud y margen estimado. El dashboard muestra esto en la tab de Suppliers: para cada producto trending, qué proveedores lo tienen y a qué precio.
Batch paralelo: la optimización final
El pipeline original hacía un request por producto, secuencialmente, con 0.3 segundos de sleep entre cada uno. Para 500 productos, eso eran ~25 minutos solo en la fase de detalle.
Refactoricé a procesamiento en batch con httr2::req_perform_parallel():
get_products_batch = function(product_ids, batch_size = 10L) {
chunks <- split(product_ids, ceiling(seq_along(product_ids) / batch_size))
for (chunk in chunks) {
reqs <- lapply(chunk, function(pid) {
httr2::request(paste0(self$base_url, "/products/", pid)) |>
httr2::req_headers(Authorization = paste("Bearer", self$access_token))
})
resps <- httr2::req_perform_parallel(reqs, on_error = "continue")
}
}
10 requests concurrentes por lote, con 0.5 segundos de sleep entre lotes. El tiempo bajó de 25 minutos a menos de 3. Para los precios faltantes, un segundo batch usa el multi-get endpoint de items (hasta 20 IDs por llamada).
El sistema completo
El pipeline final tiene 7 pasos que se ejecutan cada mañana automáticamente:
- Autenticación — Refresh del token OAuth
- Tendencias — Keywords trending por categoría
- Google Trends — Volumen de búsqueda real para cada keyword
- Búsqueda en catálogo — Productos asociados a cada keyword
- Detalle en batch — Nombre, marca, precio, atributos (paralelo)
- Backfill de precios — Items API para productos sin buy_box_winner
- Persistencia — DELETE+INSERT idempotente por snapshot_date
Toda la data fluye a Neon PostgreSQL, donde Django la lee con modelos managed=False y la expone via API. React la visualiza con recharts. El scraping de proveedores corre bajo demanda desde el backend Django.
Es un sistema que combina R, Python, Django, React, Cloud Run Jobs, Secret Manager, Vertex AI y dos bases de datos PostgreSQL. ¿Excesivo? Tal vez. Pero cada pieza resuelve un problema concreto, y el resultado es un radar de mercado que se actualiza solo y que cruza tres señales independientes: oferta del marketplace, demanda de consumidores y disponibilidad mayorista.