AlleksDev commited on
Commit
d4bc59b
·
unverified ·
1 Parent(s): 0d02fb4

Add pgAdmin migration and Colab embedding loaders

Browse files
app/jobs/sync_place_embeddings.py CHANGED
@@ -73,6 +73,10 @@ async def main() -> None:
73
  counters.upserted,
74
  counters.errors,
75
  )
 
 
 
 
76
 
77
 
78
  async def _flush_batch(
 
73
  counters.upserted,
74
  counters.errors,
75
  )
76
+ if counters.errors:
77
+ raise RuntimeError(
78
+ f"Place embedding sync finished with {counters.errors} failed records"
79
+ )
80
 
81
 
82
  async def _flush_batch(
app/jobs/sync_post_embeddings.py CHANGED
@@ -73,6 +73,10 @@ async def main() -> None:
73
  counters.upserted,
74
  counters.errors,
75
  )
 
 
 
 
76
 
77
 
78
  async def _flush_batch(
 
73
  counters.upserted,
74
  counters.errors,
75
  )
76
+ if counters.errors:
77
+ raise RuntimeError(
78
+ f"Post embedding sync finished with {counters.errors} failed records"
79
+ )
80
 
81
 
82
  async def _flush_batch(
docs/fasttext_deployment.md CHANGED
@@ -68,6 +68,10 @@ psql "host=<host> port=5432 dbname=nlp_vectors user=<admin> sslmode=require" `
68
  La migracion trunca `place_embeddings` y `post_embeddings` porque son indices
69
  derivados incompatibles. No toca la base transaccional de la API principal.
70
 
 
 
 
 
71
  ## 4. Repoblar PGVector
72
 
73
  Con las credenciales writer en `.env`:
 
68
  La migracion trunca `place_embeddings` y `post_embeddings` porque son indices
69
  derivados incompatibles. No toca la base transaccional de la API principal.
70
 
71
+ Para hacerlo desde pgAdmin y ejecutar la carga pesada desde Google Colab consulta
72
+ `docs/pgadmin_colab_fasttext.md`. Incluye un SQL unico para Query Tool y dos scripts
73
+ independientes de carga.
74
+
75
  ## 4. Repoblar PGVector
76
 
77
  Con las credenciales writer en `.env`:
docs/pgadmin_colab_fasttext.md ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Migracion FastText Con pgAdmin Y Google Colab
2
+
3
+ ## 1. Antes De Modificar RDS
4
+
5
+ 1. Publica esta version en la rama `hf-deploy` de GitHub.
6
+ 2. Crea un snapshot de RDS.
7
+ 3. Pausa el Space de Hugging Face y cualquier job de sincronizacion.
8
+ 4. No ejecutes todavia los cargadores de Colab.
9
+
10
+ ## 2. Migrar Desde pgAdmin
11
+
12
+ En pgAdmin selecciona la base `nlp_vectors`, abre **Tools > Query Tool**, carga el
13
+ archivo `sql/pgadmin_migrate_fasttext_300.sql` y ejecutalo completo.
14
+
15
+ El script hace en una sola transaccion:
16
+
17
+ - valida que ambas columnas sigan siendo `VECTOR(16)`;
18
+ - elimina las funciones e indices incompatibles;
19
+ - trunca solamente `place_embeddings` y `post_embeddings`;
20
+ - cambia ambas columnas a `VECTOR(300)`;
21
+ - reconstruye HNSW, funciones y permisos;
22
+ - confirma al final la dimension y que ambas tablas quedaron vacias.
23
+
24
+ No lo ejecutes una segunda vez: esta protegido y abortara si ya encuentra
25
+ `VECTOR(300)`.
26
+
27
+ ## 3. Preparar Secrets En Colab
28
+
29
+ En el panel **Secrets** de Colab agrega y habilita **Notebook access** para:
30
+
31
+ | Nombre | Requerido | Uso |
32
+ |---|---:|---|
33
+ | `PGVECTOR_WRITER_PASSWORD` | Si | Password del rol `nlp_writer`. |
34
+ | `MAIN_API_INTERNAL_TOKEN` | Solo si aplica | Token para leer lugares/posts de la API principal. |
35
+ | `HF_TOKEN` | No | Puede ayudar con la descarga, pero el modelo es publico. |
36
+
37
+ Los scripts incluyen los valores no secretos actuales del host, base, usuario y URL
38
+ principal. Tambien puedes sobrescribirlos con Secrets llamados
39
+ `PGVECTOR_HOST`, `PGVECTOR_DATABASE`, `PGVECTOR_WRITER_USER` y
40
+ `MAIN_API_BASE_URL`.
41
+
42
+ ## 4. Permitir Temporalmente La IP De Colab
43
+
44
+ RDS debe ser accesible desde el runtime. Obten la IP publica actual en una celda:
45
+
46
+ ```python
47
+ !curl -s https://api.ipify.org
48
+ ```
49
+
50
+ En el Security Group de RDS agrega temporalmente una regla de entrada TCP 5432 con
51
+ origen `<IP_OBTENIDA>/32`. No uses `0.0.0.0/0`. Elimina la regla cuando terminen
52
+ ambas cargas.
53
+
54
+ Si RDS es privado y no tiene una ruta publica, Colab no podra conectarse directamente;
55
+ en ese caso ejecuta los jobs desde una instancia dentro de la VPC.
56
+
57
+ ## 5. Ejecutar Las Cargas
58
+
59
+ En un runtime de Colab con memoria suficiente:
60
+
61
+ ```python
62
+ !git clone --branch hf-deploy https://github.com/AlleksDev/Frimeet-API-NLP.git
63
+ %cd Frimeet-API-NLP
64
+ ```
65
+
66
+ Primero lugares:
67
+
68
+ ```python
69
+ !python scripts/colab_initial_load_places.py
70
+ ```
71
+
72
+ Despues publicaciones, reutilizando dependencias y el modelo ya descargado:
73
+
74
+ ```python
75
+ !python scripts/colab_initial_load_posts.py --skip-install --skip-download
76
+ ```
77
+
78
+ Para una prueba sin escrituras se pueden agregar `--dry-run --max-pages 1`. Para una
79
+ carga pequena real usa solamente `--max-pages 1`; despues puedes ejecutar otra vez
80
+ sin esa opcion y el job completara los registros faltantes. La ejecucion completa no
81
+ debe usar `--dry-run` ni `--max-pages`. El resultado correcto termina con `errors=0`
82
+ y el mensaje de carga terminada.
83
+
84
+ ## 6. Verificar Desde pgAdmin
85
+
86
+ Despues de ambas cargas abre nuevamente Query Tool y ejecuta el contenido de
87
+ `sql/verify_fasttext_embeddings.sql`. Debe mostrar:
88
+
89
+ - filas mayores que cero;
90
+ - dimensiones minima y maxima iguales a `300`;
91
+ - modelo `facebook/fasttext-es-vectors`;
92
+ - version `common-crawl-300-v1`;
93
+ - normas cercanas a `1`.
94
+
95
+ Finalmente elimina la regla temporal del Security Group, actualiza las variables del
96
+ Space y despliega la nueva imagen.
scripts/colab_initial_load_places.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Carga inicial de embeddings FastText de lugares desde Google Colab.
2
+
3
+ Ejecutar desde la raiz de un clon de este repositorio:
4
+
5
+ !python scripts/colab_initial_load_places.py
6
+
7
+ El script lee credenciales desde los Secrets de Colab usando los mismos nombres
8
+ de las variables de entorno. Nunca imprime los valores secretos.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import os
15
+ from pathlib import Path
16
+ import subprocess
17
+ import sys
18
+
19
+
20
+ REPO_ROOT = Path(__file__).resolve().parents[1]
21
+ DEFAULT_MODEL_PATH = "/content/fasttext-es/model.bin"
22
+
23
+
24
+ def main() -> None:
25
+ args = _parse_args()
26
+ os.chdir(REPO_ROOT)
27
+ _configure_environment()
28
+
29
+ if not args.skip_install:
30
+ _run(
31
+ sys.executable,
32
+ "-m",
33
+ "pip",
34
+ "install",
35
+ "--quiet",
36
+ "-r",
37
+ str(REPO_ROOT / "requirements.txt"),
38
+ )
39
+
40
+ if not args.skip_download:
41
+ _run(
42
+ sys.executable,
43
+ "-m",
44
+ "app.shared.nlp.embeddings.download_fasttext_model",
45
+ "--repo-id",
46
+ os.environ["FASTTEXT_MODEL_REPO_ID"],
47
+ "--filename",
48
+ os.environ["FASTTEXT_MODEL_FILENAME"],
49
+ "--destination",
50
+ os.environ["FASTTEXT_MODEL_PATH"],
51
+ )
52
+
53
+ command = [
54
+ sys.executable,
55
+ "-m",
56
+ "app.jobs.initial_load_place_embeddings",
57
+ "--batch-size",
58
+ str(args.batch_size),
59
+ "--page-limit",
60
+ str(args.page_limit),
61
+ ]
62
+ if args.max_pages is not None:
63
+ command.extend(["--max-pages", str(args.max_pages)])
64
+ if args.dry_run:
65
+ command.append("--dry-run")
66
+
67
+ _run(*command)
68
+ print("Carga de lugares terminada correctamente.")
69
+
70
+
71
+ def _configure_environment() -> None:
72
+ defaults = {
73
+ "ENV": "colab",
74
+ "MAIN_API_BASE_URL": "http://52.86.8.11",
75
+ "MAIN_API_PLACES_SEARCH_PATH": "/api/v1/places/search",
76
+ "MAIN_API_TIMEOUT_SECONDS": "60",
77
+ "MAIN_API_PLACES_PAGE_LIMIT": "50",
78
+ "MAIN_API_PLACES_PAGINATION_MODE": "cursor",
79
+ "VECTOR_STORE_PROVIDER": "aws_pgvector",
80
+ "PGVECTOR_HOST": "nlp-vector-db.c2jwncm87zsa.us-east-1.rds.amazonaws.com",
81
+ "PGVECTOR_PORT": "5432",
82
+ "PGVECTOR_DATABASE": "nlp_vectors",
83
+ "PGVECTOR_WRITER_USER": "nlp_writer",
84
+ "PGVECTOR_SSL_MODE": "require",
85
+ "EMBEDDING_PROVIDER": "fasttext",
86
+ "EMBEDDING_DIMENSION": "300",
87
+ "EMBEDDING_MODEL": "facebook/fasttext-es-vectors",
88
+ "EMBEDDING_VERSION": "common-crawl-300-v1",
89
+ "FASTTEXT_MODEL_PATH": DEFAULT_MODEL_PATH,
90
+ "FASTTEXT_MODEL_REPO_ID": "facebook/fasttext-es-vectors",
91
+ "FASTTEXT_MODEL_FILENAME": "model.bin",
92
+ "FASTTEXT_AUTO_DOWNLOAD": "false",
93
+ "LOG_LEVEL": "INFO",
94
+ }
95
+ for name, default in defaults.items():
96
+ os.environ[name] = _read_setting(name, default=default)
97
+
98
+ os.environ["PGVECTOR_WRITER_PASSWORD"] = _read_setting(
99
+ "PGVECTOR_WRITER_PASSWORD",
100
+ required=True,
101
+ )
102
+
103
+ for optional_name in (
104
+ "MAIN_API_INTERNAL_TOKEN",
105
+ "MAIN_API_AUTH_TOKEN",
106
+ "HF_TOKEN",
107
+ ):
108
+ value = _read_setting(optional_name)
109
+ if value:
110
+ os.environ[optional_name] = value
111
+
112
+
113
+ def _read_setting(
114
+ name: str,
115
+ default: str | None = None,
116
+ required: bool = False,
117
+ ) -> str:
118
+ value = os.getenv(name) or _read_colab_secret(name) or default
119
+ if required and not value:
120
+ raise RuntimeError(
121
+ f"Falta el Secret {name!r} en Colab. Activa tambien Notebook access."
122
+ )
123
+ return value or ""
124
+
125
+
126
+ def _read_colab_secret(name: str) -> str | None:
127
+ try:
128
+ from google.colab import userdata
129
+
130
+ value = userdata.get(name)
131
+ return str(value).strip() if value else None
132
+ except Exception:
133
+ return None
134
+
135
+
136
+ def _run(*command: str) -> None:
137
+ subprocess.run(list(command), cwd=REPO_ROOT, check=True)
138
+
139
+
140
+ def _parse_args() -> argparse.Namespace:
141
+ parser = argparse.ArgumentParser(description=__doc__)
142
+ parser.add_argument("--batch-size", type=int, default=25)
143
+ parser.add_argument("--page-limit", type=int, default=50)
144
+ parser.add_argument("--max-pages", type=int, default=None)
145
+ parser.add_argument("--dry-run", action="store_true")
146
+ parser.add_argument("--skip-install", action="store_true")
147
+ parser.add_argument("--skip-download", action="store_true")
148
+ return parser.parse_args()
149
+
150
+
151
+ if __name__ == "__main__":
152
+ main()
scripts/colab_initial_load_posts.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Carga inicial de embeddings FastText de publicaciones desde Google Colab.
2
+
3
+ Ejecutar desde la raiz de un clon de este repositorio:
4
+
5
+ !python scripts/colab_initial_load_posts.py
6
+
7
+ El script lee credenciales desde los Secrets de Colab usando los mismos nombres
8
+ de las variables de entorno. Nunca imprime los valores secretos.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import os
15
+ from pathlib import Path
16
+ import subprocess
17
+ import sys
18
+
19
+
20
+ REPO_ROOT = Path(__file__).resolve().parents[1]
21
+ DEFAULT_MODEL_PATH = "/content/fasttext-es/model.bin"
22
+
23
+
24
+ def main() -> None:
25
+ args = _parse_args()
26
+ os.chdir(REPO_ROOT)
27
+ _configure_environment()
28
+
29
+ if not args.skip_install:
30
+ _run(
31
+ sys.executable,
32
+ "-m",
33
+ "pip",
34
+ "install",
35
+ "--quiet",
36
+ "-r",
37
+ str(REPO_ROOT / "requirements.txt"),
38
+ )
39
+
40
+ if not args.skip_download:
41
+ _run(
42
+ sys.executable,
43
+ "-m",
44
+ "app.shared.nlp.embeddings.download_fasttext_model",
45
+ "--repo-id",
46
+ os.environ["FASTTEXT_MODEL_REPO_ID"],
47
+ "--filename",
48
+ os.environ["FASTTEXT_MODEL_FILENAME"],
49
+ "--destination",
50
+ os.environ["FASTTEXT_MODEL_PATH"],
51
+ )
52
+
53
+ command = [
54
+ sys.executable,
55
+ "-m",
56
+ "app.jobs.initial_load_post_embeddings",
57
+ "--batch-size",
58
+ str(args.batch_size),
59
+ "--page-limit",
60
+ str(args.page_limit),
61
+ ]
62
+ if args.max_pages is not None:
63
+ command.extend(["--max-pages", str(args.max_pages)])
64
+ if args.dry_run:
65
+ command.append("--dry-run")
66
+
67
+ _run(*command)
68
+ print("Carga de publicaciones terminada correctamente.")
69
+
70
+
71
+ def _configure_environment() -> None:
72
+ defaults = {
73
+ "ENV": "colab",
74
+ "MAIN_API_BASE_URL": "http://52.86.8.11",
75
+ "MAIN_API_POSTS_SEARCH_PATH": "/api/v1/posts/search",
76
+ "MAIN_API_TIMEOUT_SECONDS": "60",
77
+ "MAIN_API_POSTS_PAGE_LIMIT": "50",
78
+ "MAIN_API_POSTS_PAGINATION_MODE": "cursor",
79
+ "VECTOR_STORE_PROVIDER": "aws_pgvector",
80
+ "PGVECTOR_HOST": "nlp-vector-db.c2jwncm87zsa.us-east-1.rds.amazonaws.com",
81
+ "PGVECTOR_PORT": "5432",
82
+ "PGVECTOR_DATABASE": "nlp_vectors",
83
+ "PGVECTOR_WRITER_USER": "nlp_writer",
84
+ "PGVECTOR_SSL_MODE": "require",
85
+ "EMBEDDING_PROVIDER": "fasttext",
86
+ "EMBEDDING_DIMENSION": "300",
87
+ "EMBEDDING_MODEL": "facebook/fasttext-es-vectors",
88
+ "EMBEDDING_VERSION": "common-crawl-300-v1",
89
+ "FASTTEXT_MODEL_PATH": DEFAULT_MODEL_PATH,
90
+ "FASTTEXT_MODEL_REPO_ID": "facebook/fasttext-es-vectors",
91
+ "FASTTEXT_MODEL_FILENAME": "model.bin",
92
+ "FASTTEXT_AUTO_DOWNLOAD": "false",
93
+ "LOG_LEVEL": "INFO",
94
+ }
95
+ for name, default in defaults.items():
96
+ os.environ[name] = _read_setting(name, default=default)
97
+
98
+ os.environ["PGVECTOR_WRITER_PASSWORD"] = _read_setting(
99
+ "PGVECTOR_WRITER_PASSWORD",
100
+ required=True,
101
+ )
102
+
103
+ for optional_name in (
104
+ "MAIN_API_INTERNAL_TOKEN",
105
+ "MAIN_API_AUTH_TOKEN",
106
+ "HF_TOKEN",
107
+ ):
108
+ value = _read_setting(optional_name)
109
+ if value:
110
+ os.environ[optional_name] = value
111
+
112
+
113
+ def _read_setting(
114
+ name: str,
115
+ default: str | None = None,
116
+ required: bool = False,
117
+ ) -> str:
118
+ value = os.getenv(name) or _read_colab_secret(name) or default
119
+ if required and not value:
120
+ raise RuntimeError(
121
+ f"Falta el Secret {name!r} en Colab. Activa tambien Notebook access."
122
+ )
123
+ return value or ""
124
+
125
+
126
+ def _read_colab_secret(name: str) -> str | None:
127
+ try:
128
+ from google.colab import userdata
129
+
130
+ value = userdata.get(name)
131
+ return str(value).strip() if value else None
132
+ except Exception:
133
+ return None
134
+
135
+
136
+ def _run(*command: str) -> None:
137
+ subprocess.run(list(command), cwd=REPO_ROOT, check=True)
138
+
139
+
140
+ def _parse_args() -> argparse.Namespace:
141
+ parser = argparse.ArgumentParser(description=__doc__)
142
+ parser.add_argument("--batch-size", type=int, default=25)
143
+ parser.add_argument("--page-limit", type=int, default=50)
144
+ parser.add_argument("--max-pages", type=int, default=None)
145
+ parser.add_argument("--dry-run", action="store_true")
146
+ parser.add_argument("--skip-install", action="store_true")
147
+ parser.add_argument("--skip-download", action="store_true")
148
+ return parser.parse_args()
149
+
150
+
151
+ if __name__ == "__main__":
152
+ main()
sql/pgadmin_migrate_fasttext_300.sql ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Ejecutar completo una sola vez desde pgAdmin > Query Tool.
2
+ -- Requisitos:
3
+ -- 1. Tener un snapshot reciente de RDS.
4
+ -- 2. Detener temporalmente el Space y cualquier job de embeddings.
5
+ -- 3. Conectarse a la base nlp_vectors como propietario de las tablas o administrador.
6
+ --
7
+ -- Este script elimina solamente los indices derivados de busqueda. No toca la
8
+ -- base transaccional de la API principal. Si las columnas ya no son VECTOR(16),
9
+ -- aborta para evitar truncar accidentalmente una migracion ya terminada.
10
+
11
+ CREATE EXTENSION IF NOT EXISTS vector;
12
+
13
+ BEGIN;
14
+
15
+ LOCK TABLE public.place_embeddings IN ACCESS EXCLUSIVE MODE;
16
+ LOCK TABLE public.post_embeddings IN ACCESS EXCLUSIVE MODE;
17
+
18
+ DO $$
19
+ DECLARE
20
+ place_type TEXT;
21
+ post_type TEXT;
22
+ BEGIN
23
+ SELECT format_type(attribute.atttypid, attribute.atttypmod)
24
+ INTO place_type
25
+ FROM pg_attribute attribute
26
+ WHERE attribute.attrelid = 'public.place_embeddings'::regclass
27
+ AND attribute.attname = 'embedding'
28
+ AND NOT attribute.attisdropped;
29
+
30
+ SELECT format_type(attribute.atttypid, attribute.atttypmod)
31
+ INTO post_type
32
+ FROM pg_attribute attribute
33
+ WHERE attribute.attrelid = 'public.post_embeddings'::regclass
34
+ AND attribute.attname = 'embedding'
35
+ AND NOT attribute.attisdropped;
36
+
37
+ IF place_type <> 'vector(16)' OR post_type <> 'vector(16)' THEN
38
+ RAISE EXCEPTION
39
+ 'Se esperaba VECTOR(16). Tipos encontrados: places=%, posts=%',
40
+ place_type,
41
+ post_type;
42
+ END IF;
43
+ END $$;
44
+
45
+ -- Las funciones antiguas se eliminan antes del cambio de dimension y se
46
+ -- reconstruyen dentro de la misma transaccion.
47
+ DROP FUNCTION IF EXISTS public.match_places(vector, integer, jsonb);
48
+ DROP FUNCTION IF EXISTS public.match_posts(vector, integer, jsonb);
49
+ DROP FUNCTION IF EXISTS public.upsert_place_embedding(
50
+ text, text, jsonb, vector, text, text, text, boolean
51
+ );
52
+ DROP FUNCTION IF EXISTS public.upsert_post_embedding(
53
+ text, text, jsonb, vector, text, text, text, boolean
54
+ );
55
+
56
+ DROP INDEX IF EXISTS public.place_embeddings_embedding_hnsw_idx;
57
+ DROP INDEX IF EXISTS public.post_embeddings_embedding_hnsw_idx;
58
+
59
+ -- No existe una conversion valida de los vectores mock de 16 dimensiones a
60
+ -- FastText. Ambas tablas son indices derivados y se reconstruiran desde cero.
61
+ TRUNCATE TABLE public.place_embeddings, public.post_embeddings;
62
+
63
+ ALTER TABLE public.place_embeddings
64
+ ALTER COLUMN embedding TYPE vector(300);
65
+
66
+ ALTER TABLE public.post_embeddings
67
+ ALTER COLUMN embedding TYPE vector(300);
68
+
69
+ CREATE INDEX place_embeddings_embedding_hnsw_idx
70
+ ON public.place_embeddings USING hnsw (embedding vector_cosine_ops);
71
+
72
+ CREATE INDEX post_embeddings_embedding_hnsw_idx
73
+ ON public.post_embeddings USING hnsw (embedding vector_cosine_ops);
74
+
75
+ CREATE INDEX IF NOT EXISTS place_embeddings_metadata_gin_idx
76
+ ON public.place_embeddings USING gin (metadata);
77
+
78
+ CREATE INDEX IF NOT EXISTS post_embeddings_metadata_gin_idx
79
+ ON public.post_embeddings USING gin (metadata);
80
+
81
+ CREATE OR REPLACE FUNCTION public.match_places(
82
+ query_embedding vector(300),
83
+ match_count integer,
84
+ filters jsonb DEFAULT '{}'::jsonb
85
+ )
86
+ RETURNS TABLE (
87
+ external_id text,
88
+ document text,
89
+ metadata jsonb,
90
+ score double precision
91
+ )
92
+ LANGUAGE sql
93
+ STABLE
94
+ SECURITY DEFINER
95
+ SET search_path = public
96
+ AS $$
97
+ SELECT
98
+ place.external_id,
99
+ place.document,
100
+ place.metadata,
101
+ 1 - (place.embedding <=> query_embedding) AS score
102
+ FROM public.place_embeddings place
103
+ WHERE place.is_active = true
104
+ AND COALESCE((filters->>'is_active')::boolean, true) = true
105
+ AND ((filters ? 'city') IS FALSE OR lower(place.metadata->>'city') = lower(filters->>'city'))
106
+ AND ((filters ? 'state') IS FALSE OR lower(place.metadata->>'state') = lower(filters->>'state'))
107
+ AND ((filters ? 'category') IS FALSE OR lower(place.metadata->>'category') = lower(filters->>'category'))
108
+ AND ((filters ? 'price_range') IS FALSE OR place.metadata->>'price_range' = filters->>'price_range')
109
+ AND ((filters ? 'occasion') IS FALSE OR place.metadata->>'occasion' ILIKE ('%' || (filters->>'occasion') || '%'))
110
+ AND (
111
+ (filters ? 'place_ids') IS FALSE
112
+ OR place.external_id IN (
113
+ SELECT jsonb_array_elements_text(filters->'place_ids')
114
+ )
115
+ )
116
+ ORDER BY place.embedding <=> query_embedding
117
+ LIMIT match_count;
118
+ $$;
119
+
120
+ CREATE OR REPLACE FUNCTION public.match_posts(
121
+ query_embedding vector(300),
122
+ match_count integer,
123
+ filters jsonb DEFAULT '{}'::jsonb
124
+ )
125
+ RETURNS TABLE (
126
+ external_id text,
127
+ document text,
128
+ metadata jsonb,
129
+ score double precision
130
+ )
131
+ LANGUAGE sql
132
+ STABLE
133
+ SECURITY DEFINER
134
+ SET search_path = public
135
+ AS $$
136
+ SELECT
137
+ post.external_id,
138
+ post.document,
139
+ post.metadata,
140
+ 1 - (post.embedding <=> query_embedding) AS score
141
+ FROM public.post_embeddings post
142
+ WHERE post.is_active = true
143
+ AND COALESCE((filters->>'is_active')::boolean, true) = true
144
+ AND ((filters ? 'city') IS FALSE OR lower(post.metadata->>'city') = lower(filters->>'city'))
145
+ ORDER BY post.embedding <=> query_embedding
146
+ LIMIT match_count;
147
+ $$;
148
+
149
+ CREATE OR REPLACE FUNCTION public.upsert_place_embedding(
150
+ p_external_id text,
151
+ p_document text,
152
+ p_metadata jsonb,
153
+ p_embedding vector(300),
154
+ p_content_hash text,
155
+ p_embedding_model text,
156
+ p_embedding_version text,
157
+ p_is_active boolean
158
+ )
159
+ RETURNS void
160
+ LANGUAGE sql
161
+ SECURITY DEFINER
162
+ SET search_path = public
163
+ AS $$
164
+ INSERT INTO public.place_embeddings (
165
+ external_id,
166
+ document,
167
+ metadata,
168
+ embedding,
169
+ content_hash,
170
+ embedding_model,
171
+ embedding_version,
172
+ is_active,
173
+ updated_at
174
+ )
175
+ VALUES (
176
+ p_external_id,
177
+ p_document,
178
+ p_metadata,
179
+ p_embedding,
180
+ p_content_hash,
181
+ p_embedding_model,
182
+ p_embedding_version,
183
+ p_is_active,
184
+ now()
185
+ )
186
+ ON CONFLICT (external_id) DO UPDATE SET
187
+ document = EXCLUDED.document,
188
+ metadata = EXCLUDED.metadata,
189
+ embedding = EXCLUDED.embedding,
190
+ content_hash = EXCLUDED.content_hash,
191
+ embedding_model = EXCLUDED.embedding_model,
192
+ embedding_version = EXCLUDED.embedding_version,
193
+ is_active = EXCLUDED.is_active,
194
+ updated_at = now();
195
+ $$;
196
+
197
+ CREATE OR REPLACE FUNCTION public.upsert_post_embedding(
198
+ p_external_id text,
199
+ p_document text,
200
+ p_metadata jsonb,
201
+ p_embedding vector(300),
202
+ p_content_hash text,
203
+ p_embedding_model text,
204
+ p_embedding_version text,
205
+ p_is_active boolean
206
+ )
207
+ RETURNS void
208
+ LANGUAGE sql
209
+ SECURITY DEFINER
210
+ SET search_path = public
211
+ AS $$
212
+ INSERT INTO public.post_embeddings (
213
+ external_id,
214
+ document,
215
+ metadata,
216
+ embedding,
217
+ content_hash,
218
+ embedding_model,
219
+ embedding_version,
220
+ is_active,
221
+ updated_at
222
+ )
223
+ VALUES (
224
+ p_external_id,
225
+ p_document,
226
+ p_metadata,
227
+ p_embedding,
228
+ p_content_hash,
229
+ p_embedding_model,
230
+ p_embedding_version,
231
+ p_is_active,
232
+ now()
233
+ )
234
+ ON CONFLICT (external_id) DO UPDATE SET
235
+ document = EXCLUDED.document,
236
+ metadata = EXCLUDED.metadata,
237
+ embedding = EXCLUDED.embedding,
238
+ content_hash = EXCLUDED.content_hash,
239
+ embedding_model = EXCLUDED.embedding_model,
240
+ embedding_version = EXCLUDED.embedding_version,
241
+ is_active = EXCLUDED.is_active,
242
+ updated_at = now();
243
+ $$;
244
+
245
+ CREATE OR REPLACE FUNCTION public.get_place_content_hashes(
246
+ p_external_ids text[]
247
+ )
248
+ RETURNS TABLE (
249
+ external_id text,
250
+ content_hash text
251
+ )
252
+ LANGUAGE sql
253
+ STABLE
254
+ SECURITY DEFINER
255
+ SET search_path = public
256
+ AS $$
257
+ SELECT place.external_id, place.content_hash
258
+ FROM public.place_embeddings place
259
+ WHERE place.external_id = ANY(p_external_ids);
260
+ $$;
261
+
262
+ CREATE OR REPLACE FUNCTION public.get_post_content_hashes(
263
+ p_external_ids text[]
264
+ )
265
+ RETURNS TABLE (
266
+ external_id text,
267
+ content_hash text
268
+ )
269
+ LANGUAGE sql
270
+ STABLE
271
+ SECURITY DEFINER
272
+ SET search_path = public
273
+ AS $$
274
+ SELECT post.external_id, post.content_hash
275
+ FROM public.post_embeddings post
276
+ WHERE post.external_id = ANY(p_external_ids);
277
+ $$;
278
+
279
+ -- Reaplica permisos solo cuando los roles ya existen.
280
+ DO $$
281
+ BEGIN
282
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'nlp_reader') THEN
283
+ EXECUTE 'GRANT USAGE ON SCHEMA public TO nlp_reader';
284
+ EXECUTE 'GRANT EXECUTE ON FUNCTION public.match_places(vector, integer, jsonb) TO nlp_reader';
285
+ EXECUTE 'GRANT EXECUTE ON FUNCTION public.match_posts(vector, integer, jsonb) TO nlp_reader';
286
+ END IF;
287
+
288
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'nlp_writer') THEN
289
+ EXECUTE 'GRANT USAGE ON SCHEMA public TO nlp_writer';
290
+ EXECUTE 'GRANT EXECUTE ON FUNCTION public.get_place_content_hashes(text[]) TO nlp_writer';
291
+ EXECUTE 'GRANT EXECUTE ON FUNCTION public.get_post_content_hashes(text[]) TO nlp_writer';
292
+ EXECUTE 'GRANT EXECUTE ON FUNCTION public.upsert_place_embedding(text, text, jsonb, vector, text, text, text, boolean) TO nlp_writer';
293
+ EXECUTE 'GRANT EXECUTE ON FUNCTION public.upsert_post_embedding(text, text, jsonb, vector, text, text, text, boolean) TO nlp_writer';
294
+ END IF;
295
+ END $$;
296
+
297
+ COMMIT;
298
+
299
+ -- Resultado esperado inmediatamente despues de migrar: dimension 300 y cero
300
+ -- filas. Las filas apareceran despues de ejecutar los scripts de Colab.
301
+ SELECT
302
+ table_name,
303
+ column_name,
304
+ udt_name,
305
+ format_type(attribute.atttypid, attribute.atttypmod) AS formatted_type
306
+ FROM information_schema.columns column_info
307
+ JOIN pg_attribute attribute
308
+ ON attribute.attrelid = (
309
+ quote_ident(column_info.table_schema) || '.' || quote_ident(column_info.table_name)
310
+ )::regclass
311
+ AND attribute.attname = column_info.column_name
312
+ WHERE column_info.table_schema = 'public'
313
+ AND column_info.table_name IN ('place_embeddings', 'post_embeddings')
314
+ AND column_info.column_name = 'embedding'
315
+ ORDER BY table_name;
316
+
317
+ SELECT 'place_embeddings' AS table_name, count(*) AS rows
318
+ FROM public.place_embeddings
319
+ UNION ALL
320
+ SELECT 'post_embeddings' AS table_name, count(*) AS rows
321
+ FROM public.post_embeddings;
322
+