• Featured post

Embeddings, Vector Search & BM25

Un ordenador no puede entender texto ni relaciónes semánticas o significados entre palabras. Solo puede entender números. Esto lo resolvemos mediante el uso de embeddings.

Un embedding es la representación de texto (en forma de números) en un espacio vectorial. Esto permite a los modelos de IA comparar y operar sobre el significado de las palabras.

flowchart TD
    A["perro"] --> B
    B --> C["[-0.003, 0.043, ..., -0.01]"]
    
    N1["(texto que queremos convertir)"]:::note --> A
    N2["(vectores con contenido semántico)"]:::note --> C
    
    classDef note fill:none,stroke:none,color:#777;    

Los vectores de cada palabra o documento capturan el significado semántico del texto.

  • perro estará cerca de mascota
  • contrato estará lejos de playa

Vector vs SQL databases

El problema con las BBDD típicas es que solo buscan matches exactos. Si yo busco por coche solo me sacará las entradas que contengan coche.

En cambio, como las BBDD vectoriales pueden interpretar la semántica de las palabras mediante los vectores, si busco por coche puede sacarme valores como sedán, SUV, Land Rover, etc.

Las BBDD vectoriales son muy buenas cuando necesitamos buscar items similares por proximidad uno respecto al otro. Un ejemplo de uso es buscar películas parecidas (Netflix). Otro ejemplo son los recomendadores de items parecidos en tiendas online (Amazon).

Como ejecutar una búsqueda (query) mediante vectores

(You can see the code here)

Necesitamos:

  • Una BBDD Vectorial (CosmosDB)
  • Un modelo para transformar los embeddings (text-embedding-3-large)

El flujo completo es el siguiente:

  1. Usar un embedding model para obtener los vectores del contenido que queremos indexar
  2. Insertar el texto original y los vectores del contenido en una BBDD vectorial
  3. Cuando queramos ejecutar una query usar el mismo embedding model de antes con la query a buscar. Con el embedding resultante buscamos vectores similares en la BBDD y sacamos el texto original de original_text

    Introducir vectores en CosmosDB

    Para poder buscar necesitamos rellenar antes la BBDD con contenido. Lo mantenemos simple. Metemos

    • un ID a mano
    • el texto original
    • los vectores resultado de hacer el embedding sobre el texto original

El pseudocódigo se ve así y se ejecuta de uno en uno

text = "A shiba walks alone in the park"
# this sends the text to the model text-embedding-3-large 
vectors = createEmbeddingsForText(text)
item = {
	"id": "1",
	"original_text": text,
	"vectors": vectors
}
uploadToCosmosDB(item)

ejemplos de los datos que guardo

{
	"id": "1",
	"original_text": "A shiba walks alone in the park",
	"vectors": [-0.003, 0.043, ..., -0.001]
}

Read More

Python 101 (env, tools and Poetry)

TL;DR

Stack a usar:

  • python -> lenguaje e intérprete
  • py -> python launcher para windows
  • pipx -> instalar tools python globales
  • .env -> variables de entorno local; UAT y PROD usar config de donde se despliegue
  • pyenv -> gestor de versiones de Python
  • poetry -> gestor de proyecto. Abstrae pip y sustituye venv y requeriments.txt. Detalles de como usar poetry aquí <- TODO: linkear a post

No usar pero si conocer:

  • pip -> gestor de paquetes - incluye uso de requirements.txt (sustituir por poetry)
  • pip3 -> No usarlo. Leftover histórico de cuando python2 y python3 convivian hasta el 2020
  • venv -> entornos virtuales para aislamiento (sustituir por poetry)
  • conda -> package y environment manager para data science & ML

    Stack a usar

    Python (lenguaje/intérprete)

    Python como tal es el lenguaje y el intérprete. Es un lenguaje interpretado (no compilado) sobre una MV propia. Python por sí mismo no gestiona dependencias ni aislamiento.

Python. Solo. Ejecuta. Código.

(py) python en Windows

Python no es un único ejecutable, puedes tener varios intérpretes instalados a la vez. El caos normalmente viene a que cada uno es una versión distinta, con librerías distintas y sus propios paths.

python.exe
python3.exe
py.exe

Si usas lo que hace el sistema es buscar python en $PATH y usar el primero que encuentre

python main.py

En windows usar siempre py. Está diseñado para gestionar múltiples versiones de Python en windows.

