Product Search — Busca Semântica com Filtro Geográfico¶
Arquitetura e implementação da busca de produtos (experiências, guias, transfers, tickets) na plataforma iFriend.
Visão Geral¶
A busca de produtos usa similaridade semântica via embeddings no BigQuery ML, combinada com filtro geográfico por embedding multilíngue. O LLM nunca acessa dados brutos — toda a consulta é feita via ferramenta busca_produtos.
Usuário → "experiences in Florence Italy"
│
├─ metadados_busca="experiences in Florence Italy"
└─ destino="Florence" ← extraído pelo LLM
│
▼
┌──────────────────────────────────────┐
│ BigQuery ML — 2 embeddings por query│
│ │
│ 1. full_query_emb → score semântico │
│ (title 0.4 + desc 0.3 + │
│ location 0.2 + features 0.1) │
│ │
│ 2. dest_emb → filtro geográfico │
│ COSINE < threshold (0.30) │
│ (multilíngue: Florence=Firenze) │
└──────────────────────────────────────┘
Arquivo Principal¶
| Arquivo | Descrição |
|---|---|
ifriend_agent/tools/busca_produtos_tool.py |
Tool que executa a busca no BigQuery |
ifriend_agent/tools/context/enrich_products.py |
Enriquecimento pós-query (preço, catálogo, URL) |
ifriend_agent/tools/blocks/experience_cards.py |
UI blocks para experiences |
ifriend_agent/tools/blocks/guide_cards.py |
UI blocks para guias |
Parâmetros da Tool¶
busca_produtos(tool_context, metadados_busca, destino, tipo_produto, guia_pro, limit, suppress_blocks)¶
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
metadados_busca |
str |
Sim | Texto livre da busca do usuário |
destino |
str |
Não | Cidade/estado/pais para filtro geográfico via embedding multilíngue |
tipo_produto |
str |
Não | "guia", "experience", "ticket", "transfer" |
guia_pro |
bool |
Não | Se True, filtra apenas guias profissionais |
limit |
int |
Não | Máx produtos no resultado (padrão: 10) |
suppress_blocks |
bool |
Não | Se True, retorna só lista sem UI blocks |
SQL Gerado Dinamicamente¶
Sem destino (busca ampla)¶
WITH input_embedding_raw AS (
SELECT ml_generate_embedding_result AS raw_emb
FROM ML.GENERATE_EMBEDDING(MODEL `...mte002`, (SELECT @query_text))
),
input_embedding AS (
SELECT CASE WHEN ARRAY_LENGTH(raw_emb)=768 THEN raw_emb
ELSE ARRAY(SELECT 0.0 FROM UNNEST(GENERATE_ARRAY(1,768))) END AS embedding
FROM input_embedding_raw
),
fallback_vector AS (
SELECT ARRAY(SELECT 0.0 FROM UNNEST(GENERATE_ARRAY(1,768))) AS zero_embedding
)
SELECT
p.*,
(0.40 * ML.DISTANCE(ie.emb, p.title_embedding, 'COSINE') +
0.30 * ML.DISTANCE(ie.emb, p.description_embedding, 'COSINE') +
0.20 * ML.DISTANCE(ie.emb, p.location_embedding, 'COSINE') +
0.10 * ML.DISTANCE(ie.emb, p.features_embedding, 'COSINE')
) AS distance
FROM `table` AS p, input_embedding AS ie, fallback_vector AS f
WHERE p.published=1 AND p.salable='online' AND p.deleted_at IS NULL
ORDER BY distance ASC
LIMIT {limit}
Com destino (filtro geográfico ativado)¶
Adiciona um CTE extra dest_emb que embedda o nome do destino separadamente, e um filtro WHERE comparando com location_embedding via distância cosseno:
WITH
...input_embedding... (mesmo de cima),
dest_emb_raw AS (
SELECT ml_generate_embedding_result AS raw_emb
FROM ML.GENERATE_EMBEDDING(MODEL `...mte002`, (SELECT @destino))
),
dest_emb AS (
SELECT CASE WHEN ARRAY_LENGTH(raw_emb)=768 THEN raw_emb
ELSE ARRAY(SELECT 0.0 FROM UNNEST(GENERATE_ARRAY(1,768))) END AS embedding
FROM dest_emb_raw
),
...fallback_vector...
SELECT ... FROM `table` AS p, input_embedding AS ie, dest_emb AS de, fallback_vector AS f
WHERE p.published=1 AND p.salable='online' AND p.deleted_at IS NULL
AND ML.DISTANCE(de.embedding, p.location_embedding, 'COSINE') < @geo_threshold
ORDER BY distance ASC
LIMIT {limit}
Pesos do Score Semântico¶
Os pesos foram normalizados para soma = 1.0 na implementação atual:
| Campo | Peso | Função |
|---|---|---|
title_embedding |
0.40 | Título do produto |
description_embedding |
0.30 | Descrição do produto |
location_embedding |
0.20 | Cidade, estado, país |
features_embedding |
0.10 | Highlights + includes |
Histórico de pesos:
| Versão | title | desc | location | features | Soma |
|---|---|---|---|---|---|
| Anterior (pré-jun/2026) | 0.5 | 0.4 | 0.3 | 0.1 | 1.3 |
| Atual (jun/2026+) | 0.40 | 0.30 | 0.20 | 0.10 | 1.0 |
O peso do
location_embeddingfoi reduzido de 0.3 para 0.2 porque o filtro geográfico explícito (threshold cosseno) já garante que produtos fora do destino sejam excluídos. O score residual de localização serve apenas para ranquear produtos dentro do destino.
Filtro Geográfico via Embedding¶
Como funciona¶
Quando o LLM extrai um destino e passa como destino="Florence":
- O texto
"Florence"é embeddado pelo mesmo modelomte002em um CTE separado - Esse embedding é comparado via
ML.DISTANCE(..., 'COSINE')com olocation_embeddingde cada produto - Produtos com distância cosseno ≥ threshold são excluídos do resultado
Vantagens do embedding para filtro geográfico¶
| Cenário | String match falha | Embedding funciona |
|---|---|---|
| "Florence" vs cidade="Firenze" | ❌ | ✅ (mesmo embedding semântico) |
| "Switzerland" vs país="Suíça" | ❌ | ✅ |
| "Munique" vs cidade="München" | ❌ | ✅ |
| "Rio de Janeiro" vs state="Rio de Janeiro" | ✅ (exato) | ✅ |
| "Nova York" vs city="New York" | ❌ | ✅ |
Threshold¶
| Variável | Default | Descrição |
|---|---|---|
GEO_FILTER_THRESHOLD |
0.30 |
Distância cosseno máxima entre embedding do destino e location_embedding do produto |
Guia de ajuste:
| Threshold | Comportamento | Recomendado para |
|---|---|---|
| 0.30 | Restritivo — só produtos no destino | Padrão recomendado |
| 0.45 | Permissivo — pode incluir cidades com perfil turístico similar | Destinos sem muitos produtos |
| 0.60 | Permissivo — inclui destinos próximos | Destinos sem muitos produtos |
Normalização de texto auxiliar¶
Além do embedding, o destino passa por normalização _normalize_text() para remover acentos e lowercase:
" São Paulo " → "sao paulo"
"Grecía" → "grecia"
"München" → "munchen"
" " → None (sem filtro)
Essa normalização não substitui o embedding — serve apenas como preparação do texto para gerar o embedding no BigQuery.
Fallback Progressivo¶
Quando uma busca retorna zero resultados, a tool tenta variações automaticamente:
Tentativa 1: com tipo_produto + destino
↓ (vazio)
Tentativa 2: sem tipo_produto, com destino
↓ (vazio)
Tentativa 3: com tipo_produto, sem destino
↓ (vazio)
Tentativa 4: sem tipo_produto, sem destino (busca ampla)
Cada tentativa é logada com [busca_produtos] Fallback (...): N produtos.
Enriquecimento Pós-Query¶
Após a query SQL, os resultados passam por 4 estágios de enriquecimento:
| Estágio | Função | Descrição |
|---|---|---|
| 1 | context_product_price |
Aplica markup do afiliado sobre price_net |
| 2 | context_product_catalog_status |
Verifica status online/offline via API |
| 3 | context_product_affiliate_url |
Substitui URL base pela do afiliado |
| 4 | context_product_whitelabel_url |
Substitui URL base pela do whitelabel |
Agentes que Utilizam a Busca¶
| Agente | Tipo de busca | Parâmetros típicos |
|---|---|---|
discovery_agent |
Por destino + tipo | destino="Salvador", tipo_produto="experience" |
itinerary_agent |
Multi-destino | destino="Paris", suppress_blocks=True |
support_agent |
Pré-qualificação | destino="<se conhecido>", limit=5 |
Testes¶
| Arquivo | Cobertura |
|---|---|
ifriend_agent/evaluations/unit/busca_produtos.test.json |
Cenários com e sem destino, tipos de produto |
ifriend_agent/tests/test_sub_agents.py |
Ferramentas dos agentes que usam busca_produtos |
Variáveis de Ambiente¶
| Variável | Default | Descrição |
|---|---|---|
DATASET |
Ifriend_produto |
Dataset BigQuery |
TABLE |
mv_product_search_flat_embeddings |
Tabela materializada com embeddings |
EMBEDDING_MODEL_NAME |
mte002 |
Modelo de embedding (768D) |
GEO_FILTER_THRESHOLD |
0.30 |
Threshold para filtro geográfico via embedding |
Roadmap Futuro¶
- [ ] Hybrid search: Dense + BM25 para melhor recall em termos exatos
- [ ] Reranker: BGE-reranker-v2 após retrieval para refinar top-k
- [ ] Query rewriting: LLM reescreve a query do usuário antes de embeddar
- [ ] Fine-tuning: Ajustar modelo de embedding para domínio de turismo brasileiro
- [ ] Logs de feedback: Capturar cliques e conversões para treino contínuo
Detalhes no roadmap de fine-tuning em
docs/archive/roadmap_finetuning_embeddings_turismo_rag.md