Initial clean commit
This commit is contained in:
commit
6784d81c2c
141 changed files with 25219 additions and 0 deletions
3
models/__init__.py
Normal file
3
models/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# models/__init__.py
|
||||
# Para que Python reconozca el directorio como paquete.
|
||||
|
||||
9
models/categorias.py
Normal file
9
models/categorias.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from psycopg2 import extras
|
||||
from typing import List, Dict
|
||||
|
||||
|
||||
def get_categorias(conn) -> List[Dict]:
|
||||
with conn.cursor(cursor_factory=extras.DictCursor) as cur:
|
||||
cur.execute("SELECT id, nombre FROM categorias ORDER BY nombre;")
|
||||
return cur.fetchall()
|
||||
|
||||
337
models/describe.txt
Normal file
337
models/describe.txt
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
models/
|
||||
├── __init__.py # Paquete Python (vacío)
|
||||
├── categorias.py # Operaciones con categorías
|
||||
├── feeds.py # Operaciones con feeds RSS
|
||||
├── noticias.py # Búsqueda y consulta de noticias
|
||||
└── paises.py # Operaciones con países
|
||||
└── traducciones.py # Operaciones con traducciones
|
||||
|
||||
init.py
|
||||
Propósito: Archivo necesario para que Python reconozca este directorio como un paquete.
|
||||
|
||||
Contenido: Vacío o comentario explicativo.
|
||||
|
||||
Uso: Permite importar módulos desde models:
|
||||
|
||||
python
|
||||
from models.noticias import buscar_noticias
|
||||
categorias.py
|
||||
Propósito: Maneja todas las operaciones relacionadas con categorías de noticias.
|
||||
|
||||
Funciones principales:
|
||||
|
||||
get_categorias(conn) -> List[Dict]
|
||||
Descripción: Obtiene todas las categorías disponibles ordenadas alfabéticamente.
|
||||
|
||||
Parámetros:
|
||||
|
||||
conn: Conexión a PostgreSQL activa
|
||||
|
||||
Consulta SQL:
|
||||
|
||||
sql
|
||||
SELECT id, nombre FROM categorias ORDER BY nombre;
|
||||
Retorna: Lista de diccionarios con estructura:
|
||||
|
||||
python
|
||||
[
|
||||
{"id": 1, "nombre": "Política"},
|
||||
{"id": 2, "nombre": "Deportes"},
|
||||
...
|
||||
]
|
||||
Uso típico: Para llenar dropdowns de filtrado en la interfaz web.
|
||||
|
||||
feeds.py
|
||||
Propósito: Maneja operaciones relacionadas con feeds RSS.
|
||||
|
||||
Funciones principales:
|
||||
|
||||
get_feed_by_id(conn, feed_id: int) -> Optional[Dict]
|
||||
Descripción: Obtiene un feed específico por su ID.
|
||||
|
||||
Parámetros:
|
||||
|
||||
conn: Conexión a PostgreSQL
|
||||
|
||||
feed_id: ID numérico del feed
|
||||
|
||||
Consulta SQL:
|
||||
|
||||
sql
|
||||
SELECT * FROM feeds WHERE id = %s;
|
||||
Retorna: Un diccionario con todos los campos del feed o None si no existe.
|
||||
|
||||
get_feeds_activos(conn) -> List[Dict]
|
||||
Descripción: Obtiene todos los feeds activos y no caídos.
|
||||
|
||||
Criterios de activos:
|
||||
|
||||
activo = TRUE
|
||||
|
||||
fallos < 5 (o NULL)
|
||||
|
||||
Consulta SQL:
|
||||
|
||||
sql
|
||||
SELECT id, nombre, url, categoria_id, pais_id, fallos, activo
|
||||
FROM feeds
|
||||
WHERE activo = TRUE
|
||||
AND (fallos IS NULL OR fallos < 5)
|
||||
ORDER BY id;
|
||||
Retorna: Lista de feeds activos para el ingestor RSS.
|
||||
|
||||
Uso crítico: Esta función es utilizada por rss_ingestor.py para determinar qué feeds procesar.
|
||||
|
||||
noticias.py
|
||||
Propósito: Módulo más complejo que maneja todas las operaciones de búsqueda y consulta de noticias.
|
||||
|
||||
Funciones auxiliares:
|
||||
|
||||
_extraer_tags_por_traduccion(cur, traduccion_ids: List[int]) -> Dict[int, List[tuple]]
|
||||
Descripción: Función privada que obtiene tags agrupados por ID de traducción.
|
||||
|
||||
Parámetros:
|
||||
|
||||
cur: Cursor de base de datos
|
||||
|
||||
traduccion_ids: Lista de IDs de traducciones
|
||||
|
||||
Consulta SQL:
|
||||
|
||||
sql
|
||||
SELECT tn.traduccion_id, tg.valor, tg.tipo
|
||||
FROM tags_noticia tn
|
||||
JOIN tags tg ON tg.id = tn.tag_id
|
||||
WHERE tn.traduccion_id = ANY(%s);
|
||||
Retorna: Diccionario donde:
|
||||
|
||||
Clave: traduccion_id
|
||||
|
||||
Valor: Lista de tuplas (valor_tag, tipo_tag)
|
||||
|
||||
Optimización: Evita el problema N+1 al cargar tags.
|
||||
|
||||
Funciones principales:
|
||||
|
||||
buscar_noticias(...) -> Tuple[List[Dict], int, int, Dict]
|
||||
Descripción: Búsqueda avanzada con múltiples filtros, paginación y traducciones.
|
||||
|
||||
Parámetros:
|
||||
|
||||
conn: Conexión a PostgreSQL
|
||||
|
||||
page: Número de página (1-based)
|
||||
|
||||
per_page: Noticias por página
|
||||
|
||||
q: Término de búsqueda (opcional)
|
||||
|
||||
categoria_id: Filtrar por categoría (opcional)
|
||||
|
||||
continente_id: Filtrar por continente (opcional)
|
||||
|
||||
pais_id: Filtrar por país (opcional)
|
||||
|
||||
fecha: Filtrar por fecha exacta YYYY-MM-DD (opcional)
|
||||
|
||||
lang: Idioma objetivo para traducciones (default: "es")
|
||||
|
||||
use_tr: Incluir traducciones en búsqueda (default: True)
|
||||
|
||||
Retorna: Tupla con 4 elementos:
|
||||
|
||||
noticias: Lista de noticias con datos completos
|
||||
|
||||
total_results: Total de resultados (sin paginación)
|
||||
|
||||
total_pages: Total de páginas calculado
|
||||
|
||||
tags_por_tr: Diccionario de tags por traducción
|
||||
|
||||
Características de búsqueda:
|
||||
|
||||
Filtrado por fecha: Coincidencia exacta de fecha
|
||||
|
||||
Filtrado geográfico: País o continente (jerárquico)
|
||||
|
||||
Filtrado por categoría: Selección única
|
||||
|
||||
Búsqueda de texto:
|
||||
|
||||
Búsqueda full-text con PostgreSQL (websearch_to_tsquery)
|
||||
|
||||
Búsqueda ILIKE en múltiples campos
|
||||
|
||||
Incluye campos originales y traducidos
|
||||
|
||||
Paginación: Offset/Limit estándar
|
||||
|
||||
Traducciones: JOIN condicional con tabla traducciones
|
||||
|
||||
Optimización: Single query para contar y obtener datos
|
||||
|
||||
Consulta SQL principal (simplificada):
|
||||
|
||||
sql
|
||||
-- Contar total
|
||||
SELECT COUNT(DISTINCT n.id)
|
||||
FROM noticias n
|
||||
-- joins con categorias, paises, traducciones
|
||||
WHERE [condiciones dinámicas]
|
||||
|
||||
-- Obtener datos paginados
|
||||
SELECT
|
||||
n.id, n.titulo, n.resumen, n.url, n.fecha,
|
||||
n.imagen_url, n.fuente_nombre,
|
||||
c.nombre AS categoria,
|
||||
p.nombre AS pais,
|
||||
t.id AS traduccion_id,
|
||||
t.titulo_trad AS titulo_traducido,
|
||||
t.resumen_trad AS resumen_traducido,
|
||||
-- flag de traducción disponible
|
||||
CASE WHEN t.id IS NOT NULL THEN TRUE ELSE FALSE END AS tiene_traduccion,
|
||||
-- campos originales
|
||||
n.titulo AS titulo_original,
|
||||
n.resumen AS resumen_original
|
||||
FROM noticias n
|
||||
-- joins...
|
||||
WHERE [condiciones dinámicas]
|
||||
ORDER BY n.fecha DESC NULLS LAST, n.id DESC
|
||||
LIMIT %s OFFSET %s
|
||||
Campos retornados por noticia:
|
||||
|
||||
python
|
||||
{
|
||||
"id": 123,
|
||||
"titulo": "Título original",
|
||||
"resumen": "Resumen original",
|
||||
"url": "https://ejemplo.com/noticia",
|
||||
"fecha": datetime(...),
|
||||
"imagen_url": "https://.../imagen.jpg",
|
||||
"fuente_nombre": "BBC News",
|
||||
"categoria": "Política",
|
||||
"pais": "España",
|
||||
"traduccion_id": 456, # o None
|
||||
"titulo_traducido": "Título en español",
|
||||
"resumen_traducido": "Resumen en español",
|
||||
"tiene_traduccion": True, # o False
|
||||
"titulo_original": "Original title",
|
||||
"resumen_original": "Original summary"
|
||||
}
|
||||
Uso en la aplicación: Esta función es el corazón de la búsqueda en la web, utilizada por los blueprints de Flask.
|
||||
|
||||
paises.py
|
||||
Propósito: Maneja operaciones relacionadas con países.
|
||||
|
||||
Funciones principales:
|
||||
|
||||
get_paises(conn) -> List[Dict]
|
||||
Descripción: Obtiene todos los países ordenados alfabéticamente.
|
||||
|
||||
Parámetros:
|
||||
|
||||
conn: Conexión a PostgreSQL
|
||||
|
||||
Consulta SQL:
|
||||
|
||||
sql
|
||||
SELECT id, nombre FROM paises ORDER BY nombre;
|
||||
Retorna: Lista de diccionarios con id y nombre de cada país.
|
||||
|
||||
Uso típico: Para dropdowns de filtrado por país en la interfaz web.
|
||||
|
||||
traducciones.py
|
||||
Propósito: Maneja operaciones relacionadas con traducciones específicas.
|
||||
|
||||
Funciones principales:
|
||||
|
||||
get_traduccion(conn, traduccion_id: int) -> Optional[Dict]
|
||||
Descripción: Obtiene una traducción específica por su ID.
|
||||
|
||||
Parámetros:
|
||||
|
||||
conn: Conexión a PostgreSQL
|
||||
|
||||
traduccion_id: ID numérico de la traducción
|
||||
|
||||
Consulta SQL:
|
||||
|
||||
sql
|
||||
SELECT * FROM traducciones WHERE id = %s;
|
||||
Retorna: Diccionario con todos los campos de la traducción o None.
|
||||
|
||||
Campos incluidos: id, noticia_id, lang_from, lang_to, titulo_trad, resumen_trad, status, error, created_at, etc.
|
||||
|
||||
Uso típico: Para páginas de detalle de traducciones o debugging.
|
||||
|
||||
Patrones de Diseño Observados
|
||||
1. Separación de Responsabilidades
|
||||
Cada archivo maneja una entidad específica de la base de datos
|
||||
|
||||
Lógica de consultas separada de lógica de negocio
|
||||
|
||||
2. Interfaz Consistente
|
||||
Todas las funciones reciben conn como primer parámetro
|
||||
|
||||
Retornan diccionarios (usando DictCursor)
|
||||
|
||||
Nombres descriptivos y consistentes
|
||||
|
||||
3. Optimización de Consultas
|
||||
Uso de _extraer_tags_por_traduccion para evitar N+1 queries
|
||||
|
||||
Consultas COUNT y SELECT en la misma transacción
|
||||
|
||||
Índices implícitos en ORDER BY fecha DESC
|
||||
|
||||
4. Manejo de Traducciones
|
||||
JOIN condicional con tabla traducciones
|
||||
|
||||
Flag tiene_traduccion para fácil verificación en frontend
|
||||
|
||||
Campos originales siempre disponibles como fallback
|
||||
|
||||
5. Seguridad
|
||||
Uso de parámetros preparados (%s)
|
||||
|
||||
No concatenación directa de strings en SQL
|
||||
|
||||
Validación implícita de tipos
|
||||
|
||||
Flujo de Datos Típico
|
||||
python
|
||||
# En un blueprint de Flask
|
||||
from db import get_conn
|
||||
from models.noticias import buscar_noticias
|
||||
|
||||
def ruta_buscar():
|
||||
conn = get_conn()
|
||||
try:
|
||||
noticias, total, paginas, tags = buscar_noticias(
|
||||
conn=conn,
|
||||
page=request.args.get('page', 1, type=int),
|
||||
per_page=20,
|
||||
q=request.args.get('q', ''),
|
||||
categoria_id=request.args.get('categoria_id'),
|
||||
pais_id=request.args.get('pais_id'),
|
||||
lang='es'
|
||||
)
|
||||
# Procesar resultados...
|
||||
finally:
|
||||
conn.close()
|
||||
Dependencias y Relaciones
|
||||
Requisito: psycopg2.extras.DictCursor para retornar diccionarios
|
||||
|
||||
Usado por: Todos los blueprints en routers/
|
||||
|
||||
Base de datos: Asume estructura de tablas específica (feeds, noticias, traducciones, etc.)
|
||||
|
||||
Índices necesarios: Para optimizar búsquedas, se recomiendan índices en:
|
||||
|
||||
noticias(fecha DESC, id DESC)
|
||||
|
||||
traducciones(noticia_id, lang_to, status)
|
||||
|
||||
feeds(activo, fallos)
|
||||
|
||||
|
||||
24
models/feeds.py
Normal file
24
models/feeds.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from psycopg2 import extras
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
def get_feed_by_id(conn, feed_id: int) -> Optional[Dict]:
|
||||
with conn.cursor(cursor_factory=extras.DictCursor) as cur:
|
||||
cur.execute("SELECT * FROM feeds WHERE id = %s;", (feed_id,))
|
||||
return cur.fetchone()
|
||||
|
||||
|
||||
def get_feeds_activos(conn) -> List[Dict]:
|
||||
"""Feeds activos y no caídos, usados por el ingestor RSS."""
|
||||
with conn.cursor(cursor_factory=extras.DictCursor) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, nombre, url, categoria_id, pais_id, fallos, activo
|
||||
FROM feeds
|
||||
WHERE activo = TRUE
|
||||
AND (fallos IS NULL OR fallos < 5)
|
||||
ORDER BY id;
|
||||
"""
|
||||
)
|
||||
return cur.fetchall()
|
||||
|
||||
282
models/noticias.py
Normal file
282
models/noticias.py
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
from psycopg2 import extras
|
||||
from typing import List, Dict, Optional, Tuple, Any
|
||||
import os
|
||||
import torch
|
||||
from sentence_transformers import SentenceTransformer
|
||||
|
||||
|
||||
def _extraer_tags_por_traduccion(cur, traduccion_ids: List[int]) -> Dict[int, List[tuple]]:
|
||||
"""Obtiene tags agrupados por traducción."""
|
||||
tags_por_tr = {}
|
||||
|
||||
if not traduccion_ids:
|
||||
return tags_por_tr
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT tn.traduccion_id, tg.valor, tg.tipo
|
||||
FROM tags_noticia tn
|
||||
JOIN tags tg ON tg.id = tn.tag_id
|
||||
WHERE tn.traduccion_id = ANY(%s);
|
||||
""",
|
||||
(traduccion_ids,),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
for tr_id, valor, tipo in rows:
|
||||
tags_por_tr.setdefault(tr_id, []).append((valor, tipo))
|
||||
|
||||
return tags_por_tr
|
||||
|
||||
|
||||
def buscar_noticias(
|
||||
conn,
|
||||
page: int,
|
||||
per_page: int,
|
||||
q: str = "",
|
||||
categoria_id: Optional[str] = None,
|
||||
continente_id: Optional[str] = None,
|
||||
pais_id: Optional[str] = None,
|
||||
fecha: Optional[str] = None,
|
||||
lang: str = "es",
|
||||
use_tr: bool = True,
|
||||
skip_count: bool = False,
|
||||
) -> Tuple[List[Dict], int, int, Dict]:
|
||||
"""
|
||||
Búsqueda avanzada de noticias con filtros:
|
||||
- fecha
|
||||
- país / continente
|
||||
- categoría
|
||||
- búsqueda fulltext + ILIKE
|
||||
- traducciones
|
||||
- paginación
|
||||
"""
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
where = ["1=1"]
|
||||
params = []
|
||||
|
||||
# Filtro por fecha exacta
|
||||
if fecha:
|
||||
where.append("n.fecha::date = %s")
|
||||
params.append(fecha)
|
||||
|
||||
# Categoría
|
||||
if categoria_id:
|
||||
where.append("n.categoria_id = %s")
|
||||
params.append(int(categoria_id))
|
||||
|
||||
# País o continente
|
||||
if pais_id:
|
||||
where.append("n.pais_id = %s")
|
||||
params.append(int(pais_id))
|
||||
elif continente_id:
|
||||
where.append("p.continente_id = %s")
|
||||
params.append(int(continente_id))
|
||||
|
||||
# Búsqueda
|
||||
if q:
|
||||
search_like = f"%{q}%"
|
||||
if use_tr:
|
||||
where.append(
|
||||
"""
|
||||
(
|
||||
n.tsv @@ websearch_to_tsquery('spanish', %s)
|
||||
OR t.titulo_trad ILIKE %s
|
||||
OR t.resumen_trad ILIKE %s
|
||||
OR n.titulo ILIKE %s
|
||||
OR n.resumen ILIKE %s
|
||||
)
|
||||
"""
|
||||
)
|
||||
params.extend([q, search_like, search_like, search_like, search_like])
|
||||
else:
|
||||
where.append(
|
||||
"""
|
||||
(
|
||||
n.tsv @@ websearch_to_tsquery('spanish', %s)
|
||||
OR n.titulo ILIKE %s
|
||||
OR n.resumen ILIKE %s
|
||||
)
|
||||
"""
|
||||
)
|
||||
params.extend([q, search_like, search_like])
|
||||
|
||||
where_sql = " AND ".join(where)
|
||||
|
||||
with conn.cursor(cursor_factory=extras.DictCursor) as cur:
|
||||
|
||||
# =====================================================================
|
||||
# TOTAL DE RESULTADOS (OPTIMIZADO)
|
||||
# =====================================================================
|
||||
total_results = 0
|
||||
total_pages = 0
|
||||
|
||||
if not skip_count:
|
||||
# Si no hay filtros de búsqueda de texto ni filtros complejos, usar estimación rápida
|
||||
if not q and not categoria_id and not pais_id and not continente_id and not fecha:
|
||||
cur.execute("SELECT reltuples::bigint FROM pg_class WHERE relname = 'noticias'")
|
||||
row = cur.fetchone()
|
||||
total_results = row[0] if row else 0
|
||||
else:
|
||||
# Conteo exacto si hay filtros (necesario para paginación filtrada)
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT COUNT(n.id)
|
||||
FROM noticias n
|
||||
LEFT JOIN categorias c ON c.id = n.categoria_id
|
||||
LEFT JOIN paises p ON p.id = n.pais_id
|
||||
LEFT JOIN traducciones t
|
||||
ON t.noticia_id = n.id
|
||||
AND t.lang_to = %s
|
||||
AND t.status = 'done'
|
||||
WHERE {where_sql}
|
||||
""",
|
||||
[lang] + params,
|
||||
)
|
||||
total_results = cur.fetchone()[0]
|
||||
|
||||
total_pages = (total_results // per_page) + (1 if total_results % per_page else 0)
|
||||
|
||||
# =====================================================================
|
||||
# LISTA DE NOTICIAS PAGINADAS
|
||||
# =====================================================================
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT
|
||||
n.id,
|
||||
n.titulo,
|
||||
n.resumen,
|
||||
n.url,
|
||||
n.fecha,
|
||||
n.imagen_url,
|
||||
n.fuente_nombre,
|
||||
c.nombre AS categoria,
|
||||
p.nombre AS pais,
|
||||
|
||||
-- traducciones
|
||||
t.id AS traduccion_id,
|
||||
t.titulo_trad AS titulo_traducido,
|
||||
t.resumen_trad AS resumen_traducido,
|
||||
CASE WHEN t.id IS NOT NULL THEN TRUE ELSE FALSE END AS tiene_traduccion,
|
||||
|
||||
-- originales
|
||||
n.titulo AS titulo_original,
|
||||
n.resumen AS resumen_original
|
||||
|
||||
FROM noticias n
|
||||
LEFT JOIN categorias c ON c.id = n.categoria_id
|
||||
LEFT JOIN paises p ON p.id = n.pais_id
|
||||
LEFT JOIN traducciones t
|
||||
ON t.noticia_id = n.id
|
||||
AND t.lang_to = %s
|
||||
AND t.status = 'done'
|
||||
WHERE {where_sql}
|
||||
ORDER BY n.fecha DESC NULLS LAST, n.id DESC
|
||||
LIMIT %s OFFSET %s
|
||||
""",
|
||||
[lang] + params + [per_page, offset],
|
||||
)
|
||||
noticias = cur.fetchall()
|
||||
|
||||
# =====================================================================
|
||||
# TAGS POR TRADUCCIÓN
|
||||
# =====================================================================
|
||||
tr_ids = [n["traduccion_id"] for n in noticias if n["traduccion_id"]]
|
||||
tags_por_tr = _extraer_tags_por_traduccion(cur, tr_ids)
|
||||
|
||||
return noticias, total_results, total_pages, tags_por_tr
|
||||
|
||||
|
||||
# Cache del modelo para no cargarlo en cada petición
|
||||
_model_cache = {}
|
||||
|
||||
def _get_emb_model():
|
||||
model_name = os.environ.get("EMB_MODEL", "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
|
||||
if model_name not in _model_cache:
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
_model_cache[model_name] = SentenceTransformer(model_name, device=device)
|
||||
return _model_cache[model_name], model_name
|
||||
|
||||
def buscar_noticias_semantica(
|
||||
conn,
|
||||
page: int,
|
||||
per_page: int,
|
||||
q: str,
|
||||
categoria_id: Optional[str] = None,
|
||||
continente_id: Optional[str] = None,
|
||||
pais_id: Optional[str] = None,
|
||||
fecha: Optional[str] = None,
|
||||
lang: str = "es",
|
||||
) -> Tuple[List[Dict], int, int, Dict]:
|
||||
"""
|
||||
Búsqueda semántica usando embeddings y similitud coseno (vía producto punto si están normalizados).
|
||||
"""
|
||||
if not q.strip():
|
||||
return buscar_noticias(conn, page, per_page, "", categoria_id, continente_id, pais_id, fecha, lang)
|
||||
|
||||
offset = (page - 1) * per_page
|
||||
model, model_name = _get_emb_model()
|
||||
|
||||
# Generar embedding de la consulta
|
||||
q_emb = model.encode([q], normalize_embeddings=True)[0].tolist()
|
||||
|
||||
where = ["t.status = 'done'", "t.lang_to = %s"]
|
||||
params = [lang]
|
||||
|
||||
if fecha:
|
||||
where.append("n.fecha::date = %s")
|
||||
params.append(fecha)
|
||||
if categoria_id:
|
||||
where.append("n.categoria_id = %s")
|
||||
params.append(int(categoria_id))
|
||||
if pais_id:
|
||||
where.append("n.pais_id = %s")
|
||||
params.append(int(pais_id))
|
||||
elif continente_id:
|
||||
where.append("p.continente_id = %s")
|
||||
params.append(int(continente_id))
|
||||
|
||||
where_sql = " AND ".join(where)
|
||||
|
||||
with conn.cursor(cursor_factory=extras.DictCursor) as cur:
|
||||
# Consulta de búsqueda vectorial (usamos un array_agg o similar para el producto punto si no hay pgvector)
|
||||
# Nota: Aquí asumo que usamos producto punto entre arrays de double precision
|
||||
query_sql = f"""
|
||||
WITH similarity AS (
|
||||
SELECT
|
||||
te.traduccion_id,
|
||||
(
|
||||
SELECT SUM(a*b)
|
||||
FROM unnest(te.embedding, %s::double precision[]) AS t(a,b)
|
||||
) AS score
|
||||
FROM traduccion_embeddings te
|
||||
WHERE te.model = %s
|
||||
)
|
||||
SELECT
|
||||
n.id, n.titulo, n.resumen, n.url, n.fecha, n.imagen_url, n.fuente_nombre,
|
||||
c.nombre AS categoria, p.nombre AS pais,
|
||||
t.id AS traduccion_id, t.titulo_trad AS titulo_traducido, t.resumen_trad AS resumen_traducido,
|
||||
TRUE AS tiene_traduccion, s.score
|
||||
FROM similarity s
|
||||
JOIN traducciones t ON t.id = s.traduccion_id
|
||||
JOIN noticias n ON n.id = t.noticia_id
|
||||
LEFT JOIN categorias c ON c.id = n.categoria_id
|
||||
LEFT JOIN paises p ON p.id = n.pais_id
|
||||
WHERE {where_sql}
|
||||
ORDER BY n.fecha DESC NULLS LAST, s.score DESC
|
||||
LIMIT %s OFFSET %s
|
||||
"""
|
||||
|
||||
# Para el conteo total en semántica podemos simplificar o usar el mismo WHERE
|
||||
cur.execute(f"SELECT COUNT(*) FROM traducciones t JOIN noticias n ON n.id = t.noticia_id LEFT JOIN paises p ON p.id = n.pais_id WHERE {where_sql}", params)
|
||||
total_results = cur.fetchone()[0]
|
||||
total_pages = (total_results // per_page) + (1 if total_results % per_page else 0)
|
||||
|
||||
cur.execute(query_sql, [q_emb, model_name] + params + [per_page, offset])
|
||||
noticias = cur.fetchall()
|
||||
|
||||
tr_ids = [n["traduccion_id"] for n in noticias]
|
||||
tags_por_tr = _extraer_tags_por_traduccion(cur, tr_ids)
|
||||
|
||||
return noticias, total_results, total_pages, tags_por_tr
|
||||
9
models/paises.py
Normal file
9
models/paises.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from typing import List, Dict
|
||||
from psycopg2 import extras
|
||||
|
||||
|
||||
def get_paises(conn) -> List[Dict]:
|
||||
with conn.cursor(cursor_factory=extras.DictCursor) as cur:
|
||||
cur.execute("SELECT id, nombre FROM paises ORDER BY nombre;")
|
||||
return cur.fetchall()
|
||||
|
||||
16
models/traducciones.py
Normal file
16
models/traducciones.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from psycopg2 import extras
|
||||
from typing import Optional, Dict
|
||||
|
||||
|
||||
def get_traduccion(conn, traduccion_id: int) -> Optional[Dict]:
|
||||
with conn.cursor(cursor_factory=extras.DictCursor) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT *
|
||||
FROM traducciones
|
||||
WHERE id = %s;
|
||||
""",
|
||||
(traduccion_id,),
|
||||
)
|
||||
return cur.fetchone()
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue