Dataflow: Base de Conocimiento Local
Construye una base de conocimiento en tu propia máquina — crea el vector store, luego divide en chunks e
ingiere documentos en él. Este es el complemento de creación de datos del
tutorial RAG: aquí levantas y llenas una KB local; allí
recuperas de ella y generas respuestas. Ambos usan el módulo wippy/embeddings respaldado por
un vector store SQLite local.
Lo que construirás
- Una app local cuya base de datos contiene un vector store de 512 dimensiones.
- La migración que crea la tabla
embeddings_512al arrancar. - Una función de ingesta que divide markdown en chunks y escribe los embeddings en el store.
Requisitos previos
- Un proyecto Wippy (clona app-template, o
wippy init). - Un proveedor LLM con un modelo de embeddings configurado (por ejemplo,
text-embedding-3-small) — consulta Framework LLM. El vector store se crea localmente sin él, pero la ingesta (que llama allm.embed) necesita un proveedor configurado.
Instala las dependencias:
wippy add wippy/embeddings
wippy add wippy/migration
wippy add wippy/bootloader
wippy add wippy/llm
wippy install
Crear el store
La KB vive en una base de datos SQLite local. wippy/embeddings incluye una migración que
crea la tabla vectorial; el bootloader la ejecuta al arrancar. Conecta las piezas:
version: "1.0"
namespace: app
entries:
- name: db
kind: db.sql.sqlite
file: ./data/app.db
lifecycle:
auto_start: true
- name: processes
kind: process.host
host:
max_processes: 1000
workers: 8
- name: embeddings
kind: ns.dependency
component: wippy/embeddings
parameters:
- name: target_db
value: app:db
- name: migration
kind: ns.dependency
component: wippy/migration
parameters:
- name: app_db
value: app:db
- name: bootloader
kind: ns.dependency
component: wippy/bootloader
parameters:
- name: application_host
value: app:processes
- name: app_db
value: app:db
- name: env_storage
value: app.env:store
El bootloader necesita un store de entorno; añade el estándar en su propio namespace:
# src/env/_index.yaml
version: "1.0"
namespace: app.env
entries:
- name: file
kind: env.storage.file
auto_create: true
file_path: .env
lifecycle:
auto_start: true
- name: os
kind: env.storage.os
lifecycle:
auto_start: true
- name: store
kind: env.storage.router
lifecycle:
auto_start: true
storages:
- app.env:file
- app.env:os
Crea el directorio de datos e inicia la app:
mkdir -p data
wippy run
Al arrancar, la migración se ejecuta y el store aparece en data/app.db:
$ sqlite3 data/app.db ".tables"
_migrations embeddings_512 embeddings_512_chunks
embeddings_512_info embeddings_512_rowids embeddings_512_vector_chunks00
...
embeddings_512 es una tabla virtual vec0 de SQLite; las tablas sombra embeddings_512_*
contienen sus chunks, row ids y metadatos. (En PostgreSQL la misma migración usa
pgvector en su lugar.)
Ingerir documentos
La ingesta consta de dos pasos: dividir el texto en chunks con el módulo text, luego escribirlos
con embeddings.add_batch, que embebe y persiste cada chunk.
-- src/ingest.lua
local text = require("text")
local embeddings = require("embeddings")
local function ingest(doc_id, title, markdown)
local splitter, err = text.splitter.markdown({
chunk_size = 800,
chunk_overlap = 100,
heading_hierarchy = true,
code_blocks = true,
})
if err then return nil, err end
local chunks, split_err = splitter:split_text(markdown)
if split_err then return nil, split_err end
local batch = {}
for i, chunk in ipairs(chunks) do
table.insert(batch, {
content = chunk,
content_type = "doc_chunk",
origin_id = doc_id,
context_id = tostring(i),
meta = { title = title, chunk = i },
})
end
return embeddings.add_batch(batch)
end
return { ingest = ingest }
Registra la función:
- name: ingest
kind: function.lua
source: file://ingest.lua
method: ingest
modules:
- text
imports:
embeddings: wippy.embeddings:embeddings
Puntos clave:
origin_idagrupa todos los chunks de un documento fuente — elimina y reingiere por documento conembedding_repo.delete_by_origin(doc_id).content_typete permite mantener corpus distintos (doc_chunk,faq,code_snippet) en un solo store y filtrar en tiempo de consulta.add_batchdivide automáticamente cuando el batch supera el límite de 8000 tokens por solicitud.
Verificar el contenido
Una vez ingeridos los documentos, confirma que las filas se guardaron y ejecuta una búsqueda por similitud:
local embeddings = require("embeddings")
local results, err = embeddings.search("how do I configure TLS?", {
content_type = "doc_chunk",
limit = 5,
})
-- results[i].content, .similarity, .meta, .origin_id, .context_id
A partir de ahí, el tutorial RAG muestra cómo alimentar estos resultados a un LLM para obtener respuestas fundamentadas.
Notas operativas
- Tamaño de chunk: 500–1000 tokens es un buen valor por defecto. Usa
chunk_overlap(~10–20 % del tamaño del chunk) para que las frases no se corten a través de los límites. - Dimensiones:
text-embedding-3-smalla 512 dimensiones es rentable y coincide con la tablaembeddings_512. Vectores más grandes significan mayor almacenamiento y búsqueda más lenta. - Local vs. compartido: SQLite (
vec0) mantiene toda la KB en un solo archivo local — ideal para desarrollo y apps de un solo nodo. Apuntatarget_dba undb.sql.postgresconpgvectorpara un store compartido y de producción; el código de ingesta no cambia.
Siguientes Pasos
- RAG — recupera de este store y genera respuestas fundamentadas
- Framework LLM —
llm.embed, modelos de embeddings, proveedores - Módulo Text — splitters y tokenización