Catálogo y extracción
El valor es opaco para el motor, así que un índice necesita una función que extraiga las claves a indexar. Se registra cuando la base de datos se abre:
db.define_index(IndexDef {
id: 1,
name: "by_endpoint",
version: 3,
unique: false,
extract: |key, value| -> Vec<Vec<u8>> { /* ... */ },
})?;El motor llama a extract en cada put y delete, y mantiene el índice dentro de la misma transacción que los datos. Esa atomicidad es exactamente lo que se compra al meter los índices en el motor en lugar de mantenerlos en el código de quien llama.
Almacenamiento, un espacio de claves por índice:
No único:
[0x80 | id][indexed_key][primary_key] → emptyÚnico:
[0x80 | id][indexed_key] → primary_key, con la unicidad garantizada por colisión de clave — no hace falta comprobación extra.
Versionado del extractor. El campo version se almacena en el catálogo. Si una base de datos se reabre con una función de extracción distinta y la versión no se sube, el índice mentiría sin poder detectarlo. Con la versión almacenada, el motor detecta el desajuste y, según la configuración, se niega a abrir o reconstruye el índice. Es la única defensa posible cuando la lógica de indexación vive en una función que aporta quien llama.
Borrado por rango
delete_range sobre un rango de claves contiguo desengancha subárboles enteros. Los números de página de las hojas viven en sus nodos padre, así que las hojas nunca se leen: se recorren los nodos internos, sus punteros se vuelcan a la lista libre en bloque, y el subárbol se desengancha. El coste es proporcional al número de páginas internas, no al número de entradas borradas.
Podar 833.000 registros con páginas de 4 KB significa leer unas 25 páginas internas en lugar de las 8.000 hojas que contienen los datos — dos órdenes de magnitud menos E/S. Sin esta operación, la poda diaria de retención en cada nodo sería un recorrido completo del árbol, exactamente el coste de CPU que el proyecto existe para evitar.
Regla derivada. Cualquier índice definido sobre un espacio de claves con retención debe poner el componente temporal primero en su clave de índice. Con [bucket][endpoint][pk], borrar las entradas de índice correspondientes es otro rango contiguo. Sin el prefijo temporal, las entradas de índice acaban dispersas por todo el índice y la poda vuelve a ser un escaneo completo.