py # lanza la version por defecto. Normalmente el último Python3 instalado
py -3.11
py -0p # muy útil para debuggear todas las instalaciones detectadas

No usar python y py indistintamente

No se puede usar indistintamente python y py ya que son comandos diferentes.

# DON'T DO THIS
python -m venv .venv
.venv/Scripts/activate
pip install fastapi
py main.py # puede ignorar el .venv activado y lanzar un python global distinto

Read More

Git normalize line endings (.gitattributes)

Esto soluciona el problema con Git que a veces Gitkraken me muestra ficheros en la lista de cambios como si hubieran sido modificados, pero realmente no los tienen y genera ruido.

Añadir .gitattributes al repo con el siguiente contenido

# Normaliza texto automáticamente
* text=auto

# Markdown SIEMPRE en LF
*.md text eol=lf

# Scripts Unix
*.sh text eol=lf

# Binarios
*.png binary
*.jpg binary
*.pdf binary

ir a la carpeta del repo. Ejecutar lo siguiente.

git add --renormalize .
git status

hacer un commit con los contenidos normalizados.

Rob Pike's 5 Rules of Programming

  • Rule 1. You can’t tell where a program is going to spend its time. Bottlenecks occur in surprising places, so don’t try to second guess and put in a speed hack until you’ve proven that’s where the bottleneck is.
  • Rule 2. Measure. Don’t tune for speed until you’ve measured, and even then don’t unless one part of the code overwhelms the rest.
  • Rule 3. Fancy algorithms are slow when n is small, and n is usually small. Fancy algorithms have big constants. Until you know that n is frequently going to be big, don’t get fancy. (Even if n does get big, use Rule 2 first.)
  • Rule 4. Fancy algorithms are buggier than simple ones, and they’re much harder to implement. Use simple algorithms as well as simple data structures.
  • Rule 5. Data dominates. If you’ve chosen the right data structures and organized things well, the algorithms will almost always be self-evident. Data structures, not algorithms, are central to programming.

Takeaways

  • Premature optimization is the root of all evil. Don’t do it until it’s necessary and then measure before and after.
  • Don’t try to be fancy. When in doubt, use brute force (KISS)
  • Improve my data knowledge.

Source(s)

https://www.cs.unc.edu/~stotts/COMP590-059-f24/robsrules.html
https://news.ycombinator.com/item?id=47423647

MCP (Model Context Protocol)

Un MCP Server (Model Context Protocol) es un componente que implemente el protocolo MCP, un estándar abierto diseñado para conectar LLMs y agentes de IA con datos y herramientas externas de forma segura y estándar.

Dotan a los LLMs de capacidades adicionales permitiendo:

  • Acceso a datos en tiempo real: conectar la IA a APIs, BBDD, archivos locales…
  • Automatizaciones: permiten que los agentes no solo respondan, si no que ejecuten acciones (ejecutar flujos, crear registros, aprobar procesos)

Sin esto, los LLMs por sí solos están limitados a sus datos de entrenamiento.

(como funciona MCP - se conecta a diferentes fuentes de información independientemente de cuáles sean)

LLM Hyperparameters

Hyperparameters are configuration settings which are manually tuned to optimize efficiency or response creativity.

Los hyperparámetros son settings de configuración, los cuales se configuran manualmente para optimizar la eficiencia o la creatividad de la respuesta

Temperature (creatividad)

La temperatura controla la cuanto riesgo acepta el modelo a la hora de elegir las siguientes palabras. Controla la probabilidad de distribución de las siguientes palabras.

  • Low (0.2-0.3): El modelo es cauto y elige las palabras más probables. Output factual y predecible.
  • Medium (0.5-0.7): Un mix de confiabilidad y engagement
  • High (0.9-1.0): Toma riesgos y es impredecible

Respuesta posibles con la siguiente frase

Once upon a time, there was a dragon…

  • Temp 0.2: …who lived in a cave guarding treasure
  • Temp 0.7: …who dreamed of becoming friends with the villagers
  • Temp 1.0: …who loved baking cookies and singing karaoke

Cuanta más alta la temperatura, más imaginativo, creativo y loco es el modelo.

Top-k (candidate pool fija)

Top-k es un filtro de candidatos más probables. Fuerza al modelo a elegir el siguiente token sobre una pool k de candidatos. Límite de tamaño fijo.

Candidatos posibles con la siguiente frase

