chore: 添加虚拟环境到仓库

- 添加 backend_service/venv 虚拟环境
- 包含所有Python依赖包
- 注意:虚拟环境约393MB,包含12655个文件
This commit is contained in:
2025-12-03 10:19:25 +08:00
parent a6c2027caa
commit c4f851d387
12655 changed files with 3009376 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
from abc import abstractmethod
from chromadb.api.types import GetResult, QueryResult
from chromadb.config import Component
from chromadb.execution.expression.plan import CountPlan, GetPlan, KNNPlan
class Executor(Component):
@abstractmethod
def count(self, plan: CountPlan) -> int:
pass
@abstractmethod
def get(self, plan: GetPlan) -> GetResult:
pass
@abstractmethod
def knn(self, plan: KNNPlan) -> QueryResult:
pass

View File

@@ -0,0 +1,242 @@
import threading
import random
from typing import Callable, Dict, List, Optional, TypeVar
import grpc
from overrides import overrides
from chromadb.api.types import GetResult, Metadata, QueryResult
from chromadb.config import System
from chromadb.execution.executor.abstract import Executor
from chromadb.execution.expression.operator import Scan
from chromadb.execution.expression.plan import CountPlan, GetPlan, KNNPlan
from chromadb.proto import convert
from chromadb.proto.query_executor_pb2_grpc import QueryExecutorStub
from chromadb.segment.impl.manager.distributed import DistributedSegmentManager
from chromadb.telemetry.opentelemetry.grpc import OtelInterceptor
from tenacity import (
RetryCallState,
Retrying,
stop_after_attempt,
wait_exponential_jitter,
retry_if_exception,
)
from opentelemetry.trace import Span
def _clean_metadata(metadata: Optional[Metadata]) -> Optional[Metadata]:
"""Remove any chroma-specific metadata keys that the client shouldn't see from a metadata map."""
if not metadata:
return None
result = {}
for k, v in metadata.items():
if not k.startswith("chroma:"):
result[k] = v
if len(result) == 0:
return None
return result
def _uri(metadata: Optional[Metadata]) -> Optional[str]:
"""Retrieve the uri (if any) from a Metadata map"""
if metadata and "chroma:uri" in metadata:
return str(metadata["chroma:uri"])
return None
# Type variables for input and output types of the round-robin retry function
I = TypeVar("I") # noqa: E741
O = TypeVar("O") # noqa: E741
class DistributedExecutor(Executor):
_mtx: threading.Lock
_grpc_stub_pool: Dict[str, QueryExecutorStub]
_manager: DistributedSegmentManager
_request_timeout_seconds: int
_query_replication_factor: int
def __init__(self, system: System):
super().__init__(system)
self._mtx = threading.Lock()
self._grpc_stub_pool = {}
self._manager = self.require(DistributedSegmentManager)
self._request_timeout_seconds = system.settings.require(
"chroma_query_request_timeout_seconds"
)
self._query_replication_factor = system.settings.require(
"chroma_query_replication_factor"
)
def _round_robin_retry(self, funcs: List[Callable[[I], O]], args: I) -> O:
"""
Retry a list of functions in a round-robin fashion until one of them succeeds.
funcs: List of functions to retry
args: Arguments to pass to each function
"""
attempt_count = 0
sleep_span: Optional[Span] = None
def before_sleep(_: RetryCallState) -> None:
# HACK(hammadb) 1/14/2024 - this is a hack to avoid the fact that tracer is not yet available and there are boot order issues
# This should really use our component system to get the tracer. Since our grpc utils use this pattern
# we are copying it here. This should be removed once we have a better way to get the tracer
from chromadb.telemetry.opentelemetry import tracer
nonlocal sleep_span
if tracer is not None:
sleep_span = tracer.start_span("Waiting to retry RPC")
for attempt in Retrying(
stop=stop_after_attempt(5),
wait=wait_exponential_jitter(0.1, jitter=0.1),
reraise=True,
retry=retry_if_exception(
lambda x: isinstance(x, grpc.RpcError)
and x.code() in [grpc.StatusCode.UNAVAILABLE, grpc.StatusCode.UNKNOWN]
),
before_sleep=before_sleep,
):
if sleep_span is not None:
sleep_span.end()
sleep_span = None
with attempt:
return funcs[attempt_count % len(funcs)](args)
attempt_count += 1
# NOTE(hammadb) because Retrying() will always either return or raise an exception, this line should never be reached
raise Exception("Unreachable code error - should never reach here")
@overrides
def count(self, plan: CountPlan) -> int:
endpoints = self._get_grpc_endpoints(plan.scan)
count_funcs = [self._get_stub(endpoint).Count for endpoint in endpoints]
count_result = self._round_robin_retry(
count_funcs, convert.to_proto_count_plan(plan)
)
return convert.from_proto_count_result(count_result)
@overrides
def get(self, plan: GetPlan) -> GetResult:
endpoints = self._get_grpc_endpoints(plan.scan)
get_funcs = [self._get_stub(endpoint).Get for endpoint in endpoints]
get_result = self._round_robin_retry(get_funcs, convert.to_proto_get_plan(plan))
records = convert.from_proto_get_result(get_result)
ids = [record["id"] for record in records]
embeddings = (
[record["embedding"] for record in records]
if plan.projection.embedding
else None
)
documents = (
[record["document"] for record in records]
if plan.projection.document
else None
)
uris = (
[_uri(record["metadata"]) for record in records]
if plan.projection.uri
else None
)
metadatas = (
[_clean_metadata(record["metadata"]) for record in records]
if plan.projection.metadata
else None
)
# TODO: Fix typing
return GetResult(
ids=ids,
embeddings=embeddings, # type: ignore[typeddict-item]
documents=documents, # type: ignore[typeddict-item]
uris=uris, # type: ignore[typeddict-item]
data=None,
metadatas=metadatas, # type: ignore[typeddict-item]
included=plan.projection.included,
)
@overrides
def knn(self, plan: KNNPlan) -> QueryResult:
endpoints = self._get_grpc_endpoints(plan.scan)
knn_funcs = [self._get_stub(endpoint).KNN for endpoint in endpoints]
knn_result = self._round_robin_retry(knn_funcs, convert.to_proto_knn_plan(plan))
results = convert.from_proto_knn_batch_result(knn_result)
ids = [[record["record"]["id"] for record in records] for records in results]
embeddings = (
[
[record["record"]["embedding"] for record in records]
for records in results
]
if plan.projection.embedding
else None
)
documents = (
[
[record["record"]["document"] for record in records]
for records in results
]
if plan.projection.document
else None
)
uris = (
[
[_uri(record["record"]["metadata"]) for record in records]
for records in results
]
if plan.projection.uri
else None
)
metadatas = (
[
[_clean_metadata(record["record"]["metadata"]) for record in records]
for records in results
]
if plan.projection.metadata
else None
)
distances = (
[[record["distance"] for record in records] for records in results]
if plan.projection.rank
else None
)
# TODO: Fix typing
return QueryResult(
ids=ids,
embeddings=embeddings, # type: ignore[typeddict-item]
documents=documents, # type: ignore[typeddict-item]
uris=uris, # type: ignore[typeddict-item]
data=None,
metadatas=metadatas, # type: ignore[typeddict-item]
distances=distances, # type: ignore[typeddict-item]
included=plan.projection.included,
)
def _get_grpc_endpoints(self, scan: Scan) -> List[str]:
# Since grpc endpoint is endpoint is determined by collection uuid,
# the endpoint should be the same for all segments of the same collection
grpc_urls = self._manager.get_endpoints(
scan.record, self._query_replication_factor
)
# Shuffle the grpc urls to distribute the load evenly
random.shuffle(grpc_urls)
return grpc_urls
def _get_stub(self, grpc_url: str) -> QueryExecutorStub:
with self._mtx:
if grpc_url not in self._grpc_stub_pool:
channel = grpc.insecure_channel(
grpc_url,
options=[
("grpc.max_concurrent_streams", 1000),
("grpc.max_receive_message_length", 32000000), # 32 MB
],
)
interceptors = [OtelInterceptor()]
channel = grpc.intercept_channel(channel, *interceptors)
self._grpc_stub_pool[grpc_url] = QueryExecutorStub(channel)
return self._grpc_stub_pool[grpc_url]

