Ir al contenido

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.

  1. Una app local cuya base de datos contiene un vector store de 512 dimensiones.
  2. La migración que crea la tabla embeddings_512 al arrancar.
  3. Una función de ingesta que divide markdown en chunks y escribe los embeddings en el store.
  • 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 a llm.embed) necesita un proveedor configurado.

Instala las dependencias:

Ventana de terminal
wippy add wippy/embeddings
wippy add wippy/migration
wippy add wippy/bootloader
wippy add wippy/llm
wippy install

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:

Ventana de terminal
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.)

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_id agrupa todos los chunks de un documento fuente — elimina y reingiere por documento con embedding_repo.delete_by_origin(doc_id).
  • content_type te permite mantener corpus distintos (doc_chunk, faq, code_snippet) en un solo store y filtrar en tiempo de consulta.
  • add_batch divide automáticamente cuando el batch supera el límite de 8000 tokens por solicitud.

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.

  • 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-small a 512 dimensiones es rentable y coincide con la tabla embeddings_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. Apunta target_db a un db.sql.postgres con pgvector para un store compartido y de producción; el código de ingesta no cambia.
  • RAG — recupera de este store y genera respuestas fundamentadas
  • Framework LLMllm.embed, modelos de embeddings, proveedores
  • Módulo Text — splitters y tokenización