Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions chromadb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
Include,
Metadata,
Metadatas,
ReadLevel,
Where,
QueryResult,
GetResult,
Expand Down
2 changes: 2 additions & 0 deletions chromadb/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
IncludeMetadataDocuments,
Loadable,
Metadatas,
ReadLevel,
Schema,
URIs,
Where,
Expand Down Expand Up @@ -713,6 +714,7 @@ def _search(
searches: List[Search],
tenant: str = DEFAULT_TENANT,
database: str = DEFAULT_DATABASE,
read_level: ReadLevel = ReadLevel.INDEX_AND_WAL,
) -> SearchResult:
pass

Expand Down
2 changes: 2 additions & 0 deletions chromadb/api/async_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
IndexingStatus,
Loadable,
Metadatas,
ReadLevel,
Schema,
URIs,
Where,
Expand Down Expand Up @@ -665,6 +666,7 @@ async def _search(
searches: List[Search],
tenant: str = DEFAULT_TENANT,
database: str = DEFAULT_DATABASE,
read_level: ReadLevel = ReadLevel.INDEX_AND_WAL,
) -> SearchResult:
pass

Expand Down
7 changes: 6 additions & 1 deletion chromadb/api/async_fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
IndexingStatus,
Schema,
Metadatas,
ReadLevel,
URIs,
Where,
WhereDocument,
Expand Down Expand Up @@ -444,9 +445,13 @@ async def _search(
searches: List[Search],
tenant: str = DEFAULT_TENANT,
database: str = DEFAULT_DATABASE,
read_level: ReadLevel = ReadLevel.INDEX_AND_WAL,
) -> SearchResult:
"""Performs hybrid search on a collection"""
payload = {"searches": [s.to_dict() for s in searches]}
payload = {
"searches": [s.to_dict() for s in searches],
"read_level": read_level,
}

