update .gitignore to include model and docker initialization files; add new script for database creation and search functionality
This commit is contained in:
@@ -9,3 +9,7 @@ wheels/
|
||||
# Virtual environments
|
||||
.venv
|
||||
.env
|
||||
|
||||
# Bases de datos y modelos
|
||||
.model/
|
||||
docker-entrypoint-initdb.d/
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user