I like to drink…

  • small k (5): El modelo solo tiene unas pocas elecciones posibles, las cuales serán las más seguras posibles. Ej: [water, coffee, tea, juice, milk]
  • large k (50k): El modelo puede elegir de unas opciones muy ampliar. Amplía la variedad pero también la posibilidad de que salgan resultados inesperados Ej: […, smothies, kombucha, cocktails, hot chocolate, …]

La contra que tiene es que si el modelo está muy seguro, puede incluir tokens con una baja probabilidad solo por el hecho que tiene que llegar hasta k candidatos.

Read More

AI EU Legislation

European Regulation 2024/1689

La IA se clasifica por riesgos:

riesgo medidas ejemplo
inaceptable prohibido social scoring systems; manipulative AI
alto riesgo regulado  
riesgo limitado obligaciones de transparencia; los usuarios finales deben tener conocimiento que interactuan con un sistema de IA chatbots; deepfakes
riesgo mínimo sin regular juegos; filtros de spam;

Sobre todo establece obligaciones para providers (developers) de sistemas de alto riesgo que operen en EU, independientemente de dónde se encuentren localizados.

ISO/IEC 42001

Standard internacional que especifica los requerimientos para establecer, implementar, mantener y mejorar un Artificial Intelligence Management Systen (AIMS) dentro de organizaciones.

LangChain

LangChain es un framework open-source implementado en Python. Es un orquestador de aplicaciones basadas en LLMs que proporciona diferentes abstracciones de alto nivel para construir pipelines sobre LLMs.

La gran ventaja que nos introduce es abstraernos de la manera de consumir los diferentes LLMs para no tener que reinventar el código cada vez que queramos cambiar de modelo.

Ahora mismo LangChain dispone de 6 módulos diferentes

  • Model I/O - Handle input/output ops related to the model
  • Retrieval - Retrieves relevant texts for the LLM
  • Chains (Runnables) - Enables construction of sequences of LLM operations or function calls
  • Agents - Allows chains to make decisions on which tools to use based on high-level instructions
  • Memory - perstist the state of an application between different runs of a chain
  • Callbacks - for running additional code on specific events

Install

poetry add langchain
poetry add langchain-openai
poetry add openai # this is in case we want to use OpenAI's models

ChatModels

We have the following classes to interact with LLMs in the context of LangChain

  • SystemMessage - instructions for the AI System (Behaviour)
  • HumanMessage - messages coming from an human to interact with the LLM (questions, commands…)
  • AIMessage - information coming from the AI itself. This is typically the AI’s response

Chunking techniques (RAG)

En el contexto de RAG, chunking es el proceso de partir la información que contienen nuestros documentos en pedazos digestibles. Estos chunks los usaremos después para hacer queries contra ellos.

A veces si parece que no obtenemos los resultados que queremos de nuestras queries no es por falta de información, si no por un mal chunking. Si hacemos una query y por cómo hemos partido nuestros documentos tenemos la pregunta en un chunk y partes de la respuesta divididas entre múltiples chunks sin solapamiento, no obtendremos buenas respuestas.

La idea es simple, el proceso no tanto. Hay varias técnicas según lo que nos queramos complicar y los resultados que obtenemos varían entre ellas.

Chunking estático

El más simple. Tenemos un documento y partimos cada (por ejemplo) 500 tokens con 100 tokens de solapamiento entre chunks.

Tenemos la siguiente frase de prueba

La casa grande es bonita aunque cara, pero tiene un buen garaje. En el futuro quiero comprarme un coche

Visualizando los tokens a nivel de subword con test-multilingual-embedding me salen con 28 tokens aprox. ![[Pasted image 20260306132554.png]]

Si establecemos un limite de 10 tokens por chunk, con 3 de solapamiento, cada chunk se podría ver así:

La casa grande es bonita aunque cara
aunque cara, pero tiene un buen garaje
buen garaje. En el futuro quiero comprar
quiero comprarme un coche

El problema es que puede cortar frases a media palabra según como coincida ya que la palabra bonita son dos tokens bonit y a

Esto se puede aliviar un poco con librerías de chunking inteligente que existen que parten usando separadores como espacios o saltos de línea, pero el contexto se puede perder mucho si cae entre varios chunks.

Chunking semántico