View File

@@ -0,0 +1,205 @@
from typing import Optional, Sequence
from overrides import overrides
from chromadb.api.types import GetResult, Metadata, QueryResult
from chromadb.config import System
from chromadb.execution.executor.abstract import Executor
from chromadb.execution.expression.plan import CountPlan, GetPlan, KNNPlan
from chromadb.segment import MetadataReader, VectorReader
from chromadb.segment.impl.manager.local import LocalSegmentManager
from chromadb.types import Collection, VectorQuery, VectorQueryResult
def _clean_metadata(metadata: Optional[Metadata]) -> Optional[Metadata]:
"""Remove any chroma-specific metadata keys that the client shouldn't see from a metadata map."""
if not metadata:
return None
result = {}
for k, v in metadata.items():
if not k.startswith("chroma:"):
result[k] = v
if len(result) == 0:
return None
return result
def _doc(metadata: Optional[Metadata]) -> Optional[str]:
"""Retrieve the document (if any) from a Metadata map"""
if metadata and "chroma:document" in metadata:
return str(metadata["chroma:document"])
return None
def _uri(metadata: Optional[Metadata]) -> Optional[str]:
"""Retrieve the uri (if any) from a Metadata map"""
if metadata and "chroma:uri" in metadata:
return str(metadata["chroma:uri"])
return None
class LocalExecutor(Executor):
_manager: LocalSegmentManager
def __init__(self, system: System):
super().__init__(system)
self._manager = self.require(LocalSegmentManager)
@overrides
def count(self, plan: CountPlan) -> int:
return self._metadata_segment(plan.scan.collection).count(plan.scan.version)
@overrides
def get(self, plan: GetPlan) -> GetResult:
records = self._metadata_segment(plan.scan.collection).get_metadata(
request_version_context=plan.scan.version,
where=plan.filter.where,
where_document=plan.filter.where_document,
ids=plan.filter.user_ids,
limit=plan.limit.limit,
offset=plan.limit.offset,
include_metadata=True,
)
ids = [r["id"] for r in records]
embeddings = None
documents = None
uris = None
metadatas = None
included = list()
if plan.projection.embedding:
if len(records) > 0:
vectors = self._vector_segment(plan.scan.collection).get_vectors(
ids=ids, request_version_context=plan.scan.version
)
embeddings = [v["embedding"] for v in vectors]
else:
embeddings = list()
included.append("embeddings")
if plan.projection.document:
documents = [_doc(r["metadata"]) for r in records]
included.append("documents")
if plan.projection.uri:
uris = [_uri(r["metadata"]) for r in records]
included.append("uris")
if plan.projection.metadata:
metadatas = [_clean_metadata(r["metadata"]) for r in records]
included.append("metadatas")
# TODO: Fix typing
return GetResult(
ids=ids,
embeddings=embeddings,
documents=documents, # type: ignore[typeddict-item]
uris=uris, # type: ignore[typeddict-item]
data=None,
metadatas=metadatas, # type: ignore[typeddict-item]
included=included,
)
@overrides
def knn(self, plan: KNNPlan) -> QueryResult:
prefiltered_ids = None
if plan.filter.user_ids or plan.filter.where or plan.filter.where_document:
records = self._metadata_segment(plan.scan.collection).get_metadata(
request_version_context=plan.scan.version,
where=plan.filter.where,
where_document=plan.filter.where_document,
ids=plan.filter.user_ids,
limit=None,
offset=0,
include_metadata=False,
)
prefiltered_ids = [r["id"] for r in records]
knns: Sequence[Sequence[VectorQueryResult]] = [[]] * len(plan.knn.embeddings)
# Query vectors only when the user did not specify a filter or when the filter
# yields non-empty ids. Otherwise, the user specified a filter but it yields
# no matching ids, in which case we can return an empty result.
if prefiltered_ids is None or len(prefiltered_ids) > 0:
query = VectorQuery(
vectors=plan.knn.embeddings,
k=plan.knn.fetch,
allowed_ids=prefiltered_ids,
include_embeddings=plan.projection.embedding,
options=None,
request_version_context=plan.scan.version,
)
knns = self._vector_segment(plan.scan.collection).query_vectors(query)
ids = [[r["id"] for r in result] for result in knns]
embeddings = None
documents = None
uris = None
metadatas = None
distances = None
included = list()
if plan.projection.embedding:
embeddings = [[r["embedding"] for r in result] for result in knns]
included.append("embeddings")
if plan.projection.rank:
distances = [[r["distance"] for r in result] for result in knns]
included.append("distances")
if plan.projection.document or plan.projection.metadata or plan.projection.uri:
merged_ids = list(set([id for result in ids for id in result]))
hydrated_records = self._metadata_segment(
plan.scan.collection
).get_metadata(
request_version_context=plan.scan.version,
where=None,
where_document=None,
ids=merged_ids,
limit=None,
offset=0,
include_metadata=True,
)
metadata_by_id = {r["id"]: r["metadata"] for r in hydrated_records}
if plan.projection.document:
documents = [
[_doc(metadata_by_id.get(id, None)) for id in result]
for result in ids
]
included.append("documents")
if plan.projection.uri:
uris = [
[_uri(metadata_by_id.get(id, None)) for id in result]
for result in ids
]
included.append("uris")
if plan.projection.metadata:
metadatas = [
[_clean_metadata(metadata_by_id.get(id, None)) for id in result]
for result in ids
]
included.append("metadatas")
# TODO: Fix typing
return QueryResult(
ids=ids,
embeddings=embeddings, # type: ignore[typeddict-item]
documents=documents, # type: ignore[typeddict-item]
uris=uris, # type: ignore[typeddict-item]
data=None,
metadatas=metadatas, # type: ignore[typeddict-item]
distances=distances,
included=included,
)
def _metadata_segment(self, collection: Collection) -> MetadataReader:
return self._manager.get_segment(collection.id, MetadataReader)
def _vector_segment(self, collection: Collection) -> VectorReader:
return self._manager.get_segment(collection.id, VectorReader)

