>_ josetilabs.com
← blog

12 Oct 2026 · ia, sqlite, busca · 4 min

Semantic search over your own files with sqlite-vec

A similarity query over 5000 documents answered in 1.7 ms, all inside a SQLite database. That is what sqlite-vec gives you: vector search as a 160 KB extension, with no separate server. It is dual licensed, MIT or Apache 2.0, and it costs nothing.

sqlite-vec is written in pure C with no dependencies, and it runs anywhere SQLite runs, from Linux to the browser via WASM. It is a Mozilla Builders project. You create a vec0 virtual table, insert vectors, and query nearest neighbors with a where ... match clause. The rest of your SQL keeps working as usual.

What goes in and what comes out

Diagrama do fluxo da busca: os arquivos passam pelo embed, os vetores entram na tabela vec0 do SQLite e uma consulta devolve os documentos mais próximos por KNN
Files become vectors, vectors go into SQLite, and the query returns the closest ones.

The drawing has one piece sqlite-vec does not provide: the embed() that turns text into a 256-dimensional vector. sqlite-vec stores and searches the vectors, it does not make them. In the script below, embed() uses character trigram hashing, no model and no dependency, just so the demo runs anywhere. For real semantic search, swap embed() for a model like sentence-transformers or an embeddings API, and the rest of the code stays the same.

The script

python
#!/usr/bin/env python3
# busca.py - busca por similaridade no seu acervo, com sqlite-vec quando existe.
#
# Uso:
#   python3 busca.py                demo com documentos de exemplo
#   python3 busca.py /minha/pasta   indexa os .txt/.md da pasta e responde 3 consultas
#
# O vetor de cada texto sai de um hash de trigramas de caractere (sem modelo, sem
# dependencia). Com o sqlite-vec instalado, os vetores ficam numa tabela vec0 e a
# busca e por KNN dentro do SQLite. Sem ele, o mesmo resultado sai de uma varredura
# em Python puro. Troque o embed() por um modelo de verdade e o resto nao muda.
import hashlib
import math
import os
import sqlite3
import struct
import sys

DIM = 256
TOP = 3

def embed(texto):
    texto = texto.lower()
    v = [0.0] * DIM
    for i in range(len(texto) - 2):
        h = int.from_bytes(hashlib.md5(texto[i:i + 3].encode("utf-8")).digest()[:4], "little")
        v[h % DIM] += 1.0
    norma = math.sqrt(sum(x * x for x in v))
    if norma == 0.0:
        return v
    return [x / norma for x in v]

def pack(vec):
    return struct.pack("%df" % DIM, *vec)

def carregar_vec(conn):
    try:
        conn.enable_load_extension(True)
    except Exception:
        return False
    try:
        so = os.environ.get("SQLITE_VEC_SO", "")
        if so and os.path.exists(so):
            conn.load_extension(so)
            return True
        import sqlite_vec
        sqlite_vec.load(conn)
        return True
    except Exception:
        return False
    finally:
        try:
            conn.enable_load_extension(False)
        except Exception:
            pass

DOCS = [
    ("backup.txt", "como fazer backup com rsync, guardar versoes com hardlink e testar a restauracao antes de precisar dela"),
    ("busca.txt", "indice de vetores dentro do proprio sqlite para busca por similaridade e consulta knn"),
    ("docker.txt", "rodar aplicacoes em contenedores isolados, com imagem, volume e rede propria"),
    ("rede.txt", "tunel seguro para expor um servico local sem abrir porta no roteador nem liberar firewall"),
    ("dados.txt", "consulta sql para agregar milhoes de linhas, montar relatorio e contar ocorrencias"),
]

def trecho(texto, n=64):
    return texto if len(texto) <= n else texto[:n] + "..."

def indexar(docs):
    conn = sqlite3.connect(":memory:")
    tem_vec = carregar_vec(conn)
    vetores = [embed(t) for _c, t in docs]
    meta = [(c, trecho(t)) for c, t in docs]
    if tem_vec:
        conn.execute("create virtual table docs using vec0(e float[256] distance_metric=cosine)")
        for i, v in enumerate(vetores):
            conn.execute("insert into docs(rowid, e) values (?, ?)", (i + 1, pack(v)))
    return conn, meta, vetores, tem_vec

def consultar(conn, meta, vetores, tem_vec, consulta):
    q = embed(consulta)
    if tem_vec:
        rows = conn.execute(
            "select rowid, distance from docs where e match ? and k = ? order by distance",
            (pack(q), TOP),
        ).fetchall()
        return [(meta[r - 1][0], meta[r - 1][1], d) for r, d in rows]
    pares = []
    for i, v in enumerate(vetores):
        dist = 1.0 - sum(a * b for a, b in zip(q, v))
        pares.append((dist, i))
    pares.sort()
    return [(meta[i][0], meta[i][1], d) for d, i in pares[:TOP]]

