retoques
This commit is contained in:
parent
86ee083b90
commit
e3a99d9604
8 changed files with 489 additions and 483 deletions
|
|
@ -6,6 +6,7 @@ from typing import List, Dict, Any, Optional
|
|||
import numpy as np
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
from psycopg2.extras import Json
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
|
|
@ -43,29 +44,12 @@ def get_conn():
|
|||
|
||||
def ensure_schema(conn):
|
||||
"""
|
||||
Asegura que la tabla de eventos y las columnas necesarias existen.
|
||||
Aquí se asume el esquema original de eventos con centroid JSONB.
|
||||
Asumimos que las tablas y columnas (eventos, traducciones.evento_id,
|
||||
eventos_noticias, función/trigger) ya existen por los scripts init-db.
|
||||
Aquí solo nos aseguramos de que existan ciertos índices clave
|
||||
(idempotente).
|
||||
"""
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS eventos (
|
||||
id SERIAL PRIMARY KEY,
|
||||
creado_en TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
actualizado_en TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
centroid JSONB NOT NULL,
|
||||
total_traducciones INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
ALTER TABLE traducciones
|
||||
ADD COLUMN IF NOT EXISTS evento_id INTEGER REFERENCES eventos(id);
|
||||
"""
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_traducciones_evento
|
||||
|
|
@ -78,27 +62,6 @@ def ensure_schema(conn):
|
|||
ON traducciones(evento_id, noticia_id);
|
||||
"""
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE OR REPLACE FUNCTION actualizar_evento_modificado()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.actualizado_en = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
"""
|
||||
)
|
||||
cur.execute("DROP TRIGGER IF EXISTS trg_evento_modificado ON eventos;")
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TRIGGER trg_evento_modificado
|
||||
BEFORE UPDATE ON eventos
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION actualizar_evento_modificado();
|
||||
"""
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
|
|
@ -161,6 +124,7 @@ def fetch_embeddings_for(conn, tr_ids: List[int]) -> Dict[int, np.ndarray]:
|
|||
def fetch_centroids(conn) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Carga todos los centroides actuales desde eventos.
|
||||
Solo usamos campos de clustering: id, centroid, total_traducciones.
|
||||
"""
|
||||
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
||||
cur.execute(
|
||||
|
|
@ -178,6 +142,7 @@ def fetch_centroids(conn) -> List[Dict[str, Any]]:
|
|||
raw = r["centroid"]
|
||||
cnt = int(r["total_traducciones"] or 1)
|
||||
if not isinstance(raw, (list, tuple)):
|
||||
# centroid se almacena como JSONB array → en Python suele llegar como list
|
||||
continue
|
||||
arr = np.array([float(x or 0.0) for x in raw], dtype="float32")
|
||||
if arr.size == 0:
|
||||
|
|
@ -201,6 +166,54 @@ def cosine_distance(a: np.ndarray, b: np.ndarray) -> float:
|
|||
return 1.0 - cos
|
||||
|
||||
|
||||
def fetch_traduccion_info(conn, tr_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Devuelve info básica para un tr_id:
|
||||
- noticia_id
|
||||
- fecha de la noticia
|
||||
- un título “representativo” para el evento (traducido u original).
|
||||
"""
|
||||
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT
|
||||
t.id AS traduccion_id,
|
||||
t.noticia_id AS noticia_id,
|
||||
n.fecha AS fecha,
|
||||
COALESCE(NULLIF(t.titulo_trad, ''), n.titulo) AS titulo_evento
|
||||
FROM traducciones t
|
||||
JOIN noticias n ON n.id = t.noticia_id
|
||||
WHERE t.id = %s;
|
||||
""",
|
||||
(tr_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
return {
|
||||
"traduccion_id": int(row["traduccion_id"]),
|
||||
"noticia_id": row["noticia_id"],
|
||||
"fecha": row["fecha"],
|
||||
"titulo_evento": row["titulo_evento"],
|
||||
}
|
||||
|
||||
|
||||
def _insert_evento_noticia(cur, evento_id: int, info: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Inserta relación en eventos_noticias (idempotente).
|
||||
"""
|
||||
if not info or not info.get("noticia_id"):
|
||||
return
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO eventos_noticias (evento_id, noticia_id, traduccion_id)
|
||||
VALUES (%s, %s, %s)
|
||||
ON CONFLICT (evento_id, traduccion_id) DO NOTHING;
|
||||
""",
|
||||
(evento_id, info["noticia_id"], info["traduccion_id"]),
|
||||
)
|
||||
|
||||
|
||||
def assign_to_event(
|
||||
conn,
|
||||
tr_id: int,
|
||||
|
|
@ -210,31 +223,63 @@ def assign_to_event(
|
|||
"""
|
||||
Asigna una traducción a un evento existente (si distancia <= umbral)
|
||||
o crea un evento nuevo con este vector como centroide.
|
||||
"""
|
||||
from psycopg2.extras import Json
|
||||
|
||||
Además:
|
||||
- Actualiza fecha_inicio, fecha_fin, n_noticias del evento.
|
||||
- Rellena eventos_noticias (evento_id, noticia_id, traduccion_id).
|
||||
"""
|
||||
if vec is None or vec.size == 0:
|
||||
return
|
||||
|
||||
info = fetch_traduccion_info(conn, tr_id)
|
||||
|
||||
# Si no hay centroides todavía → primer evento
|
||||
if not centroids:
|
||||
centroid_list = [float(x) for x in vec.tolist()]
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO eventos (centroid, total_traducciones)
|
||||
VALUES (%s, %s)
|
||||
RETURNING id;
|
||||
""",
|
||||
(Json(centroid_list), 1),
|
||||
)
|
||||
if info and info.get("fecha"):
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO eventos (centroid, total_traducciones,
|
||||
fecha_inicio, fecha_fin, n_noticias, titulo)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
RETURNING id;
|
||||
""",
|
||||
(
|
||||
Json(centroid_list),
|
||||
1,
|
||||
info["fecha"],
|
||||
info["fecha"],
|
||||
1,
|
||||
info.get("titulo_evento"),
|
||||
),
|
||||
)
|
||||
else:
|
||||
# Fallback mínimo si no hay info de noticia
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO eventos (centroid, total_traducciones)
|
||||
VALUES (%s, %s)
|
||||
RETURNING id;
|
||||
""",
|
||||
(Json(centroid_list), 1),
|
||||
)
|
||||
|
||||
new_id = cur.fetchone()[0]
|
||||
|
||||
# Vincular traducción al evento
|
||||
cur.execute(
|
||||
"UPDATE traducciones SET evento_id = %s WHERE id = %s;",
|
||||
(new_id, tr_id),
|
||||
)
|
||||
|
||||
# Rellenar tabla de relación
|
||||
_insert_evento_noticia(cur, new_id, info or {})
|
||||
|
||||
centroids.append({"id": new_id, "vec": vec.copy(), "n": 1})
|
||||
return
|
||||
|
||||
# Buscar el centroide más cercano
|
||||
best_idx: Optional[int] = None
|
||||
best_dist: float = 1.0
|
||||
|
||||
|
|
@ -244,47 +289,98 @@ def assign_to_event(
|
|||
best_dist = d
|
||||
best_idx = i
|
||||
|
||||
if best_idx is not None and best_dist <= EVENT_DIST_THRESHOLD:
|
||||
c = centroids[best_idx]
|
||||
n_old = c["n"]
|
||||
new_n = n_old + 1
|
||||
new_vec = (c["vec"] * n_old + vec) / float(new_n)
|
||||
with conn.cursor() as cur:
|
||||
# Asignar a evento existente si está por debajo del umbral
|
||||
if best_idx is not None and best_dist <= EVENT_DIST_THRESHOLD:
|
||||
c = centroids[best_idx]
|
||||
n_old = c["n"]
|
||||
new_n = n_old + 1
|
||||
new_vec = (c["vec"] * n_old + vec) / float(new_n)
|
||||
|
||||
c["vec"] = new_vec
|
||||
c["n"] = new_n
|
||||
c["vec"] = new_vec
|
||||
c["n"] = new_n
|
||||
|
||||
centroid_list = [float(x) for x in new_vec.tolist()]
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE eventos
|
||||
SET centroid = %s,
|
||||
total_traducciones = total_traducciones + 1
|
||||
WHERE id = %s;
|
||||
""",
|
||||
(Json(centroid_list), c["id"]),
|
||||
)
|
||||
centroid_list = [float(x) for x in new_vec.tolist()]
|
||||
|
||||
if info and info.get("fecha"):
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE eventos
|
||||
SET centroid = %s,
|
||||
total_traducciones = total_traducciones + 1,
|
||||
fecha_inicio = COALESCE(LEAST(fecha_inicio, %s), %s),
|
||||
fecha_fin = COALESCE(GREATEST(fecha_fin, %s), %s),
|
||||
n_noticias = n_noticias + 1
|
||||
WHERE id = %s;
|
||||
""",
|
||||
(
|
||||
Json(centroid_list),
|
||||
info["fecha"],
|
||||
info["fecha"],
|
||||
info["fecha"],
|
||||
info["fecha"],
|
||||
c["id"],
|
||||
),
|
||||
)
|
||||
else:
|
||||
# Sin info de fecha: solo actualizamos centroid/contador
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE eventos
|
||||
SET centroid = %s,
|
||||
total_traducciones = total_traducciones + 1
|
||||
WHERE id = %s;
|
||||
""",
|
||||
(Json(centroid_list), c["id"]),
|
||||
)
|
||||
|
||||
# Vincular traducción y relación
|
||||
cur.execute(
|
||||
"UPDATE traducciones SET evento_id = %s WHERE id = %s;",
|
||||
(c["id"], tr_id),
|
||||
)
|
||||
return
|
||||
_insert_evento_noticia(cur, c["id"], info or {})
|
||||
|
||||
return
|
||||
|
||||
# Si no hay evento adecuado → crear uno nuevo
|
||||
centroid_list = [float(x) for x in vec.tolist()]
|
||||
|
||||
if info and info.get("fecha"):
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO eventos (centroid, total_traducciones,
|
||||
fecha_inicio, fecha_fin, n_noticias, titulo)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
RETURNING id;
|
||||
""",
|
||||
(
|
||||
Json(centroid_list),
|
||||
1,
|
||||
info["fecha"],
|
||||
info["fecha"],
|
||||
1,
|
||||
info.get("titulo_evento"),
|
||||
),
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO eventos (centroid, total_traducciones)
|
||||
VALUES (%s, %s)
|
||||
RETURNING id;
|
||||
""",
|
||||
(Json(centroid_list), 1),
|
||||
)
|
||||
|
||||
centroid_list = [float(x) for x in vec.tolist()]
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO eventos (centroid, total_traducciones)
|
||||
VALUES (%s, %s)
|
||||
RETURNING id;
|
||||
""",
|
||||
(Json(centroid_list), 1),
|
||||
)
|
||||
new_id = cur.fetchone()[0]
|
||||
|
||||
cur.execute(
|
||||
"UPDATE traducciones SET evento_id = %s WHERE id = %s;",
|
||||
(new_id, tr_id),
|
||||
)
|
||||
_insert_evento_noticia(cur, new_id, info or {})
|
||||
|
||||
centroids.append({"id": new_id, "vec": vec.copy(), "n": 1})
|
||||
|
||||
|
||||
|
|
@ -309,11 +405,16 @@ def main():
|
|||
time.sleep(EVENT_SLEEP_IDLE)
|
||||
continue
|
||||
|
||||
log.info("Traducciones pendientes de asignar evento: %d", len(pending_ids))
|
||||
log.info(
|
||||
"Traducciones pendientes de asignar evento: %d",
|
||||
len(pending_ids),
|
||||
)
|
||||
|
||||
emb_by_tr = fetch_embeddings_for(conn, pending_ids)
|
||||
if not emb_by_tr:
|
||||
log.warning("No se encontraron embeddings para las traducciones pendientes.")
|
||||
log.warning(
|
||||
"No se encontraron embeddings para las traducciones pendientes."
|
||||
)
|
||||
time.sleep(EVENT_SLEEP_IDLE)
|
||||
continue
|
||||
|
||||
|
|
@ -329,7 +430,10 @@ def main():
|
|||
processed += 1
|
||||
|
||||
conn.commit()
|
||||
log.info("Asignación de eventos completada. Traducciones procesadas: %d", processed)
|
||||
log.info(
|
||||
"Asignación de eventos completada. Traducciones procesadas: %d",
|
||||
processed,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
log.exception("Error en cluster_worker: %s", e)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue