What VecturaDB has beyond Chroma 1.5.9

Disclaimer. This page is provided as is, without warranty of any kind, as stated in the Apache License 2.0. It describes what we read in the code and measured up to 7 November 2026: your data, versions and setup may behave differently. Back up your data and test on a copy first. You are responsible for your data.

Not sure about your case? Open an issue in the VecturaDB repository, describe your data and your setup, and we will take a look on a best-effort basis.

VecturaDB is a single-node server built from Chroma. It has the features of the API of distributed Chroma, the one Chroma Cloud runs on, that Chroma 1.5.9 refuses on a single node: the Search API, sparse vectors and BM25, SPANN, read levels, indexing status, resource names, collection fork, conditional transactions and attached functions. Two attached functions of Chroma Cloud stay out.

VecturaDB is an independent project, not affiliated with Chroma. We read the code of distributed Chroma at commit 52ce844, the main branch of Chroma on 22 October 2026, and of Chroma 1.5.9 on a single node. This page lists each feature that the first has and the second does not, as it is in VecturaDB 1.0.0-preview.8.

At a glance

Feature Chroma 1.5.9, single node VecturaDB 1.0.0-preview.8
Search API: dense and sparse ranks, RRF, select Search operation is not implemented for local executor Yes
Group by in a search The same error Yes
Sparse vector indexes and BM25 Sparse vector indexing is not enabled in local Yes
Sparse vectors in the metadata Sparse vector is not supported for local chroma Yes
SPANN index A collection that asks for SPANN gets HNSW Yes, for the collections that ask for it
Read levels of a search Refused with the Search API Yes, on SPANN collections
Indexing status Method scout_logs is not implemented Yes
Resource name of a tenant, collection by CRN The name is not saved; a collection by CRN closes the connection Yes
Collection fork and fork count Collection forking is unsupported for local chroma Yes, as a copy
Conditional transactions No Yes
Attached functions Refused Yes, except http_generate and http_currents
Databases: get several by id, get one by id, count No Yes

The errors in the middle column are those of Chroma 1.5.9 with the Python client chromadb 1.5.9, on 4 November 2026.

Each feature below ends with how it is tested. In our tests on 7 November 2026, every example on this page ran as it is on VecturaDB 1.0.0-preview.8, over HTTP: the Python examples with the Python client chromadb 1.5.9, the conditional transactions with the Python package VecturaDB, and the commands with curl. All 49 checks passed. At every release, the CI of VecturaDB also runs its tests and the Python tests of Chroma, in process: the run of 1.0.0-preview.8.

A search ranks records by a dense vector, a sparse vector or both, merged with RRF. The client computes the BM25 vectors from the documents.

hybrid = Rrf(
    ranks=[
        Knn(query=[0, 1, 0], return_rank=True, default=1000),
        Knn(query="apple pie", key="doc_bm25", return_rank=True, default=1000),
    ],
    weights=[0.7, 0.3],
    k=60,
)
results = collection.search(Search().where(K("topic") != "tech").rank(hybrid).limit(2))

The full example, with Python, JavaScript and .NET: Hybrid search on your own server.

Tested. In our tests on 7 November 2026, the hybrid search above returned pie and fox. A BM25 rank alone, with select, returned pie and its document. In the CI, test_multiple_sparse_vector_indices_e2e ranks records by two sparse indexes, with a weighted sum and with RRF, and test_bm25_scores_of_chroma_cloud checks the BM25 scores that Chroma Cloud gives on 304 documents.

Group by

A search can keep the best records of each value of a metadata key.

from chromadb.execution.expression import GroupBy, MinK

results = collection.search(
    Search()
    .rank(Knn(query=[0, 1, 0]))
    .group_by(GroupBy(keys=K("topic"), aggregate=MinK(keys=K.SCORE, k=1)))
    .limit(3)
)

Tested. In our tests on 7 November 2026, the search above kept the nearest record of each topic, on an HNSW collection and on a SPANN collection. With MaxK it kept the farthest one. In the CI, the tests of the group-by operator that the single node shares with distributed Chroma pass (ranked_group_by), and so do searches with a group by on SPANN collections.

Sparse vectors in the metadata

from chromadb.api.types import SparseVector

collection.add(
    ids=["a"],
    embeddings=[[1, 0]],
    metadatas=[{"terms": SparseVector(indices=[1, 5], values=[0.3, 0.8])}],
)

Tested. In our tests on 7 November 2026, the record above read back with its sparse vector. An update changed it, and None removed it. In the CI, test_sparse_metadata_public_roundtrip also reads sparse vectors with a query and a search.