def main():
    if len(sys.argv) >= 2:
        pasta = sys.argv[1]
        nomes = sorted(n for n in os.listdir(pasta) if n.endswith((".txt", ".md")))
        if not nomes:
            print("nenhum .txt/.md em", pasta)
            return 1
        docs = []
        for nome in nomes:
            with open(os.path.join(pasta, nome), encoding="utf-8", errors="replace") as f:
                docs.append((nome, f.read()))
        print("indexados %d documentos de %s" % (len(docs), pasta))
    else:
        docs = DOCS
        print("sem argumentos: demo com %d documentos de exemplo" % len(docs))

    conn, meta, vetores, tem_vec = indexar(docs)
    print("motor:", "sqlite-vec" if tem_vec else "python puro (sqlite-vec nao encontrado)")

    consultas = [
        "como fazer backup e restaurar arquivos",
        "buscar documentos por similaridade",
        "expor um servico com tunel seguro",
    ]
    for c in consultas:
        print()
        print("consulta: %s" % c)
        for caminho, trecho_, d in consultar(conn, meta, vetores, tem_vec, c):
            print("  %.3f  %-14s %s" % (d, caminho, trecho_))
    return 0

if __name__ == "__main__":
    sys.exit(main())

The script detects sqlite-vec and, if it is not installed, falls back to a pure Python scan that returns the same result. The part that matters is the vec0 table and the where e match ? and k = 3 query, which is the KNN running inside SQLite.

How to verify

Run python3 busca.py with no arguments. The demo indexes five documents and answers three queries:

text
motor: sqlite-vec

consulta: como fazer backup e restaurar arquivos
  0.430  backup.txt     como fazer backup com rsync, guardar versoes...

consulta: buscar documentos por similaridade
  0.514  busca.txt      indice de vetores dentro do proprio sqlite...

consulta: expor um servico com tunel seguro
  0.437  rede.txt       tunel seguro para expor um servico local...

Cosine distance runs from 0 (identical) to 1 (orthogonal). The right document lands on top of all three queries, with distances between 0.43 and 0.51. In the run with 5000 documents, the k=5 query took 1.7 ms with sqlite-vec against 63 ms in pure Python, 36 times faster. The gain is C against Python, not an approximate index: for large collections, sqlite-vec adds IVF and DiskANN indexes to skip the scan.

If it fails

motor: python puro. sqlite-vec was not found and the script fell back. Install with pip install sqlite-vec or point SQLITE_VEC_SO at the downloaded vec0.so.

load_extension fails. The binary does not match the SQLite version. Download the loadable for your system (Linux x86_64, macOS, Windows) or use the Python wheel.

Dimension error on insert. The table declares float[256] and you inserted a vector of another size. embed() must always return the same dimension.

The syntax changed between versions. sqlite-vec is pre-1.0. Across 0.1.x, the vec0 table signature changed more than once. Pin the version in your requirements.txt.

sqlite-vec against the alternatives

option where it runs cost to maintain
sqlite-vec inside SQLite, 160 KB free (MIT/Apache) one extension
pgvector a Postgres extension free (PostgreSQL license) a Postgres
Qdrant, Milvus, Weaviate a dedicated server free self-host or paid one more service

For a personal archive that already lives in files, sqlite-vec avoids spinning up a vector service. For production scale with sharding and metrics, a dedicated server makes more sense. The choice is about operations, not price, since all of them have a free tier.

FAQ

Is this real semantic search? The index and the cosine distance are real. What is simple is the example’s embed(), which uses trigrams and captures word overlap, not paraphrase. Swap in an embeddings model and the search becomes truly semantic. On the model side, it is worth seeing what a model that decides without writing showed.

Why inside SQLite instead of a service? Because the archive is small and the data can live in a single file that you copy, back up and version. The same good laziness as not letting Python import what it doesn’t use: fewer parts to look after.

Does it work with millions of documents? It works, but the plain search scans everything. At scale, sqlite-vec has IVF and DiskANN indexes that trade a bit of precision for speed. Measure before assuming.

Do I need a GPU? No. The vectors are already-made numbers; generating embeddings with a local model may use a GPU, but the index itself does not.

Sources

Read next

// By e-mail

Posts published here also go out by e-mail. Delivery has not started yet: whoever leaves an address now joins the first batch.

I store only your e-mail, the date and the record of this consent. No cookies, no trackers, nothing shared with third parties. Details in the privacy policy.