Spaces:
Running
Running
| # API HTTP de Frimeet NLP | |
| Documentacion del contrato HTTP publicado por el servicio NLP de Frimeet. | |
| ## URLs base | |
| Produccion en Hugging Face Spaces: | |
| ```text | |
| https://alleksdev-frimeet-api-nlp.hf.space | |
| ``` | |
| Desarrollo local: | |
| ```text | |
| http://localhost:8080 | |
| ``` | |
| Documentacion interactiva: | |
| - Swagger UI: `GET /docs` | |
| - ReDoc: `GET /redoc` | |
| - Especificacion OpenAPI: `GET /openapi.json` | |
| Todas las peticiones y respuestas con body utilizan `application/json`. | |
| ## Resumen de endpoints | |
| | Metodo | Ruta | Proposito | Autenticacion | | |
| | --- | --- | --- | --- | | |
| | `GET` | `/` | Informacion basica del servicio | No | | |
| | `GET` | `/health` | Comprobar que el proceso responde | No | | |
| | `GET` | `/ready` | Comprobar dependencias y contrato pgvector | No | | |
| | `GET`, `POST` | `/places/search/metrics` | Evaluar el buscador con el benchmark integrado | No | | |
| | `POST` | `/places/search` | Buscar lugares semanticamente | No | | |
| | `POST` | `/places/recommendations` | Recomendar lugares y redactar una respuesta | No | | |
| | `POST` | `/places/chat` | Conversacion orientada a lugares | No | | |
| | `POST` | `/internal/places/chat` | Interpretar chat y devolver candidatos tecnicos | Servicio | | |
| | `POST` | `/posts/recommendations` | Recomendar publicaciones | No | | |
| | `POST` | `/internal/posts/feed/rank` | Ordenar allowlist de posts | Servicio | | |
| | `POST` | `/internal/posts/clusters/runs` | Agendar entrenamiento de clusters | Servicio | | |
| | `GET` | `/internal/posts/clusters/status` | Estado del run activo | Servicio | | |
| | `GET` | `/internal/posts/clusters/runs/{id}` | Detalle de un run de clusters | Servicio | | |
| | `POST` | `/internal/posts/clusters/runs/{id}/activate` | Activar/rollback de run | Servicio | | |
| | `POST` | `/search` | Busqueda global sobre todos los recursos | Condicional | | |
| | `POST` | `/internal/search/candidates` | Candidatos tecnicos para la API principal | Servicio | | |
| La autenticacion de `/search` solo es obligatoria cuando el body contiene | |
| `requester_id`. En ese caso se envia: | |
| ```http | |
| Authorization: Bearer <SEARCH_INTERNAL_TOKEN> | |
| ``` | |
| El token debe ser enviado por la API principal, no directamente por la aplicacion movil. | |
| ## 1. Endpoints del sistema | |
| ### `GET /` | |
| Devuelve informacion basica y enlaces del servicio. | |
| ```bash | |
| curl "https://alleksdev-frimeet-api-nlp.hf.space/" | |
| ``` | |
| Respuesta `200 OK`: | |
| ```json | |
| { | |
| "service": "frimeet-api-nlp", | |
| "status": "ok", | |
| "docs": "/docs", | |
| "health": "/health", | |
| "ready": "/ready" | |
| } | |
| ``` | |
| ### `GET /health` | |
| Indica que el proceso HTTP esta vivo. No comprueba PostgreSQL ni las funciones de | |
| pgvector. | |
| Respuesta `200 OK`: | |
| ```json | |
| { | |
| "status": "ok" | |
| } | |
| ``` | |
| ### `GET /ready` | |
| Comprueba que el servicio puede utilizar su almacenamiento vectorial. Cuando | |
| `VECTOR_STORE_PROVIDER=aws_pgvector`, valida: | |
| - disponibilidad de la extension `vector`; | |
| - existencia y permiso de ejecucion de `match_places(...)`; | |
| - existencia y permiso de ejecucion de `match_posts(...)`; | |
| - existencia y permiso de ejecucion de `search_resource_embeddings(...)`. | |
| - presencia de los filtros V2 (`event_active_at`, umbrales y parseo temporal seguro). | |
| Respuesta correcta: `200 OK` con `status: "ready"`. | |
| Si una dependencia o funcion SQL no esta disponible, responde `503 Service Unavailable` | |
| con `status: "not_ready"` y el detalle en `dependencies.vector_store.contract`. | |
| ## 2. Lugares | |
| ### Campos comunes de busqueda de lugares | |
| `POST /places/search` y `POST /places/recommendations` utilizan el mismo body base. | |
| | Campo | Tipo | Obligatorio | Default | Restricciones | | |
| | --- | --- | --- | --- | --- | | |
| | `query` | `string` | Si | - | 1 a 500 caracteres | | |
| | `city` | `string \| null` | No | `null` | Maximo 80 caracteres | | |
| | `state` | `string \| null` | No | `null` | Maximo 80 caracteres | | |
| | `filters` | `object` | No | `{}` | Filtros adicionales | | |
| | `limit` | `integer` | No | `10` | Entre 1 y 20 | | |
| | `lat` | `number \| null` | No | `null` | Entre -90 y 90 | | |
| | `lng` | `number \| null` | No | `null` | Entre -180 y 180 | | |
| | `radius` | `integer` | No | `5000` | Entre 1 y 50000 metros | | |
| `lat` y `lng` deben enviarse juntos. Si se omite cualquiera de los dos, la API responde | |
| `422 Unprocessable Entity`. | |
| Campos de `filters`: | |
| | Campo | Tipo | Default | | |
| | --- | --- | --- | | |
| | `city` | `string \| null` | `null` | | |
| | `state` | `string \| null` | `null` | | |
| | `category` | `string \| null` | `null` | | |
| | `price_range` | `string \| null` | `null` | | |
| | `is_active` | `boolean \| null` | `true` | | |
| | `occasion` | `string \| null` | `null` | | |
| Si `city` o `state` aparecen tanto en el nivel principal como dentro de `filters`, tiene | |
| prioridad el valor del nivel principal. | |
| ### `POST /places/search` | |
| Realiza recuperacion semantica de lugares mediante FastText y pgvector. Llama a la API | |
| principal para obtener un allowlist de lugares cercanos cuando se incluyen coordenadas. | |
| No utiliza Llama/Groq para elegir resultados. | |
| Ejemplo: | |
| ```bash | |
| curl -X POST "https://alleksdev-frimeet-api-nlp.hf.space/places/search" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "query": "cafeteria tranquila para estudiar", | |
| "city": "Tuxtla Gutierrez", | |
| "filters": { | |
| "category": "cafeteria", | |
| "is_active": true | |
| }, | |
| "limit": 5 | |
| }' | |
| ``` | |
| Ejemplo con filtro geografico: | |
| ```json | |
| { | |
| "query": "un lugar tranquilo para cenar", | |
| "lat": 16.7531, | |
| "lng": -93.1156, | |
| "radius": 10000, | |
| "limit": 5 | |
| } | |
| ``` | |
| Respuesta `200 OK`: | |
| ```json | |
| { | |
| "query": "cafeteria tranquila para estudiar", | |
| "places": [ | |
| { | |
| "id": "place-id", | |
| "name": "Nombre del lugar", | |
| "score": 0.7312, | |
| "category": "cafeteria", | |
| "city": "Tuxtla Gutierrez", | |
| "state": "Chiapas", | |
| "metadata": {} | |
| } | |
| ], | |
| "metrics": { | |
| "engine": "fasttext_mean_embeddings", | |
| "candidate_retrieval": "pgvector", | |
| "score_metric": "cosine_similarity", | |
| "field_weights": { | |
| "tags": 6, | |
| "category": 4, | |
| "description": 3, | |
| "name": 1 | |
| }, | |
| "ranking_parameters": { | |
| "dimension": 300.0 | |
| }, | |
| "relevance_threshold": 0.5, | |
| "match_quality": "confident", | |
| "query_token_count": 4, | |
| "matched_query_token_count": 4, | |
| "query_coverage": 1.0, | |
| "scope": "request", | |
| "ground_truth_available": false, | |
| "candidate_count": 5, | |
| "returned_count": 5, | |
| "nonzero_score_count": 5, | |
| "min_score": 0.42, | |
| "max_score": 0.7312, | |
| "mean_score": 0.57, | |
| "location_filter_applied": false, | |
| "nearby_place_count": null, | |
| "radius_meters": null | |
| } | |
| } | |
| ``` | |
| `match_quality` puede ser `confident`, `low_confidence` o `no_match`. | |
| ### `POST /places/recommendations` | |
| Busca lugares con el mismo flujo de `/places/search` y despues utiliza Llama/Groq para | |
| redactar un mensaje conversacional. El LLM solo recibe los lugares recuperados; no elige | |
| lugares adicionales ni puede inventar IDs. | |
| Body: igual al de `POST /places/search`. | |
| ```bash | |
| curl -X POST "https://alleksdev-frimeet-api-nlp.hf.space/places/recommendations" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "query": "quiero una cena romantica", | |
| "city": "Tuxtla Gutierrez", | |
| "limit": 5 | |
| }' | |
| ``` | |
| Respuesta `200 OK`: | |
| ```json | |
| { | |
| "query": "quiero una cena romantica", | |
| "message": "Encontre algunas opciones que pueden interesarte.", | |
| "places": [], | |
| "metrics": { | |
| "engine": "fasttext_mean_embeddings", | |
| "candidate_retrieval": "pgvector", | |
| "score_metric": "cosine_similarity", | |
| "field_weights": { | |
| "tags": 6, | |
| "category": 4, | |
| "description": 3, | |
| "name": 1 | |
| }, | |
| "ranking_parameters": { | |
| "dimension": 300.0 | |
| }, | |
| "relevance_threshold": 0.5, | |
| "match_quality": "confident", | |
| "query_token_count": 4, | |
| "matched_query_token_count": 4, | |
| "query_coverage": 1.0, | |
| "scope": "request", | |
| "ground_truth_available": false, | |
| "candidate_count": 5, | |
| "returned_count": 5, | |
| "nonzero_score_count": 5, | |
| "min_score": 0.42, | |
| "max_score": 0.7312, | |
| "mean_score": 0.57, | |
| "location_filter_applied": false, | |
| "nearby_place_count": null, | |
| "radius_meters": null | |
| }, | |
| "metadata": { | |
| "strategy": "pgvector_candidates_plus_fasttext_mean_embeddings", | |
| "ranking": "fasttext_mean_embeddings", | |
| "response_mode": "confident", | |
| "relevance_threshold": 0.5, | |
| "llm_provider": "groq", | |
| "llm_model": "llama-3.1-8b-instant", | |
| "used_llm": true, | |
| "guard_reason": null, | |
| "places_used_as_context": [], | |
| "timestamp": "2026-07-04T12:00:00+00:00" | |
| } | |
| } | |
| ``` | |
| `metrics` tiene el mismo esquema completo mostrado en `/places/search`. | |
| ### `POST /places/chat` | |
| Endpoint conversacional orientado a lugares. | |
| Body: | |
| | Campo | Tipo | Obligatorio | Default | Restricciones | | |
| | --- | --- | --- | --- | --- | | |
| | `message` | `string` | Si | - | 1 a 1000 caracteres | | |
| | `city` | `string \| null` | No | `null` | Maximo 80 caracteres | | |
| | `state` | `string \| null` | No | `null` | Maximo 80 caracteres | | |
| | `filters` | `object` | No | `{}` | Mismo esquema de filtros de lugares | | |
| | `limit` | `integer` | No | `5` | Entre 1 y 8 | | |
| Ejemplo: | |
| ```json | |
| { | |
| "message": "Quiero salir con amigos a un lugar con musica", | |
| "city": "Tuxtla Gutierrez", | |
| "limit": 5 | |
| } | |
| ``` | |
| Respuesta: | |
| ```json | |
| { | |
| "response_id": "uuid-de-respuesta", | |
| "nlp_trace_id": "uuid-de-traza", | |
| "message": "Estas opciones pueden funcionar para tu salida.", | |
| "places": [], | |
| "metadata": {} | |
| } | |
| ``` | |
| ### `GET|POST /places/search/metrics` | |
| Ejecuta el benchmark offline integrado para evaluar la calidad del buscador de lugares. | |
| No evalua una consulta enviada por el usuario y no acepta body. | |
| Query parameter: | |
| | Parametro | Tipo | Default | Restricciones | | |
| | --- | --- | --- | --- | | |
| | `k` | `integer` | `5` | Entre 1 y 20 | | |
| Ambos metodos son equivalentes: | |
| ```bash | |
| curl "https://alleksdev-frimeet-api-nlp.hf.space/places/search/metrics?k=5" | |
| ``` | |
| La respuesta incluye: | |
| - `engine`, `benchmark` y `qrels_source`; | |
| - `precision_at_k`, `recall_at_k`, `mrr`, `map` y `ndcg_at_k` agregados; | |
| - metricas individuales de cada consulta del benchmark; | |
| - definiciones de las metricas; | |
| - `recommended_metric`, actualmente nDCG@k. | |
| ### `POST /internal/places/chat` | |
| Endpoint V2 consumido exclusivamente por la API principal. La app movil nunca debe | |
| llamarlo directamente. | |
| ```http | |
| Authorization: Bearer <NLP_SERVICE_TOKEN> | |
| Content-Type: application/json | |
| ``` | |
| Request minimo: | |
| ```json | |
| { | |
| "conversation_id": "cb3456ef-598e-49d9-9bf9-b2ba99055ad7", | |
| "turn": 1, | |
| "message": "recomiendame una cafeteria cerca del Parque Central", | |
| "state": {}, | |
| "user_location": {"lat": 16.7531, "lng": -93.1156}, | |
| "candidate_limit": 30, | |
| "result_limit": 5 | |
| } | |
| ``` | |
| La respuesta devuelve `action`, mensaje, `state_patch`, `location_directive`, IDs y | |
| scores tecnicos. No devuelve cards, coordenadas ni metadata privada. `clarification` y | |
| `no_match` siempre tienen `candidates=[]`. Go debe fusionar el patch de forma atomica, | |
| hidratar los IDs, revalidar el anchor y aplicar distancia/PostGIS antes de responder a | |
| la app. | |
| `state` puede incluir `target_category`, `hard_filters`, `soft_preferences`, | |
| `exclusions`, `reference`, `explicit_target_location` y `pending_clarification`. Si | |
| `taxonomy_version` no coincide con la version desplegada, el endpoint responde `409`. | |
| Cuando `action="clarification"`, la respuesta incluye entre 2 y 5 botones y persiste | |
| la allowlist correspondiente en `state_patch.pending_clarification`: | |
| ```json | |
| { | |
| "action": "clarification", | |
| "message": "¿Como quieres usar Parque Central?", | |
| "state_patch": { | |
| "target_category": "cafe", | |
| "pending_clarification": { | |
| "id": "22222222-2222-4222-8222-222222222222", | |
| "kind": "location_scope", | |
| "options": [ | |
| { | |
| "id": "target_results", | |
| "value": "target_results", | |
| "label": "Buscar cerca de Parque Central", | |
| "place_id": null, | |
| "attributes": [] | |
| }, | |
| { | |
| "id": "reference_entity", | |
| "value": "reference_entity", | |
| "label": "Usarlo solo para identificar la referencia", | |
| "place_id": null, | |
| "attributes": [] | |
| } | |
| ], | |
| "location_anchor_text": "parque central", | |
| "radius_meters": null, | |
| "strict_radius": false | |
| } | |
| }, | |
| "candidates": [], | |
| "clarification": { | |
| "id": "22222222-2222-4222-8222-222222222222", | |
| "kind": "location_scope", | |
| "prompt": "¿Como quieres usar Parque Central?", | |
| "options": [ | |
| { | |
| "id": "target_results", | |
| "label": "Buscar cerca de Parque Central", | |
| "message": "Buscar cerca de Parque Central" | |
| }, | |
| { | |
| "id": "reference_entity", | |
| "label": "Usarlo solo para identificar la referencia", | |
| "message": "Usarlo solo para identificar la referencia" | |
| } | |
| ] | |
| } | |
| } | |
| ``` | |
| Para resolverla, Go reenvia el estado persistido y una seleccion estructurada: | |
| ```json | |
| { | |
| "conversation_id": "cb3456ef-598e-49d9-9bf9-b2ba99055ad7", | |
| "turn": 2, | |
| "message": "Buscar cerca de Parque Central", | |
| "clarification_choice": { | |
| "clarification_id": "22222222-2222-4222-8222-222222222222", | |
| "option_id": "target_results" | |
| }, | |
| "state": { | |
| "target_category": "cafe", | |
| "pending_clarification": { | |
| "id": "22222222-2222-4222-8222-222222222222", | |
| "kind": "location_scope", | |
| "options": [ | |
| { | |
| "id": "target_results", | |
| "value": "target_results", | |
| "label": "Buscar cerca de Parque Central", | |
| "place_id": null, | |
| "attributes": [] | |
| }, | |
| { | |
| "id": "reference_entity", | |
| "value": "reference_entity", | |
| "label": "Usarlo solo para identificar la referencia", | |
| "place_id": null, | |
| "attributes": [] | |
| } | |
| ], | |
| "location_anchor_text": "parque central", | |
| "radius_meters": null, | |
| "strict_radius": false | |
| }, | |
| "taxonomy_version": "places-taxonomy-v1" | |
| }, | |
| "user_location": {"lat": 16.7531, "lng": -93.1156}, | |
| "candidate_limit": 30, | |
| "result_limit": 5 | |
| } | |
| ``` | |
| NLP compara ambos IDs con la allowlist del estado, consume la aclaracion en el mismo | |
| turno y devuelve `state_patch.pending_clarification=null`. Una seleccion inexistente, | |
| obsoleta o repetida responde `409`; no se interpreta por similitud con `message`. | |
| Ejemplo de directiva sin ubicacion explicita: | |
| ```json | |
| { | |
| "source": "user_current", | |
| "scope": "user_current_location", | |
| "anchor_place_id": null, | |
| "anchor_text": null, | |
| "radius_meters": null, | |
| "strict_radius": false | |
| } | |
| ``` | |
| Para `cafeterias como la de Hello Kitty cerca del Parque Central`, NLP no elige en | |
| silencio entre usar el parque como zona de resultados o como ayuda para identificar la | |
| referencia: devuelve `action=clarification`, `unresolved=["location_scope"]` y conserva | |
| el contexto no ambiguo en `state_patch`. | |
| No es obligatorio escribir una categoria literal. El parser combina defaults de alta | |
| precision con un clasificador local de prototipos FastText: `quiero comer algo` se | |
| interpreta como `target_category=restaurant`, y formulaciones semanticamente cercanas | |
| pueden obtener la misma categoria aunque no coincidan palabra por palabra. El | |
| clasificador exige similitud minima y separacion frente a la segunda categoria; una | |
| frase abierta como `quiero salir` aun solicita aclaracion. Una categoria explicita tiene | |
| prioridad y una nueva intencion clara elimina un `pending_clarification` obsoleto. | |
| ## 3. Publicaciones | |
| ### `POST /posts/recommendations` | |
| Recupera publicaciones relacionadas semanticamente con una consulta. | |
| Body: | |
| | Campo | Tipo | Obligatorio | Default | Restricciones | | |
| | --- | --- | --- | --- | --- | | |
| | `query` | `string` | Si | - | 1 a 500 caracteres | | |
| | `city` | `string \| null` | No | `null` | Maximo 80 caracteres | | |
| | `limit` | `integer` | No | `10` | Entre 1 y 20 | | |
| Ejemplo: | |
| ```bash | |
| curl -X POST "https://alleksdev-frimeet-api-nlp.hf.space/posts/recommendations" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "query": "personas que quieran jugar futbol", | |
| "city": "Tuxtla Gutierrez", | |
| "limit": 10 | |
| }' | |
| ``` | |
| Respuesta: | |
| ```json | |
| { | |
| "query": "personas que quieran jugar futbol", | |
| "posts": [ | |
| { | |
| "id": "post-id", | |
| "title": "Partido de futbol este sabado", | |
| "score": 0.69, | |
| "city": "Tuxtla Gutierrez", | |
| "tags": ["futbol", "deporte"], | |
| "metadata": {} | |
| } | |
| ], | |
| "metadata": {} | |
| } | |
| ``` | |
| ### `POST /internal/posts/feed/rank` | |
| Endpoint servicio-a-servicio. Requiere: | |
| ```http | |
| Authorization: Bearer <NLP_SERVICE_TOKEN> | |
| ``` | |
| La API Go envia exclusivamente candidatos ya autorizados. NLP nunca introduce IDs | |
| externos y devuelve también candidatos sin embedding con score técnico de cold start. | |
| ```json | |
| { | |
| "user_id": "uuid", | |
| "candidate_posts": [ | |
| { | |
| "post_id": "uuid", | |
| "author_id": "uuid", | |
| "created_at": "2026-07-05T12:00:00Z", | |
| "social_affinity": 1.0, | |
| "engagement_score": 0.4, | |
| "author_affinity": 0.0 | |
| } | |
| ], | |
| "snapshot_at": "2026-07-05T12:01:00Z", | |
| "result_limit": 500 | |
| } | |
| ``` | |
| Respuesta: | |
| ```json | |
| { | |
| "items": [ | |
| { | |
| "post_id": "uuid", | |
| "score": 0.87, | |
| "cluster_id": 12 | |
| } | |
| ], | |
| "ranking_version": "feed-v1", | |
| "cluster_run_id": "uuid", | |
| "cold_start": false, | |
| "missing_embedding_count": 0, | |
| "diversity_relaxations": 0, | |
| "duplicate_penalized_count": 0 | |
| } | |
| ``` | |
| ### Operación interna de clusters | |
| ```http | |
| POST /internal/posts/clusters/runs | |
| GET /internal/posts/clusters/status | |
| GET /internal/posts/clusters/runs/{run_id} | |
| POST /internal/posts/clusters/runs/{run_id}/activate | |
| Authorization: Bearer <NLP_SERVICE_TOKEN> | |
| ``` | |
| `POST /internal/posts/clusters/runs` agenda el entrenamiento y responde `202 Accepted`: | |
| ```json | |
| { | |
| "status": "scheduled", | |
| "message": "entrenamiento enviado al worker en background" | |
| } | |
| ``` | |
| `GET /internal/posts/clusters/runs/{run_id}` devuelve estado, metricas y error si fallo. | |
| El entrenamiento normal debe ejecutarse con `python -m app.jobs.train_post_clusters`; | |
| los endpoints son controles administrativos. Si `KMEANS_AUTO_ACTIVATE=false`, primero | |
| queda en `validated` y despues se activa con el endpoint de activate. | |
| ## 4. Busqueda global | |
| ### `POST /search` | |
| Busca en paralelo sobre: | |
| - `places`; | |
| - `posts`; | |
| - `users`; | |
| - `clubs`; | |
| - `groups`; | |
| - `events`. | |
| Calcula una sola representacion FastText de la consulta. Para `places` utiliza el mismo | |
| motor que `/places/recommendations`: similitud coseno mediante `match_places(...)`, sin | |
| fusion lexical ni RRF. Para `posts`, `users`, `clubs`, `groups` y `events` combina | |
| busqueda semantica pgvector con full-text search de PostgreSQL mediante Reciprocal Rank | |
| Fusion (RRF). | |
| No recibe query parameters. Toda la entrada se envia en el body. | |
| #### Body | |
| | Campo | Tipo | Obligatorio | Default | Restricciones | | |
| | --- | --- | --- | --- | --- | | |
| | `query` | `string` | Si | - | 1 a 500 caracteres | | |
| | `resource_types` | `string[] \| null` | No | Todos | Solo tipos enumerados | | |
| | `per_type_limit` | `integer` | No | `5` | Entre 1 y 20 | | |
| | `top_limit` | `integer` | No | `10` | Entre 1 y 50 | | |
| | `requester_id` | `UUID \| null` | No | `null` | Activa busqueda privada | | |
| | `cursors` | `object` | No | `{}` | Cursor opaco por tipo de recurso | | |
| | `location` | `object \| null` | No | `null` | Ubicacion y radio del usuario | | |
| | `filters` | `object` | No | `{}` | Filtros tipados por recurso | | |
| No se aceptan campos adicionales. | |
| En la primera solicitud se omite `cursors`. La respuesta incluye un bloque | |
| `pagination` independiente para cada recurso consultado. Cuando `has_more` es `true`, | |
| `next_cursor` se envia en la siguiente solicitud usando la misma consulta. | |
| #### Ubicacion | |
| `location` consulta una sola vez `GET /api/v1/places/nearby` en la API principal. Las | |
| coordenadas siguen siendo propiedad de la API principal y no se almacenan en pgvector. | |
| | Campo | Tipo | Obligatorio | Default | Restricciones | | |
| | --- | --- | --- | --- | --- | | |
| | `lat` | `number` | Si | - | Entre -90 y 90 | | |
| | `lng` | `number` | Si | - | Entre -180 y 180 | | |
| | `radius` | `integer` | No | `5000` | Entre 1 y 50000 metros | | |
| | `mode` | `string` | No | `prioritize` | `prioritize` o `strict` | | |
| - `prioritize`: aumenta solamente el ranking interno de lugares cercanos, sin modificar | |
| su `score` semantico. | |
| - `strict`: excluye lugares, clubs presenciales y eventos cuyo `place_id` no este dentro | |
| del radio. | |
| - La ubicacion aplica a `places`, `clubs` y `events`. No se inventa proximidad para | |
| usuarios, posts o grupos porque esos indices no contienen una relacion geografica | |
| verificada. | |
| #### Filtros | |
| Todos los campos son opcionales. Cada filtro se aplica solamente a los recursos que | |
| poseen esa señal. | |
| | Campo | Recursos | Tipo | Descripcion | | |
| | --- | --- | --- | --- | | |
| | `city` | places, posts | `string` | Coincidencia exacta sin distinguir mayusculas | | |
| | `state` | places, posts | `string` | Estado o region | | |
| | `categories` | places, clubs | `string[]` | Una o varias categorias admitidas | | |
| | `price_ranges` | places | `string[]` | Rangos como `$`, `$$` o `$$$` | | |
| | `tags` | places, posts, events | `string[]` | Debe coincidir al menos un tag | | |
| | `published_from` | posts | `datetime` | Fecha minima de publicacion ISO 8601 | | |
| | `published_to` | posts | `datetime` | Fecha maxima de publicacion ISO 8601 | | |
| | `event_from` | events | `datetime` | Inicio minimo del evento | | |
| | `event_to` | events | `datetime` | Inicio maximo del evento | | |
| | `club_mode` | clubs | `string` | `online` o `in_person` | | |
| | `user_roles` | users | `string[]` | Roles admitidos, por ejemplo `cliente` | | |
| Ejemplo completo: | |
| ```json | |
| { | |
| "query": "actividad para conocer personas", | |
| "resource_types": ["places", "clubs", "events", "posts"], | |
| "per_type_limit": 5, | |
| "top_limit": 12, | |
| "location": { | |
| "lat": 16.7531, | |
| "lng": -93.1156, | |
| "radius": 10000, | |
| "mode": "prioritize" | |
| }, | |
| "filters": { | |
| "city": "Tuxtla Gutierrez", | |
| "categories": ["cafe", "community"], | |
| "price_ranges": ["$", "$$"], | |
| "tags": ["musica", "cultura"], | |
| "published_from": "2026-07-01T00:00:00Z", | |
| "event_from": "2026-07-04T00:00:00Z", | |
| "event_to": "2026-08-04T23:59:59Z", | |
| "club_mode": "in_person" | |
| } | |
| } | |
| ``` | |
| #### Busqueda publica | |
| No requiere `Authorization`. | |
| ```bash | |
| curl -X POST "https://alleksdev-frimeet-api-nlp.hf.space/search" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "query": "ajedrez universitario", | |
| "resource_types": ["clubs", "events", "users"], | |
| "per_type_limit": 5, | |
| "top_limit": 10 | |
| }' | |
| ``` | |
| #### Busqueda con contexto de usuario | |
| Cuando se incluye `requester_id`, la API principal debe agregar el Bearer token interno: | |
| ```bash | |
| curl -X POST "https://alleksdev-frimeet-api-nlp.hf.space/search" \ | |
| -H "Content-Type: application/json" \ | |
| -H "Authorization: Bearer $SEARCH_INTERNAL_TOKEN" \ | |
| -d '{ | |
| "query": "amigos de la universidad", | |
| "resource_types": ["groups"], | |
| "requester_id": "00000000-0000-0000-0000-000000000001", | |
| "per_type_limit": 5, | |
| "top_limit": 10 | |
| }' | |
| ``` | |
| Si falta el token, es incorrecto o no esta configurado en el servicio, responde | |
| `403 Forbidden`. | |
| `requester_id` nunca debe confiarse directamente desde la app movil. La API principal | |
| debe obtenerlo de la sesion autenticada y construir la llamada interna. | |
| #### Respuesta | |
| ```json | |
| { | |
| "query": "ajedrez universitario", | |
| "normalized_query": "ajedrez universitario", | |
| "top_results": [ | |
| { | |
| "id": "club-id", | |
| "resource_type": "clubs", | |
| "title": "Club de Ajedrez Universitario", | |
| "subtitle": "ajedrez", | |
| "score": 0.92, | |
| "semantic_score": 0.81, | |
| "lexical_score": 0.38, | |
| "is_nearby": true, | |
| "proximity_boost": 0.12, | |
| "metadata": {} | |
| } | |
| ], | |
| "sections": { | |
| "clubs": [ | |
| { | |
| "id": "club-id", | |
| "resource_type": "clubs", | |
| "title": "Club de Ajedrez Universitario", | |
| "subtitle": "ajedrez", | |
| "score": 0.92, | |
| "semantic_score": 0.81, | |
| "lexical_score": 0.38, | |
| "is_nearby": true, | |
| "proximity_boost": 0.12, | |
| "metadata": {} | |
| } | |
| ], | |
| "events": [], | |
| "users": [] | |
| }, | |
| "pagination": { | |
| "clubs": { | |
| "page_size": 5, | |
| "returned_count": 1, | |
| "has_more": false, | |
| "next_cursor": null | |
| }, | |
| "events": { | |
| "page_size": 5, | |
| "returned_count": 0, | |
| "has_more": false, | |
| "next_cursor": null | |
| }, | |
| "users": { | |
| "page_size": 5, | |
| "returned_count": 0, | |
| "has_more": false, | |
| "next_cursor": null | |
| } | |
| }, | |
| "metadata": { | |
| "strategy": "parallel_hybrid_fasttext_full_text_rrf", | |
| "queried_resources": ["clubs", "events", "users"], | |
| "failed_resources": {}, | |
| "embedding_computed_once": true | |
| } | |
| } | |
| ``` | |
| `top_results` es una seleccion global diversificada. `sections` conserva los resultados | |
| separados por tipo de recurso. | |
| #### Solicitar la siguiente pagina | |
| Cada recurso se pagina de manera independiente. Para cargar mas lugares, la interfaz | |
| debe reutilizar exactamente `query`, declarar `resource_types: ["places"]` y enviar el | |
| cursor recibido en `pagination.places.next_cursor`: | |
| ```json | |
| { | |
| "query": "cafeteria tranquila", | |
| "resource_types": ["places"], | |
| "per_type_limit": 5, | |
| "top_limit": 5, | |
| "cursors": { | |
| "places": "CURSOR_DEVUELTO_POR_LA_PAGINA_ANTERIOR" | |
| } | |
| } | |
| ``` | |
| La respuesta de cada seccion contiene: | |
| | Campo | Descripcion | | |
| | --- | --- | | |
| | `page_size` | Limite solicitado para esa pagina | | |
| | `returned_count` | Cantidad realmente devuelta | | |
| | `has_more` | Indica si existe al menos otra pagina | | |
| | `next_cursor` | Cursor opaco de continuacion o `null` si termino | | |
| Reglas de los cursores: | |
| - estan asociados al tipo de recurso, consulta, filtros, ubicacion, usuario y tamano de pagina; | |
| - no deben interpretarse ni construirse en la app cliente; | |
| - no pueden reutilizarse con otra consulta o con otro recurso; | |
| - cuando se envia `cursors`, `resource_types` es obligatorio y debe contener sus claves; | |
| - se pueden pedir varias continuaciones en una llamada enviando un cursor por recurso. | |
| El contrato SQL utiliza el ID externo como desempate estable cuando dos resultados tienen | |
| el mismo puntaje. Esto evita cambios arbitrarios de orden entre paginas consecutivas. | |
| Si falla un proveedor individual, los demas pueden responder normalmente. El recurso | |
| fallido aparece en `metadata.failed_resources`. | |
| #### Privacidad | |
| - Sin `requester_id` solo se consideran recursos activos y publicos. | |
| - Con `requester_id`, un grupo privado puede aparecer si el usuario es creador, miembro | |
| agregado o invitado segun `authorized_user_ids`. | |
| - `creator_id` y `authorized_user_ids` se utilizan internamente, pero se eliminan de la | |
| metadata enviada al cliente. | |
| ## 5. Errores comunes | |
| | Estado | Significado | | |
| | --- | --- | | |
| | `403` | Falta o es incorrecto el Bearer token de una busqueda con `requester_id` | | |
| | `413` | El body supera el limite configurado por `MAX_REQUEST_BODY_BYTES` | | |
| | `422` | Body, UUID, coordenadas, limites o query parameters invalidos | | |
| | `503` | PostgreSQL, pgvector o una funcion SQL requerida no esta disponible | | |
| Ejemplo de error de validacion `422`: | |
| ```json | |
| { | |
| "detail": [ | |
| { | |
| "type": "missing", | |
| "loc": ["body", "query"], | |
| "msg": "Field required" | |
| } | |
| ] | |
| } | |
| ``` | |
| ## 6. Candidatos internos de busqueda | |
| ### `POST /internal/search/candidates` | |
| Endpoint de servicio consumido exclusivamente por la API principal. La app movil no | |
| debe llamarlo directamente. | |
| ```http | |
| Authorization: Bearer <NLP_SERVICE_TOKEN> | |
| Content-Type: application/json | |
| ``` | |
| Ejemplo minimo: | |
| ```json | |
| { | |
| "query": "club universitario de ajedrez", | |
| "resource_types": ["clubs", "groups", "events", "posts"], | |
| "candidate_limit_per_type": 50, | |
| "top_limit": 100, | |
| "as_of": "2026-07-12T14:00:00Z" | |
| } | |
| ``` | |
| La respuesta contiene secciones independientes con `id`, `resource_type`, `score`, | |
| `semantic_score` y `lexical_score`. No incluye metadata de UI ni registros hidratados. | |
| La API principal aplica permisos y reglas de negocio, carga los recursos vigentes y | |
| conserva el orden del ranking. | |
| NLP aplica antes del `LIMIT` los umbrales configurados. Un candidato se acepta cuando | |
| supera el umbral semantico **o** el lexical; si ninguno lo supera se descarta y no se | |
| rellena la respuesta. Para eventos tambien exige que | |
| `start_time + duration_minutes > as_of`, evitando calcular ranking sobre eventos ya | |
| finalizados. Los cursores quedan ligados a query, recursos, filtros, ubicacion, | |
| `as_of` y version de la politica de umbrales. | |
| ## 7. Variables relacionadas | |
| | Variable | Uso | | |
| | --- | --- | | |
| | `SEARCH_INTERNAL_TOKEN` | Valida `Authorization: Bearer ...` cuando `/search` recibe `requester_id` | | |
| | `NLP_SERVICE_TOKEN` | Autentica `/internal/posts/*`, `/internal/search/candidates` y `/internal/places/chat`; debe coincidir con Go | | |
| | `MAIN_API_INTERNAL_TOKEN` | Autentica jobs y resolucion de anchors al consumir endpoints internos de Go | | |
| | `MAIN_API_PLACE_ANCHOR_RESOLVE_PATH` | Ruta interna Go para resolver nombres de lugares usados como anchors o referencias | | |
| | `PLACES_CHAT_V2_ENABLED` | Feature flag del chat interno; debe iniciar en `false` | | |
| | `PLACES_CHAT_LLM_ENABLED` | Habilita solo la redaccion opcional; no cambia candidatos ni accion | | |
| | `PLACES_CHAT_CANDIDATE_LIMIT` | Maximo tecnico de candidatos de contenido, entre 1 y 40 | | |
| | `PLACES_CHAT_MIN_CONTENT_SCORE` | Umbral minimo antes de devolver candidatos | | |
| | `PLACES_CHAT_INTENT_MIN_CONFIDENCE` | Confianza minima; por debajo se solicita aclaracion | | |
| | `PLACES_CHAT_AMBIGUITY_DELTA` | Diferencia maxima para considerar ambiguos dos anchors | | |
| | `PLACES_CHAT_RANKING_VERSION` | Version observable de la politica de ranking | | |
| | `PLACES_CHAT_TAXONOMY_VERSION` | Version del parser y del estado conversacional | | |
| | `MAX_REQUEST_BODY_BYTES` | Debe ser al menos `131072`; valor recomendado `262144` para 500 candidatos | | |
| | `REQUEST_TIMEOUT_SECONDS` | Presupuesto maximo para search interno y timeout de conexion/consulta pgvector | | |
| | `RATE_LIMIT_REQUESTS_PER_WINDOW` | Limite local por IP/ruta para endpoints publicos | | |
| | `INTERNAL_RATE_LIMIT_REQUESTS_PER_WINDOW` | Limite local por IP/ruta para endpoints internos | | |
| | `RATE_LIMIT_WINDOW_SECONDS` | Ventana del limitador local; el gateway puede agregar un limite distribuido | | |
| | `VECTOR_STORE_PROVIDER` | Selecciona `aws_pgvector` o el proveedor mock | | |
| | `PGVECTOR_*` | Conexion y roles de PostgreSQL/pgvector | | |
| | `EMBEDDING_PROVIDER` | Proveedor de embeddings; produccion utiliza `fasttext` | | |
| | `EMBEDDING_DIMENSION` | Dimension vectorial; FastText utiliza `300` | | |
| | `GROQ_API_KEY` | Habilita Groq/Llama para redactar respuestas conversacionales | | |
| | `MAIN_API_BASE_URL` | API principal usada para fuentes y filtros geograficos | | |
| | `MAIN_API_USERS_SNAPSHOT_PATH` | Snapshot interno paginado de usuarios para search | | |
| | `MAIN_API_CLUBS_SNAPSHOT_PATH` | Snapshot interno paginado de clubs para search | | |
| | `MAIN_API_GROUPS_SNAPSHOT_PATH` | Snapshot interno paginado de grupos para search | | |
| | `MAIN_API_EVENTS_SNAPSHOT_PATH` | Snapshot interno paginado de eventos para search | | |
| | `MAIN_API_PLACES_NEARBY_PATH` | Endpoint que resuelve los IDs dentro del radio solicitado | | |
| | `GLOBAL_SEARCH_NEARBY_BOOST` | Peso de priorizacion geografica; default `0.12` | | |
| | `PUBLIC_GLOBAL_SEARCH_ENABLED` | Mantiene o deshabilita el endpoint publico heredado `POST /search` | | |
| | `GLOBAL_SEARCH_MIN_SEMANTIC_SCORE` | Umbral semantico global; default `0.30` | | |
| | `GLOBAL_SEARCH_MIN_LEXICAL_SCORE` | Umbral lexical global; default `0.05` | | |
| | `GLOBAL_SEARCH_RESOURCE_THRESHOLDS_JSON` | Overrides opcionales por recurso con `semantic_min` y `lexical_min` | | |
| | `GLOBAL_SEARCH_THRESHOLD_POLICY_VERSION` | Version estable incluida en cursores para invalidarlos al cambiar la politica | | |
| `top_limit` se conserva en el request y en la huella del cursor para mantener el | |
| contexto coordinado con Go. NLP devuelve candidatos por seccion; la composicion final de | |
| `top_results` pertenece a la API principal. | |