LocalVectorDB
A high-performance, document-first vector database with SQLite + FAISS backend, featuring intelligent chunking, unified search, and optional HTTP server.
Beyond basic RAG. LocalVectorDB pairs a zero-infrastructure SQLite + FAISS core with capabilities most vector stores don't have:
- 🧬 Section-level retrieval, measured — in a long, structured document the answer is usually spread across a whole section, not concentrated in one stray sentence — and flat chunking cannot see it. LocalVectorDB embeds each section's own text and retrieves it alongside chunks: +0.03–0.08 nDCG@10 for finding the right document and +0.07–0.17 for the right section, over a chunk-only baseline, across three local encoders on real papers (Qasper, 15 papers / 48 queries). Read the study.
- ✅ Reverse-RAG fact-checking — ground LLM-generated text against your corpus, flagging unsupported or contradicted claims with citations.
- 📊 Document comparison & visualization — synteny ribbons and chord diagrams that show how two documents (or a document's own chunks) relate.
Contents
- Quick Start — install, index, search
- Use with Claude Code & other AI agents
- Features
- Measured, Not Guessed — the retrieval study behind the defaults
- Server Deployment
- TypeScript SDK
- API Reference
- CLI Reference
- Architecture
- File Extraction
- Configuration Options
- Production Deployment
- Examples
- Contributing
📖 Full documentation: https://thomas-villani.github.io/localvectordb/
🚀 Quick Start
Installation
LocalVectorDB is a standard PyPI package — install it with uv (recommended) or pip.
# Add to your project with uv (recommended)
uv add localvectordb
# For the lvdb CLI without the HTTP server (optional, light: click + tomli-w + bcrypt)
# Enough for create/inspect/search/chunk/backup/migrate/config — everything except `lvdb serve`
uv add "localvectordb[cli]"
# For server features (optional; includes the CLI)
uv add "localvectordb[server]"
# For all file extraction formats (optional)
uv add "localvectordb[server,file-extraction]"
Prefer pip? Every command above has a pip equivalent, e.g. pip install "localvectordb[server,file-extraction]".
To run the CLI/server without adding it to a project, use uv's tool runner:
uvx --from "localvectordb[server]" lvdb serve
Prerequisites
LocalVectorDB needs an embedding provider. By default it embeds through Ollama running locally:
ollama pull embeddinggemma
Any other provider works via embedding_provider= — OpenAI, OpenRouter, Google,
Jina, HuggingFace (Inference API or local), or Sentence Transformers. See the
Embeddings guide.
Retrieval prefixes are handled for you. Many retrieval models — including the
default embeddinggemma — are trained with a different instruction prepended to a
stored passage than to a search query, and rank noticeably worse if you embed both
sides the same way. LocalVectorDB looks the prefixes up from the model name and
applies them automatically on ingest and on query. For a model it doesn't know,
set them yourself:
db = VectorDB(
"my_docs", "./data",
embedding_model="my-private-encoder",
embedding_config={"document_prefix": "passage: ", "query_prefix": "query: "},
)
Databases created before this feature keep their existing behaviour: they store the prefixes they were built with, so an old index is never silently re-embedded into a different space. See the Embeddings guide for the full model table.
No provider handy? The built-in mock provider needs no service and ships in
the base install, so you can verify the install and explore the API immediately:
from localvectordb import VectorDB
db = VectorDB("demo", ":memory:", embedding_provider="mock", embedding_model="mock")
db.upsert(["Python is a programming language"])
print(db.query("programming", k=1))
Mock vectors are deterministic but carry no semantic meaning, so rankings from the mock provider are arbitrary. It is for wiring up code, not for judging retrieval quality.
Basic Usage
from localvectordb import VectorDB
# Create or connect to a database (defaults to Ollama + embeddinggemma)
db = VectorDB("my_docs", "./data")
# Add documents
doc_ids = db.upsert([
"Python is a programming language",
"Machine learning with neural networks"
])
# Search documents
results = db.query("programming", k=5)
for result in results:
print(f"{result.id}: {result.content} (score: {result.score:.3f})")
# Get specific document
doc = db.get(doc_ids[0])
print(f"Content: {doc.content}")
With Metadata Schema
from localvectordb import VectorDB
from localvectordb.core import MetadataField, MetadataFieldType
# Define metadata schema
schema = {
'title': MetadataField(type=MetadataFieldType.TEXT, indexed=True),
'author': MetadataField(type=MetadataFieldType.TEXT, indexed=True),
'created_date': MetadataField(type=MetadataFieldType.DATE, indexed=True),
'tags': MetadataField(type=MetadataFieldType.JSON)
}
db = VectorDB("articles", "./data", metadata_schema=schema)
# Add documents with metadata
db.upsert(
documents=["Article about Python programming"],
metadata=[{
'title': 'Python Guide',
'author': 'Jane Doe',
'created_date': '2024-01-15',
'tags': ['python', 'programming', 'tutorial']
}]
)
# Search with metadata filters
results = db.query(
"programming tutorial",
filters={'author': 'Jane Doe', 'tags': {'$contains': 'python'}}
)
Remote Server Usage
from localvectordb import VectorDB
# Use HTTP server (automatically detected by URL)
db = VectorDB(
"my_docs",
"http://localhost:8000",
api_key="your_api_key"
)
# Same API as local database
doc_ids = db.upsert(["Remote document content"])
results = db.query("content", search_type="hybrid")
🤖 Use with Claude Code & other AI agents
LocalVectorDB ships a built-in Model Context Protocol server, so an AI agent can search your knowledge bases directly — no glue code. It is read-only by default, which makes it safe to point at a corpus you care about.
uv add "localvectordb[mcp]" # or: pip install "localvectordb[mcp]"
Build a knowledge base with the CLI first (rich formats like PDF and DOCX are extracted to Markdown automatically):
lvdb create technical_docs --embedding-model embeddinggemma
lvdb db technical_docs add ./docs/*.md ./manual.pdf
Claude Code
Register the server once, from your project directory:
claude mcp add lvdb -e LVDB_MCP_MODE=read-only -e LVDB_MCP_DATABASES_ROOT=/path/to/databases -- lvdb mcp serve
Everything after -- is the launch command, passed through untouched. Use
-s project to write a committed .mcp.json your team shares, -s user to make
it available in every project; the default scope is local (this project, just
you). Then claude mcp list to check status, /mcp inside a session to manage
it, and claude mcp remove lvdb to undo.
Claude Desktop, and any other MCP client
Add the same server to claude_desktop_config.json (or your client's equivalent
config — the mcpServers block is a shared convention):
{
"mcpServers": {
"lvdb": {
"type": "stdio",
"command": "lvdb",
"args": ["mcp", "serve"],
"env": {
"LVDB_MCP_MODE": "read-only",
"LVDB_MCP_DATABASES_ROOT": "/path/to/databases"
}
}
}
}
What the agent can do
The server exposes focused tools — query_database, find_related_documents,
filter_documents, get_document (a whole document, or a chunk / line range /
section of one), and list_databases. Tool sets are configurable per deployment,
so you can expose only what a given agent needs.
Granting writes is explicit — lvdb mcp serve --mode read-write (or
LVDB_MCP_MODE=read-write) additionally enables upsert_documents,
create_database, and friends. Only do this for databases the agent is meant to
modify.
See the MCP documentation for the full tool list, configuration reference, and security guidance.
✨ Features
🗃️ Document-First Architecture
- Smart Chunking: Position-tracking chunking — the default chunker reconstructs documents byte-for-byte from their chunks
- Metadata Schema: Structured, indexed metadata fields with validation
- Filesystem-like Document IDs: Use relative paths as document ids (
docs/reports/q1) and browse them like folders withdb.list_prefixes()— S3-style virtual folders over shared id prefixes, no schema change - Unified API: Single interface for vector, keyword, and hybrid search
- In-place Patch API: Edit a stored document with exact find/replace (or span splice) instead of re-sending the whole content — with an optional
expect_hashprecondition to guard against lost updates. Available in the library, HTTP API, MCP tool, CLI, and JS SDK
🔍 Advanced Search
- Vector Search: Semantic similarity via pluggable embedding providers — Ollama, OpenAI, OpenRouter, Google, Jina, HuggingFace (Inference API + local), and Sentence Transformers
- Keyword Search: Full-text search with SQLite FTS5
- Hybrid Search: Combined vector + keyword with configurable weighting
- Lexical (grep) Search: Exact/regex, line-oriented
db.grep()with line numbers and surrounding context — a precise-string complement to semantic search that agents combine with vector + keyword to great effect - Reranking: Optional cross-encoder reranking via Jina, Sentence Transformers, or HuggingFace
- Metadata Filtering: MongoDB-style queries on structured metadata
- Document Scoring: Four chunk-to-document aggregation strategies (
best,average,frequency_boost,percentile) plus a measuredautodefault that picks by search type
🧬 Hierarchical Retrieval
- Raw-Span Section Vectors: Each section is embedded from its own text, not averaged from its chunks — averaging blurs away the cross-chunk structure that makes a section retrievable in the first place. This costs one extra embedding call per section at ingest (sections are far fewer than chunks, so it is modest — but it is not free). Sections longer than the encoder's context are window-pooled, never truncated
- Search a Level, or Fuse Two:
search_level="sections"retrieves sections directly;search_level="fused"blends the section and chunk rankings with a tunablesection_weight(default 0.65). Sections alone are the stronger choice when relevance is genuinely section-shaped; fusion leans toward document-level accuracy - Automatic Section Detection: Sections derived from document structure (Markdown headings by default, custom patterns supported)
- Measured, Not Asserted: A controlled study across three local encoders, two chunk sizes, and real papers — raw-span sections beat the chunk-only baseline at every target, and beat the "free" centroid decisively. Full tables, methodology, and caveats: Raw-Span Hierarchical Retrieval
- Check It Yourself: Don't take the study on faith —
examples/section_vs_chunk_retrieval.pyruns the same comparison on your corpus and prints the same nDCG@10 table. Your documents are the only ones that decide whether this is worth enabling - Section Metadata: Pluggable extractors (heading path, keywords, word/char counts, or your own)
- Opt-in: Off by default (
hierarchical_embeddings=Trueto enable); the flat retrieval path is unchanged. Document-level search (search_level="documents") is also available
✅ Reverse-RAG Fact-Checking
- Grounding Verification: Check LLM-generated text against your databases claim-by-claim
- Citations & Contradictions: Each claim scored, cited to a source excerpt, and flagged if contradicted
- Multi-Provider LLMs: Works with Anthropic, OpenAI, or Gemini clients (auto-detected)
📊 Document Comparison & Visualization
- Similarity & Neighbors: Compare documents, find nearest neighbors, build similarity matrices
- Embedding Maps: t-SNE / PCA projections with clustering
- Synteny & Chord Diagrams: Visualize chunk-level alignment between documents or within one
🌐 Flexible Deployment
- Local Database: Direct SQLite + FAISS for maximum performance
- HTTP Server: RESTful API with permission-based authentication, rate limiting, CORS
- Remote Client: Seamless local/remote switching via factory pattern
- Multi-Worker: Redis-based coordination for distributed deployments
📄 File Processing
- Text Extraction: PDF, DOCX, PPTX, XLSX, RTF, EPUB support
- Batch Upload: Multi-file processing with metadata extraction
- Format Detection: Automatic MIME type detection and processing
🤖 AI / LLM Integration
- MCP Server: Built-in Model Context Protocol server for Claude Desktop, Claude Code, and other MCP clients
- Read-Only by Default: Safe knowledge-base access; opt into read-write explicitly
- TypeScript SDK: First-class browser/Node client (
@localvectordb/sdk)
🛠️ Developer Experience
- CLI Tools: Database management, server control, interactive shell
- Configuration: TOML/JSON config with environment variable support
- Comprehensive Logging: Structured logging with performance monitoring
- Type Safety: Full type annotations and validation
🔬 Measured, Not Guessed
Nearly every retrieval default in this library was chosen by measurement: an exhaustive study across four real corpora (research papers, Wikipedia QA, long documents, legal contracts), three encoders, and every knob we ship — with confidence intervals on everything. Four findings that shape how the library works:
- The keyword (BM25) leg is worth +0.08 to +0.13 nDCG@10 per retrieval level — roughly 10× any vector-side tuning we did. Every level (chunks, sections, documents, fused) carries one.
- Reranking is a model choice, not a technique: a 0.067 spread between cross-encoders, with popular MS MARCO models measuring indistinguishable from no reranking at all. The default is one that measurably works.
- Six retrieval knobs have no defensible global default — their optima are
corpus properties. So instead of pretending otherwise,
lvdb doctor/db.diagnose()reads your built index and tells you which regime you're in. - Exactly one rule generalised: how chunk scores aggregate depends on the unit
being ranked, never the corpus — shipped as
document_scoring_method="auto".
Read the study: The Retrieval Study
(findings), and The Lab Notebook
(how we measured — including the six wrong conclusions our own controls caught).
Or run the headline comparison on your own corpus: examples/section_vs_chunk_retrieval.py.
🖥️ Server Deployment
Start the Server
# Quick start: with no config file, serves on localhost:8000 with built-in
# defaults. Run `lvdb config init` first to customize (host, auth, CORS, ...).
lvdb serve
# Production configuration
lvdb --config ./config.toml serve --host 0.0.0.0 --port 8000
Configuration
Create a configuration file:
# Interactive setup wizard
lvdb config init --interactive
# Production setup with Redis
lvdb config init --redis-registry redis://localhost:6379/1 \
--enable-cache --cache-type redis \
--enable-rate-limiting --enable-cors \
--enable-auth
Example configuration (config.toml):
[database]
root_dir = "./databases"
chunk_size = 500
chunking_method = "sentences"
chunk_overlap = 1
[embedding]
provider = "ollama"
model = "embeddinggemma"
[server]
host = "0.0.0.0"
port = 8000
enable_rate_limiting = true
rate_limit = "100 per minute"
# Authentication and CORS live under the [server.security] table.
[server.security]
require_api_key = true
cors_enabled = true
cors_allowed_origins = ["http://localhost:3000"]
API Key Management
# Create API key with permission level
lvdb auth create-key --description "Production API" --permission-level read_write
# Create read-only key for analytics
lvdb auth create-key --description "Analytics Dashboard" --permission-level read_only
# List keys with their permissions
lvdb auth list-keys --active-only
# Revoke key
lvdb auth revoke-key key_20241201_abc123
🟦 TypeScript SDK
A zero-dependency TypeScript/JavaScript client is available for the HTTP server (Node.js 18+ and modern browsers):
npm install @localvectordb/sdk
import { LocalVectorDBClient } from "@localvectordb/sdk";
const client = new LocalVectorDBClient({ baseUrl: "http://localhost:8000" });
const db = client.database("my_docs");
await db.upsert(["First document", "Second document"]);
const results = await db.query("search text", { search_type: "hybrid", k: 5 });
See sdk/js/README.md for the full SDK API.
📚 API Reference
Core Methods
upsert(documents, metadata=None, ids=None)
Insert or update documents.
# Single document
db.upsert("Document content")
# Multiple documents with metadata
doc_ids = db.upsert(
documents=["Doc 1", "Doc 2"],
metadata=[{"type": "article"}, {"type": "blog"}],
ids=["doc_1", "doc_2"]
)
query(query, search_type='hybrid', k=10, filters=None)
Unified search interface.
# Vector search
results = db.query("search text", search_type="vector", k=5)
# Hybrid search with metadata filter
results = db.query(
"machine learning",
search_type="hybrid",
vector_weight=0.5,
filters={"category": "AI"}
)
# Keyword search
results = db.query("exact phrase", search_type="keyword")
Note: Filter fields (and metadata keys on upsert) must be declared in the database's
metadata_schema. Filtering on an undeclared field raisesDatabaseError; undeclared metadata keys are dropped on upsert with a warning.
get(ids) / delete(ids) / exists(ids)
Document management.
# Single document
doc = db.get("doc_1")
exists = db.exists("doc_1")
deleted_count = db.delete("doc_1")
# Multiple documents
docs = db.get(["doc_1", "doc_2"])
exist_flags = db.exists(["doc_1", "doc_2"])
deleted_count = db.delete(["doc_1", "doc_2"])
patch(doc_id, ops, *, expect_hash=None, metadata=None)
Edit a document in place without re-sending its whole content. Ops resolve against the current content, touch disjoint spans, and apply atomically.
# Exact find/replace (must match exactly `count` times, default 1)
result = db.patch("doc_1", [{"op": "replace", "find": "draft", "replace": "final"}])
print(result.updated, result.new_hash, result.ops_applied)
# Span splice + append/prepend
db.patch("doc_1", [{"op": "splice", "start": 0, "end": 5, "text": "Hello"}])
db.patch("doc_1", [{"op": "append", "text": " (revised)"}])
# Optimistic concurrency — raises PatchConflictError if the doc changed
doc = db.get("doc_1")
db.patch("doc_1", [{"op": "replace", "find": "v1", "replace": "v2"}],
expect_hash=doc.content_hash)
filter(where=None, order_by=None, limit=None, offset=0)
MongoDB-style filtering on metadata.
# Simple filters
docs = db.filter(where={"author": "Jane Doe", "status": "published"})
# Complex queries with operators
docs = db.filter(
where={"created_date": {"$gte": "2024-01-01"}},
order_by="created_date DESC",
limit=10
)
# Logical operators and pattern matching
docs = db.filter(
where={"$and": [{"author": {"$like": "%Smith%"}}, {"rating": {"$gt": 4.0}}]}
)
HTTP API Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/databases |
List databases |
POST |
/api/v1/databases |
Create database |
GET |
/api/v1/databases/{db}/info |
Database info |
POST |
/api/v1/databases/{db}/documents |
Upsert documents |
GET |
/api/v1/databases/{db}/documents/{id} |
Get document |
PATCH |
/api/v1/databases/{db}/documents/{id} |
Update document |
DELETE |
/api/v1/databases/{db}/documents/{id} |
Delete document |
POST |
/api/v1/databases/{db}/query |
Search documents |
POST |
/api/v1/databases/{db}/query/stream |
Stream results (SSE) |
POST |
/api/v1/databases/{db}/filter |
Filter documents |
POST |
/api/v1/databases/{db}/upload |
Upload files |
POST |
/api/v1/databases/{db}/compare |
Compare documents |
POST |
/api/v1/databases/{db}/nearest-neighbors |
Find similar documents |
Example API usage:
# Create database
curl -X POST http://localhost:8000/api/v1/databases \
-H "Authorization: Bearer your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "my_db"}'
# Search documents
curl -X POST http://localhost:8000/api/v1/databases/my_db/query \
-H "Authorization: Bearer your_api_key" \
-H "Content-Type: application/json" \
-d '{"query": "machine learning", "search_type": "hybrid", "k": 5}'
🛠️ CLI Reference
Database Management
# List databases
lvdb list --details
# Create database
lvdb create mydb --embedding-model embeddinggemma --chunk-size 500
# Delete database
lvdb delete mydb --confirm
Database Operations
# Add documents
lvdb db mydb add document.txt
lvdb db mydb add "docs/*.py"
cat content.txt | lvdb db mydb add -
# Search documents
lvdb db mydb search "query text" --search-type hybrid --limit 10
# Get document (or a portion: --chunk / --range / --lines / --section / --outline)
lvdb db mydb get doc_1 --format json --metadata
# Find documents related to an existing one (nearest neighbours)
lvdb db mydb related doc_1 --limit 5
# Interactive shell
lvdb db mydb shell
Configuration
# View configuration
lvdb config show --section database
# Update settings
lvdb config set server.port 8080
lvdb config set database.chunk_size 1000
All Commands
| Command | What it does |
|---|---|
lvdb serve |
Start the HTTP server |
lvdb create / list / delete / rename |
Database lifecycle |
lvdb version |
Show the installed version |
lvdb db <name> ... |
Operate on one database: info, stats, search, add, get, related, update, patch, delete, repair, shell, schema |
lvdb chunk |
Chunk text to JSONL without a database — useful for inspecting a chunking strategy |
lvdb backup |
create, list, restore, verify, cleanup, pitr |
lvdb migrate |
Metadata schema evolution: status, apply, rollback, create, list |
lvdb config |
show, get, set, init |
lvdb auth |
API keys: create-key, list-keys, revoke-key, rotate-key, key-info, prune-expired, status |
lvdb tuning |
SQLite tuning: list, get, set, set-pragma, auto-tune |
lvdb maintenance |
checkpoint, optimize, vacuum, analyze |
lvdb mcp |
MCP server: serve, status, test, tools, config-example |
Every command takes --help. Full reference with all flags and exit codes:
CLI documentation.
🏗️ Architecture
Local Architecture
┌─────────────────┐ ┌───────────────┐ ┌─────────────┐
│ Application │────│ LocalVectorDB │────│ SQLite │
└─────────────────┘ └───────────────┘ └─────────────┘
│
┌──────────────┐
│ FAISS │
│ Index │
└──────────────┘
Server Architecture
┌─────────────────┐ ┌──────────────┐ ┌─────────────┐
│ Client │────│ HTTP Server │────│ DB Manager │
│ (RemoteVectorDB)│ │ (FastAPI) │ └─────────────┘
└─────────────────┘ └──────────────┘ │
│ ┌───────────────┐
┌──────────────┐ │Multiple DBs │
│ Auth │ │(LocalVectorDB)│
│ Rate Limit │ └───────────────┘
│ CORS / SSE │
└──────────────┘
Chunking System
- Position Tracking: Exact character positions — every general-purpose chunker (
sentences,tokens,words,lines,characters,paragraphs,sections) reconstructs the source byte-for-byte. The specializedcode-blockschunker is exact only when a document fits in a single chunk. - Multiple Methods: Sentences, tokens, paragraphs, sections, code blocks
- Overlap Support: Configurable overlap between chunks
- Metadata Preservation: Document metadata inherited by all chunks
📁 File Extraction
Text extraction is powered by all2md, which converts 20+ document formats and 200+ source/text formats to Markdown. Common formats work out of the box; extended/niche formats and OCR are opt-in extras.
| Formats | Availability |
|---|---|
| PDF, DOCX, PPTX, XLSX | Built-in |
| HTML, EPUB, RTF, ODT/ODP/ODS | Built-in |
Markdown, reStructuredText, Org, CSV, JSON, YAML, .eml, .ipynb |
Built-in |
| Source code & plain text (200+ extensions) | Built-in |
LaTeX, MediaWiki, Textile, archives, .enex, .fb2, Outlook |
localvectordb[file-extraction] |
| OCR for scanned PDFs (Tesseract) | localvectordb[file-extraction-ocr] |
Extracted content is Markdown, preserving headings, tables, and lists for better chunk boundaries.
# Upload files via HTTP API
import requests
files = {'files': open('document.pdf', 'rb')}
response = requests.post(
'http://localhost:8000/api/v1/databases/mydb/upload',
files=files,
headers={'Authorization': 'Bearer your_api_key'}
)
⚙️ Configuration Options
Database Settings
root_dir: Database storage directorychunk_size: Maximum tokens per chunkchunking_method: Algorithm for splitting textchunk_overlap: Overlap between adjacent chunksdefault_metadata_schema: Schema for new databases
Embedding Settings
provider: Embedding provider — one ofollama,openai,openrouter,google,jina,huggingface,huggingface_local, orsentence_transformersmodel: Model name (e.g., "embeddinggemma")base_url: Custom API endpointapi_key: API key for providers requiring authenticationdocument_prefix/query_prefix: Retrieval instruction prefixes (see below)
Server Settings
host/port: Server bindingrequire_api_key: Enable authenticationenable_rate_limiting: Rate limiting with configurable limitscors_enabled: CORS support for web appscache_enabled: Response caching (memory, file, Redis)
🔧 Production Deployment
Scale and limits
LocalVectorDB is built for agent-native document memory, not as a general-purpose vector store at web scale. Be deliberate about the ceiling:
- The index is exact, not approximate. The default
IndexFlatL2is a brute-force flat index: every query scans every vector, so latency grows linearly with the collection. You get exact recall in return — no ANN tuning, no recall cliff. - Vectors are RAM-resident float32, unquantized. Budget
dimensions × 4 bytesper vector: 1M × 768-dim ≈ 3.1 GB (plus the SQLite store). Reader workers can share a single page-cached copy viadatabase.mmap_index(see below), which cuts per-worker RAM but not the on-disk size. - Practical ceiling: ~10⁵–10⁶ vectors on a normal machine. Beyond that you want quantization or an ANN index, which are deliberately out of scope for v0.1.
Bulk-load in one call. Every upsert/insert rewrites the entire FAISS index file
when it flushes, so ingesting N documents in N separate calls is N full-index rewrites.
Pass the whole batch to a single upsert(documents=[...]) instead — it is dramatically
faster and rewrites the index once.
Docker Deployment
A hardened Dockerfile ships in the repo: pinned base image, dependencies
isolated in a virtualenv, non-root user, and a HEALTHCHECK against /health. It builds
from source, and CI builds it on every pull request so it cannot silently rot.
docker build -t localvectordb:local .
docker run --rm -p 8000:8000 -v lvdb-data:/data localvectordb:local
Databases persist in the /data volume (LVDB_DATABASE_ROOT_DIR). Configure with LVDB_*
environment variables, or mount a TOML config file:
docker run --rm -p 8000:8000 \
-v "$PWD/config.toml:/etc/lvdb/config.toml:ro" \
-v lvdb-data:/data \
localvectordb:local \
lvdb --config /etc/lvdb/config.toml serve --host 0.0.0.0 --port 8000
The image installs the server extra only. For file upload/extraction, add
file-extraction to the pip install line in the Dockerfile.
Scaling reads across workers
LocalVectorDB is single-writer. One process owns writes to a database; to scale query throughput you fan out read-only replicas across many workers.
Build (or update) the database from a single writer, then serve reads from N
workers. Set database.mmap_index = true on the readers so every worker shares one
memory-mapped copy of the FAISS index through the OS page cache, instead of each
loading a private, RAM-resident copy:
# On the reader deployment: memory-map the index (read-only, shared page cache)
lvdb config set database.mmap_index true
# Optional: coordinate the set of database names across workers via a shared registry
lvdb config set server.db_registry_type "RedisCache"
lvdb config set server.db_registry_settings '{"host": "redis", "port": 6379, "db": 1}'
# Fan out read-only workers
uvicorn "localvectordb_server.app:create_app" --factory --host 0.0.0.0 --port 8000 --workers 4
⚠️ Single-writer only. Do not send writes (upsert / insert / update / delete) through a multi-worker deployment. Each worker holds an independent in-memory FAISS index and does not observe another worker's writes, and two writers racing the index file will diverge or corrupt it. A database opened with
mmap_index = truerefuses writes outright. Route all writes to one writer process (mmap_index = false); readers observe a writer's updates only after they reload the index (on restart or idle-eviction).
Environment Variables
export LVDB_SERVER_CONFIG="/path/to/config.toml"
export LVDB_DATABASE_ROOT_DIR="/data/databases"
export LVDB_EMBEDDING_PROVIDER="ollama"
export LVDB_EMBEDDING_MODEL="embeddinggemma"
export OPENAI_API_KEY="your-openai-key" # if using OpenAI
🧪 Examples
Runnable scripts
The snippets below are illustrative. examples/ holds complete
programs you can run, each covered by the test suite so it cannot rot:
| Script | What it does |
|---|---|
section_vs_chunk_retrieval.py |
Runs this project's headline retrieval comparison — section-level vs chunk-level — on your own corpus, and prints nDCG@10 / recall@k per mode. Bring your documents and a small judgments file; it ships with a sample of both. |
They need a real embedding backend (Ollama or sentence-transformers) and
refuse the mock provider, because mock vectors cannot tell you whether the
right thing ranked first. See examples/README.md.
Research Paper Database
from localvectordb import VectorDB
from localvectordb import get_common_metadata_schemas
# Use predefined research paper schema
schema = get_common_metadata_schemas("research_papers")
db = VectorDB("papers", "./data", metadata_schema=schema)
# Add papers
db.upsert(
documents=["Paper content..."],
metadata=[{
'title': 'Attention Is All You Need',
'authors': ['Vaswani', 'Shazeer', 'Parmar'],
'publication_date': '2017-06-12',
'journal': 'NIPS',
'keywords': ['attention', 'transformer', 'neural networks']
}]
)
# Search by topic and filter by date
results = db.query(
"transformer architecture",
filters={"publication_date": {"$gte": "2017-01-01"}},
search_type="hybrid"
)
Code Repository Search
import glob
import os
from datetime import datetime, timezone
from localvectordb import VectorDB, get_common_metadata_schemas
# Create database for code files. Declaring a metadata schema is what lets the
# metadata below be stored (undeclared fields are dropped with a warning).
db = VectorDB("code", "./data",
chunking_method="code-blocks",
chunk_size=1000,
metadata_schema=get_common_metadata_schemas("code_repository"))
# Add Python files
for file_path in glob.glob("**/*.py", recursive=True):
with open(file_path, "r") as f:
content = f.read()
db.upsert(
documents=[content],
metadata=[{
"file_path": file_path,
"language": "python",
"last_modified": datetime.fromtimestamp(
os.path.getmtime(file_path), tz=timezone.utc
).isoformat(),
}]
)
# Search for specific functions
results = db.query("async def", search_type="keyword")
Hierarchical (Section-Level) Retrieval
from localvectordb import VectorDB
# Enable the document → section → chunk hierarchy
db = VectorDB("manuals", "./data", hierarchical_embeddings=True)
db.upsert([open("user_guide.md").read()], ids=["guide"])
# Match the most relevant *section* instead of a single chunk
results = db.query("how do I reset my password?", search_level="sections")
for r in results:
print(r.metadata["section_heading"], f"{r.score:.3f}")
Reverse-RAG Fact-Checking
import anthropic
from localvectordb import VectorDB, FactChecker
db = VectorDB("kb", "./data")
db.upsert(["The Eiffel Tower is 330 metres tall and located in Paris."])
# Ground an LLM claim against the corpus (provider auto-detected from the client)
checker = FactChecker(db, llm=anthropic.Anthropic())
result = checker.check("The Eiffel Tower is 300 metres tall and stands in Berlin.")
print(f"Overall grounding score: {result.overall_score:.2f}")
for claim in result.claims:
print(claim.claim, "->", claim.polarity.value, f"(grounded={claim.grounded})")
🤝 Contributing
Contributions are welcome! By submitting a contribution you agree to license it under the project's MIT License.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Development Setup
git clone https://github.com/thomas-villani/localvectordb.git
cd localvectordb
# Install dev tooling + test extras (add --extra mcp to work on the MCP server)
uv sync --dev
# Run tests
uv run pytest
# Start development server
lvdb serve --debug
📄 License
This project is licensed under the MIT License — see the LICENSE file for details.
🆘 Support
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
- 📧 Contact: thomas.villani@gmail.com
🙏 Acknowledgments
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file localvectordb-0.1.1.tar.gz.
File metadata
- Download URL: localvectordb-0.1.1.tar.gz
- Upload date:
- Size: 2.6 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b531be9916507f368b8bff2416db4ec76ff6f17ec459ad1e4d1bc34aa52c1339
|
|
| MD5 |
82c9cff20277439df04c788993f3e146
|
|
| BLAKE2b-256 |
bb00e6905ce77ed9e8cae686336373081cd221d2946d42d5c2927cf05c4ca750
|
Provenance
The following attestation bundles were made for localvectordb-0.1.1.tar.gz:
Publisher:
release.yml on thomas-villani/localvectordb
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
localvectordb-0.1.1.tar.gz -
Subject digest:
b531be9916507f368b8bff2416db4ec76ff6f17ec459ad1e4d1bc34aa52c1339 - Sigstore transparency entry: 2558356004
- Sigstore integration time:
-
Permalink:
thomas-villani/localvectordb@8c77343896396604a6f6b0c8bbcd36f343e06894 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/thomas-villani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8c77343896396604a6f6b0c8bbcd36f343e06894 -
Trigger Event:
push
-
Statement type:
File details
Details for the file localvectordb-0.1.1-py3-none-any.whl.
File metadata
- Download URL: localvectordb-0.1.1-py3-none-any.whl
- Upload date:
- Size: 615.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d6e0e1a99d4172769c213e79185eea065474919ca300b617c093ffba84b5217c
|
|
| MD5 |
9bd0ba8f8681895ae78323f4fbc07570
|
|
| BLAKE2b-256 |
504db05af4fde32dedbe50e32d875af6e1e4023ee23da6bdc03c56c3a5475bbf
|
Provenance
The following attestation bundles were made for localvectordb-0.1.1-py3-none-any.whl:
Publisher:
release.yml on thomas-villani/localvectordb
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
localvectordb-0.1.1-py3-none-any.whl -
Subject digest:
d6e0e1a99d4172769c213e79185eea065474919ca300b617c093ffba84b5217c - Sigstore transparency entry: 2558356479
- Sigstore integration time:
-
Permalink:
thomas-villani/localvectordb@8c77343896396604a6f6b0c8bbcd36f343e06894 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/thomas-villani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8c77343896396604a6f6b0c8bbcd36f343e06894 -
Trigger Event:
push
-
Statement type: