Capítulo 2 — El núcleo: search.py
Commit:
ff6ce36—aifs(16 enero 2024, 01:08)
¿Qué se construyó?
En un solo commit explosivo, Killian agregó el corazón completo del proyecto: aifs/search.py (108 líneas), aifs/__init__.py, pyproject.toml con dependencias, y el poetry.lock con más de 2800 líneas (todas las dependencias resueltas).
Este es el salto del README vacío al producto funcional.
La arquitectura en un vistazo
Las tres grandes decisiones de diseño
1. unstructured para leer documentos
from unstructured.chunking.title import chunk_by_title
from unstructured.partition.auto import partition
def chunk_file(path):
elements = partition(filename=path)
chunks = chunk_by_title(elements, max_characters=MAX_CHARS_PER_CHUNK)
return [c.text for c in chunks]unstructured es una librería que puede leer cualquier tipo de archivo: PDF, Word, HTML, código fuente, texto plano. En lugar de reinventar la rueda, aifs delega la lectura al experto.
El método chunk_by_title divide el documento en fragmentos inteligentes respetando la estructura (títulos, secciones), con un máximo de MAX_CHARS_PER_CHUNK = 500 caracteres.
2. ChromaDB para búsqueda vectorial
import chromadb
from chromadb.utils.embedding_functions import DefaultEmbeddingFunction as setup_embed
embed = setup_embed()ChromaDB es una base de datos vectorial embebida (no necesita servidor). Funciona como SQLite pero para vectores.
DefaultEmbeddingFunction usa el modelo all-MiniLM-L6-v2 de sentence-transformers — pequeño (22MB), rápido, y lo suficientemente bueno para búsqueda semántica local.
3. Caché en _.aifs
path_to_index = os.path.join(path, "_.aifs")
if not os.path.exists(path_to_index):
print("Indexing for AI search. This will take time, but only happens once.")
else:
with open(path_to_index, 'r') as f:
index = json.load(f)El índice se guarda como un archivo JSON llamado _.aifs en el directorio raíz que se está indexando. La primera vez es lento (hay que embedear todo), pero las siguientes veces es instantáneo porque se reutiliza el caché.
La función pública: search()
def search(query, path=None, max_results=5):
if path == None:
path = os.getcwd()
# ... carga el índice ...
# ... indexa lo nuevo ...
# Crea colección temporal en ChromaDB
chroma_client = chromadb.Client()
collection = chroma_client.get_or_create_collection(name="temp")
# Inserta todos los embeddings
for file_path, file_index in index.items():
collection.add(
ids=ids,
embeddings=file_index["embeddings"],
documents=file_index["chunks"],
metadatas=[{"source": file_path}] * len(ids),
)
# Busca semánticamente
results = collection.query(query_texts=[query], n_results=max_results)
# Limpia y retorna
chroma_client.delete_collection("temp")
return results["documents"][0]Nótese que ChromaDB se usa como colección temporal en memoria — se crea, se llena con los embeddings del caché, se consulta, y se borra. El estado persistente real es el archivo _.aifs.
El problema con numpy
En este primer commit, Killian importaba numpy directamente. Pronto se daría cuenta de que generaba problemas de compatibilidad. Pero eso es para el siguiente capítulo.
Lección del capítulo
Un producto mínimo viable no tiene que ser pequeño en código — puede ser complejo internamente, pero debe hacer una cosa bien.
aifsdesde el día 1 ya hacía búsqueda semántica local completa.
Siguiente: Cap 03 — ChromaDB y embeddings