Una técnica que solventa esto es hacer el chunking mediante un modelo de embedding, el cual parte los chunks por contenido semántico cuando detecta que el tema principal cambia. Es más caro porque un modelo tiene que analizar los documentos, pero da mejores resultados.

De esta manera el mismo texto nos quedaría en dos chunks que aunque tienen distinto tamaño, están mucho más contenidos, por lo que a la hora de buscar con una query nos dará mejores resultados.

La casa grande es bonita aunque cara, pero tiene un buen garaje.
En el futuro quiero comprarme un coche

Limpieza

Limpiar los chunks es tan importante como el dónde partir. En mis primeras pruebas se metía mucho ruido en cosmos por como \n o \t o chunks que solo contenían un número sin contexto ninguno.

Para sistemas grandes o genéricos puede ser importante meter metadatos junto a los chunks para filtrar luego al buscar. Cosas como el nombre del documento al que pertenece un chunk o la fecha de creación del documento ayudan a filtrar luego y favorecer documentación reciente o saber de dónde viene algún dato erróneo o con menor calidad.

RAG implementation

El código para mi implementación está aquí en GitHub

Requerimientos

  • LangChain
  • Un modelo de embeddings (text-embedding-3-large)
  • Un modelo de chat (gpt-4.1)
  • Una base de datos vectorial (CosmosDB)
  • PyPDF (libreria para chunking inteligente)
  • Documentos como base de conocimiento para el RAG (documentos sobre una herramienta propietaria)

Sobre CosmosDB for NoSQL: Se puede usar como una base NoSQL tradicional pero también soporta búsqueda nativa con vectores (hay que habilitarlo primero y establecer una policy vectorial). Yo la uso en vez de ChromaDB por estar en Azure y ser entorno Microsoft ya que personalmente me entra dentro del tier gratuito. Si no, ChromaDB es una buena opción gratuita.

Implementación

Se divide en tres procesos:

Habilitar búsqueda vectorial para CosmosDB. Tras hacer el siguiente paso, podemos ejecutar el script para recrear el contenedor con una policy para vectores

(dentro de CosmsoDB) Settings < Features < Vector Search for NoSQL API < Enable

Cargar documentos en CosmosDB:

  • Cargamos los documentos en memoria
  • Los dividimos en chunks
  • Limpiamos los chunks de caracteres especiales (\n, \t, \r)
  • Creamos los embeddings en batches
  • Para cada chunk subimos el original y su embeddings

Realizar una query:

  • Obtenemos los embeddings para la query (tiene que ser el mismo modelo que se usó para crear los embeddings de los ficheros)
  • Hacer una búsqueda vectorial en CosmosDB para sacar el texto original de los chunks con contexto relevante a nuestra query
  • Invocar el modelo de chat a través de LangChain, pasándole ambos pregunta y chunks con contexto en la misma llamada

Conceptos aprendidos

Calidad del dato

La calidad del dato es de máxima importancia. Hay que revisar manualmente los datos que se meten y ver que sean aporten valor. Si metemos datos que no aporten, solo generamos ruido.

También hay que revisar que los PDFs sean texto puro, ya que si son puramente imágenes o contienen imágenes importantes habrá que pasarlos por algún tipo de OCR para extraer texto.

Chunking

La parte de dónde y cómo hacer chunking es complicada. Lo importante es que haya un buen overlap entre chunks para que la respuesta a tu pregunta no caiga en tierra de nadie. Yo estoy probando con un chunk_size de 500 tokens y un overlap de 100 tokens.

Limpiar los chunks después de partirlos es igual de importante. En mis primeras pruebas se metían en cosmos muchos caracteres como \n o \t y esto genera mucho ruido.

Para sistemas grandes o genéricos puede ser importante meter metadatos junto a los chunks para filtrar luego al buscar. Cosas como el nombre del documento al que pertenece un chunk o la fecha de creación del documento ayudan a filtrar luego y favorecer documentación reciente o saber de dónde viene algún dato erróneo o con menor calidad.

Debilidades BBDD Vectorial

Una base de datos vectorial es muy buena para hacer búsquedas relacionadas o búsquedas semánticas, pero se queda corta para hacer búsquedas por keywords. En esas situaciones se puede implementar una búsqueda híbrida (algoritmo BM25).

Referencia(s)

RAG Systems in 5 Levels of Difficulty (With Full Code) | Data Science Collective *RAG vs Fine Tuning. The Great LLM Showdown | by Agneya Pathare | Medium