feat: módulo embedding — encode, model CRUD, stores sqlvec y usearch
Funciones Python para embeddings: carga/guardado de modelos, encoding de texto, y almacenamiento/búsqueda vectorial con sqlite-vec y usearch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
"""Embedding functions — model management, encoding, and vector storage/retrieval."""
|
||||
|
||||
from embedding.model import embedding_save_model, embedding_load_model, embedding_encode
|
||||
from embedding.sqlvec import embedding_store_sqlvec, embedding_search_sqlvec
|
||||
from embedding.usearch_store import embedding_store_usearch, embedding_search_usearch
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: embedding_encode
|
||||
kind: function
|
||||
lang: py
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def embedding_encode(model: SentenceTransformer, texts: list, mode: str = 'document') -> list"
|
||||
description: "Genera embeddings normalizados para textos. Aplica prefijos e5 automaticamente segun mode (document/query)."
|
||||
tags: [embedding, encode, e5, multilingual, python]
|
||||
uses_functions: [embedding_load_model_py_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [sentence_transformers]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/embedding/model.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
model = embedding_load_model(".local/models/e5-small")
|
||||
|
||||
# Indexar documentos
|
||||
doc_embs = embedding_encode(model, ["La IA transforma la industria", "Python es versatil"], mode="document")
|
||||
|
||||
# Buscar
|
||||
query_embs = embedding_encode(model, ["¿Que es machine learning?"], mode="query")
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
mode="document" agrega prefijo "passage: ", mode="query" agrega "query: ".
|
||||
Estos prefijos son requeridos por modelos e5 para retrieval optimo.
|
||||
Los embeddings retornados son float32 normalizados (norma L2 = 1).
|
||||
Para e5-small la dimension es 384. Throughput ~1900 docs/s en CPU.
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: embedding_load_model
|
||||
kind: function
|
||||
lang: py
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def embedding_load_model(path: str) -> SentenceTransformer"
|
||||
description: "Carga modelo de embeddings desde path local. Retorna instancia lista para encode."
|
||||
tags: [embedding, model, load, python]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [sentence_transformers]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/embedding/model.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
model = embedding_load_model(".local/models/e5-small")
|
||||
# model listo para usar con embedding_encode
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Carga desde path local (~1.8s) es mas rapida que desde HF cache (~4.1s).
|
||||
El modelo retornado es compatible con embedding_encode.
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: embedding_save_model
|
||||
kind: function
|
||||
lang: py
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def embedding_save_model(model_id: str, path: str) -> str"
|
||||
description: "Descarga modelo de embeddings de HuggingFace y lo guarda en path local para carga rapida sin red."
|
||||
tags: [embedding, model, save, huggingface, e5, python]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [sentence_transformers]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/embedding/model.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
path = embedding_save_model("intfloat/multilingual-e5-small", ".local/models/e5-small")
|
||||
# path = "/home/lucas/fn_registry/.local/models/e5-small"
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
El modelo se guarda en formato sentence-transformers (safetensors + tokenizer).
|
||||
Para multilingual-e5-small ocupa ~465 MB en disco.
|
||||
Carga local es ~2.3x mas rapida que desde HF cache.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: embedding_search_sqlvec
|
||||
kind: function
|
||||
lang: py
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def embedding_search_sqlvec(db_path: str, table: str, query_embedding: list, k: int = 10) -> list"
|
||||
description: "Busca los k vecinos mas cercanos en tabla sqlite-vec. Retorna rowids y distancias ordenados."
|
||||
tags: [embedding, sqlite, vector, search, retrieval, sqlite-vec, python]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [sqlite3, sqlite_vec, numpy]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/embedding/sqlvec.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
model = embedding_load_model(".local/models/e5-small")
|
||||
q_emb = embedding_encode(model, ["¿Que es machine learning?"], mode="query")[0]
|
||||
|
||||
results = embedding_search_sqlvec("vectors.db", "doc_embeddings", q_emb, k=5)
|
||||
# [{"rowid": 0, "distance": 0.23}, {"rowid": 1, "distance": 0.45}, ...]
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Busqueda brute-force (exacta, no aproximada). Para 50k vectores tarda ~19ms/query.
|
||||
El campo distance es distancia coseno (menor = mas similar) porque los embeddings estan normalizados.
|
||||
Cold start rapido (~18ms) porque SQLite no carga todo el indice a RAM.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: embedding_search_usearch
|
||||
kind: function
|
||||
lang: py
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def embedding_search_usearch(path: str, query_embedding: list, k: int = 10, dim: int = 384) -> list"
|
||||
description: "Busca los k vecinos mas cercanos en indice USearch persistido. Busqueda sub-milisegundo."
|
||||
tags: [embedding, usearch, vector, search, retrieval, ann, python]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [usearch, numpy]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/embedding/usearch_store.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
model = embedding_load_model(".local/models/e5-small")
|
||||
q_emb = embedding_encode(model, ["¿Que es machine learning?"], mode="query")[0]
|
||||
|
||||
results = embedding_search_usearch("docs.usearch", q_emb, k=5)
|
||||
# [{"key": 0, "distance": 0.82}, {"key": 1, "distance": 0.65}, ...]
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Carga el indice completo a RAM antes de buscar. Cold start ~190ms para 50k vectores.
|
||||
Busqueda aproximada (HNSW) — puede no encontrar el vecino exacto pero es 150x mas rapido que brute-force.
|
||||
Distance es inner product (mayor = mas similar, al reves que sqlite-vec).
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: embedding_store_sqlvec
|
||||
kind: function
|
||||
lang: py
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def embedding_store_sqlvec(db_path: str, table: str, ids: list, embeddings: list, dim: int = 384) -> int"
|
||||
description: "Inserta embeddings en tabla sqlite-vec. Crea la tabla virtual si no existe. Insercion en batches."
|
||||
tags: [embedding, sqlite, vector, store, sqlite-vec, python]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [sqlite3, sqlite_vec, numpy]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/embedding/sqlvec.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
model = embedding_load_model(".local/models/e5-small")
|
||||
docs = ["La IA transforma la industria", "Python es versatil"]
|
||||
embs = embedding_encode(model, docs, mode="document")
|
||||
|
||||
n = embedding_store_sqlvec("vectors.db", "doc_embeddings", [0, 1], embs)
|
||||
# n = 2
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Usa sqlite-vec (extension pura C para SQLite). Los vectores se almacenan como blobs float32.
|
||||
Compatible con cualquier SQLite — se puede usar el mismo archivo para metadata con tablas normales.
|
||||
Insercion en batches de 500 para evitar limits de SQLite.
|
||||
Para 50k vectores dim=384: ~75 MB en disco, busqueda ~19ms/query.
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: embedding_store_usearch
|
||||
kind: function
|
||||
lang: py
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def embedding_store_usearch(path: str, ids: list, embeddings: list, dim: int = 384) -> int"
|
||||
description: "Crea indice USearch con embeddings y lo persiste a archivo. Busqueda sub-milisegundo."
|
||||
tags: [embedding, usearch, vector, store, ann, python]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [usearch, numpy]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/embedding/usearch_store.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
model = embedding_load_model(".local/models/e5-small")
|
||||
docs = ["La IA transforma la industria", "Python es versatil"]
|
||||
embs = embedding_encode(model, docs, mode="document")
|
||||
|
||||
n = embedding_store_usearch("docs.usearch", [0, 1], embs)
|
||||
# n = 2
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
USearch usa HNSW (approximate nearest neighbors). Para 50k vectores dim=384:
|
||||
~80 MB en disco, busqueda ~0.13ms/query (150x mas rapido que sqlite-vec).
|
||||
El tradeoff es que no soporta metadata nativa — usar junto con SQLite para metadata.
|
||||
Sobreescribe el archivo si ya existe.
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Embedding model management — save, load, and encode with multilingual-e5-small."""
|
||||
|
||||
import os
|
||||
|
||||
from sentence_transformers import SentenceTransformer
|
||||
|
||||
|
||||
def embedding_save_model(model_id: str, path: str) -> str:
|
||||
"""Descarga modelo de HuggingFace y lo guarda en path local.
|
||||
|
||||
Args:
|
||||
model_id: ID del modelo en HuggingFace (ej: "intfloat/multilingual-e5-small").
|
||||
path: Directorio destino para guardar el modelo.
|
||||
|
||||
Returns:
|
||||
Path absoluto donde se guardo el modelo.
|
||||
|
||||
Raises:
|
||||
OSError: Si no se puede escribir en el path.
|
||||
Exception: Si el modelo no existe en HuggingFace.
|
||||
"""
|
||||
os.makedirs(path, exist_ok=True)
|
||||
model = SentenceTransformer(model_id)
|
||||
model.save(path)
|
||||
return os.path.abspath(path)
|
||||
|
||||
|
||||
def embedding_load_model(path: str) -> SentenceTransformer:
|
||||
"""Carga modelo de embeddings desde path local.
|
||||
|
||||
Args:
|
||||
path: Directorio con el modelo guardado por embedding_save_model.
|
||||
|
||||
Returns:
|
||||
Instancia de SentenceTransformer lista para encode.
|
||||
|
||||
Raises:
|
||||
OSError: Si el path no existe o no contiene un modelo valido.
|
||||
"""
|
||||
return SentenceTransformer(path)
|
||||
|
||||
|
||||
def embedding_encode(model: SentenceTransformer, texts: list, mode: str = "document") -> list:
|
||||
"""Genera embeddings normalizados para una lista de textos.
|
||||
|
||||
Aplica automaticamente los prefijos requeridos por modelos e5:
|
||||
- mode="document" -> "passage: " prefix
|
||||
- mode="query" -> "query: " prefix
|
||||
|
||||
Args:
|
||||
model: Modelo cargado con embedding_load_model.
|
||||
texts: Lista de strings a codificar.
|
||||
mode: "document" para indexar, "query" para buscar.
|
||||
|
||||
Returns:
|
||||
Lista de arrays numpy float32 normalizados (dim depende del modelo).
|
||||
|
||||
Raises:
|
||||
ValueError: Si mode no es "document" ni "query".
|
||||
"""
|
||||
if mode not in ("document", "query"):
|
||||
raise ValueError(f"mode must be 'document' or 'query', got '{mode}'")
|
||||
|
||||
prefix = "passage: " if mode == "document" else "query: "
|
||||
prefixed = [f"{prefix}{t}" for t in texts]
|
||||
embeddings = model.encode(prefixed, normalize_embeddings=True, show_progress_bar=False)
|
||||
return embeddings
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Vector storage and retrieval with sqlite-vec."""
|
||||
|
||||
import sqlite3
|
||||
|
||||
import numpy as np
|
||||
import sqlite_vec
|
||||
|
||||
|
||||
def embedding_store_sqlvec(db_path: str, table: str, ids: list, embeddings: list, dim: int = 384) -> int:
|
||||
"""Inserta embeddings en una tabla sqlite-vec.
|
||||
|
||||
Crea la tabla virtual si no existe. Inserta en batches de 500.
|
||||
|
||||
Args:
|
||||
db_path: Path al archivo SQLite.
|
||||
table: Nombre de la tabla virtual vec0.
|
||||
ids: Lista de IDs (int) para cada embedding.
|
||||
embeddings: Lista de arrays numpy float32.
|
||||
dim: Dimension de los embeddings (default 384 para e5-small).
|
||||
|
||||
Returns:
|
||||
Numero de embeddings insertados.
|
||||
|
||||
Raises:
|
||||
sqlite3.Error: Si hay error de escritura en la BD.
|
||||
"""
|
||||
db = sqlite3.connect(db_path)
|
||||
db.enable_load_extension(True)
|
||||
sqlite_vec.load(db)
|
||||
|
||||
db.execute(f"CREATE VIRTUAL TABLE IF NOT EXISTS [{table}] USING vec0(embedding float[{dim}])")
|
||||
|
||||
batch_size = 500
|
||||
count = 0
|
||||
for i in range(0, len(ids), batch_size):
|
||||
batch = [
|
||||
(int(ids[j]), np.asarray(embeddings[j], dtype=np.float32).tobytes())
|
||||
for j in range(i, min(i + batch_size, len(ids)))
|
||||
]
|
||||
db.executemany(f"INSERT INTO [{table}](rowid, embedding) VALUES (?, ?)", batch)
|
||||
count += len(batch)
|
||||
|
||||
db.commit()
|
||||
db.close()
|
||||
return count
|
||||
|
||||
|
||||
def embedding_search_sqlvec(db_path: str, table: str, query_embedding: list, k: int = 10) -> list:
|
||||
"""Busca los k vecinos mas cercanos en una tabla sqlite-vec.
|
||||
|
||||
Args:
|
||||
db_path: Path al archivo SQLite con la tabla vec0.
|
||||
table: Nombre de la tabla virtual.
|
||||
query_embedding: Array numpy float32 del query.
|
||||
k: Numero de resultados a retornar.
|
||||
|
||||
Returns:
|
||||
Lista de dicts con 'rowid' y 'distance' ordenados por cercania.
|
||||
|
||||
Raises:
|
||||
sqlite3.Error: Si la tabla no existe o hay error de lectura.
|
||||
"""
|
||||
db = sqlite3.connect(db_path)
|
||||
db.enable_load_extension(True)
|
||||
sqlite_vec.load(db)
|
||||
|
||||
q_bytes = np.asarray(query_embedding, dtype=np.float32).tobytes()
|
||||
rows = db.execute(
|
||||
f"SELECT rowid, distance FROM [{table}] WHERE embedding MATCH ? ORDER BY distance LIMIT ?",
|
||||
(q_bytes, k),
|
||||
).fetchall()
|
||||
|
||||
db.close()
|
||||
return [{"rowid": r[0], "distance": r[1]} for r in rows]
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Vector storage and retrieval with USearch."""
|
||||
|
||||
import numpy as np
|
||||
from usearch.index import Index
|
||||
|
||||
|
||||
def embedding_store_usearch(path: str, ids: list, embeddings: list, dim: int = 384) -> int:
|
||||
"""Crea o sobreescribe un indice USearch con los embeddings dados.
|
||||
|
||||
Args:
|
||||
path: Path del archivo .usearch para persistir el indice.
|
||||
ids: Lista de IDs (int) para cada embedding.
|
||||
embeddings: Lista de arrays numpy float32.
|
||||
dim: Dimension de los embeddings (default 384 para e5-small).
|
||||
|
||||
Returns:
|
||||
Numero de embeddings insertados.
|
||||
|
||||
Raises:
|
||||
OSError: Si no se puede escribir en el path.
|
||||
"""
|
||||
index = Index(ndim=dim, metric="ip", dtype="f32")
|
||||
keys = np.array(ids, dtype=np.uint64)
|
||||
vecs = np.array(embeddings, dtype=np.float32)
|
||||
index.add(keys, vecs)
|
||||
index.save(path)
|
||||
return len(ids)
|
||||
|
||||
|
||||
def embedding_search_usearch(path: str, query_embedding: list, k: int = 10, dim: int = 384) -> list:
|
||||
"""Busca los k vecinos mas cercanos en un indice USearch persistido.
|
||||
|
||||
Args:
|
||||
path: Path del archivo .usearch.
|
||||
query_embedding: Array numpy float32 del query.
|
||||
k: Numero de resultados a retornar.
|
||||
dim: Dimension de los embeddings.
|
||||
|
||||
Returns:
|
||||
Lista de dicts con 'key' y 'distance' ordenados por cercania.
|
||||
|
||||
Raises:
|
||||
OSError: Si el archivo no existe.
|
||||
"""
|
||||
index = Index(ndim=dim, metric="ip", dtype="f32")
|
||||
index.load(path)
|
||||
q = np.asarray(query_embedding, dtype=np.float32)
|
||||
results = index.search(q, k)
|
||||
keys = np.atleast_1d(results.keys)
|
||||
distances = np.atleast_1d(results.distances)
|
||||
return [{"key": int(keys[i]), "distance": float(distances[i])} for i in range(len(keys))]
|
||||
Reference in New Issue
Block a user