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
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
#!/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:
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
- asg017/sqlite-vec on GitHub, README and licenses.
- sqlite-vec documentation, install and examples.