View File

@@ -0,0 +1,90 @@
"""
Chromadb execution expression module for search operations.
"""
from chromadb.execution.expression.operator import (
# Field proxy for building Where conditions
Key,
K,
# Where expressions
Where,
And,
Or,
Eq,
Ne,
Gt,
Gte,
Lt,
Lte,
In,
Nin,
Regex,
NotRegex,
Contains,
NotContains,
# Search configuration
Limit,
Select,
# Rank expressions
Rank,
Abs,
Div,
Exp,
Log,
Max,
Min,
Mul,
Knn,
Rrf,
Sub,
Sum,
Val,
)
from chromadb.execution.expression.plan import (
Search,
)
SearchWhere = Where
__all__ = [
# Main search class
"Search",
# Field proxy
"Key",
"K",
# Where expressions
"SearchWhere",
"Where",
"And",
"Or",
"Eq",
"Ne",
"Gt",
"Gte",
"Lt",
"Lte",
"In",
"Nin",
"Regex",
"NotRegex",
"Contains",
"NotContains",
# Search configuration
"Limit",
"Select",
# Rank expressions
"Rank",
"Abs",
"Div",
"Exp",
"Log",
"Max",
"Min",
"Mul",
"Knn",
"Rrf",
"Sub",
"Sum",
"Val",
]