SPANN

HNSW stays the default, as in Chroma 1.5.9. A collection that asks for SPANN in its configuration or schema gets a SPANN index. Writes go to the files of the index later, as the compactor of distributed Chroma does, and reads see them at once.

In our tests on one Azure Standard_D8ads_v6 VM (3 November 2026), at commit b64c37f66, before this release, with 1,000 records a second asked for 12 minutes, two runs each:

VecturaDB Distributed Chroma 52ce844
Records written 717,300 / 720,000 573,700 / 573,400
Writes refused for backpressure (429) 1 / 0 10 / 15

Distributed Chroma ran on a single-node Kubernetes cluster on the same VM, with all its services. This is a result on one VM, not against a cluster. Recall@10 on 200,000 records: 0.9947 for VecturaDB (Azure Standard_D8ads_v7) and 0.9953 for distributed Chroma (Azure Standard_D8ads_v6).

To turn SPANN off, set executor: local: spann: false: every collection then gets HNSW.

Chroma 1.5.9 cannot read a SPANN collection. On a data path written by VecturaDB, Chroma 1.5.9 lists it, but count, get and query fail with Collection is missing HNSW configuration. An add to a SPANN collection on Chroma 1.5.9 answers Error in compaction: Error constructing hnsw segment reader: Error creating hnsw segment reader: Collection is missing HNSW configuration, yet the record is written: VecturaDB shows it once it opens the data path again. The HNSW collections stay readable, and VecturaDB reads every collection again afterwards.

Tested. In our tests on 7 November 2026, a collection with no index settings got HNSW. A collection that asked for SPANN in its configuration got SPANN, and so did one that asked for it in its schema. A query of a SPANN collection returned the nearest records. In another run on the same day, VecturaDB wrote a data folder with two HNSW collections, one of them with a BM25 index, and three SPANN collections, one of them with 300 records already indexed. Chroma 1.5.9 then opened it, in Docker: it listed every collection, but count, get and query failed on the three SPANN collections. On the HNSW collections they returned the records, and an add went through. An add of a new record to each of the three SPANN collections answered Error in compaction. VecturaDB then read every collection again: the three SPANN collections had 4, 4 and 301 records, with the new one, and the add to the HNSW collection was there too. In the CI, more than 100 tests run on SPANN collections, among them reads after deletes and after a restart (a_spann_collection_reads_back_its_records).

Read levels

A search of a SPANN collection takes the read level of distributed Chroma.

from chromadb.api.types import ReadLevel

results = collection.search(Search().rank(Knn(query=[0, 1, 0])).limit(1), read_level=ReadLevel.INDEX_ONLY)

Tested. In our tests on 7 November 2026, 300 records went into a SPANN collection. Right after the write, index_only found none of them, index_and_bounded_wal found 250 and index_and_wal found 300. After the flush, every level found 300. In the CI: a_spann_search_reads_the_log_as_its_read_level_asks.

Indexing status

status = collection.get_indexing_status()
# IndexingStatus(num_indexed_ops=3, num_unindexed_ops=0, total_ops=3, op_indexing_progress=1.0)

Tested. In our tests on 7 November 2026, a collection of 3 records gave the status above. In the CI: indexing_status_on_a_single_node_has_nothing_waiting.

Resource names and CRN

A tenant takes a resource name once, and a collection can be read by its CRN, <resource name>:<database>:<collection>.

curl -X PATCH http://127.0.0.1:8000/api/v2/tenants/default_tenant -H "Content-Type: application/json" -d '{"resource_name": "acme"}'
curl http://127.0.0.1:8000/api/v2/collections/acme:default_database:docs

Tested. In our tests on 7 November 2026, in plain HTTP, the two commands above answered 200, and the second one returned the collection docs. A second resource name for the same tenant answered 409, and a CRN with a wrong part answered 404. In the CI, over HTTP: test_resource_name_and_collection_by_crn_on_a_single_node.

Collection fork

copy = collection.fork("docs-copy")
collection.fork_count()  # 1

Tested. In our tests on 7 November 2026, the copy had the records of its source, and fork_count() answered 1. A write to the copy left the source as it was. In the CI: local_fork_copies_records_and_stays_independent. A fork copies the files of the collection: see Differences from distributed Chroma.

Conditional transactions

A transaction reads records, buffers writes, and commits them only if no record it read or writes changed after its reads.

