sqlite-vec vs pgvector: Embedded Local Vector Search for Desktop & Edge Applications

Table of Contents(11 sections)
The proliferation of vector embeddings has driven a demand for efficient similarity search. While pgvector has become a de-facto standard for server-side vector databases, embedded and edge applications present unique architectural constraints. This guide empirically compares sqlite-vec (Mozilla/Alex Garcia) with pgvector, focusing on their suitability for desktop (Electron, Tauri) and edge worker environments. We will evaluate RAM consumption, index build times, int8 scalar quantization, and benchmark recall across 100k embeddings.
Architectural Overview: sqlite-vec vs pgvector
pgvector extends PostgreSQL, a robust client-server relational database. It offers ACID compliance, complex query capabilities, and high availability features. sqlite-vec, conversely, is a SQLite extension, embedding vector search directly into the application's process. This fundamental difference dictates their respective use cases and performance characteristics.
| Feature | sqlite-vec | pgvector |
|---|---|
| Architecture | Embedded (in-process) | Client-Server |
| Deployment | Single file, no server | Requires PostgreSQL server |
| Data Model | SQLite tables | PostgreSQL tables |
| Indexing | HNSW, IVF, Flat | IVF, HNSW, Flat |
| Quantization | int8 scalar | None (as of 0.6.0) |
| Concurrency | SQLite's concurrency model | PostgreSQL's MVCC |
| Use Case | Edge, Desktop, Mobile | Server, Cloud, Distributed |
| Setup Complexity | Low | Moderate |
sqlite-vec Internals
sqlite-vec leverages SQLite's loadable extension mechanism. It introduces new SQL functions and virtual tables for vector operations. The core indexing algorithms (HNSW, IVF) are implemented in Rust, compiled to a shared library (.so, .dylib, .dll) that SQLite loads at runtime. This allows for native performance without external dependencies beyond the SQLite library itself. int8 scalar quantization is a key differentiator, significantly reducing storage and memory footprint for vectors.
pgvector Internals
pgvector is a PostgreSQL extension written in C. It adds a new vector data type and operators for similarity search. It integrates seamlessly with PostgreSQL's query planner and transaction system. While powerful, its client-server model introduces network latency and requires a separate PostgreSQL instance, which is often impractical for embedded scenarios.
Setup and Data Preparation
For our benchmark, we'll use a dataset of 100,000 384-dimensional embeddings generated from a sentence transformer model (e.g., all-MiniLM-L6-v2).
First, generate synthetic embeddings:
import numpy as np
import sqlite3
import time
from pgvector.psycopg import register_vector
import psycopg
import os
# Configuration
NUM_EMBEDDINGS = 100_000
DIMENSIONS = 384
SQLITE_DB_PATH = "embeddings.db"
PG_DB_NAME = "vector_benchmark"
PG_USER = "postgres"
PG_PASSWORD = "password" # Use a strong password in production
PG_HOST = "localhost"
PG_PORT = 5432
# Generate synthetic embeddings
print(f"Generating {NUM_EMBEDDINGS} synthetic embeddings...")
embeddings = np.random.rand(NUM_EMBEDDINGS, DIMENSIONS).astype(np.float32)
# Normalize embeddings to unit length, common practice for cosine similarity
embeddings = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)
print("Embeddings generated.")
# --- SQLite-vec Setup ---
print("\n--- Setting up sqlite-vec ---")
try:
# Connect to SQLite database
sqlite_conn = sqlite3.connect(SQLITE_DB_PATH)
sqlite_cursor = sqlite_conn.cursor()
# Load sqlite-vec extension (adjust path as necessary for your OS)
# On Linux: libsqlite_vec.so, macOS: libsqlite_vec.dylib, Windows: sqlite_vec.dll
# Ensure you have compiled/downloaded the correct extension for your system.
# Example for a pre-compiled Linux extension:
sqlite_conn.enable_load_extension(True)
try:
sqlite_cursor.execute("SELECT load_extension('./libsqlite_vec.so');")
except sqlite3.OperationalError as e:
print(f"Error loading sqlite-vec extension: {e}")
print("Please ensure 'libsqlite_vec.so' (or .dylib/.dll) is in the current directory or specified path.")
print("You might need to compile it from source: https://github.com/asg017/sqlite-vec")
exit(1)
sqlite_conn.enable_load_extension(False)
# Create table for embeddings
sqlite_cursor.execute(f"""
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY,
embedding BLOB
);
""")
sqlite_conn.commit()
# Insert embeddings
print(f"Inserting {NUM_EMBEDDINGS} embeddings into sqlite-vec...")
insert_start_time = time.time()
batch_size = 1000
for i in range(0, NUM_EMBEDDINGS, batch_size):
batch = embeddings[i:i+batch_size]
sqlite_cursor.executemany(
"INSERT INTO items (id, embedding) VALUES (?, ?)",
[(i + j, vec.tobytes()) for j, vec in enumerate(batch)]
)
sqlite_conn.commit()
insert_end_time = time.time()
print(f"sqlite-vec insertion time: {insert_end_time - insert_start_time:.2f} seconds")
except Exception as e:
print(f"Error during sqlite-vec setup: {e}")
if 'sqlite_vec' in str(e):
print("Ensure the sqlite-vec extension is correctly compiled and loaded.")
exit(1)
# --- pgvector Setup ---
print("\n--- Setting up pgvector ---")
try:
# Connect to PostgreSQL
pg_conn = psycopg.connect(f"dbname={PG_DB_NAME} user={PG_USER} password={PG_PASSWORD} host={PG_HOST} port={PG_PORT}")
pg_conn.autocommit = True # For CREATE DATABASE/EXTENSION
pg_cursor = pg_conn.cursor()
# Create database if not exists
try:
pg_cursor.execute(f"CREATE DATABASE {PG_DB_NAME};")
print(f"Database '{PG_DB_NAME}' created.")
except psycopg.errors.DuplicateDatabase:
print(f"Database '{PG_DB_NAME}' already exists.")
except Exception as e:
print(f"Error creating database: {e}")
print("Ensure PostgreSQL is running and user has permissions.")
exit(1)
pg_conn.close() # Close and reconnect to the new database
pg_conn = psycopg.connect(f"dbname={PG_DB_NAME} user={PG_USER} password={PG_PASSWORD} host={PG_HOST} port={PG_PORT}")
pg_cursor = pg_conn.cursor()
# Enable pgvector extension
pg_cursor.execute("CREATE EXTENSION IF NOT EXISTS vector;")
print("pgvector extension enabled.")
# Register vector type for psycopg
register_vector(pg_conn)
# Create table for embeddings
pg_cursor.execute(f"""
CREATE TABLE IF NOT EXISTS items (
id SERIAL PRIMARY KEY,
embedding vector({DIMENSIONS})
);
""")
pg_conn.commit()
# Insert embeddings
print(f"Inserting {NUM_EMBEDDINGS} embeddings into pgvector...")
insert_start_time = time.time()
batch_size = 1000
for i in range(0, NUM_EMBEDDINGS, batch_size):
batch = embeddings[i:i+batch_size]
pg_cursor.executemany(
"INSERT INTO items (embedding) VALUES (%s)",
[(vec,) for vec in batch]
)
pg_conn.commit()
insert_end_time = time.time()
print(f"pgvector insertion time: {insert_end_time - insert_start_time:.2f} seconds")
except Exception as e:
print(f"Error during pgvector setup: {e}")
print("Ensure PostgreSQL is running and accessible.")
exit(1)
print("\nData setup complete for both databases.")
Note on sqlite-vec extension: You must compile sqlite-vec from source or download a pre-compiled binary for your specific OS and architecture. Place the shared library (libsqlite_vec.so, libsqlite_vec.dylib, or sqlite_vec.dll) in a location accessible to your application, or specify its full path.
Indexing and Benchmarking
We'll benchmark index build times, RAM consumption, and search performance (recall and latency) for both sqlite-vec and pgvector. For sqlite-vec, we'll also evaluate the impact of int8 scalar quantization.
Indexing Strategies
sqlite-vec: We'll use HNSW (Hierarchical Navigable Small Worlds) for its balance of speed and recall. We'll also testint8quantized HNSW.pgvector: We'll use IVF (Inverted File Index) and HNSW. IVF is often faster for exact nearest neighbor search on smaller datasets, while HNSW offers better recall at scale.
import numpy as np
import sqlite3
import time
from pgvector.psycopg import register_vector
import psycopg
import os
import faiss # For ground truth and recall calculation
from sklearn.metrics import top_k_accuracy_score
# Configuration (same as above, ensure NUM_EMBEDDINGS, DIMENSIONS match)
NUM_EMBEDDINGS = 100_000
DIMENSIONS = 384
SQLITE_DB_PATH = "embeddings.db"
PG_DB_NAME = "vector_benchmark"
PG_USER = "postgres"
PG_PASSWORD = "password"
PG_HOST = "localhost"
PG_PORT = 5432
K_SEARCH = 10 # Number of neighbors to retrieve
NUM_QUERIES = 100 # Number of queries for benchmarking
# Reconnect to databases
sqlite_conn = sqlite3.connect(SQLITE_DB_PATH)
sqlite_conn.enable_load_extension(True)
sqlite_cursor = sqlite_conn.cursor()
try:
sqlite_cursor.execute("SELECT load_extension('./libsqlite_vec.so');")
except sqlite3.OperationalError as e:
print(f"Error loading sqlite-vec extension: {e}")
exit(1)
sqlite_conn.enable_load_extension(False)
pg_conn = psycopg.connect(f"dbname={PG_DB_NAME} user={PG_USER} password={PG_PASSWORD} host={PG_HOST} port={PG_PORT}")
pg_cursor = pg_conn.cursor()
register_vector(pg_conn)
# Load all embeddings for ground truth and query generation
print("Loading all embeddings for ground truth...")
sqlite_cursor.execute("SELECT embedding FROM items ORDER BY id")
all_embeddings_bytes = sqlite_cursor.fetchall()
all_embeddings = np.array([np.frombuffer(e[0], dtype=np.float32) for e in all_embeddings_bytes])
print(f"Loaded {len(all_embeddings)} embeddings.")
# Generate query embeddings (random subset of existing embeddings)
query_indices = np.random.choice(NUM_EMBEDDINGS, NUM_QUERIES, replace=False)
query_embeddings = all_embeddings[query_indices]
# --- Ground Truth (using FAISS for exact nearest neighbors) ---
print("\nCalculating ground truth using FAISS...")
faiss_index = faiss.IndexFlatL2(DIMENSIONS) # L2 for cosine similarity on normalized vectors
faiss_index.add(all_embeddings)
D_gt, I_gt = faiss_index.search(query_embeddings, K_SEARCH)
print("Ground truth calculated.")
# Function to calculate recall@K
def calculate_recall(retrieved_ids, ground_truth_ids, k):
hits = 0
for i in range(len(retrieved_ids)):
# Check if any of the top K retrieved IDs are in the ground truth top K
# Note: For exact matches, we expect the query itself to be in the top K.
# For approximate search, we check if the retrieved set overlaps with the ground truth set.
retrieved_set = set(retrieved_ids[i])
ground_truth_set = set(ground_truth_ids[i])
if not ground_truth_set.isdisjoint(retrieved_set):
hits += 1
return hits / len(retrieved_ids)
# --- Benchmarking Function ---
def benchmark_search(db_type, index_type, index_params, query_sql, conn, cursor, is_sqlite_vec=False, is_quantized=False):
print(f"\n--- Benchmarking {db_type} with {index_type} ({index_params}) ---")
# Drop existing index if any
if db_type == "sqlite-vec":
try:
cursor.execute(f"DROP TABLE IF EXISTS items_vec_idx;")
conn.commit()
except sqlite3.OperationalError:
pass # Index might not exist
elif db_type == "pgvector":
try:
cursor.execute(f"DROP INDEX IF EXISTS items_embedding_idx;")
conn.commit()
except psycopg.errors.UndefinedObject:
pass # Index might not exist
# Build Index
print(f"Building {index_type} index...")
build_start_time = time.time()
if db_type == "sqlite-vec":
if is_quantized:
cursor.execute(f"CREATE VIRTUAL TABLE items_vec_idx USING vec0(items, embedding, {DIMENSIONS}, 'quantizer=int8', '{index_type}={index_params}');")
else:
cursor.execute(f"CREATE VIRTUAL TABLE items_vec_idx USING vec0(items, embedding, {DIMENSIONS}, '{index_type}={index_params}');")
conn.commit()
elif db_type == "pgvector":
cursor.execute(f"CREATE INDEX items_embedding_idx ON items USING {index_type}(embedding {index_params});")
conn.commit()
build_end_time = time.time()
print(f"Index build time: {build_end_time - build_start_time:.2f} seconds")
# Measure RAM usage (approximate, depends on OS/tooling)
# For sqlite-vec, this is the process's RAM. For pgvector, it's the PostgreSQL server's RAM.
# This is hard to measure programmatically in a cross-platform way.
# For a real benchmark, use OS-specific tools (e.g., `ps aux` on Linux, Activity Monitor on macOS).
print("RAM usage measurement requires external OS tools.")
# Perform searches
search_latencies = []
retrieved_ids_list = []
print(f"Performing {NUM_QUERIES} searches...")
for i, query_vec in enumerate(query_embeddings):
search_start_time = time.time()
if db_type == "sqlite-vec":
# For sqlite-vec, the query uses the virtual table
cursor.execute(query_sql, (query_vec.tobytes(), K_SEARCH))
elif db_type == "pgvector":
cursor.execute(query_sql, (query_vec, K_SEARCH))
results = cursor.fetchall()
search_end_time = time.time()
search_latencies.append(search_end_time - search_start_time)
retrieved_ids_list.append([r[0] for r in results]) # Assuming ID is the first column
avg_latency = np.mean(search_latencies)
p95_latency = np.percentile(search_latencies, 95)
print(f"Average search latency: {avg_latency * 1000:.2f} ms")
print(f"P95 search latency: {p95_latency * 1000:.2f} ms")
# Calculate recall
# We need to map the ground truth IDs to the retrieved IDs.
# For simplicity, we assume the IDs are 0-indexed and correspond to the original `all_embeddings` array.
# The ground truth `I_gt` contains indices, which are effectively IDs.
recall = calculate_recall(retrieved_ids_list, I_gt, K_SEARCH)
print(f"Recall@{K_SEARCH}: {recall:.4f}")
return {
"db_type": db_type,
"index_type": index_type,
"index_params": index_params,
"build_time": build_end_time - build_start_time,
"avg_latency_ms": avg_latency * 1000,
"p95_latency_ms": p95_latency * 1000,
"recall_at_k": recall
}
# --- Run Benchmarks ---
results = []
# sqlite-vec: HNSW (default float32)
results.append(benchmark_search(
"sqlite-vec", "hnsw", "M=16,ef_construction=100",
"SELECT id FROM items_vec_idx WHERE embedding MATCH ? ORDER BY distance LIMIT ?",
sqlite_conn, sqlite_cursor, is_sqlite_vec=True
))
# sqlite-vec: HNSW with int8 quantization
results.append(benchmark_search(
"sqlite-vec", "hnsw", "M=16,ef_construction=100",
"SELECT id FROM items_vec_idx WHERE embedding MATCH ? ORDER BY distance LIMIT ?",
sqlite_conn, sqlite_cursor, is_sqlite_vec=True, is_quantized=True
))
# pgvector: IVF (lists=100)
# For 100k embeddings, lists=100 is a reasonable starting point.
# The `vector_l2_ops` is for L2 distance, which is equivalent to cosine for normalized vectors.
results.append(benchmark_search(
"pgvector", "ivfflat", "vector_l2_ops, lists=100",
f"SELECT id FROM items ORDER BY embedding <-> %s LIMIT %s",
pg_conn, pg_cursor
))
# pgvector: HNSW (m=16, ef_construction=64)
# Note: pgvector's HNSW parameters are slightly different from sqlite-vec's.
# `m` is the number of neighbors, `ef_construction` is for build time.
results.append(benchmark_search(
"pgvector", "hnsw", "vector_l2_ops, m=16, ef_construction=64",
f"SELECT id FROM items ORDER BY embedding <-> %s LIMIT %s",
pg_conn, pg_cursor
))
# Print results table
print("\n--- Benchmark Summary ---")
print("| DB | Index Type | Params | Build Time (s) | Avg Latency (ms) | P95 Latency (ms) | Recall@10 |")
print("|---|---|---|---|---|---|---|")
for r in results:
print(f"| {r['db_type']} | {r['index_type']} | {r['index_params']} | {r['build_time']:.2f} | {r['avg_latency_ms']:.2f} | {r['p95_latency_ms']:.2f} | {r['recall_at_k']:.4f} |")
# Clean up
sqlite_conn.close()
pg_conn.close()
Interpreting Benchmark Results
The specific numbers will vary based on hardware, exact sqlite-vec and pgvector versions, and PostgreSQL configuration. However, general trends are expected:
- Index Build Time:
sqlite-vecoften builds indexes faster due to its in-process nature, avoiding network overhead and complex server-side resource management.pgvector's HNSW can be resource-intensive during build. - RAM Consumption:
sqlite-vec'sint8quantization will dramatically reduce RAM footprint compared tofloat32indexes in bothsqlite-vec(unquantized) andpgvector. This is critical for edge devices. - Search Latency: For small to medium datasets (e.g., 100k-1M vectors),
sqlite-veccan achieve comparable or even lower latencies thanpgvectordue to zero network overhead. As datasets scale,pgvector's server-side optimizations and ability to distribute workloads might give it an edge. - Recall:
int8quantization introduces a slight drop in recall due to precision loss. The trade-off is significantly reduced memory and storage. For many applications, this recall drop is acceptable. HNSW generally offers better recall than IVF at similar performance levels.
Production Gotchas & Troubleshooting
sqlite-vec
-
Extension Loading Errors:
- Failure Mode:
sqlite3.OperationalError: unable to load extension: ... - Cause: Incorrect path to
libsqlite_vec.so(or.dylib,.dll), architecture mismatch (e.g., trying to load an ARM binary on x86), or missing dependencies. - Fix:
- Verify the extension file exists at the specified path.
- Ensure the extension is compiled for the correct OS and CPU architecture of the target system.
- Check
ldd libsqlite_vec.so(Linux) orotool -L libsqlite_vec.dylib(macOS) for missing shared libraries. - For Electron/Tauri, ensure the extension is bundled correctly and loaded from the application's resource path.
- Failure Mode:
-
int8Quantization Recall Drop:- Failure Mode: Search results are noticeably worse after enabling
int8quantization. - Cause:
int8quantization reduces the precision of each vector component from 32-bit float to 8-bit integer. This loss of information can impact similarity calculations, especially for embeddings that are already very close. - Fix:
- Evaluate if the recall drop is acceptable for your application's requirements.
- Consider using a larger
Moref_constructionfor HNSW withint8to compensate, though this increases index build time and size. - If high precision is paramount, stick to
float32(default).
- Failure Mode: Search results are noticeably worse after enabling
-
Concurrency Issues:
- Failure Mode:
sqlite3.OperationalError: database is locked - Cause: SQLite is designed for single-writer, multiple-reader concurrency. If multiple threads/processes try to write simultaneously, this error occurs.
- Fix:
- Implement proper locking mechanisms (e.g.,
threading.Lockin Python,Mutexin Rust/C++). - Use WAL (Write-Ahead Logging) mode:
PRAGMA journal_mode=WAL;. This significantly improves concurrency for readers while writers are active. - For high-write concurrency, SQLite might not be the right choice; consider a client-server database.
- Implement proper locking mechanisms (e.g.,
- Failure Mode:
pgvector
-
PostgreSQL Connection Errors:
- Failure Mode:
psycopg.OperationalError: connection to server at "localhost" (::1), port 5432 failed: Connection refused - Cause: PostgreSQL server is not running, incorrect host/port, or firewall blocking the connection.
- Fix:
- Start the PostgreSQL service.
- Verify
postgresql.confforlisten_addressesandport. - Check
pg_hba.conffor client authentication rules. - Adjust firewall rules to allow connections to port 5432.
- Failure Mode:
-
Index Build Time and Resource Consumption:
- Failure Mode:
CREATE INDEXtakes an excessively long time or consumes all available RAM/CPU on the server. - Cause: Large dataset, insufficient server resources, or suboptimal index parameters (e.g., too high
ef_constructionfor HNSW). - Fix:
- Increase
work_meminpostgresql.conffor index builds. - Allocate more RAM and CPU to the PostgreSQL server.
- Adjust HNSW parameters (
m,ef_construction) or IVF parameters (lists) to balance build time, size, and recall. Start with lower values and incrementally increase. - Consider building indexes during off-peak hours.
- Increase
- Failure Mode:
-
vectorType Not Found:- Failure Mode:
psycopg.errors.UndefinedObject: type "vector" does not exist - Cause:
pgvectorextension is not enabled in the database. - Fix: Run
CREATE EXTENSION IF NOT EXISTS vector;in the target database.
- Failure Mode:
Frequently Asked Questions
-
When should I choose
sqlite-vecoverpgvector? Choosesqlite-vecfor embedded applications (desktop, mobile, edge devices) where a full client-server database is overkill or impractical. This includes Electron/Tauri apps, IoT devices, or serverless edge functions where you need local, low-latency vector search without external dependencies. Itsint8quantization is a strong advantage for memory-constrained environments. -
Can
sqlite-vechandle millions of vectors? Yes,sqlite-veccan handle millions of vectors, especially with HNSW indexing andint8quantization. However, performance will ultimately be limited by the host device's CPU, RAM, and disk I/O. For datasets exceeding tens of millions or requiring distributed processing,pgvector(potentially with sharding) or dedicated vector databases like Milvus/Weaviate become more suitable. -
What is the performance impact of
int8quantization insqlite-vec?int8quantization typically reduces the memory footprint of vectors by 75% (from 4 bytes per dimension to 1 byte). This leads to smaller index sizes, faster loading, and reduced RAM consumption. The trade-off is a slight, but often acceptable, drop in recall (typically 1-5% depending on the dataset and embedding quality). -
How do I ensure
sqlite-vecis secure in a desktop application? Sincesqlite-vecis an embedded database, its security is tied to the application's security.- Data Encryption: Use SQLite's encryption extensions (e.g., SQLCipher) to encrypt the database file at rest.
- Code Integrity: Ensure your application binary is signed and tamper-proof.
- Input Validation: Sanitize all user inputs to prevent SQL injection, even if the database is local.
- Access Control: Implement application-level access control if multiple users or components interact with the database.
-
What are the best practices for tuning HNSW parameters in
sqlite-vecandpgvector?M(Neighbors per node): Controls graph connectivity. HigherMimproves recall but increases index size and build time. Typical values: 16-64.ef_construction(Build time search scope): Controls the quality of the graph during index construction. Higheref_constructionleads to better recall but much longer build times. Typical values: 64-200.ef_search(Query time search scope): Controls the search accuracy at query time. Higheref_searchimproves recall but increases query latency. This can often be tuned dynamically without rebuilding the index. Typical values: 32-128.lists(IVF): Forpgvector's IVF,lists(number of inverted lists) should besqrt(N)to10*sqrt(N)whereNis the number of vectors. More lists reduce search time but increase index size and build time.- Iterative Tuning: Start with conservative parameters, benchmark, and then incrementally increase
M,ef_construction, orlistsuntil you achieve the desired recall/latency trade-off.
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Vector Search at Scale: HNSW vs IVFFlat Indexing in pgvector and SQLite-vec
Compare HNSW and IVFFlat vector indexing algorithms in pgvector and sqlite-vec. Analyze recall accuracy, build times, memory footprints, and query latency.
Read more
SQLite in Production: WAL Mode, High Concurrency, and Battle-Tested PRAGMAs
Master SQLite in high-throughput production environments. Learn Write-Ahead Logging (WAL), busy timeout tuning, concurrent reader/writer limits, and pragmatic benchmarks.
Read more
pgvector vs Qdrant at Scale: Memory Overhead, HNSW Recall & Tail Latencies (2026)
Comprehensive guide covering pgvector vs qdrant at scale: memory overhead, hnsw recall & tail latencies (2026) with production-grade architecture and code examples.
Read more