View File

@@ -0,0 +1,263 @@
from dataclasses import dataclass, field
from typing import List, Dict, Any, Union, Set, Optional
from chromadb.execution.expression.operator import (
KNN,
Filter,
Limit,
Projection,
Scan,
Rank,
Select,
Val,
Where,
Key,
)
@dataclass
class CountPlan:
scan: Scan
@dataclass
class GetPlan:
scan: Scan
filter: Filter = field(default_factory=Filter)
limit: Limit = field(default_factory=Limit)
projection: Projection = field(default_factory=Projection)
@dataclass
class KNNPlan:
scan: Scan
knn: KNN
filter: Filter = field(default_factory=Filter)
projection: Projection = field(default_factory=Projection)
class Search:
"""Payload for hybrid search operations.
Can be constructed directly or using builder pattern:
Direct construction with expressions:
Search(
where=Key("status") == "active",
rank=Knn(query=[0.1, 0.2]),
limit=Limit(limit=10),
select=Select(keys={Key.DOCUMENT})
)
Direct construction with dicts:
Search(
where={"status": "active"},
rank={"$knn": {"query": [0.1, 0.2]}},
limit=10, # Creates Limit(limit=10, offset=0)
select=["#document", "#score"]
)
Builder pattern:
(Search()
.where(Key("status") == "active")
.rank(Knn(query=[0.1, 0.2]))
.limit(10)
.select(Key.DOCUMENT))
Builder pattern with dicts:
(Search()
.where({"status": "active"})
.rank({"$knn": {"query": [0.1, 0.2]}})
.limit(10)
.select(Key.DOCUMENT))
Filter by IDs:
Search().where(Key.ID.is_in(["id1", "id2", "id3"]))
Combined with metadata filtering:
Search().where((Key.ID.is_in(["id1", "id2"])) & (Key("status") == "active"))
Empty Search() is valid and will use defaults:
- where: None (no filtering)
- rank: None (no ranking - results ordered by default order)
- limit: No limit
- select: Empty selection
"""
def __init__(
self,
where: Optional[Union[Where, Dict[str, Any]]] = None,
rank: Optional[Union[Rank, Dict[str, Any]]] = None,
limit: Optional[Union[Limit, Dict[str, Any], int]] = None,
select: Optional[Union[Select, Dict[str, Any], List[str], Set[str]]] = None,
):
"""Initialize a Search with optional parameters.
Args:
where: Where expression or dict for filtering results (defaults to None - no filtering)
Dict will be converted using Where.from_dict()
rank: Rank expression or dict for scoring (defaults to None - no ranking)
Dict will be converted using Rank.from_dict()
Note: Primitive numbers are not accepted - use {"$val": number} for constant ranks
limit: Limit configuration for pagination (defaults to no limit)
Can be a Limit object, a dict for Limit.from_dict(), or an int
When passing an int, it creates Limit(limit=value, offset=0)
select: Select configuration for keys (defaults to empty selection)
Can be a Select object, a dict for Select.from_dict(),
or a list/set of strings (e.g., ["#document", "#score"])
"""
# Handle where parameter
if where is None:
self._where = None
elif isinstance(where, Where):
self._where = where
elif isinstance(where, dict):
self._where = Where.from_dict(where)
else:
raise TypeError(
f"where must be a Where object, dict, or None, got {type(where).__name__}"
)
# Handle rank parameter
if rank is None:
self._rank = None
elif isinstance(rank, Rank):
self._rank = rank
elif isinstance(rank, dict):
self._rank = Rank.from_dict(rank)
else:
raise TypeError(
f"rank must be a Rank object, dict, or None, got {type(rank).__name__}"
)
# Handle limit parameter
if limit is None:
self._limit = Limit()
elif isinstance(limit, Limit):
self._limit = limit
elif isinstance(limit, int):
self._limit = Limit.from_dict({"limit": limit, "offset": 0})
elif isinstance(limit, dict):
self._limit = Limit.from_dict(limit)
else:
raise TypeError(
f"limit must be a Limit object, dict, int, or None, got {type(limit).__name__}"
)
# Handle select parameter
if select is None:
self._select = Select()
elif isinstance(select, Select):
self._select = select
elif isinstance(select, dict):
self._select = Select.from_dict(select)
elif isinstance(select, (list, set)):
# Convert list/set of strings to Select object
self._select = Select.from_dict({"keys": list(select)})
else:
raise TypeError(
f"select must be a Select object, dict, list, set, or None, got {type(select).__name__}"
)
def to_dict(self) -> Dict[str, Any]:
"""Convert the Search to a dictionary for JSON serialization"""
return {
"filter": self._where.to_dict() if self._where is not None else None,
"rank": self._rank.to_dict() if self._rank is not None else None,
"limit": self._limit.to_dict(),
"select": self._select.to_dict(),
}
# Builder methods for chaining
def select_all(self) -> "Search":
"""Select all predefined keys (document, embedding, metadata, score)"""
new_select = Select(keys={Key.DOCUMENT, Key.EMBEDDING, Key.METADATA, Key.SCORE})
return Search(
where=self._where, rank=self._rank, limit=self._limit, select=new_select
)
def select(self, *keys: Union[Key, str]) -> "Search":
"""Select specific keys
Args:
*keys: Variable number of Key objects or string key names
Returns:
New Search object with updated select configuration
"""
new_select = Select(keys=set(keys))
return Search(
where=self._where, rank=self._rank, limit=self._limit, select=new_select
)
def where(self, where: Optional[Union[Where, Dict[str, Any]]]) -> "Search":
"""Set the where clause for filtering
Args:
where: A Where expression, dict, or None for filtering
Dicts will be converted using Where.from_dict()
Example:
search.where((Key("status") == "active") & (Key("score") > 0.5))
search.where({"status": "active"})
search.where({"$and": [{"status": "active"}, {"score": {"$gt": 0.5}}]})
"""
# Convert dict to Where if needed
if where is None:
converted_where = None
elif isinstance(where, Where):
converted_where = where
elif isinstance(where, dict):
converted_where = Where.from_dict(where)
else:
raise TypeError(
f"where must be a Where object, dict, or None, got {type(where).__name__}"
)
return Search(
where=converted_where, rank=self._rank, limit=self._limit, select=self._select
)
def rank(self, rank_expr: Optional[Union[Rank, Dict[str, Any]]]) -> "Search":
"""Set the ranking expression
Args:
rank_expr: A Rank expression, dict, or None for scoring
Dicts will be converted using Rank.from_dict()
Note: Primitive numbers are not accepted - use {"$val": number} for constant ranks
Example:
search.rank(Knn(query=[0.1, 0.2]) * 0.8 + Val(0.5) * 0.2)
search.rank({"$knn": {"query": [0.1, 0.2]}})
search.rank({"$sum": [{"$knn": {"query": [0.1, 0.2]}}, {"$val": 0.5}]})
"""
# Convert dict to Rank if needed
if rank_expr is None:
converted_rank = None
elif isinstance(rank_expr, Rank):
converted_rank = rank_expr
elif isinstance(rank_expr, dict):
converted_rank = Rank.from_dict(rank_expr)
else:
raise TypeError(
f"rank_expr must be a Rank object, dict, or None, got {type(rank_expr).__name__}"
)
return Search(
where=self._where, rank=converted_rank, limit=self._limit, select=self._select
)
def limit(self, limit: int, offset: int = 0) -> "Search":
"""Set the limit and offset for pagination
Args:
limit: Maximum number of results to return
offset: Number of results to skip (default: 0)
Example:
search.limit(20, offset=10)
"""
new_limit = Limit(offset=offset, limit=limit)
return Search(
where=self._where, rank=self._rank, limit=new_limit, select=self._select
)