¿Qué es SACS Arena?
SACS Arena es una API REST que ejecuta un flujo de búsqueda inteligente en 7 pasos contra un índice Azure Cognitive Search, usando OpenAI para clasificar la intención del usuario y devolver resultados contextuales para el Mundial 2026.
El flujo completo ocurre en el servidor — el frontend solo necesita enviar el query
y reaccionar al decision que llega en la respuesta para saber qué mostrar.
El campo decision puede ser CasoA (servicio dominante),
CasoC (torneo dominante) o CasoD (ambiguo / navegar por filtros).
URL base
https://eventos.sacspro.com
Flujo de llamadas (máximo 3)
Llamada 2: POST /api/search — query inicial
Llamada 3a: POST /api/search/refine — usuario elige intent (filter1)
Llamada 3b: POST /api/search/filter2 — filtro directo por intent + entidad
Las llamadas 3a y 3b son opcionales según lo que devuelva el servidor en
filter1.show y suggestFilter2.show.
Configuración
Todas las variables se configuran en
Azure App Service → Configuration → Application Settings.
No incluir valores reales en appsettings.json del repositorio.
Variables requeridas
Azure Search
| Variable | Descripción |
|---|---|
AzureSearch__Endpoint | URL del servicio Azure Search (ej: https://mi-servicio.search.windows.net). La app no arranca si está vacío. |
AzureSearch__ApiKey | API Key de Azure Search. La app no arranca si está vacío. |
AzureSearch__IndexName | Nombre del índice en Azure Search donde están los documentos. |
OpenAI
| Variable | Descripción |
|---|---|
OpenAI__ApiKey | API Key de OpenAI. Requerida — la app no arranca si no está configurada a nivel global o por sitio. |
OpenAI__Model | Modelo a usar (ej: gpt-4o-mini, gpt-4o). Default: gpt-4o-mini. |
Sitios (mínimo un sitio)
| Variable | Descripción |
|---|---|
Sites__0__SiteId | ID numérico del sitio. Se envía en cada request como siteId. |
Sites__0__Name | Nombre descriptivo del sitio (solo para logs y dashboard). |
Sites__0__ApiKey | API Key para autenticar clientes de este sitio. Se envía en el header X-Api-Key. |
Sites__0__AllowedOrigins__0 | Dominio autorizado para CORS. Agregar tantos como sean necesarios incrementando el índice (0, 1, 2…). |
Variables opcionales
Búsqueda — cantidad de resultados
| Variable | Default | Descripción |
|---|---|---|
AzureSearch__ExploratorySearchTop | 15 | Documentos retornados en la búsqueda exploratoria léxica inicial (sin filtros de intent). |
AzureSearch__FinalSearchTop | 10 | Documentos retornados en la búsqueda final (con filtros OData aplicados). Afecta el total en results.items. |
AzureSearch__DirectSearchTop | 5 | Candidatos para búsqueda directa de texto en título/intro (evaluación de relevancia directa). |
AzureSearch__PrimarySemanticTop | 20 | Documentos retornados en la búsqueda semántica primaria (con reranker). Base para la decisión UX. |
AzureSearch__SemanticSupplementTop | 5 | Documentos adicionales de búsqueda semántica complementaria cuando el resultado principal es escaso. |
AzureSearch__ConsoleDisplayTop | 20 | Resultados mostrados en la consola CLI del proyecto (no afecta la API). |
Semantic (requiere Azure Search tier S1+)
| Variable | Default | Descripción |
|---|---|---|
AzureSearch__SemanticConfigurationName | "" | Nombre de la configuración semántica en Azure Search. Si está vacío, el reranker semántico se desactiva completamente. Este valor es global y no se puede sobreescribir por sitio. |
AzureSearch__SemanticOnExploratory | false | Activa el reranker semántico en la fase exploratoria. Aumenta latencia y consumo de cuota semántica. |
AzureSearch__SemanticOnFinalSearch | false | Activa el reranker semántico en la búsqueda final (resultados que se muestran al usuario). |
AzureSearch__SemanticCallsLoggingEnabled | false | Habilita log detallado de cada llamada semántica (endpoint, score, ms) para auditoría. |
Vector Search (Hybrid Search)
| Variable | Default | Descripción |
|---|---|---|
AzureSearch__UseVectorSearch | false | Activa búsqueda vectorial (hybrid: léxica + vectorial). Requiere que el índice tenga campos de embeddings configurados con un vector profile. |
AzureSearch__VectorProfileName | "" | Nombre del vector profile configurado en Azure Search (ej: default-vector-profile). Requerido si UseVectorSearch es true. |
AzureSearch__VectorFieldName | "titleVector" | Nombre del campo en el índice que contiene los embeddings vectoriales (ej: titleVector). |
Scoring
| Variable | Default | Descripción |
|---|---|---|
AzureSearch__ScoringProfileName | "" | Nombre del scoring profile en Azure Search (ej: editorial-boost). Aplica boosting editorial a documentos con contentPriority alto y a contenido reciente. |
Umbrales de decisión
| Variable | Default | Descripción |
|---|---|---|
AzureSearch__RelevanceRerankerThreshold | 2.0 | Score mínimo del reranker semántico (escala 0–4) para considerar un documento como relevante. Documentos por debajo de este umbral se descartan del resultado principal. |
AzureSearch__StrongRerankerThreshold | 3.0 | Score muy alto del reranker. Si el top doc supera este umbral, se muestra directamente sin necesitar confirmar dominancia en facets. |
AzureSearch__AmbiguityDominanceThreshold | 0.40 | Ratio mínimo de dominancia del intent principal (docs_del_intent_top / total_docs). Si es menor a este valor y no hay score fuerte → flujo va a Filter1 (ambigüedad → CasoD). |
AzureSearch__DirectAnswerMinScore | 0.85 | Score mínimo para mostrar una respuesta extractiva directa (directAnswer). Si el extracto semántico supera este umbral, se presenta como respuesta destacada antes de los resultados. |
AzureSearch__TitleMatchMinRerankerScore | 2.0 | Score mínimo del reranker (0–4) para validar que un documento con match en título es suficientemente relevante. |
AzureSearch__TitleMatchMinLexicalScore | 5.0 | Score mínimo léxico (@search.score) para validar match en título vía búsqueda léxica. |
AzureSearch__SemanticSupplementMinScore | 2.5 | Score mínimo del reranker para incluir un documento en la búsqueda semántica complementaria. Documentos con score menor se descartan del suplemento. |
Rate Limiting
| Variable | Default | Descripción |
|---|---|---|
RateLimit__RequestsPerMinute | 60 | Requests permitidos por minuto por API Key. Al exceder retorna HTTP 429. |
Redis (caché distribuida)
| Variable | Default | Descripción |
|---|---|---|
Redis__Enabled | false | Activa Redis como caché distribuida. Si es false, se usa caché en memoria (no se comparte entre instancias). Recomendado activar en producción con múltiples instancias. |
Redis__ConnectionString | "" | Connection string de Azure Cache for Redis (ej: mi-redis.redis.cache.windows.net:6380,password=...,ssl=True). Requerido si Redis__Enabled es true. |
Slack (notificaciones)
| Variable | Default | Descripción |
|---|---|---|
Slack__Enabled | false | Activa envío de notificaciones a Slack (errores críticos, alertas de cobertura, métricas). Si es false, los errores solo se logean localmente. |
Slack__WebhookUrl | "" | URL del Incoming Webhook de Slack. Requerido si Slack__Enabled es true. |
Monitoreo
| Variable | Default | Descripción |
|---|---|---|
ApplicationInsights__ConnectionString | "" | Connection string de Azure Application Insights. Si está vacío, la telemetría de App Insights se desactiva. |
Variables de override por sitio
Cada sitio puede sobreescribir los valores globales. Si una variable del sitio no se define → usa el valor global. Para agregar múltiples sitios incrementar el índice (0, 1, 2…).
SemanticConfigurationName y las credenciales de Azure Search
(Endpoint, ApiKey) son siempre globales — no se pueden sobreescribir por sitio.
| Variable por sitio | Descripción |
|---|---|
Sites__N__OpenAIApiKey | API Key de OpenAI específica para este sitio (sobreescribe la global). |
Sites__N__OpenAIModel | Modelo OpenAI para este sitio (ej: gpt-4o para mejor calidad). |
Sites__N__ExploratorySearchTop | Top exploratorio para este sitio. |
Sites__N__FinalSearchTop | Top resultados finales para este sitio. |
Sites__N__DirectSearchTop | Top búsqueda directa para este sitio. |
Sites__N__PrimarySemanticTop | Docs en búsqueda semántica primaria para este sitio. |
Sites__N__SemanticOnExploratory | Activar reranker semántico en exploratoria para este sitio. |
Sites__N__SemanticOnFinalSearch | Activar reranker semántico en búsqueda final para este sitio. |
Sites__N__ScoringProfileName | Scoring profile de Azure Search para este sitio. |
Sites__N__UseVectorSearch | Activar vector search (hybrid) para este sitio. |
Sites__N__VectorProfileName | Perfil de vector search para este sitio. |
Sites__N__VectorFieldName | Campo de embeddings para este sitio (ej: titleVector). |
Sites__N__RelevanceRerankerThreshold | Umbral mínimo de reranker (0–4) para considerar doc relevante. |
Sites__N__StrongRerankerThreshold | Umbral de reranker "muy alto" para mostrar doc sin confirmar dominancia. |
Sites__N__AmbiguityDominanceThreshold | Ratio de dominancia mínimo; si menor → CasoD (ambigüedad). |
Sites__N__DirectAnswerMinScore | Score mínimo para mostrar respuesta extractiva directa. |
Sites__N__TitleMatchMinRerankerScore | Umbral reranker para validar match de título. |
Sites__N__TitleMatchMinLexicalScore | Umbral léxico para validar match de título. |
Sites__N__SemanticCallsLimit | Cuota mensual (30 días) de llamadas semánticas para este sitio. Default: 80000. |
Sites__N__WelcomeTemplate | Template de la pantalla de bienvenida del widget (template1, default, etc.). |
Sites__N__GamificationEnabled | Activa gamificación en el widget (devuelto en /api/site-config). |
Sites__N__NewsletterEnabled | Activa suscripción a newsletter en el widget. |
Sites__N__ShowFixtures | Muestra fixtures/calendario en el widget. |
Sites__N__TimeZoneId | Timezone IANA o Windows para calcular la fecha local del sitio (ej: Central Standard Time (Mexico)). |
Sites__N__EnableFeedback | Habilita feedback de usuario sobre los resultados en este sitio. |
# Sitio 0 (primer sitio) Sites__0__SiteId = 61 Sites__0__Name = Posta MX Sites__0__ApiKey = sf-postamx-xxxxxxxx Sites__0__AllowedOrigins__0 = https://www.postamx.com Sites__0__SemanticOnExploratory = true Sites__0__SemanticOnFinalSearch = true Sites__0__SemanticCallsLimit = 80000 Sites__0__TimeZoneId = Central Standard Time (Mexico) Sites__0__GamificationEnabled = true # Sitio 1 (segundo sitio — con overrides) Sites__1__SiteId = 42 Sites__1__Name = Otro Sitio Sites__1__ApiKey = sf-otrop-xxxxxxxx Sites__1__AllowedOrigins__0 = https://www.otrosite.com Sites__1__OpenAIModel = gpt-4o # override del modelo global Sites__1__FinalSearchTop = 30 # override del top global Sites__1__UseVectorSearch = true # hybrid search para este sitio
Notas de seguridad
$site = "nombrecliente" $key = [System.Guid]::NewGuid().ToString("N").Substring(0, 8) Write-Host "sf-$site-$key"
http://localhost:3000
y recordar quitarlo antes del deploy a producción.
Autenticación
Todos los endpoints de búsqueda requieren dos headers HTTP:
| Header | Descripción | Ejemplo |
|---|---|---|
X-Api-Key | API Key asignada al sitio | sf-postamx-a3b5c7d9 |
X-Site-Id | ID numérico del sitio | 61 |
/api/dashboard/* solo requieren X-Api-Key, no X-Site-Id.
Códigos de error
| Código | Descripción |
|---|---|
400 | Query vacío, demasiado largo o SiteId inválido |
401 | API Key no enviada |
403 | API Key inválida o dominio no autorizado |
404 | SiteId no encontrado en configuración |
429 | Rate limit excedido (60 req/min por API Key) |
500 | Error interno del servidor |
Formato de error
{ "error": "API Key requerida", "header": "X-Api-Key" }
Flujo de búsqueda — 7 pasos
El flujo completo se ejecuta en SearchFlowOrchestrator.ExecuteAsync().
Cada paso alimenta al siguiente. El frontend recibe un único JSON con todos los bloques
y solo necesita respetar los flags show de cada sección.
userExplicitlySelected: true
y un intent ya elegido, el orquestador hace early return directo a la búsqueda final
sin ejecutar QU ni exploratoria completa. Si viene de una sugerencia cacheada o paginación,
también toma atajos para reducir latencia y consumo de cuota semántica.
Query Understanding — OpenAI
QueryUnderstandingService.AnalyzeAsync() envía el query a OpenAI y recibe:
| Campo | Descripción |
|---|---|
intentPrimary | Intent principal del torneo detectado (del catálogo de 23 valores) |
intentService | Intent de servicio detectado (del catálogo de 15 valores), si aplica |
isServiceContent | true si el query busca información de servicio local (movilidad, fan zones, etc.) |
confidence | Confianza del modelo en la clasificación (0–1). >= 0.70 se considera confiable. |
teams | Equipos detectados en el query (ej: ["México", "Uruguay"]) |
places | Ciudades/sedes detectadas (ej: ["Monterrey"]) |
people | Personas detectadas (jugadores, entrenadores, etc.) |
Si la llamada a OpenAI falla, el orquestador hace fallback a
LocalQueryClassifier.Classify() — un clasificador por reglas heurísticas
que garantiza que siempre haya un intent mínimo para continuar el flujo.
El paso 1 y el paso 2 se ejecutan en paralelo para minimizar latencia.
Búsqueda semántica primaria
AzureSearchService.ExecutePrimarySemanticAsync() ejecuta una búsqueda
contra Azure Search con filtro base siteid eq {SiteId} (sin filtros de intent)
usando el reranker semántico si está configurado.
Retorna:
- Documentos con
rerankerScore(escala 0–4) yscoreléxico - Facets de
intentPrimaryeintentService— distribución de intents en resultados - Extracto de respuesta directa (
captions) si el reranker semántico lo genera
Facets globales (caché Redis)
Se obtienen los facets globales del sitio (distribución de documentos por intent en todo el índice)
desde caché Redis. Se usan para construir las opciones de navegación en
filter1.otherOptions — la sección "otras categorías" que aparece cuando
el usuario navega sin un query específico.
Si Redis no está configurado, se calculan en memoria por cada request (mayor latencia). El TTL de caché de facets globales es de varios minutos para balancear frescura y rendimiento.
SemanticDecisionEngine → FlowState
SemanticDecisionEngine.Decide() evalúa los resultados del paso 2 y la clasificación
del paso 1 para producir un FlowState que determina qué bloque mostrar.
| FlowState | Condición | Decision final |
|---|---|---|
DirectAnswer | Extracto semántico con score >= DirectAnswerMinScore | CasoC (con bloque directAnswer visible) |
Results (servicio) | Dominancia >= umbral o QU confidence >= 0.70, intent es servicio | CasoA (isServicePath: true) |
Results (torneo) | Dominancia >= umbral o QU confidence >= 0.70, intent es torneo | CasoC (isServicePath: false) |
Filter1 | Hay docs pero dominancia ambigua (< AmbiguityDominanceThreshold) | CasoD (navegación por intents) |
EmptyCascade | Sin docs relevantes o intent detectado no tiene cobertura | CasoD (cascade + OtherOptions) |
La dominancia se calcula como: count(docs con intent top) / total_docs_semánticos.
Si el intent que detectó QU no aparece en los facets semánticos y confidence >= 0.70 → fuerza EmptyCascade.
EmptyCascade (fallback)
Cuando el FlowState es EmptyCascade, el orquestador construye un resultado alternativo:
- Si QU detectó entidades (equipos, ciudades, personas) → busca por esas entidades directamente
- Si no hay entidades → presenta OtherOptions con los intents de mayor cobertura del sitio
- El bloque
noContenten el response tieneshow: truecon mensaje y contenido relacionado
FilterSuggestions (chips de refinamiento)
Se construyen chips de refinamiento a partir de los facets semánticos del paso 2: top 4 intents de torneo + top 2 intents de servicio. También se agregan sugerencias QA generadas por QU si no están ya repetidas en los facets.
Estos chips aparecen en el response como filterSuggestions.semantic[]
y son opcionales para el frontend — funcionan como atajos para refinar el query activo.
Construcción del response
Según el FlowState del paso 4, el orquestador llama al constructor correspondiente:
BuildDirectAnswerResult()→ CasoC condirectAnswer.show: trueBuildResultsResult()→ CasoA o CasoC conresults.itemspobladosBuildFilter1Result()→ CasoD confilter1.show: truey opciones de intentBuildEmptyCascadeResultAsync()→ CasoD connoContent.show: true+ OtherOptions
Todos los constructores populan siempre filterSuggestions, availableCities
y debug independientemente del caso.
Tabla de decisiones completa
| Condición | FlowState | Decision | Bloques visibles |
|---|---|---|---|
| Extracto semántico >= DirectAnswerMinScore | DirectAnswer | CasoC | directAnswer, results |
| Top reranker >= StrongRerankerThreshold | Results | CasoA / CasoC | results, filterSuggestions |
| Dominancia >= AmbiguityDominanceThreshold + clarity | Results | CasoA / CasoC | results, filterSuggestions |
| QU confidence >= 0.70 + intent en facets | Results | CasoA / CasoC | results, filterSuggestions |
| Hay docs, dominancia ambigua | Filter1 | CasoD | filter1, results (parciales) |
| Sin docs relevantes o intent sin cobertura | EmptyCascade | CasoD | noContent, filter1.otherOptions |
POST /api/search
Ejecuta el flujo completo de búsqueda (7 pasos). Primera llamada del flujo — siempre con
userSelection: null. Retorna SearchResponse con todos los bloques
según la decisión del motor.
const res = await fetch('https://eventos.sacspro.com/api/search', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Api-Key': 'sf-postamx-a3b5c7d9', 'X-Site-Id': '61' }, body: JSON.stringify({ siteId: 61, query: "resultado mexico uruguay", activeCity: null, // ciudad activa del filtro (opcional) userSelection: null }) }) const data = await res.json()
Lógica del frontend — response inicial
// Respuesta directa extractiva (responde la pregunta directamente) if (data.directAnswer?.show) { renderDirectAnswer(data.directAnswer) } // Navegación por intents (Filter1) if (data.filter1.show) { renderFilter1(data.filter1) } // Resultados principales if (data.results.show) { renderResults(data.results) } // Sin resultados — mostrar mensaje + OtherOptions if (data.noContent.show) { renderNoContent(data.noContent) } // Chips de refinamiento (siempre presentes) renderFilterSuggestions(data.filterSuggestions) // Flujo terminado si no hay más filtros pendientes const flowDone = !data.filter1.show && !data.suggestFilter2.show
POST /api/search/refine
Llamar cuando el usuario selecciona una opción del Filter1 (intent).
Requiere userSelection no nulo. Devuelve resultados filtrados por ese intent
y, si hay entidades disponibles, muestra suggestFilter2.
filter1.show = true en el response anterior y el usuario hace clic
en una de las opciones de filter1.relatedOptions[] o filter1.otherOptions[].
const opcion = data.filter1.relatedOptions[0] const body = { siteId: 61, query: "resultado mexico uruguay", activeCity: null, userSelection: { selectedIntent: opcion.intent, // "Resultados y marcadores" isServiceIntent: opcion.isService, // false userExplicitlySelected: true, selectedTeam: null, selectedPlace: null, selectedPerson: null } }
Response tras elegir Filter1
false (ya fue elegido)suggestFilter2.show =
true si hay entidades disponibles (equipos, ciudades, personas)results.items contiene resultados filtrados por el intent elegido
POST /api/search/filter2
Filtro directo por intent + entidad (equipo, ciudad o persona) sin re-ejecutar
Query Understanding. Más eficiente que /refine cuando ya se conoce el
intent y la entidad exacta. Siempre retorna resultados finales
(filter1.show y suggestFilter2.show = false).
| Campo | Tipo | Descripción |
|---|---|---|
siteId | integer | ID del sitio |
intent | string | Intent del catálogo (ej: "Resultados y marcadores") |
isService | boolean | true si es intent de servicio |
activeCity | string? | Ciudad activa del filtro (CanonicalValue) |
entityTeam | string? | Equipo a filtrar (ej: "México"). Solo uno de team/place/person. |
entityPlace | string? | Ciudad/sede a filtrar (ej: "Monterrey") |
entityPerson | string? | Persona a filtrar (jugador, entrenador, etc.) |
{ "siteId": 61, "intent": "Resultados y marcadores", "isService": false, "activeCity": null, "entityTeam": "México", "entityPlace":null, "entityPerson":null }
GET /api/cities
Retorna la lista de ciudades disponibles para filtrar en este sitio.
Llamar al inicializar el widget para poblar el selector de ciudad.
No requiere parámetros — usa el X-Site-Id del header.
[ { "label": "Ciudad de México", "canonicalValue": "Ciudad de México" // usar este valor en activeCity }, { "label": "Monterrey", "canonicalValue": "Monterrey" } ]
GET /api/suggestions
Retorna sugerencias de autocompletado para el campo de búsqueda. Incluye sugerencias textuales, facets recientes y distribución de intents para mostrar al usuario antes de escribir.
| Parámetro | Tipo | Descripción |
|---|---|---|
city | string? | Filtrar sugerencias por ciudad activa (CanonicalValue) |
GET /api/today-news
Retorna las últimas 6 noticias del día del sitio, ordenadas por fecha de creación. Usado para poblar la pantalla de bienvenida del widget antes de que el usuario escriba.
GET /api/tag-cloud
Retorna una nube de tags con la distribución de documentos por intent del sitio. Usado para mostrar categorías al usuario en la pantalla inicial del widget.
GET /api/site-config
Retorna la configuración pública del sitio: template de bienvenida, flags habilitados (gamificación, newsletter, fixtures), ciudades disponibles. Llamar al inicializar el widget.
{ "siteId": 61, "welcomeTemplate": "template1", "gamificationEnabled":true, "newsletterEnabled": true, "showFixtures": true, "enableFeedback": false, "availableCities": [ { "label": "Ciudad de México", "canonicalValue": "Ciudad de México" } ] }
POST /api/subscribe
Suscribe un email al newsletter del sitio. Solo activo si NewsletterEnabled: true.
| Campo | Tipo | Descripción |
|---|---|---|
email | string | Email del suscriptor |
name | string? | Nombre (opcional) |
city | string? | Ciudad de interés |
siteId | integer | ID del sitio |
Response: { "ok": true, "alreadySubscribed": false }
POST /api/feedback
Registra feedback del usuario sobre los resultados de búsqueda.
Solo activo si EnableFeedback: true en la configuración del sitio.
POST /api/telemetry/*
Endpoints de tracking para análisis de comportamiento. El widget los llama automáticamente — no requieren integración manual adicional por parte del cliente.
/click — registra cuando el usuario hace clic en un resultado.
/zero-click — registra cuando la búsqueda no produjo ningún clic.
Modelo: SearchRequest
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
siteId | integer | Sí | ID del sitio |
query | string | Sí | Texto de búsqueda (máx 500 chars). Puede ser "*" para listar sin filtro de texto. |
activeCity | string? | No | Ciudad activa del filtro. Valor de canonicalValue retornado por /api/cities. Si cambia con query activa, el widget relanza la búsqueda. |
page | integer? | No | Página de resultados (default: 1). Solo aplica cuando directFilter: true. |
directFilter | boolean? | No | true para paginación directa sin re-ejecutar QU ni semántica. |
userSelection | UserSelection? | No | Siempre null en la primera búsqueda. Ver campos abajo. |
suggestionId | integer? | No | ID de sugerencia seleccionada del autocompletado. Usa caché si está disponible. |
fromQuestion | boolean? | No | true cuando el query viene de una pregunta relacionada del response anterior. |
Campos de UserSelection
| Campo | Tipo | Descripción |
|---|---|---|
selectedIntent | string | Intent elegido (valor de option.intent del filter1) |
isServiceIntent | boolean | true si es intent de servicio |
userExplicitlySelected | boolean | Siempre true cuando el usuario elige. Indica al servidor que use filtro OData exacto (sin OR flexible). |
selectedTeam | string? | Equipo seleccionado en Filter2 (null en Filter1) |
selectedPlace | string? | Ciudad/sede seleccionada en Filter2 |
selectedPerson | string? | Persona seleccionada en Filter2 |
Modelo: SearchResponse
| Campo | Tipo | Descripción |
|---|---|---|
siteId | integer | ID del sitio |
query | string | Query tal como fue recibido |
decision | string | CasoA | CasoC | CasoD. Identifica el tipo de resultado para analytics. |
decisionReason | string | Razón técnica de la decisión (ej: "dominant_facets:0.72") |
activeCity | string? | Ciudad activa del filtro aplicado |
activeCityLabel | string? | Label legible de la ciudad activa |
hasCityConflict | boolean | true si el filtro de ciudad activa no produjo resultados (la ciudad no tiene cobertura para este query) |
availableCities | CityDto[] | Lista de ciudades configuradas del sitio (igual a /api/cities) |
directAnswer | DirectAnswerDto? | Respuesta extractiva directa. Ver campos abajo. |
filter1 | Filter1Block | Navegación por intents. Ver campos abajo. |
suggestFilter2 | SuggestFilter2Dto | Sugerencia para refinar por entidad (equipo/ciudad/persona). Ver campos abajo. |
filterSuggestions | FilterSuggestionsDto | Chips de refinamiento por intent. Ver campos abajo. |
noContent | NoContentDto | Bloque de "sin resultados". Ver campos abajo. |
results | ResultsBlock | Resultados principales. Ver campos abajo. |
debug | DebugBlock | Información de diagnóstico (QU, métricas, filtro OData final) |
DirectAnswerDto
| Campo | Tipo | Descripción |
|---|---|---|
show | boolean | Mostrar el bloque de respuesta directa |
text | string | Texto de la respuesta extractiva |
highlights | string[] | Fragmentos destacados del texto fuente |
friendly | string? | Slug del documento fuente para construir la URL |
others | DocumentDto[] | Documentos relacionados complementarios |
Filter1Block
| Campo | Tipo | Descripción |
|---|---|---|
show | boolean | Mostrar el bloque de navegación por intents |
isNavigationStep | boolean? | true cuando el usuario navega sin query activo |
uiText | string? | Texto introductorio del bloque |
relatedTitle | string | Título para opciones relacionadas con el query (mayor relevancia) |
relatedOptions | Filter1Option[] | Opciones de intent relacionadas con el query actual |
otherTitle | string | Título para "otras categorías" (opciones de menor relevancia) |
otherOptions | Filter1Option[] | Resto de intents disponibles del sitio |
Filter1Option
| Campo | Tipo | Descripción |
|---|---|---|
number | integer | Número secuencial de la opción |
intent | string | Valor del intent (del catálogo) |
isService | boolean | true si es intent de servicio |
isRecommended | boolean | true si el motor recomienda esta opción |
documentCount | integer | Cantidad de documentos disponibles para este intent |
questionTemplate | string | Pregunta en lenguaje natural para mostrar al usuario |
SuggestFilter2Dto
| Campo | Tipo | Descripción |
|---|---|---|
show | boolean | Hay entidades disponibles para refinar |
intent | string? | Intent activo (para construir el request filter2) |
isService | boolean | true si el intent activo es de servicio |
FilterSuggestionsDto
| Campo | Tipo | Descripción |
|---|---|---|
semantic | SuggestionChip[] | Chips de intent primario/servicio (top facets semánticos). Siempre presentes. |
qa | SuggestionChip[] | Sugerencias de preguntas relacionadas generadas por QU |
NoContentDto
| Campo | Tipo | Descripción |
|---|---|---|
show | boolean | Mostrar bloque "sin resultados" |
detectedIntent | string? | Intent que detectó QU pero sin cobertura en el índice |
message | string | Mensaje de UI para el usuario |
cityFilterActive | boolean | true si el filtro de ciudad está activo (puede ser la causa de sin resultados) |
activeCityLabel | string? | Ciudad activa (para mostrar en el mensaje) |
relatedContent | DocumentDto[] | Contenido alternativo sugerido cuando no hay resultados exactos |
ResultsBlock
| Campo | Tipo | Descripción |
|---|---|---|
show | boolean | Mostrar bloque de resultados |
uiText | string | Texto introductorio (ej: "Esto es lo que encontré para…") |
total | integer | Cantidad de items en esta respuesta |
items | DocumentDto[] | Lista de documentos. El primer elemento (items[0]) se renderiza con card destacada. |
hasMore | boolean | true si hay más páginas disponibles |
isServicePath | boolean | true cuando el resultado es de tipo servicio (CasoA). Afecta el estilo visual del widget. |
Modelo: DocumentDto
Cada elemento en results.items, directAnswer.others y noContent.relatedContent:
| Campo | Tipo | Descripción |
|---|---|---|
id | string | ID único del documento en el índice |
title | string | Título de la nota |
intro | string? | Introducción breve (primeros ~150 chars) |
introSearch | string? | Variante de intro optimizada para búsqueda (puede diferir del intro editorial) |
intent | string? | Intent principal del documento (intentPrimary o intentService) |
intentPrimary | string? | Intent de torneo del documento |
intentTopics | string[] | Intents de torneo secundarios (sub-categorías) |
intentService | string? | Intent de servicio del documento |
intentServiceTopics | string[] | Intents de servicio secundarios |
isServiceContent | boolean | true = contenido de servicio local; false = contenido de torneo |
city | string? | Sede editorial del documento (ciudad donde ocurre el evento) |
state | string? | Estado/región relacionado |
entitiesTeams | string[] | Equipos mencionados en el contenido |
entitiesPlaces | string[] | Lugares mencionados en el contenido |
entitiesPeople | string[] | Personas mencionadas (jugadores, entrenadores, etc.) |
imagen | string? | URL de imagen principal |
friendly | string? | Slug amigable para construir la URL del artículo |
creationDate | datetime? | Fecha de publicación en ISO 8601 UTC |
isEvergreen | boolean | true = contenido atemporal (no penalizado por antigüedad en scoring) |
contentPriority | double? | Prioridad editorial (0–1). Usada por el scoring profile para boosting. |
score | double? | Score léxico de Azure Search (@search.score) |
rerankerScore | double? | Score del reranker semántico (0–4). Presente solo si semantic está activo. |
relatedQuestionsTitle | string? | Título del bloque de preguntas relacionadas |
relatedQuestions | string[] | Lista de preguntas relacionadas generadas editorialmente |
Modelo: Filter2Request
Request para POST /api/search/filter2:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
siteId | integer | Sí | ID del sitio |
intent | string | Sí | Intent del catálogo (ej: "Resultados y marcadores") |
isService | boolean | Sí | true si es intent de servicio |
activeCity | string? | No | Ciudad activa del filtro |
entityTeam | string? | No | Equipo a filtrar. Solo uno de team/place/person debe ser non-null. |
entityPlace | string? | No | Ciudad/sede a filtrar |
entityPerson | string? | No | Persona a filtrar |
Endpoints de Dashboard
Solo requieren el header X-Api-Key. No requieren X-Site-Id.
| Método | Endpoint | Descripción |
|---|---|---|
| GET | /api/dashboard/overview?siteId=61 |
KPIs: total noticias, tendencias activas, producción reciente |
| GET | /api/dashboard/stats?siteId=61 |
Cobertura de intents: documentos por categoría (torneo y servicio) |
| GET | /api/dashboard/production?siteId=61&days=15 |
Producción diaria por intent (heatmap de los últimos N días) |
| GET | /api/dashboard/coverage?siteId=61 |
Intents con baja cobertura (alertas editoriales) |
| GET | /api/dashboard/alerts?siteId=61&staleDays=7 |
Intents con contenido desactualizado hace más de N días |
| GET | /api/dashboard/sites |
Lista todos los sitios configurados (sin contenido sensible) |