chore: 添加虚拟环境到仓库
- 添加 backend_service/venv 虚拟环境 - 包含所有Python依赖包 - 注意:虚拟环境约393MB,包含12655个文件
This commit is contained in:
Binary file not shown.
@@ -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
|
||||
@@ -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]
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
)
|
||||
Reference in New Issue
Block a user