All notable changes to SimpleVecDB will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[2.6.0] - 2026-05-06
Review pass 3 — final correctness/security pass before tag
Critical fixes
UsearchIndex.savelost-update race — the_dirty = Falseclear was outside thefile_lockwindow, so a concurrentadd()betweenos.replace()and the dirty-flag clear could be silently overwritten. Moved insidefile_lock.UsearchIndex.savedata fsync onO_RDONLYfd —fsync(2)on a read-only file descriptor has implementation-defined behavior on Linux (some kernels returnEBADF, swallowed by the warning branch). Switched toO_RDWRso the data fsync is guaranteed._rebuild_index_lockedbareconn.execute— replaced the bareself.conn.execute("SELECT id FROM ...")with the newCatalogManager.list_all_ids(), which routes the read throughself._lockinstead of relying on RLock re-entrancy from a single caller.- PBKDF2 iteration bump — raised from 480 000 → 600 000 to match the OWASP 2024 minimum for PBKDF2-HMAC-SHA256.
- AES-GCM AAD now binds the v1 header —
encrypt_file/decrypt_filepass the magic+version bytes asassociated_data, so any tampering with the header (including downgrade attempts) fails authentication instead of silently succeeding. - Bounded normalize-key cache —
_NORMALIZE_KEY_CACHEis now an LRU capped at 64 entries, serialized by athreading.Lock. Long-running multi-tenant processes no longer leak derived key material indefinitely. - LlamaIndex
delete()no longer swallowssqlite3.DatabaseError— narrowed the exception in the metadata-fallback path to(TypeError, NotImplementedError). A locked DB, closed connection, or schema mismatch now propagates to the caller instead of becoming a silent no-op. - Hybrid-search RRF rank symmetry — vector candidates now use the original HNSW position as their RRF rank (via
enumerate(vector_keys_list)), matching how keyword candidates use raw BM25 position. Previously, a metadata filter that rejected vector candidates inflated surviving vector scores relative to keyword scores, corrupting result ordering. add_documentsFTS sentinel guard — added a defense-in-depth check that raisesRuntimeErrorif any-1sentinel rowid remains inreal_idsbefore the FTS upsert. Prevents a hypothetical retry-loop interaction from corrupting the FTS index with rowid-1.
Important fixes
delete_collectionTOCTOU — moved thelist_collections()existence check inside thewith self._lock:block so two concurrentdelete_collection(name)calls cannot both pass the check; the second now sees a cleanKeyErrorinstead of a SQLite error.- Salt sidecar
O_EXCLguard —_resolve_salt(create_if_missing=True)now creates the sidecar withO_CREAT | O_EXCL. If two processes race, the loser reads the winner's salt; if a sidecar already exists out-of-band, it is preserved instead of being clobbered (which would have rendered an existing DB unreadable). encrypt_index_filev0→v1 sidecar migration — re-encrypting a legacy v0 blob (no sidecar) now creates a fresh sidecar, completing the migration path to per-DB salts. Previously,is_first_encryptionwas keyed on.encpresence rather than.saltpresence.- LlamaIndex legacy-collection warning —
SimpleVecDBLlamaStore.__init__now emits a one-shotDeprecationWarningwhen it detects rows lacking_simplevecdb_node_id, telling the operator to callmigrate_node_id_metadata()and noting the inherent limitation that pre-2.6 rows can only be stamped withstr(doc_id)(the original LlamaIndex node ids were never persisted). - INT8 quantization range break softened — instead of raising
ValueErroronmax(|x|) > 1.0 + 1e-5, the strategy now emits a one-shotDeprecationWarningand clips. Restores backwards compatibility for callers that relied on the prior silent-clip behavior. scripts/check_version_sync.pynow validatesCHANGELOG.md— the hook fails if the latest CHANGELOG entry header does not matchpyproject.toml's version, preventing a release from shipping with a stale changelog.
Test coverage added (review pass 3 gaps)
tests/unit/test_v26_review_pass_3.py— covers parent-directory fsync on save,.tmpcleanup on save failure,db._lock is catalog._lockshared-RLock identity, adversarial inputs to_validate_table_name, hybrid-search RRF rank symmetry under filter, and same-text-different-id deduplication.tests/unit/test_v26_encryption_review_pass_3.py— covers nonce uniqueness across saves, wrong-key decrypt does not create the output file, AAD-bound header tampering fails authentication, salt sidecar O_EXCL preservation, and v0→v1 migration round-trip.tests/unit/integrations/test_llamaindex_review_pass_3.py— covers theadd → queryround-trip preserving the original LlamaIndex node id, end-to-end migration-then-delete on v2.5-shaped data, the legacy-collectionDeprecationWarningat__init__time, and thatsqlite3.DatabaseErrorfrom the metadata-fallback path now propagates instead of being swallowed.
Fixed (concurrency & durability)
- Atomic
UsearchIndex.save— now writes to a sibling.tmp, fsyncs, thenos.replace()s onto the live path and fsyncs the parent directory. A crash mid-save can no longer corrupt the only copy of the index. Also moved the_dirtyshort-circuit inside_write_lockso a concurrentaddcannot have its dirty flag silently cleared. - Atomic
rebuild_index— builds the new index at a sibling.rebuildpath and atomically swaps it onto the live path; the old index remains the canonical copy until the swap succeeds. - Atomic encrypted save —
encrypt_file/decrypt_filenow write to a sibling.tmp, fsync, set mode0o600, thenos.replace().encrypt_index_fileonly unlinks the plaintext after the encrypted output is durably on disk. A torn write can no longer leave the index unrecoverable. VectorDB-levelRLock— a single re-entrant lock now serializes the_collectionscache (no more check-then-insert TOCTOU oncollection()) and is shared with everyCatalogManagerso allwith self.conn:blocks across collections cannot interleave on the sharedsqlite3.Connection. Reads remain lock-free at the SQLite level via WAL.AsyncVectorDB.closedrains — switched fromexecutor.shutdown(wait=False)towait=Trueso in-flight pool tasks finish their cursors before the SQLite connection is closed. Pending (not-yet-started) work is still cancelled.set_parentcycle check is transactional — descendant lookup and parent UPDATE now run inside the samewith self._lock, self.conn:block, closing a TOCTOU window where a concurrent edge could form a cycle.- Cluster persistence —
_ensure_cluster_table,save_cluster_state,delete_cluster_statenow usewith self._lock, self.conn:instead of bareconn.commit(); an exception during the execute is properly rolled back. add_documentsID recovery is correct under upsert — replaced thelast_insert_rowid()arithmetic (which silently returned wrong IDs for batches mixing explicit andNoneIDs because UPSERTs do not advance the auto-increment counter) with a singleINSERT … RETURNING idfor the auto-ID rows. Explicit-ID rows still take the upsert path.delete_collectioncloses cached indexes first — anyVectorCollectioninstances cached for the deleted name have theirUsearchIndexclosed before the file is unlinked, so a stale mmap view cannot race the unlink.
Changed
upsert_fts_rows/delete_fts_rowsare now_upsert_fts_rows/_delete_fts_rows(private). The FTS shadow table must be updated inside the same transaction as the main table or it can desync on crash; the rename signals the contract.get_legacy_vectors,drop_legacy_vec_tablenow validate the supplied table name via_validate_table_namebefore interpolating into SQL.
Added
- Declared
python-dotenvdependency —simplevecdb.configalready imported and calledload_dotenvat package import; the missing dependency wouldImportErroron a clean install of the base package without optional extras.
Fixed (correctness & quality)
- RRF deduplication keys by document ID, not text —
hybrid_searchpreviously deduped bydoc.page_content, silently merging two distinct documents that happened to share text into one inflated-score result. - NaN/Inf guard at insert —
add_textsandadd_texts_streamingreject non-finite vectors instead of feeding them to HNSW, which would produce undefined neighbours and could corrupt the graph. normalize_l2handles subnormals — replaced the exactnorm == 0compare with a< 1e-12check (matching the existing usearch_index guard); subnormal floats no longer produce wildly large normalized vectors.- Silhouette score samples on large collections —
silhouette_scoreis O(n²); now caps the evaluation sample atSILHOUETTE_MAX_SAMPLE = 10_000. Large collections no longer OOM. - MMR maintains the selected matrix incrementally — replaced per-iteration
np.stack(selected_embs)withnp.vstackof a running matrix. O(k²·d) wasted allocations dropped to O(k·d). _parse_bool_envtreatsKEY=as unset — empty strings now fall through to the default; previously they were truthy because"".strip()is not in the falsey set.- LangChain async methods use
asyncio.to_thread—aadd_texts/asimilarity_search/amax_marginal_relevance_searchno longer block the event loop. - LlamaIndex
delete()survives a process restart — node IDs are persisted into document metadata under_simplevecdb_node_id;delete()falls back to a metadata query when the in-memory_id_mapis empty. - LlamaIndex query results carry stable node IDs — replaced
str(hash(page_content))(process-randomized, collision-prone) with the persisted_simplevecdb_node_id. AsyncVectorDB.collectionacceptsstore_embeddings— async callers can now enable embedding storage (required forrebuild_index()); previously they had no way to set it.
Security
- API key comparison uses
hmac.compare_digest— the priortoken not in allowed_keysshort-circuit leaked key prefixes via response time. - SQLCipher PRAGMA key always uses the
x'hex'form — every key path now goes through_normalize_keyfirst, eliminating string interpolation of user-supplied passphrase characters into a quoted PRAGMA argument. is_database_encryptedrejects zero-byte files — previously a missing/empty DB looked like an unencrypted DB becausesqlite3.connectwould create a fresh one.
Changed (tooling)
- Ruff and mypy targets aligned with
requires-python>=3.10— both werepy312, hiding 3.10/3.11 incompatibilities. Cleaned three resultingF401unused-import warnings (signalin models.py,_batchedandconstantsre-imports). - Pre-commit version-sync hook —
__init__.pyderives__version__dynamically viaimportlib.metadata, socheck_version_sync.pywas failing on every commit looking for a literal__version__ = "x.y.z"line that does not exist. The hook now validates onlypyproject.toml's version field.bump_version.pysimilarly stops trying to rewrite__init__.pyand uses an anchored regex to update only the canonical version field.
Security (2.6.0 final)
- Per-DB random PBKDF2 salt — encrypted databases and index files now generate a random 16-byte salt at creation time, written to a
<resource>.saltsidecar with mode0o600. The previous fixedb"simplevecdb-sqlcipher-key"salt let an attacker precompute one rainbow table that broke every simplevecdb installation with the same passphrase. Pre-2.6.0 encrypted resources keep working unchanged: when no sidecar exists, the loader falls back to the legacy fixed salt automatically. - HuggingFace
repo_idallowlist +trust_remote_code=False— the embeddings server validates model names against a strict regex (namespace/namewith[A-Za-z0-9_.-]only) before passing them tosnapshot_download/SentenceTransformer, blocking path traversal and local-filesystem inputs.SentenceTransformeris constructed withtrust_remote_code=Falseso a malicious model card cannot trigger arbitrary downloaded Python on load. - CORS is opt-in — the server no longer adds CORS middleware unless
EMBEDDING_SERVER_CORS_ORIGINSis set. When the operator does set wildcard origins (["*"]),allow_credentialsis forced off so the spec-violating wildcard-with-credentials combo can't be produced.
Migration helpers
SimpleVecDBLlamaStore.migrate_node_id_metadata()— backfills_simplevecdb_node_idfor documents inserted before 2.6.0. Pre-2.6.0 versions did not persist the LlamaIndex node_id into metadata, sodelete()could not find the right row after a process restart. Idempotent — already-stamped rows are skipped.
Added (hygiene & polish)
ClusterResultandClusterTagCallbackexported fromsimplevecdb— they were return/argument types of public methods but had no public import path; users had to reach intosimplevecdb.types.NullHandlerattached to the package's root logger at import time, per the Python logging HOWTO. Idempotent — duplicate calls do not stack handlers.SimpleVecDBLlamaStore.delete_nodesraisesNotImplementedErrorwhen called withfilters, instead of silently dropping the filter portion and pretending the deletion succeeded.- Recursive CTE depth bound as a parameter in
get_descendants/get_ancestors. The previous f-string interpolation was safe due toint()coercion but is now one less line away from injection on a future refactor. Config.from_env()documented as returning the import-time-frozen instance; setting env vars after import does not refresh.ModelRegistry(allow_unlisted=...)defaults toFalseto match the secure-by-default config setting; programmatic instantiations no longer get an open registry by accident./v1/usagereturns aggregated totals when auth is disabled instead of leaking the per-IP buckets to anyone who hits the endpoint.- Server validates
EMBEDDING_SERVER_MAX_REQUEST_ITEMS <= _MAX_ENCODE_BATCHat startup so an out-of-range env var fails fast at boot rather than per request. pyproject.tomlgains[project.urls],classifiers, andkeywordsfor a useful PyPI listing..banditdocuments the B104 skip and warns that any future0.0.0.0binding requires removing the skip.- Encrypted file format now carries a 3-byte header (
'SV' + version) so future format changes are detectable.decrypt_fileaccepts both the new v1 format and the v0 (pre-2.6.0) format, so existing encrypted indexes still load without re-encryption.
Fixed (review pass 2)
- NaN/Inf rejection no longer leaves orphan catalog rows —
add_textsand_process_streaming_batchnow validate vectors before the SQLite insert. Previously the catalog row committed first and a non-finite vector then raised, leaving rows visible viaget_documents_by_idsbut unreachable through similarity search. VectorCollection.__repr__no longer issues SQL — the previouscount()call would raiseProgrammingErrorafterclose(), breaking debuggers and exception formatters that auto-stringify objects. The 2.6.0 fix only coveredVectorDB.__repr__.EMBEDDING_SERVER_MAX_REQUEST_ITEMSvalidation runs at module import — the guard was previously insiderun_server()and was bypassed under any non-CLI ASGI deployment (gunicorn, programmatic uvicorn).- LlamaIndex empty-
node_idpath is atomic —SimpleVecDBLlamaStore.addnow generates a UUID for nodes that arrive without anode_idand stamps it into metadata before the row insert, so the metadata commit is in the same SQLite transaction as the catalog row. Previously a separateUPDATEfollowedadd_texts; a crash in the gap left rows un-stampable and cross-restartdelete()silently no-op'd. - Catalog read paths serialize on
self._lock—get_documents_by_ids,get_embeddings_by_ids,get_documents_and_embeddings_by_ids,find_ids_by_texts,find_ids_by_filter,keyword_search,count,get_all_docs_with_text,check_legacy_sqlite_vec,get_legacy_vectors,get_children,get_parent,get_descendants,get_ancestors,load_cluster_state,list_cluster_states, andVectorDB.list_collectionsnow acquire the connection-level lock aroundconn.execute.sqlite3.Connectionis not safe for concurrent statement execution from multiple threads even under WAL. rebuild_indexis fully serialized — the entire fetch + build + swap now runs insidewith self._lock:so concurrentadd/deletecannot mutate the catalog mid-rebuild and produce a stale snapshot._ensure_cluster_tabledouble-checked under lock — the_cluster_table_readyflag is now re-checked inside the lock and set inside thewithblock. Concurrent first-callers no longer both run the DDL.utils.file_lockopens viaos.open(O_CREAT | O_RDWR, 0o600)— no truncation of stale lock files from a crashed prior run, restricted permissions on the lock sentinel.
[2.5.0] - 2026-04-07
Added
delete_collection(name)— drop a collection's SQLite tables, FTS index, and usearch file in one call. Available on bothVectorDBandAsyncVectorDB.store_embeddingsparameter oncollection()— opt into storing embedding BLOBs in SQLite (defaultFalse). Saves ~2x storage; MMR transparently fetches vectors from the usearch index when BLOBs are absent.async_retry_on_lockdecorator — async variant ofretry_on_lockusingasyncio.sleepinstead oftime.sleep, avoiding executor thread blocking.file_lockcontext manager — advisory cross-process file locking (fcntl/msvcrt) for usearch index files. Prevents corruption from concurrent processes.__repr__onVectorDB,VectorCollection,AsyncVectorDB,AsyncVectorCollectionfor debuggable string representations.- FLOAT16 quantization fully implemented in
serialize()/deserialize()— was previously defined in the enum but raisedValueErrorat runtime. - Pagination on
get_documents(limit=, offset=)and catalog methods (find_ids_by_filter,find_ids_by_texts) — previously returned unbounded result sets. - Embeddings server enhancements:
- Graceful shutdown with SIGTERM/SIGINT draining (10s timeout)
- CORS middleware with configurable origins for browser-based clients
- Model warm-up on startup (skip with
--no-warmup) - Input validation: rejects empty strings (422) and texts exceeding 100k chars (413)
- Proper
argparseCLI with--host,--port,--no-warmup,--help - Startup banner logging config summary (host, port, model, auth, rate limits)
- Nested token array normalization (
list[list[int]]input format) - Async executor offload for
embed_texts(non-blocking event loop) - OpenAPI version synced from package metadata
- Module
__init__.pyexports (embed_texts,get_embedder,load_model,app,run_server)
Fixed
delete_by_idsordering — SQLite deletion now happens first (transactional, can rollback), then usearch. Previously usearch removed first, leaving orphaned catalog entries on SQLite failure._matches_filterstring semantics — now uses exact equality, consistent with SQLbuild_filter_clause. Was using substring match (value in str(meta_value)).list_collections— scanssqlite_masterfor persisted collection tables instead of returning only session-cached names. Works across reopened databases.- WAL mode for encrypted databases —
PRAGMA journal_mode=WALandPRAGMA synchronous=NORMALnow set for SQLCipher connections (was only set for unencrypted). collection()cache key — includesdistance_strategyandquantizationin cache key (sync version). Previously cached by name only, silently ignoring differing params on cache hit._ensure_fts_table— retries up to 3 times on transient "database is locked" errors instead of permanently disabling FTS on first failure.- Connection health check —
SELECT 1probe after connection creation; raisesRuntimeErrorimmediately on corrupt databases.
Improved
- Usearch batch operations —
add(),remove(), andget()now use batch usearch APIs instead of per-key loops. Significant speedup for large operations. - Filtered search iterative deepening — replaces fixed
k*3overfetch with adaptive doubling (up tok*30). Highly selective filters now reliably returnkresults. - Memory-map heuristic — uses file size threshold (50MB) instead of inaccurate
file_size // 100vector count estimate for mmap vs load decision. - Apple chip detection — uses
platform.processor()instead of spawning asysctlsubprocess.
Removed
- Duplicate
_dimproperty — removed in favor of the publicdimproperty.
Breaking Changes
- String metadata filters now use exact equality (was substring match).
store_embeddingsdefaults toFalse—rebuild_index()requiresstore_embeddings=Trueor re-adding documents.
2.4.0 - 2026-03-22
Added
- Public catalog API on VectorCollection + AsyncVectorCollection:
get_documents(filter_dict=)— replaces private_catalogaccessget_embeddings_by_ids(ids)— fetch stored embeddingsupdate_metadata(updates)— batch metadata mergecount(),save(),dimproperty — async wrappersadd_texts(parent_ids=, threads=)— full param support on asyncrebuild_index,get_children/parent/descendants/ancestors,set_parent— async hierarchy API- Executor injection on AsyncVectorDB — accept optional
executorkeyword argument so consumers can share a single-threaded executor for ONNX/usearch thread safety;close()only shuts down executor when_owns_executoris True - Safety constants in
constants.py:SEARCH_COLLECTION_TIMEOUT,EXECUTOR_SHUTDOWN_TIMEOUT,MAX_HIERARCHY_DEPTH
Fixed
- VectorDB.close() now calls
conn.close()— was leaking file descriptors whensave()succeeded but connection was never closed - VectorDB.close() wraps
save()intry/finallysoconn.close()always runs even if index serialization fails - add_documents ID recovery uses
last_insert_rowid()arithmetic instead ofORDER BY id DESC LIMIT N, which raced under concurrent inserts - String metadata filter uses exact equality (
=) instead ofLIKEsubstring match —{"type": "doc"}no longer matches"markdown_doc" - update_metadata_batch wrapped in single transaction (
with self.conn) to prevent partial commits on crash - rebuild_index uses
if x is not Noneinstead ofx or defaultso passingconnectivity=0no longer silently uses the default - search_collections parallel futures now have a 30s timeout — one hung collection can no longer block the entire cross-collection search
- AsyncVectorDB.close() uses
shutdown(wait=False, cancel_futures=True)instead of blockingshutdown(wait=True)which could hang forever on stuck tasks - Recursive CTE safety cap —
get_descendants/get_ancestorsapplyMAX_HIERARCHY_DEPTH=100whenmax_depth=Noneto prevent infinite recursion from parent_id cycles - RateLimiter cleanup capped to 500 evictions per call to bound lock hold time under high bucket counts
- HuggingFace download now uses
etag_timeout=30with local-cache fallback on network failure - embed_texts rejects batches over 10,000 texts to prevent unbounded CPU time
- retry_on_lock adds
total_timeout=10sbudget — gives up early if cumulative sleep would exceed the budget
Changed
__version__now read from package metadata viaimportlib.metadata(single source of truth inpyproject.toml)- Upsert in usearch_index separates conflict detection from removal for clearer flow
2.3.0 - 2026-03-08
Breaking Changes
- Integration dependencies are now optional. LangChain and LlamaIndex packages are no longer installed by default. Install with
pip install simplevecdb[integrations]to use them. Existing users upgrading from v2.2.x will see a clear ImportError with migration instructions.
Added
[integrations]optional extra — Install LangChain and LlamaIndex dependencies only when needed, reducing default install footprint- Runtime import guards in integration modules with v2.3.0 migration messaging
- Lazy
__getattr__loading inintegrations/__init__.py— integration classes are only imported when accessed - Input validation guards on search methods:
similarity_search,similarity_search_batch,keyword_search,hybrid_searchnow rejectk <= 0add_textsvalidates length consistency ofmetadatas,embeddings,ids, andparent_idsagainsttexts- NaN/Inf validation for float values in metadata filters (
utils.validate_filter) - Empty list rejection for list filter values
- Double-close protection on
VectorDBwith_closedflag - Context manager protocol (
__enter__/__exit__) onVectorDB - Table name validation in
check_migration(defense-in-depth against SQL injection) - Graceful per-future error handling in
search_collections - Adaptive batch search threshold — queries below
USEARCH_BATCH_THRESHOLD(10) use sequential search to avoid batch overhead
Changed
- Python dev target changed to 3.12 (
.python-version),requires-pythonremains>= "3.10" - Version bumped to 2.3.0
- Performance: MMR search vectorized — pre-normalize embeddings once, use
sel_matrix @ embmatrix-vector multiply instead of Python inner loop, O(1)list.popreplaces O(n)list.remove, hoist1 - lambda_multloop invariant - Performance: merged SQL round-trips in MMR — new
get_documents_and_embeddings_by_idsfetches text, metadata, and embeddings in a single query (previously two separate SELECTs) - Performance:
get_parentcollapsed from 2 sequential SELECTs to 1 self-JOIN - Performance:
add_documentsID recovery — skip redundantSELECT ORDER BY DESCwhen explicit IDs are provided; removed unnecessarylist(texts)copy - Performance: FLOAT serialization —
np.asarray().tobytes()replacesstruct.packwith per-element Python loop (single C memcpy) - Performance:
np.array→np.asarrayon every search and insert path to avoid unnecessary copies - Performance: SQL placeholder strings —
",".join(["?"] * len(ids))replaces generator expression across all 9 call sites - Performance: batched numpy conversion in
add_texts— singlenp.asarraycall instead of per-item conversion - Performance: compact JSON separators in catalog serialization
- Performance: deduplicated
.tolist()calls in search engine - Performance:
np.unique(ravel())for batch key collection insimilarity_search_batch - Performance: usearch upsert — skip contains-check loop on empty index, cache
int(key)once per iteration - Performance: cluster table DDL —
_cluster_table_readyflag skipsCREATE TABLE IF NOT EXISTSon repeated calls; cached_cluster_table_name _normalize_keynow delegates to_derive_keyinstead of duplicating PBKDF2 logic- HNSW defaults in
usearch_index.pynow sourced fromconstants.py(removed local duplicates) - Collection name regex uses
constants.COLLECTION_NAME_PATTERNinstead of hardcoded pattern VectorDBdefaults fordistance_strategyandquantizationsourced fromconstants.DEFAULT_DISTANCE_STRATEGY/constants.DEFAULT_QUANTIZATION_batchedutility moved fromcore.pytoutils.pyfor reuse; now used incatalog.pybatch updatesauto_tagusesdefaultdict(list)instead of manual if-not-in patternimport randomhoisted to module level inutils.py(was inside retry loop)- Streaming placeholder bug fixed —
_process_streaming_batchnow correctly detectsNoneplaceholders (previously used empty list[], preventing auto-embedding replacement) - README updated to document
pip install simplevecdb[integrations]installation
Removed
- LangChain and LlamaIndex packages from core
[project.dependencies](moved to[project.optional-dependencies] integrations) - Duplicated HNSW default constants from
usearch_index.py(now single source inconstants.py) - Unused
structimport fromquantization.py - Unused
itertoolsimport fromcore.py
2.2.1 - 2026-01-27
Changed
- Moved integration dependencies (langchain-core, langchain-openai, llama-index) from dev to main dependencies for easier installation
- Added bandit to dev dependencies for security linting in pre-commit
- Cleaned up duplicate dev dependency definitions
2.2.0 - 2026-01-26
Added
- Version 2.2.0 release
2.1.0 - 2026-01-01
Added
- SQLCipher Encryption Support - Full at-rest encryption for sensitive data:
VectorDB(path, encryption_key="...")enables AES-256 page-level database encryption- Uses SQLCipher for transparent SQLite encryption (PRAGMA key)
- Usearch index files encrypted with AES-256-GCM (
.usearch.enc) - Zero performance overhead during search (decrypt on load, encrypt on save only)
- Key derivation: PBKDF2-SHA256 with 480,000 iterations for passphrases
-
Install with
pip install simplevecdb[encryption] -
New encryption module (
simplevecdb.encryption): create_encrypted_connection()- SQLCipher connection factoryis_database_encrypted()- Check if a database file is encryptedencrypt_index_file()/decrypt_index_file()- Index file encryption-
EncryptionError/EncryptionUnavailableError- New exception types -
Streaming Insert API - Memory-efficient large-scale ingestion:
collection.add_texts_streaming(iterable)- Process from any iterator/generator- Configurable
batch_sizeparameter (default: config.EMBEDDING_BATCH_SIZE) - Yields
StreamingProgressafter each batch for monitoring - Optional
on_progresscallback for custom logging/UI updates -
New types:
StreamingProgress,ProgressCallback -
Hierarchical Document Relationships - Parent/child document structure:
parent_idsparameter inadd_texts()to link documentsget_children(doc_id)- Get direct child documentsget_parent(doc_id)- Get parent documentget_descendants(doc_id, max_depth)- Recursive children traversalget_ancestors(doc_id, max_depth)- Path to rootset_parent(doc_id, parent_id)- Update relationships- Uses SQLite recursive CTE for efficient traversal
- Auto-migrates existing databases (adds
parent_idcolumn)
Changed
check_migration()now gracefully handles encrypted databases (returnsneeds_migration=False)
Dependencies
- New optional dependency group
[encryption]:sqlcipher3-binary>=0.5.0,cryptography>=41.0
2.0.0 - 2025-12-23
Breaking Changes
- Backend Migration: sqlite-vec → usearch HNSW
- Vector search now uses usearch's high-performance HNSW algorithm
- 10-100x faster similarity search for large collections
- Vector data stored in separate
.usearchfiles per collection (e.g.,mydb.db.default.usearch) -
SQLite still stores metadata, text, and FTS5 index
-
Removed
DistanceStrategy.L1- Manhattan distance not supported by usearch -
Storage Format Change
- Embeddings now stored in both usearch index AND SQLite (for MMR support)
- Existing sqlite-vec databases will auto-migrate on first open
- Migration is one-way; backup before upgrading
Added
usearch_index.py- New UsearchIndex wrapper class:- Thread-safe HNSW index operations (lock on writes, lock-free reads)
- Automatic persistence to
.usearchfiles - Upsert support (removes existing keys before add)
- BIT quantization using Hamming metric with bit packing
-
Configurable HNSW parameters (connectivity, expansion_add, expansion_search)
-
Proper MMR Implementation - Max Marginal Relevance now computes actual pairwise similarity between candidates and selected documents using stored embeddings
-
Embedding Storage in SQLite - Embeddings stored as BLOB for:
- Accurate MMR diversity computation
- Future index rebuild from SQLite backup
-
Schema auto-migrates existing tables
-
VectorCollection.rebuild_index()- Reconstruct usearch HNSW index from SQLite embeddings: - Useful for index corruption recovery
- Tune HNSW parameters (connectivity, expansion_add, expansion_search)
-
Reclaim space after many deletions
-
VectorDB.check_migration(path)- Dry-run migration check: - Reports which collections need migration
- Shows total vector count and estimated storage
-
Provides detailed rollback instructions
-
Adaptive Search - Automatically optimizes search strategy based on collection size:
- Collections < 10k vectors use brute-force (
exact=True) for perfect recall - Collections ≥ 10k vectors use HNSW for faster approximate search
-
Threshold configurable via
constants.USEARCH_BRUTEFORCE_THRESHOLD -
exactparameter - Force search mode insimilarity_search(): None(default): adaptive based on collection sizeTrue: force brute-force for perfect recall-
False: force HNSW approximate search -
Quantization.FLOAT16- Half-precision floating point: - 2x memory savings compared to FLOAT32
- 1.5x faster search with minimal precision loss
-
Ideal for embeddings where full precision isn't needed
-
threadsparameter - Parallel execution control: - Added to
add_texts()andsimilarity_search() 0(default): auto-detect optimal thread count-
Explicit value: control parallelism for batch operations
-
Auto Memory-Mapping - Large indexes automatically use memory-mapped mode:
- Indexes >100k vectors use
view=Truefor instant startup - Lower memory footprint for large collections
- Transparent upgrade to writable mode on add operations
-
Configurable via
constants.USEARCH_MMAP_THRESHOLD -
similarity_search_batch()- Multi-query batch search: - ~10x throughput for batch query workloads
- Uses usearch's native batch search under the hood
-
Same parameters as
similarity_search()but accepts list of queries -
examples/backend_benchmark.py- Benchmark script comparing usearch vs brute-force: - Measures speedup, recall, and storage efficiency
- Supports all quantization levels
- Validates 10-100x performance claims
Changed
- Dependencies: Replaced
sqlite-vec>=0.1.6withusearch>=2.12 - CatalogManager: Removed vec0 virtual table operations, added embedding column
- SearchEngine: Rewrote to use UsearchIndex for all vector operations
- VectorCollection: Creates usearch index at
{db_path}.{collection}.usearch
Migration Notes
- Backup your database before upgrading
- On first open, existing sqlite-vec data will be migrated automatically
- New
.usearchfiles will be created alongside your.dbfile - The legacy sqlite-vec table is dropped after successful migration
1.3.0 - 2025-12-07
Added
- Structured Logging Module - New
simplevecdb.loggingmodule for production-grade observability get_logger(name)- Get namespaced loggers undersimplevecdb.*configure_logging(level, format, handler)- One-call logging setuplog_operation(name, **context)- Context manager for operation timing and error tracking-
log_error(operation, error, **context)- Consistent error logging with context -
SQLite Lock Retry Logic - Automatic retry with exponential backoff for database lock contention
@retry_on_lock(max_retries, base_delay, max_delay, jitter)decoratorDatabaseLockedErrorexception for exhausted retries with attempt/wait metrics-
Applied to
add_texts()anddelete_by_ids()operations in CatalogManager -
Filter Validation - Early validation of metadata filter dictionaries
validate_filter(filter_dict)- Validates keys are strings, values are supported types- Clear error messages for invalid filter structures
-
Automatically called in
build_filter_clause()before SQL generation -
New Exports - Added to
simplevecdb.__all__: get_logger,configure_logging,log_operationDatabaseLockedError,retry_on_lock,validate_filter
Changed
- CatalogManager internal refactoring:
add_texts()now delegates to_insert_batch()which has retry logicdelete_by_ids()now has retry logic for lock contentionbuild_filter_clause()validates filters before processingdelete_by_ids()no longer auto-vacuums - CallVectorDB.vacuum()separately to reclaim disk space after large deletions. This improves performance for batch deletions.- RateLimiter now includes TTL-based cleanup to prevent memory exhaustion on long-running servers with many unique clients (default: 1 hour TTL, 10k max buckets).
- AsyncVectorDB.close() now guarantees database connection is closed even if executor shutdown fails.
Testing
- Added 25 new tests in
tests/unit/test_error_handling.py: - 7 tests for
retry_on_lockdecorator behavior - 2 tests for
DatabaseLockedErrorexception - 4 tests for
validate_filterfunction - 8 tests for logging utilities
- 4 integration tests for error handling in VectorDB operations
Example
import logging
from simplevecdb import (
VectorDB,
configure_logging,
get_logger,
log_operation,
DatabaseLockedError,
)
# Enable debug logging
configure_logging(level=logging.DEBUG)
logger = get_logger(__name__)
try:
with log_operation("bulk_insert", collection="docs", count=1000):
db = VectorDB("data.db")
collection = db.collection("docs")
collection.add_texts(texts, embeddings=embeddings)
except DatabaseLockedError as e:
logger.error(f"Insert failed after {e.attempts} attempts")
1.2.0 - 2025-11-25
Added
- Async API Support - New
AsyncVectorDBandAsyncVectorCollectionclasses - Full async/await support for all collection operations
- Uses ThreadPoolExecutor to avoid blocking event loops
- Async context manager support (
async with AsyncVectorDB(...)) - All methods mirror sync API:
add_texts,similarity_search,keyword_search,hybrid_search,max_marginal_relevance_search,delete_by_ids,remove_texts - Configurable thread pool size via
max_workersparameter
Changed
- Added
pytest-asyncioto dev dependencies for async test support
Example
import asyncio
from simplevecdb import AsyncVectorDB
async def main():
async with AsyncVectorDB("data.db") as db:
collection = db.collection("docs")
await collection.add_texts(["Hello"], embeddings=[[0.1]*384])
results = await collection.similarity_search([0.1]*384, k=5)
return results
asyncio.run(main())
1.1.1 - 2025-11-23
Changed
- Refactored configuration constants into dedicated
constants.pymodule - Extracted hardware batch size thresholds (VRAM, CPU cores, ARM variants)
- Extracted search defaults (k=5, rrf_k=60, fetch_k=20)
- Improved maintainability and centralized configuration
Fixed
- Updated dependencies
- Bumped
sentence-transformers[onnx]from 3.3.1 to 5.1.2 - All embeddings/server tests passing with new version
1.1.0 - 2025-11-23
🏗️ Architecture Refactoring
Major internal restructuring for better maintainability and extensibility while preserving backward compatibility.
Changed
- Refactored core.py (879→216 lines, 75% reduction)
- Extracted search operations to
engine/search.py(SearchEngine) - Extracted quantization logic to
engine/quantization.py(QuantizationStrategy) - Extracted catalog management to
engine/catalog.py(CatalogManager) - Core now uses clean facade pattern with delegation
- Improved documentation
- Added comprehensive Google-style docstrings to all public API methods
- Reorganized MkDocs navigation with dedicated Engine section
- Updated architecture documentation in AGENTS.md and CONTRIBUTING.md
- Simplified CODE_OF_CONDUCT.md to be more approachable
Added
- Security infrastructure
- GitHub Actions workflow for weekly security scans (Bandit, Safety, Semgrep)
- Dependabot configuration for automated dependency updates
- Bandit configuration with validated false-positive suppressions
- Automated publishing
- GitHub Actions workflow for PyPI publishing on releases
- Test coverage improvements
- Added 11 new tests covering edge cases in search engine
- Maintained 97% overall coverage across refactored modules
Fixed
- Fixed unused
filter_builderparameter in_brute_force_searchmethod - Simplified brute-force filtering to use proper filter builder delegation
- Fixed import paths for embeddings module in search engine
Internal
- All modules now follow consistent interface patterns
- Engine components properly isolated with clear responsibilities
- No breaking changes to public API
1.0.0 - 2025-11-23
🎉 Initial Release
SimpleVecDB's first stable release brings production-ready local vector search to a single SQLite file.
Added
Core Features
- Multi-collection catalog system: Organize documents in named collections within a single database
- Vector search: Cosine, L2 (Euclidean), and L1 (Manhattan) distance metrics
- Quantization: FLOAT32, INT8 (4x compression), and BIT (32x compression) support
- Metadata filtering: JSON-based filtering with SQL
WHEREclauses - Batch processing: Automatic batching for efficient bulk operations
- Persistence: Single
.dbfile with WAL mode for concurrent reads
Hybrid Search
- BM25 keyword search: Full-text search using SQLite FTS5
- Hybrid search: Reciprocal Rank Fusion combining BM25 + vector similarity
- Query vector reuse: Pass pre-computed embeddings to avoid redundant embedding calls
- Metadata filtering: Works across all search modes (vector, keyword, hybrid)
Embeddings Server
- OpenAI-compatible API:
/v1/embeddingsendpoint for local embedding generation - Model registry: Configure allowed models or allow arbitrary HuggingFace repos
- Request limits: Configurable max batch size per request
- API key authentication: Optional Bearer token / X-API-Key authentication
- Usage tracking: Per-key request and token metrics via
/v1/usage - Model listing:
/v1/modelsendpoint for registry inspection - ONNX optimization: Quantized ONNX runtime for fast CPU inference
Hardware Optimization
- Auto-detection: Automatically detects CUDA GPUs, Apple Silicon (MPS), ROCm, and CPU
- Adaptive batching: Optimal batch sizes based on:
- NVIDIA GPUs: 64-512 (scaled by VRAM 4GB-24GB+)
- AMD GPUs: 256 (ROCm)
- Apple Silicon: 32-128 (M1/M2 vs M3/M4, base vs Max/Ultra)
- ARM CPUs: 4-16 (mobile, Raspberry Pi, servers)
- x86 CPUs: 8-64 (scaled by core count)
- Manual override:
EMBEDDING_BATCH_SIZEenvironment variable
Integrations
- LangChain:
SimpleVecDBVectorStorewith async support and MMR similarity_search,similarity_search_with_scoremax_marginal_relevance_searchkeyword_search,hybrid_searchadd_texts,add_documents,delete- LlamaIndex:
SimpleVecDBLlamaStorewith query mode support VectorStoreQueryMode.DEFAULT(dense vector)VectorStoreQueryMode.SPARSE/TEXT_SEARCH(BM25)VectorStoreQueryMode.HYBRID/SEMANTIC_HYBRID(fusion)- Metadata filtering across all modes
Examples & Documentation
- RAG notebooks: LangChain, LlamaIndex, and Ollama integration examples
- Performance benchmarks: Insertion speed, query latency, storage efficiency
- API documentation: Full class and method reference via MkDocs
- Setup guide: Environment variables and configuration options
- Contributing guide: Development setup and testing instructions
Configuration
EMBEDDING_MODEL: HuggingFace model ID (default:Snowflake/snowflake-arctic-embed-xs)EMBEDDING_CACHE_DIR: Model cache directory (default:~/.cache/simplevecdb)EMBEDDING_MODEL_REGISTRY: Comma-separatedalias=repo_identriesEMBEDDING_MODEL_REGISTRY_LOCKED: Enforce registry allowlist (default:1)EMBEDDING_BATCH_SIZE: Inference batch size (auto-detected if not set)EMBEDDING_SERVER_MAX_REQUEST_ITEMS: Max prompts per/v1/embeddingscallEMBEDDING_SERVER_API_KEYS: Comma-separated API keys for authenticationDATABASE_PATH: SQLite database path (default::memory:)SERVER_HOST: Embeddings server host (default:0.0.0.0)SERVER_PORT: Embeddings server port (default:8000)
Performance
Benchmarks on i9-13900K & RTX 4090 with 10k vectors (384-dim):
| Quantization | Storage | Insert Speed | Query Time (k=10) |
|---|---|---|---|
| FLOAT32 | 15.50 MB | 15,585 vec/s | 3.55 ms |
| INT8 | 4.23 MB | 27,893 vec/s | 3.93 ms |
| BIT | 0.95 MB | 32,321 vec/s | 0.27 ms |
Testing
- 177 unit and integration tests
- 97% code coverage
- Type-safe (mypy strict mode)
- CI/CD on Python 3.10, 3.11, 3.12, 3.13
Dependencies
- Core:
sqlite-vec>=0.1.6,numpy>=2.0,python-dotenv>=1.2.1,psutil>=5.9.0 - Server extras:
fastapi>=0.115,uvicorn[standard]>=0.30,sentence-transformers[onnx]==3.3.1
Notes
- Requires SQLite builds with FTS5 enabled for keyword/hybrid search (bundled with Python 3.10+)
- Works on Linux, macOS, Windows, and WASM environments
- Zero external dependencies beyond Python for core functionality
Links
- GitHub: https://github.com/coderdayton/simplevecdb
- PyPI: https://pypi.org/project/simplevecdb/
- Documentation: https://coderdayton.github.io/simplevecdb/
- License: MIT