resp_json = await self._make_request(
"post",
Expand Down
7 changes: 6 additions & 1 deletion chromadb/api/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
IndexingStatus,
Schema,
Metadatas,
ReadLevel,
URIs,
Where,
WhereDocument,
Expand Down Expand Up @@ -407,10 +408,14 @@ def _search(
searches: List[Search],
tenant: str = DEFAULT_TENANT,
database: str = DEFAULT_DATABASE,
read_level: ReadLevel = ReadLevel.INDEX_AND_WAL,
) -> SearchResult:
"""Performs hybrid search on a collection"""
# Convert Search objects to dictionaries
payload = {"searches": [s.to_dict() for s in searches]}
payload = {
"searches": [s.to_dict() for s in searches],
"read_level": read_level,
}

resp_json = self._make_request(
"post",
Expand Down
12 changes: 12 additions & 0 deletions chromadb/api/models/AsyncCollection.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
QueryResult,
ID,
OneOrMany,
ReadLevel,
WhereDocument,
SearchResult,
maybe_cast_one_to_many,
Expand Down Expand Up @@ -311,6 +312,7 @@ async def fork(
async def search(
self,
searches: OneOrMany[Search],
read_level: ReadLevel = ReadLevel.INDEX_AND_WAL,
) -> SearchResult:
"""Perform hybrid search on the collection.
This is an experimental API that only works for Hosted Chroma for now.
Expand All @@ -321,6 +323,11 @@ async def search(
- rank: Ranking expression for hybrid search (defaults to Val(0.0))
- limit: Limit configuration for pagination (defaults to no limit)
- select: Select configuration for keys to return (defaults to empty)
read_level: Controls whether to read from the write-ahead log (WAL):
- ReadLevel.INDEX_AND_WAL: Read from both the compacted index and WAL (default).
All committed writes will be visible.
- ReadLevel.INDEX_ONLY: Read only from the compacted index, skipping the WAL.
Faster, but recent writes that haven't been compacted may not be visible.

Returns:
SearchResult: Column-major format response with:
Expand Down Expand Up @@ -368,6 +375,10 @@ async def search(
Search().where(K("type") == "paper").rank(Knn(query=[0.3, 0.4]))
]
results = await collection.search(searches)

# Skip WAL for faster queries (may miss recent uncommitted writes)
from chromadb.api.types import ReadLevel
result = await collection.search(search, read_level=ReadLevel.INDEX_ONLY)
"""
# Convert single search to list for consistent handling
searches_list = maybe_cast_one_to_many(searches)
Expand All @@ -384,6 +395,7 @@ async def search(
searches=cast(List[Search], embedded_searches),
tenant=self.tenant,
database=self.database,
read_level=read_level,
)

async def update(
Expand Down
12 changes: 12 additions & 0 deletions chromadb/api/models/Collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
QueryResult,
ID,
OneOrMany,
ReadLevel,
WhereDocument,
SearchResult,
maybe_cast_one_to_many,
Expand Down Expand Up @@ -320,6 +321,7 @@ def fork(
def search(
self,
searches: OneOrMany[Search],
read_level: ReadLevel = ReadLevel.INDEX_AND_WAL,
) -> SearchResult:
"""Perform hybrid search on the collection.
This is an experimental API that only works for Hosted Chroma for now.
Expand All @@ -330,6 +332,11 @@ def search(
- rank: Ranking expression for hybrid search (defaults to Val(0.0))
- limit: Limit configuration for pagination (defaults to no limit)
- select: Select configuration for keys to return (defaults to empty)
read_level: Controls whether to read from the write-ahead log (WAL):
- ReadLevel.INDEX_AND_WAL: Read from both the compacted index and WAL (default).
All committed writes will be visible.
- ReadLevel.INDEX_ONLY: Read only from the compacted index, skipping the WAL.
Faster, but recent writes that haven't been compacted may not be visible.

Returns:
SearchResult: Column-major format response with:
Expand Down Expand Up @@ -377,6 +384,10 @@ def search(
Search().where(K("type") == "paper").rank(Knn(query=[0.3, 0.4]))
]
results = collection.search(searches)

# Skip WAL for faster queries (may miss recent uncommitted writes)
from chromadb.api.types import ReadLevel
result = collection.search(search, read_level=ReadLevel.INDEX_ONLY)
"""
# Convert single search to list for consistent handling
searches_list = maybe_cast_one_to_many(searches)
Expand All @@ -393,6 +404,7 @@ def search(
searches=cast(List[Search], embedded_searches),
tenant=self.tenant,
database=self.database,
read_level=read_level,
)

def update(
Expand Down
6 changes: 3 additions & 3 deletions chromadb/api/rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
IncludeMetadataDocuments,
IncludeMetadataDocumentsDistances,
IncludeMetadataDocumentsEmbeddings,
ReadLevel,
Schema,
SearchResult,
)
Expand Down Expand Up @@ -341,9 +342,7 @@ def _get_indexing_status(
tenant: str = DEFAULT_TENANT,
database: str = DEFAULT_DATABASE,
) -> "IndexingStatus":
raise NotImplementedError(
"Indexing status is not implemented for Local Chroma"
)
raise NotImplementedError("Indexing status is not implemented for Local Chroma")

@override
def _search(
Expand All @@ -352,6 +351,7 @@ def _search(
searches: List[Search],
tenant: str = DEFAULT_TENANT,
database: str = DEFAULT_DATABASE,
read_level: ReadLevel = ReadLevel.INDEX_AND_WAL,
) -> SearchResult:
raise NotImplementedError("Search is not implemented for Local Chroma")

Expand Down
6 changes: 3 additions & 3 deletions chromadb/api/segment.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
Embeddings,
Metadatas,
Documents,
ReadLevel,
Schema,
URIs,
Where,
Expand Down Expand Up @@ -439,9 +440,7 @@ def _get_indexing_status(
tenant: str = DEFAULT_TENANT,
database: str = DEFAULT_DATABASE,
) -> "IndexingStatus":
raise NotImplementedError(
"Indexing status is not implemented for SegmentAPI"
)
raise NotImplementedError("Indexing status is not implemented for SegmentAPI")

@override
def _search(
Expand All @@ -450,6 +449,7 @@ def _search(
searches: List[Search],
tenant: str = DEFAULT_TENANT,
database: str = DEFAULT_DATABASE,
read_level: ReadLevel = ReadLevel.INDEX_AND_WAL,
) -> SearchResult:
raise NotImplementedError("Search is not implemented for SegmentAPI")

Expand Down
35 changes: 26 additions & 9 deletions chromadb/api/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,20 @@ class IndexMetadata(TypedDict):
Space = Literal["cosine", "l2", "ip"]


class ReadLevel(str, Enum):
"""Controls whether search queries read from the write-ahead log (WAL).

Attributes:
INDEX_AND_WAL: Read from both the compacted index and the WAL (default).
All committed writes will be visible.
INDEX_ONLY: Read only from the compacted index, skipping the WAL.
Faster, but recent writes that haven't been compacted may not be visible.
"""

INDEX_AND_WAL = "index_and_wal"
INDEX_ONLY = "index_only"


# TODO: make warnings prettier and add link to migration docs
@runtime_checkable
class EmbeddingFunction(Protocol[D]):
Expand All @@ -776,7 +790,8 @@ class EmbeddingFunction(Protocol[D]):
"""

@abstractmethod
def __call__(self, input: D) -> Embeddings: ...
def __call__(self, input: D) -> Embeddings:
...

def embed_query(self, input: D) -> Embeddings:
"""
Expand Down Expand Up @@ -960,7 +975,8 @@ def validate_embedding_function(


class DataLoader(Protocol[L]):
def __call__(self, uris: URIs) -> L: ...
def __call__(self, uris: URIs) -> L:
...


def validate_ids(ids: IDs) -> IDs:
Expand Down Expand Up @@ -1417,7 +1433,8 @@ class SparseEmbeddingFunction(Protocol[D]):
"""

@abstractmethod
def __call__(self, input: D) -> SparseVectors: ...
def __call__(self, input: D) -> SparseVectors:
...

def embed_query(self, input: D) -> SparseVectors:
"""
Expand Down Expand Up @@ -1611,9 +1628,9 @@ class VectorIndexConfig(BaseModel):

space: Optional[Space] = None
embedding_function: Optional[Any] = DefaultEmbeddingFunction()
source_key: Optional[str] = (
None # key to source the vector from (accepts str or Key)
)
source_key: Optional[
str
] = None # key to source the vector from (accepts str or Key)
hnsw: Optional[HnswIndexConfig] = None
spann: Optional[SpannIndexConfig] = None

Expand Down Expand Up @@ -1662,9 +1679,9 @@ class SparseVectorIndexConfig(BaseModel):

# TODO(Sanket): Change this to the appropriate sparse ef and use a default here.
embedding_function: Optional[Any] = None
source_key: Optional[str] = (
None # key to source the sparse vector from (accepts str or Key)
)
source_key: Optional[
str
] = None # key to source the sparse vector from (accepts str or Key)
bm25: Optional[bool] = None

@field_validator("source_key", mode="before")
Expand Down
Loading
Loading