Skip to content

Commit a384fdc

Browse files
HammadBgithub-actions[bot]
authored andcommitted
[ENH] Add eventual consistency to the frontend (#5946)
## Description of changes _Summarize the changes made by this PR._ - Improvements & Bug fixes - ... - New functionality - Adds read level enum to the frontend APIs and propagates it through to query nodes - Note js client tests are failing as they require committing generated files, this will occur in the top of the stack where client changes are addressed. ## Test plan Will be handled by e2e tests at top of stack - [x] Tests pass locally with `pytest` for python, `yarn test` for js, `cargo test` for rust ## Migration plan IndexAndWal will be the default everywhere ## Observability plan _What is the plan to instrument and monitor this change?_ ## Documentation Changes _Are all docstrings for user-facing APIs updated if required? Do we need to make documentation changes in the [docs section](https://github.com/chroma-core/chroma/tree/main/docs/docs.trychroma.com)?_
1 parent f87f41b commit a384fdc

21 files changed

Lines changed: 400 additions & 33 deletions

File tree

chromadb/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
Include,
2424
Metadata,
2525
Metadatas,
26+
ReadLevel,
2627
Where,
2728
QueryResult,
2829
GetResult,

chromadb/api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
IncludeMetadataDocuments,
6363
Loadable,
6464
Metadatas,
65+
ReadLevel,
6566
Schema,
6667
URIs,
6768
Where,
@@ -713,6 +714,7 @@ def _search(
713714
searches: List[Search],
714715
tenant: str = DEFAULT_TENANT,
715716
database: str = DEFAULT_DATABASE,
717+
read_level: ReadLevel = ReadLevel.INDEX_AND_WAL,
716718
) -> SearchResult:
717719
pass
718720

chromadb/api/async_api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
IndexingStatus,
2323
Loadable,
2424
Metadatas,
25+
ReadLevel,
2526
Schema,
2627
URIs,
2728
Where,
@@ -665,6 +666,7 @@ async def _search(
665666
searches: List[Search],
666667
tenant: str = DEFAULT_TENANT,
667668
database: str = DEFAULT_DATABASE,
669+
read_level: ReadLevel = ReadLevel.INDEX_AND_WAL,
668670
) -> SearchResult:
669671
pass
670672

chromadb/api/async_fastapi.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
IndexingStatus,
3636
Schema,
3737
Metadatas,
38+
ReadLevel,
3839
URIs,
3940
Where,
4041
WhereDocument,
@@ -444,9 +445,13 @@ async def _search(
444445
searches: List[Search],
445446
tenant: str = DEFAULT_TENANT,
446447
database: str = DEFAULT_DATABASE,
448+
read_level: ReadLevel = ReadLevel.INDEX_AND_WAL,
447449
) -> SearchResult:
448450
"""Performs hybrid search on a collection"""
449-
payload = {"searches": [s.to_dict() for s in searches]}
451+
payload = {
452+
"searches": [s.to_dict() for s in searches],
453+
"read_level": read_level,
454+
}
450455

451456
resp_json = await self._make_request(
452457
"post",

chromadb/api/fastapi.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
IndexingStatus,
3030
Schema,
3131
Metadatas,
32+
ReadLevel,
3233
URIs,
3334
Where,
3435
WhereDocument,
@@ -407,10 +408,14 @@ def _search(
407408
searches: List[Search],
408409
tenant: str = DEFAULT_TENANT,
409410
database: str = DEFAULT_DATABASE,
411+
read_level: ReadLevel = ReadLevel.INDEX_AND_WAL,
410412
) -> SearchResult:
411413
"""Performs hybrid search on a collection"""
412414
# Convert Search objects to dictionaries
413-
payload = {"searches": [s.to_dict() for s in searches]}
415+
payload = {
416+
"searches": [s.to_dict() for s in searches],
417+
"read_level": read_level,
418+
}
414419

415420
resp_json = self._make_request(
416421
"post",

chromadb/api/models/AsyncCollection.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
QueryResult,
1717
ID,
1818
OneOrMany,
19+
ReadLevel,
1920
WhereDocument,
2021
SearchResult,
2122
maybe_cast_one_to_many,
@@ -311,6 +312,7 @@ async def fork(
311312
async def search(
312313
self,
313314
searches: OneOrMany[Search],
315+
read_level: ReadLevel = ReadLevel.INDEX_AND_WAL,
314316
) -> SearchResult:
315317
"""Perform hybrid search on the collection.
316318
This is an experimental API that only works for Hosted Chroma for now.
@@ -321,6 +323,11 @@ async def search(
321323
- rank: Ranking expression for hybrid search (defaults to Val(0.0))
322324
- limit: Limit configuration for pagination (defaults to no limit)
323325
- select: Select configuration for keys to return (defaults to empty)
326+
read_level: Controls whether to read from the write-ahead log (WAL):
327+
- ReadLevel.INDEX_AND_WAL: Read from both the compacted index and WAL (default).
328+
All committed writes will be visible.
329+
- ReadLevel.INDEX_ONLY: Read only from the compacted index, skipping the WAL.
330+
Faster, but recent writes that haven't been compacted may not be visible.
324331
325332
Returns:
326333
SearchResult: Column-major format response with:
@@ -368,6 +375,10 @@ async def search(
368375
Search().where(K("type") == "paper").rank(Knn(query=[0.3, 0.4]))
369376
]
370377
results = await collection.search(searches)
378+
379+
# Skip WAL for faster queries (may miss recent uncommitted writes)
380+
from chromadb.api.types import ReadLevel
381+
result = await collection.search(search, read_level=ReadLevel.INDEX_ONLY)
371382
"""
372383
# Convert single search to list for consistent handling
373384
searches_list = maybe_cast_one_to_many(searches)
@@ -384,6 +395,7 @@ async def search(
384395
searches=cast(List[Search], embedded_searches),
385396
tenant=self.tenant,
386397
database=self.database,
398+
read_level=read_level,
387399
)
388400

389401
async def update(

chromadb/api/models/Collection.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
QueryResult,
1818
ID,
1919
OneOrMany,
20+
ReadLevel,
2021
WhereDocument,
2122
SearchResult,
2223
maybe_cast_one_to_many,
@@ -320,6 +321,7 @@ def fork(
320321
def search(
321322
self,
322323
searches: OneOrMany[Search],
324+
read_level: ReadLevel = ReadLevel.INDEX_AND_WAL,
323325
) -> SearchResult:
324326
"""Perform hybrid search on the collection.
325327
This is an experimental API that only works for Hosted Chroma for now.
@@ -330,6 +332,11 @@ def search(
330332
- rank: Ranking expression for hybrid search (defaults to Val(0.0))
331333
- limit: Limit configuration for pagination (defaults to no limit)
332334
- select: Select configuration for keys to return (defaults to empty)
335+
read_level: Controls whether to read from the write-ahead log (WAL):
336+
- ReadLevel.INDEX_AND_WAL: Read from both the compacted index and WAL (default).
337+
All committed writes will be visible.
338+
- ReadLevel.INDEX_ONLY: Read only from the compacted index, skipping the WAL.
339+
Faster, but recent writes that haven't been compacted may not be visible.
333340
334341
Returns:
335342
SearchResult: Column-major format response with:
@@ -377,6 +384,10 @@ def search(
377384
Search().where(K("type") == "paper").rank(Knn(query=[0.3, 0.4]))
378385
]
379386
results = collection.search(searches)
387+
388+
# Skip WAL for faster queries (may miss recent uncommitted writes)
389+
from chromadb.api.types import ReadLevel
390+
result = collection.search(search, read_level=ReadLevel.INDEX_ONLY)
380391
"""
381392
# Convert single search to list for consistent handling
382393
searches_list = maybe_cast_one_to_many(searches)
@@ -393,6 +404,7 @@ def search(
393404
searches=cast(List[Search], embedded_searches),
394405
tenant=self.tenant,
395406
database=self.database,
407+
read_level=read_level,
396408
)
397409

398410
def update(

chromadb/api/rust.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
IncludeMetadataDocuments,
4040
IncludeMetadataDocumentsDistances,
4141
IncludeMetadataDocumentsEmbeddings,
42+
ReadLevel,
4243
Schema,
4344
SearchResult,
4445
)
@@ -341,9 +342,7 @@ def _get_indexing_status(
341342
tenant: str = DEFAULT_TENANT,
342343
database: str = DEFAULT_DATABASE,
343344
) -> "IndexingStatus":
344-
raise NotImplementedError(
345-
"Indexing status is not implemented for Local Chroma"
346-
)
345+
raise NotImplementedError("Indexing status is not implemented for Local Chroma")
347346

348347
@override
349348
def _search(
@@ -352,6 +351,7 @@ def _search(
352351
searches: List[Search],
353352
tenant: str = DEFAULT_TENANT,
354353
database: str = DEFAULT_DATABASE,
354+
read_level: ReadLevel = ReadLevel.INDEX_AND_WAL,
355355
) -> SearchResult:
356356
raise NotImplementedError("Search is not implemented for Local Chroma")
357357

chromadb/api/segment.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
Embeddings,
4141
Metadatas,
4242
Documents,
43+
ReadLevel,
4344
Schema,
4445
URIs,
4546
Where,
@@ -439,9 +440,7 @@ def _get_indexing_status(
439440
tenant: str = DEFAULT_TENANT,
440441
database: str = DEFAULT_DATABASE,
441442
) -> "IndexingStatus":
442-
raise NotImplementedError(
443-
"Indexing status is not implemented for SegmentAPI"
444-
)
443+
raise NotImplementedError("Indexing status is not implemented for SegmentAPI")
445444

446445
@override
447446
def _search(
@@ -450,6 +449,7 @@ def _search(
450449
searches: List[Search],
451450
tenant: str = DEFAULT_TENANT,
452451
database: str = DEFAULT_DATABASE,
452+
read_level: ReadLevel = ReadLevel.INDEX_AND_WAL,
453453
) -> SearchResult:
454454
raise NotImplementedError("Search is not implemented for SegmentAPI")
455455

chromadb/api/types.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,20 @@ class IndexMetadata(TypedDict):
760760
Space = Literal["cosine", "l2", "ip"]
761761

762762

763+
class ReadLevel(str, Enum):
764+
"""Controls whether search queries read from the write-ahead log (WAL).
765+
766+
Attributes:
767+
INDEX_AND_WAL: Read from both the compacted index and the WAL (default).
768+
All committed writes will be visible.
769+
INDEX_ONLY: Read only from the compacted index, skipping the WAL.
770+
Faster, but recent writes that haven't been compacted may not be visible.
771+
"""
772+
773+
INDEX_AND_WAL = "index_and_wal"
774+
INDEX_ONLY = "index_only"
775+
776+
763777
# TODO: make warnings prettier and add link to migration docs
764778
@runtime_checkable
765779
class EmbeddingFunction(Protocol[D]):
@@ -776,7 +790,8 @@ class EmbeddingFunction(Protocol[D]):
776790
"""
777791

778792
@abstractmethod
779-
def __call__(self, input: D) -> Embeddings: ...
793+
def __call__(self, input: D) -> Embeddings:
794+
...
780795

781796
def embed_query(self, input: D) -> Embeddings:
782797
"""
@@ -960,7 +975,8 @@ def validate_embedding_function(
960975

961976

962977
class DataLoader(Protocol[L]):
963-
def __call__(self, uris: URIs) -> L: ...
978+
def __call__(self, uris: URIs) -> L:
979+
...
964980

965981

966982
def validate_ids(ids: IDs) -> IDs:
@@ -1417,7 +1433,8 @@ class SparseEmbeddingFunction(Protocol[D]):
14171433
"""
14181434

14191435
@abstractmethod
1420-
def __call__(self, input: D) -> SparseVectors: ...
1436+
def __call__(self, input: D) -> SparseVectors:
1437+
...
14211438

14221439
def embed_query(self, input: D) -> SparseVectors:
14231440
"""
@@ -1611,9 +1628,9 @@ class VectorIndexConfig(BaseModel):
16111628

16121629
space: Optional[Space] = None
16131630
embedding_function: Optional[Any] = DefaultEmbeddingFunction()
1614-
source_key: Optional[str] = (
1615-
None # key to source the vector from (accepts str or Key)
1616-
)
1631+
source_key: Optional[
1632+
str
1633+
] = None # key to source the vector from (accepts str or Key)
16171634
hnsw: Optional[HnswIndexConfig] = None
16181635
spann: Optional[SpannIndexConfig] = None
16191636

@@ -1662,9 +1679,9 @@ class SparseVectorIndexConfig(BaseModel):
16621679

16631680
# TODO(Sanket): Change this to the appropriate sparse ef and use a default here.
16641681
embedding_function: Optional[Any] = None
1665-
source_key: Optional[str] = (
1666-
None # key to source the sparse vector from (accepts str or Key)
1667-
)
1682+
source_key: Optional[
1683+
str
1684+
] = None # key to source the sparse vector from (accepts str or Key)
16681685
bm25: Optional[bool] = None
16691686

16701687
@field_validator("source_key", mode="before")

0 commit comments

Comments
 (0)