From da1801e982e775cd0735435aa9d0114008bbb2fa Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Mon, 15 Sep 2025 01:02:18 +0200 Subject: [PATCH] update .gitignore to include model and docker initialization files; add new script for database creation and search functionality --- .gitignore | 4 + crear base de datos y hacer busquedas.py | 550 +++++++++++++++++++++++ 2 files changed, 554 insertions(+) create mode 100644 crear base de datos y hacer busquedas.py diff --git a/.gitignore b/.gitignore index 110b0a6..b0e7512 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ wheels/ # Virtual environments .venv .env + +# Bases de datos y modelos +.model/ +docker-entrypoint-initdb.d/ \ No newline at end of file diff --git a/crear base de datos y hacer busquedas.py b/crear base de datos y hacer busquedas.py new file mode 100644 index 0000000..a0577a6 --- /dev/null +++ b/crear base de datos y hacer busquedas.py @@ -0,0 +1,550 @@ +import marimo + +__generated_with = "0.15.3" +app = marimo.App(width="columns") + + +@app.cell(column=0) +def _(): + import marimo as mo + return (mo,) + + +@app.cell +def _(): + import os + import textwrap + import yaml + import subprocess + from pathlib import Path + + # ==== Parámetros ==== + PUERTO_POSTGRES = 55455 + SERVICIO = "postgres_ext" + PASSWORD = "mipassword" + USER = "postgres" + DBNAME = "basededatos" + + # Lista de extensiones que quieres habilitar + EXTENSIONES = [ + # builtin + "hstore", "citext", "uuid-ossp", "pg_trgm", + # requieren paquetes + "postgis", "pgvector", + # "timescaledb" # <- si quieres usar base de timescaledb, activa la bandera abajo + ] + + # Usa imagen base de timescaledb cuando la extensión 'timescaledb' esté en la lista + timescaledb_base_image = False # pon True si quieres usar la imagen base de TimescaleDB + + RUTA_PROYECTO = Path(".").resolve() + return ( + DBNAME, + EXTENSIONES, + PASSWORD, + PUERTO_POSTGRES, + Path, + RUTA_PROYECTO, + SERVICIO, + USER, + subprocess, + textwrap, + timescaledb_base_image, + yaml, + ) + + +@app.function +def pkgs_para_extensiones(exts, pg_major=15, use_timescale_base=False): + """ + Devuelve (pkgs_apt, builtins) para las extensiones solicitadas. + builtins = extensiones que no requieren apt + pkgs_apt = paquetes apt necesarios para otras extensiones + """ + builtins = [] + pkgs_apt = [] + + for e in exts: + e_low = e.lower() + if e_low in {"hstore", "citext", "uuid-ossp", "pg_trgm"}: + builtins.append(e_low) + elif e_low == "postgis": + pkgs_apt += [ + f"postgresql-{pg_major}-postgis-3", + f"postgresql-{pg_major}-postgis-3-scripts", + "gdal-bin", + "proj-bin", + ] + elif e_low == "pgvector": + pkgs_apt += [f"postgresql-{pg_major}-pgvector"] + elif e_low == "timescaledb": + if not use_timescale_base: + # Para instalar timescaledb necesitarías repos adicionales + pass + else: + raise ValueError(f"Extensión no soportada en este helper: {e}") + + pkgs_apt = sorted(set(pkgs_apt)) + return pkgs_apt, builtins + + +@app.cell +def _(DBNAME, Path, USER, textwrap): + def generar_dockerfile(ruta: Path, exts, use_timescale_base=False, pg_major=15): + ruta.mkdir(parents=True, exist_ok=True) + pkgs_apt, builtins = pkgs_para_extensiones(exts, pg_major, use_timescale_base) + + base_image = ( + f"timescale/timescaledb:2.16-pg{pg_major}" if use_timescale_base else f"postgres:{pg_major}" + ) + + apt_block = "" + if pkgs_apt: + apt_lines = [ + "RUN apt-get update && \\", + " DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \\", + ] + # 👇 Backslash en **todas** las líneas de paquetes + for pkg in pkgs_apt: + apt_lines.append(f" {pkg} \\") + # y ya colgamos el rm del último paquete + apt_lines.append(" && rm -rf /var/lib/apt/lists/*") + apt_block = "\n".join(apt_lines) + + dockerfile = f""" + FROM {base_image} + + # Variables de entorno útiles + ENV POSTGRES_USER={USER} \\ + POSTGRES_DB={DBNAME} + + {apt_block} + + # Copiamos scripts de inicialización + COPY docker-entrypoint-initdb.d/ /docker-entrypoint-initdb.d/ + """ + (ruta / "Dockerfile").write_text(textwrap.dedent(dockerfile).strip() + "\n", encoding="utf-8") + return (generar_dockerfile,) + + +@app.cell +def _(Path): + def generar_init_sql(ruta: Path, exts): + init_dir = ruta / "docker-entrypoint-initdb.d" + init_dir.mkdir(parents=True, exist_ok=True) + + lines = ["-- Habilitar extensiones solicitadas"] + for e in exts: + e_low = e.lower() + ext_name = { + "pgvector": "vector", + "postgis": "postgis", + "hstore": "hstore", + "citext": "citext", + "uuid-ossp": "\"uuid-ossp\"", + "pg_trgm": "pg_trgm", + "timescaledb": "timescaledb", + }.get(e_low, e_low) + lines.append(f"CREATE EXTENSION IF NOT EXISTS {ext_name};") + + sql = "\n".join(lines) + "\n" + (init_dir / "10-extensions.sql").write_text(sql, encoding="utf-8") + return (generar_init_sql,) + + +@app.cell +def _(DBNAME, Path, USER, yaml): + def crear_docker_compose(ruta: Path, servicio: str, puerto_host: int, password: str): + compose = { + "version": "3.8", + "services": { + servicio: { + "build": {"context": "."}, + "restart": "always", + "ports": [f"{puerto_host}:5432"], + "environment": { + "POSTGRES_PASSWORD": password, + "POSTGRES_USER": USER, + "POSTGRES_DB": DBNAME, + }, + "healthcheck": { + "test": ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"], + "interval": "10s", + "timeout": "5s", + "retries": 5, + }, + "volumes": [ + "postgres_data:/var/lib/postgresql/data" + ], + } + }, + "volumes": {"postgres_data": {}} + } + (ruta / "docker-compose.yml").write_text(yaml.dump(compose, sort_keys=False), encoding="utf-8") + return (crear_docker_compose,) + + +@app.cell +def _(Path, subprocess): + def construir_y_levantar(ruta: Path): + def _run(cmd): + subprocess.run(cmd, cwd=ruta, check=True) + + try: + _run(["docker", "compose", "build"]) + _run(["docker", "compose", "up", "-d"]) + except Exception: + _run(["docker-compose", "build"]) + _run(["docker-compose", "up", "-d"]) + return (construir_y_levantar,) + + +@app.cell +def _( + EXTENSIONES, + PASSWORD, + PUERTO_POSTGRES, + RUTA_PROYECTO, + SERVICIO, + construir_y_levantar, + crear_docker_compose, + generar_dockerfile, + generar_init_sql, + timescaledb_base_image, +): + if __name__ == "__main__": + RUTA_PROYECTO.mkdir(parents=True, exist_ok=True) + generar_dockerfile(RUTA_PROYECTO, EXTENSIONES, timescaledb_base_image) + generar_init_sql(RUTA_PROYECTO, EXTENSIONES) + crear_docker_compose(RUTA_PROYECTO, SERVICIO, PUERTO_POSTGRES, PASSWORD) + construir_y_levantar(RUTA_PROYECTO) + return + + +@app.cell(column=1) +def _(): + from huggingface_hub import snapshot_download + + snapshot_download( + repo_id="nomic-ai/nomic-embed-text-v1.5", + local_dir=".model/nomic-embed-text-v1.5" + ) + + return + + +@app.cell +def _(): + from transformers import AutoTokenizer, AutoModel + import torch + + # Ruta al modelo descargado + model_path = ".model/nomic-embed-text-v1.5" + + tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) + model = AutoModel.from_pretrained(model_path, trust_remote_code=True) + + texts = [ + "La inteligencia artificial está transformando el mundo.", + "Los embeddings convierten texto en vectores numéricos." + ] + + # Tokenizar y obtener embeddings + inputs = tokenizer(texts, return_tensors="pt", padding=True, truncation=True, max_length=8192) + + with torch.no_grad(): + embeddings = model(**inputs).last_hidden_state.mean(dim=1) + + for text, _vector in zip(texts, embeddings): + print(f"Texto: {text}\nDimensión: {_vector.shape[0]}\nPrimeros valores: {_vector[:5].tolist()}\n") + + return model, tokenizer, torch + + +@app.cell +def _( + DBNAME, + HOST, + PASSWORD, + PUERTO_POSTGRES, + USER, + model, + psycopg2, + tokenizer, + torch, +): + + conn = psycopg2.connect( + dbname=DBNAME, + user=USER, + password=PASSWORD, + host=HOST, + port=PUERTO_POSTGRES + ) + + conn.autocommit = True # 👈 importante + cur = conn.cursor() + + + base_texts = [ + "La inteligencia artificial está transformando el mundo.", + "Los embeddings convierten texto en vectores numéricos.", + "PostgreSQL con pgvector permite búsquedas semánticas.", + "El aprendizaje profundo impulsa avances en visión computacional.", + "Transformers cambiaron el campo del procesamiento del lenguaje natural.", + "Los modelos de lenguaje grande permiten nuevas aplicaciones.", + "La ciencia de datos combina estadística y programación.", + "El big data requiere arquitecturas distribuidas.", + "El machine learning mejora con más datos y cómputo.", + "El deep learning usa redes neuronales profundas.", + ] + # Duplicamos con variaciones para llegar a 20 + _texts = base_texts + [t + f" Ejemplo {i}" for i, t in enumerate(base_texts, start=1)] + + # Tokenizar y obtener embeddings + _inputs = tokenizer(_texts, return_tensors="pt", padding=True, truncation=True, max_length=512) + with torch.no_grad(): + _embeddings = model(**_inputs).last_hidden_state.mean(dim=1) + + # Insertar en la base de datos + for _text, vector in zip(_texts, _embeddings): + vector_list = vector.tolist() + # Convertimos a formato adecuado para pgvector: [v1, v2, v3, ...] + vector_str = "[" + ",".join(f"{v:.6f}" for v in vector_list) + "]" + cur.execute( + "INSERT INTO nota (titulo, embedding) VALUES (%s, %s::vector)", + (_text, vector_str) + ) + + print("✅ Se insertaron 20 textos con sus embeddings en la tabla 'nota'.") + + cur.close() + conn.close() + return + + +@app.cell +def _(): + return + + +@app.cell +def _(Engine, PUERTO_POSTGRES, create_engine, quote_plus): + + def conectar_postgres( + host: str = "localhost", + port: int = PUERTO_POSTGRES, + dbname: str = "basededatos", + user: str = "postgres", + password: str = "mipassword" + ) -> Engine: + """ + Devuelve un objeto SQLAlchemy Engine conectado a PostgreSQL. + Compatible con pandas.to_sql y read_sql. + """ + pwd = quote_plus(password) # Escapar caracteres especiales del password + url = f"postgresql+psycopg2://{user}:{pwd}@{host}:{port}/{dbname}" + + engine = create_engine( + url, + pool_size=5, + max_overflow=10, + future=True + ) + return engine + return (conectar_postgres,) + + +@app.cell +def _(Engine, pd): + def consultar_df(conn: Engine, query: str, params: dict | None = None) -> pd.DataFrame: + """ + Ejecuta una consulta SQL usando SQLAlchemy y devuelve los resultados como un DataFrame. + - conn: Engine devuelto por conectar_postgres() + - query: str con la consulta SQL + - params: dict opcional con parámetros de la consulta + """ + return pd.read_sql(query, con=conn, params=params) + return (consultar_df,) + + +@app.cell +def _(conn2, mo): + _df = mo.sql( + f""" + SELECT * FROM public.nota + """, + engine=conn2 + ) + return + + +@app.cell +def _(conectar_postgres, consultar_df): + conn2 = conectar_postgres() + + query = """ + SELECT * + FROM information_schema.tables + + -- WHERE table_type = 'BASE TABLE' + -- AND table_schema NOT IN ('pg_catalog', 'information_schema') + + ORDER BY table_schema, table_name; + """ + + consultar_df(conn2, query) + return (conn2,) + + +@app.cell +def _(): + from sqlalchemy import create_engine + from sqlalchemy.engine import Engine + from urllib.parse import quote_plus + import pandas as pd + # import psycopg2 + import sqlglot + # import os + # import yaml + # import subprocess + return Engine, create_engine, pd, quote_plus + + +@app.cell +def _(mo): + _df = mo.sql( + f""" + SELECT * FROM + """ + ) + return + + +@app.cell(column=2) +def _(DBNAME, PASSWORD, PUERTO_POSTGRES, USER): + import psycopg2 + + # Sentencias SQL + sql = """ + CREATE EXTENSION IF NOT EXISTS vector; + + CREATE TABLE IF NOT EXISTS nota ( + id SERIAL PRIMARY KEY, + titulo TEXT NOT NULL, + embedding VECTOR(768) + ); + """ + + HOST = "127.0.0.1" + + def init_db(): + conn = psycopg2.connect( + dbname=DBNAME, + user=USER, + password=PASSWORD, + host=HOST, + port=PUERTO_POSTGRES + ) + conn.autocommit = True + cur = conn.cursor() + cur.execute(sql) + cur.close() + conn.close() + print("✅ Tabla 'nota' creada con pgvector.") + + if __name__ == "__main__": + init_db() + return HOST, psycopg2 + + +@app.cell +def _(): + return + + +@app.cell(column=3) +def _(mo): + # Cell 2 + # Caja de texto para la consulta + query_input = mo.ui.text(label="Texto de búsqueda", full_width=True) + query_input + + return (query_input,) + + +@app.cell +def _(model, query_input, tokenizer, torch): + # Cell 3 + # Generar embedding del texto introducido + if query_input.value.strip(): + _inputs = tokenizer( + [query_input.value], + return_tensors="pt", + truncation=True, + padding=True, + max_length=512 + ) + with torch.no_grad(): + embedding = model(**_inputs).last_hidden_state.mean(dim=1)[0].tolist() + embedding_str = "[" + ",".join(f"{v:.6f}" for v in embedding) + "]" + else: + embedding_str = None + + embedding_str + + return (embedding_str,) + + +@app.cell +def _( + DBNAME, + HOST, + PASSWORD, + PUERTO_POSTGRES, + USER, + embedding_str, + pd, + psycopg2, +): + if embedding_str: + + conn3 = psycopg2.connect( + dbname=DBNAME, + user=USER, + password=PASSWORD, + host=HOST, + port=PUERTO_POSTGRES + ) + + conn3.autocommit = True + _cur = conn3.cursor() + + _cur.execute( + """ + SELECT id, titulo, embedding <#> %s::vector AS distancia + FROM nota + ORDER BY embedding <#> %s::vector + LIMIT 3; + """, + (embedding_str, embedding_str) + ) + resultados = _cur.fetchall() + _cur.close() + conn3.close() + + df = pd.DataFrame(resultados, columns=["ID", "Título", "Distancia"]) + else: + df = pd.DataFrame(columns=["ID", "Título", "Distancia"]) + + df + return + + +@app.cell +def _(): + return + + +if __name__ == "__main__": + app.run()