txn = accounts.conditional()
balance = txn.get(ids="alice", include=["metadatas"])["metadatas"][0]["balance"]
txn.update(ids="alice", metadatas={"balance": balance - 30})
txn.commit()

If another client writes alice between the read and the commit, the commit raises ConditionalWriteConflictError (409) and writes nothing. collection.conditional().run(callback) starts the transaction again by itself. They are on by default, as on Chroma Cloud.

They need a client with conditional(): the Python package VecturaDB (pip install VecturaDB), or the JavaScript client in clients/new-js of the VecturaDB repository, which is not on npm. The published clients chromadb 1.5.9 on PyPI and chromadb 3.5.0 on npm do not have them.

In our tests on one Azure Standard_E8as_v7 VM (3 and 4 November 2026), at commit 955e14e74, six servers ran for 6 hours with 8 workers incrementing counters in transactions, half of them with kill -9 every 20 to 90 s: 0 wrong counters.

Tested. In our tests on 7 November 2026, with the Python package VecturaDB over HTTP, the transaction above committed. When another client wrote alice between the read and the commit, the commit raised ConditionalWriteConflictError and wrote nothing. run() started the transaction again after a conflict. In the CI: the tests in test_conditional_transaction.py.

Attached functions

A function computes from the records of a collection and writes to its output collection.

collection.attach_function(
    name="counter",
    function=RECORD_COUNTER_FUNCTION,
    output_collection="counts",
    params=None,
)

record_counter, statistics and revision_history run after each apply of the records of their input. count_to_file_async runs later, from its queue, and writes a file in the data path or on S3.

The Python packages VecturaDB and chromadb 1.5.9 have attach_function. The JavaScript client chromadb 3.5.0 does not.

In our tests on one Azure Standard_D8ds_v6 VM (4 November 2026), at commit f20b36d2, with record_counter on each collection, VecturaDB did 20.4% to 21.0% more operations than Chroma 1.5.9, which runs no function, under the same load: 3 runs of 6 minutes, 0 wrong answers on either side. record_counter reached the final count 1.5 to 1.8 s after the end of the load.

Tested. In our tests on 7 November 2026, the record_counter above counted 2 records, then 3 after more writes and a delete. The function was read back, then detached with its output collection. In plain HTTP the same day, statistics was attached and read back, an asynchronous function took a second input collection, and both were detached. In the CI: record_counter_counts_the_records_of_its_input_collection, count_to_file_async_counts_the_records_of_its_input_collection and revision_history_archives_a_delete.

Databases by id, and their count

A tenant reads several databases by their ids in one request, one database by its id, and how many databases it has.

curl -X POST http://127.0.0.1:8000/api/v2/tenants/default_tenant/databases/batch/get -H "Content-Type: application/json" -d '{"ids": ["00000000-0000-0000-0000-000000000000"]}'
curl http://127.0.0.1:8000/api/v2/tenants/default_tenant/databases/by-id/00000000-0000-0000-0000-000000000000
curl http://127.0.0.1:8000/api/v2/tenants/default_tenant/databases_count

On VecturaDB, 00000000-0000-0000-0000-000000000000 is the id of default_database.

Tested. In our tests on 7 November 2026, in plain HTTP, the three commands above returned default_database and a count of 1 on a new server. A database read by its id was the one with that id, and an id that no database has answered 404. A read of several ids returned each database once and left out the unknown id. The count went from 1 to 3 after two new databases. In the CI, over HTTP: test_bulk_database_lookup for several ids and test_count_databases_is_tenant_scoped for the count.

Differences from distributed Chroma

Differences from Chroma 1.5.9

Chroma 1.5.9 cannot read the SPANN collections of a data path: see SPANN.

On an idle server, a get by id is slower than on Chroma 1.5.9. Under writes it is faster. In our tests on one Azure Standard_E8s_v7 VM (4 November 2026), at commit ac50984b5, on HNSW with 100,000 records, two runs each:

Median, ms Chroma 1.5.9 VecturaDB
Idle, 1 id 1.04 / 1.08 1.20 / 1.20
Under writes, 1 id, p99 35.10 / 37.06 4.11 / 4.13

What stays out, and why

These are services or products of Chroma Cloud, not features of the database:

Some parts of distributed Chroma exist to run it on several services, and have no use on one node: the counter of failed compactions, the two-phase garbage collection of versions, and the system database across regions.

The hosted embedding functions chroma-cloud-qwen and chroma-cloud-splade ran on the servers of Chroma Cloud. The Python package VecturaDB computes them on your machine, with the same names and configuration.