修改为东南天坐标系

This commit is contained in:
2026-01-20 09:49:52 +08:00
parent 9538757047
commit 333fad40ac
7201 changed files with 1030888 additions and 85410 deletions

View File

@@ -0,0 +1,282 @@
from time import sleep
import threading
from chromadb.api import ClientAPI
from chromadb.api.types import IndexingStatus
from chromadb.test.conftest import skip_if_not_cluster
from chromadb.test.utils.wait_for_version_increase import (
get_collection_version,
wait_for_version_increase,
)
@skip_if_not_cluster()
def test_indexing_status_empty_collection(client: ClientAPI) -> None:
"""Test indexing status on empty collection"""
client.reset()
collection = client.create_collection(name="test_collection")
status = collection.get_indexing_status()
assert isinstance(status, IndexingStatus)
assert status.num_indexed_ops == 0
assert status.num_unindexed_ops == 0
assert status.total_ops == 0
assert status.op_indexing_progress == 1.0
@skip_if_not_cluster()
def test_indexing_status_after_add(client: ClientAPI) -> None:
"""Test indexing status after adding embeddings"""
client.reset()
collection = client.create_collection(name="test_collection")
ids = [f"id_{i}" for i in range(300)]
embeddings = [[float(i), float(i + 1), float(i + 2)] for i in range(300)]
initial_version = get_collection_version(client, collection.name)
collection.add(ids=ids, embeddings=embeddings) # type: ignore
status = collection.get_indexing_status()
assert status.total_ops == 300
if initial_version == get_collection_version(client, collection.name):
assert isinstance(status, IndexingStatus)
assert status.num_unindexed_ops == 300
assert status.num_indexed_ops == 0
assert status.op_indexing_progress == 0.0
wait_for_version_increase(client, collection.name, initial_version)
# Give some time to invalidate the frontend query cache
sleep(60)
# Check status after indexing completes
final_status = collection.get_indexing_status()
assert isinstance(final_status, IndexingStatus)
assert final_status.num_indexed_ops == 300
assert final_status.num_unindexed_ops == 0
assert final_status.op_indexing_progress == 1.0
@skip_if_not_cluster()
def test_indexing_status_after_upsert(client: ClientAPI) -> None:
"""Test indexing status after upsert operations"""
client.reset()
collection = client.create_collection(name="test_collection")
initial_version = get_collection_version(client, collection.name)
collection.upsert(ids=["id1", "id2"], embeddings=[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) # type: ignore
status = collection.get_indexing_status()
assert status.total_ops == 2
if initial_version == get_collection_version(client, collection.name):
assert isinstance(status, IndexingStatus)
assert status.num_unindexed_ops == 2
assert status.num_indexed_ops == 0
assert status.op_indexing_progress == 0.0
wait_for_version_increase(client, collection.name, initial_version)
sleep(60)
collection.upsert(ids=["id1", "id3"], embeddings=[[1.1, 2.1, 3.1], [7.0, 8.0, 9.0]]) # type: ignore
status = collection.get_indexing_status()
assert status.total_ops == 4
@skip_if_not_cluster()
def test_indexing_status_after_delete(client: ClientAPI) -> None:
"""Test indexing status after delete operations"""
client.reset()
collection = client.create_collection(name="test_collection")
initial_version = get_collection_version(client, collection.name)
collection.add(
ids=["id1", "id2", "id3"],
embeddings=[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]], # type: ignore
)
if initial_version == get_collection_version(client, collection.name):
status = collection.get_indexing_status()
assert isinstance(status, IndexingStatus)
assert status.num_unindexed_ops == 3
assert status.num_indexed_ops == 0
assert status.op_indexing_progress == 0.0
wait_for_version_increase(client, collection.name, initial_version)
sleep(60)
initial_status = collection.get_indexing_status()
assert initial_status.total_ops == 3
collection.delete(ids=["id1", "id2"])
# Delete adds operations to the log, so total_ops increases
status_after_delete = collection.get_indexing_status()
assert status_after_delete.total_ops == 5
@skip_if_not_cluster()
def test_indexing_status_field_types(client: ClientAPI) -> None:
"""Test that indexing status returns correct field types"""
client.reset()
collection = client.create_collection(name="field_types_collection")
initial_version = get_collection_version(client, collection.name)
collection.add(ids=["type_test_id"], embeddings=[[1.0, 2.0, 3.0]]) # type: ignore
status = collection.get_indexing_status()
if initial_version == get_collection_version(client, collection.name):
assert isinstance(status, IndexingStatus)
assert status.num_unindexed_ops == 1
assert status.num_indexed_ops == 0
assert status.op_indexing_progress == 0.0
wait_for_version_increase(client, collection.name, initial_version)
sleep(60)
final_status = collection.get_indexing_status()
assert isinstance(final_status.num_indexed_ops, int)
assert isinstance(final_status.num_unindexed_ops, int)
assert isinstance(final_status.total_ops, int)
assert isinstance(final_status.op_indexing_progress, float)
assert final_status.num_indexed_ops >= 0
assert final_status.num_unindexed_ops >= 0
assert final_status.total_ops >= 0
assert 0.0 <= final_status.op_indexing_progress <= 1.0
@skip_if_not_cluster()
def test_indexing_status_batch_progression(client: ClientAPI) -> None:
"""Test indexing status with 2000 records based on index version progression"""
client.reset()
collection = client.create_collection(name="batch_test_collection")
get_collection_version(client, collection.name)
# Insert 2000 records in two batches of 1000 (max batch size)
ids_1 = [f"batch_id_{i}" for i in range(1000)]
embeddings_1 = [[float(i), float(i + 1), float(i + 2)] for i in range(1000)]
collection.add(ids=ids_1, embeddings=embeddings_1) # type: ignore
ids_2 = [f"batch_id_{i}" for i in range(1000, 2000)]
embeddings_2 = [[float(i), float(i + 1), float(i + 2)] for i in range(1000, 2000)]
collection.add(ids=ids_2, embeddings=embeddings_2) # type: ignore
current_version = get_collection_version(client, collection.name)
allowed_statuses = [
IndexingStatus(
num_indexed_ops=0,
num_unindexed_ops=2000,
total_ops=2000,
op_indexing_progress=0.0,
),
IndexingStatus(
num_indexed_ops=1000,
num_unindexed_ops=1000,
total_ops=2000,
op_indexing_progress=0.5,
),
IndexingStatus(
num_indexed_ops=2000,
num_unindexed_ops=0,
total_ops=2000,
op_indexing_progress=1.0,
),
]
ops_indexed = 0
while ops_indexed < 2000:
status = collection.get_indexing_status()
assert status in allowed_statuses
print("witnessed status: ", status)
ops_indexed = status.num_indexed_ops
wait_for_version_increase(client, collection.name, current_version)
sleep(60)
@skip_if_not_cluster()
def test_indexing_status_not_found(client: ClientAPI) -> None:
"""Test indexing status on non-existent collection"""
client.reset()
collection = client.create_collection(name="temp_collection")
client.delete_collection("temp_collection")
try:
collection.get_indexing_status()
assert False, "Expected exception for non-existent collection"
except Exception as e:
assert (
"not found" in str(e).lower()
or "does not exist" in str(e).lower()
or "soft deleted" in str(e).lower()
or "collection not found" in str(e).lower()
)
@skip_if_not_cluster()
def test_indexing_status_concurrent_progress_variation(client: ClientAPI) -> None:
"""Test that progress values vary during concurrent operations"""
client.reset()
collection = client.create_collection(name="concurrent_test_collection")
progress_values = []
stop_monitoring = threading.Event()
def progress_monitor() -> None:
"""Thread that continuously monitors indexing progress"""
while not stop_monitoring.is_set():
try:
status = collection.get_indexing_status()
progress_values.append(status.op_indexing_progress)
sleep(0.1) # Poll every 100ms
except Exception:
break
def record_adder() -> None:
"""Thread that adds records one at a time"""
for i in range(50): # Add 50 records one by one
collection.add(
ids=[f"concurrent_id_{i}"],
embeddings=[[float(i), float(i + 1), float(i + 2)]], # type: ignore
)
sleep(0.05) # Small delay between additions
# Start both threads
monitor_thread = threading.Thread(target=progress_monitor)
adder_thread = threading.Thread(target=record_adder)
monitor_thread.start()
adder_thread.start()
# Wait for record addition to complete
adder_thread.join()
# Give a moment for final progress updates
sleep(1.0)
# Stop monitoring and wait for thread to finish
stop_monitoring.set()
monitor_thread.join()
# Assert that we collected progress values
assert len(progress_values) > 0, "No progress values were collected"
# Assert that not all progress values are 0
non_zero_progress = [p for p in progress_values if p > 0.0]
assert len(non_zero_progress) > 0, "All progress values remained at 0"
# Assert that there's variation in progress values
unique_progress = set(progress_values)
assert (
len(unique_progress) > 1
), f"Expected variation in progress values, but only got: {unique_progress}"
print(f"Collected {len(progress_values)} progress values")
print(f"Unique progress values: {sorted(unique_progress)}")
print(f"Progress range: {min(progress_values):.3f} - {max(progress_values):.3f}")

View File

@@ -0,0 +1,102 @@
"""Tests for the Search API endpoint."""
from typing import Tuple
from uuid import uuid4
import pytest
from chromadb.api import ClientAPI
from chromadb.api.models.Collection import Collection
from chromadb.api.types import Embeddings, ReadLevel
from chromadb.execution.expression import Knn, Search
from chromadb.test.conftest import (
ClientFactories,
is_spann_disabled_mode,
skip_reason_spann_disabled,
)
def _create_test_collection(
client_factories: ClientFactories,
) -> Tuple[Collection, ClientAPI]:
"""Create a test collection with some data."""
client = client_factories.create_client_from_system()
client.reset()
collection_name = f"search_api_test_{uuid4().hex}"
collection = client.get_or_create_collection(name=collection_name)
return collection, client
@pytest.mark.skipif(is_spann_disabled_mode, reason=skip_reason_spann_disabled)
def test_search_with_read_level_index_and_wal(
client_factories: ClientFactories,
) -> None:
"""Test search with ReadLevel.INDEX_AND_WAL (default) returns results."""
collection, _ = _create_test_collection(client_factories)
# Add some data
collection.add(
ids=["doc1", "doc2", "doc3"],
documents=["apple fruit", "banana fruit", "car vehicle"],
embeddings=[[0.1, 0.2, 0.3, 0.4], [0.2, 0.3, 0.4, 0.5], [0.9, 0.8, 0.7, 0.6]],
)
# Search with explicit INDEX_AND_WAL (default behavior)
search = Search().rank(Knn(query=[0.1, 0.2, 0.3, 0.4], limit=10))
results = collection.search(search, read_level=ReadLevel.INDEX_AND_WAL)
assert results["ids"] is not None
assert len(results["ids"]) == 1
assert len(results["ids"][0]) > 0
@pytest.mark.skipif(is_spann_disabled_mode, reason=skip_reason_spann_disabled)
def test_search_with_read_level_index_only(
client_factories: ClientFactories,
) -> None:
"""Test search with ReadLevel.INDEX_ONLY returns results."""
collection, _ = _create_test_collection(client_factories)
# Add some data
collection.add(
ids=["doc1", "doc2", "doc3"],
documents=["apple fruit", "banana fruit", "car vehicle"],
embeddings=[[0.1, 0.2, 0.3, 0.4], [0.2, 0.3, 0.4, 0.5], [0.9, 0.8, 0.7, 0.6]],
)
# Search with INDEX_ONLY - this skips the WAL
# Note: Results may or may not include recent writes depending on compaction state
search = Search().rank(Knn(query=[0.1, 0.2, 0.3, 0.4], limit=10))
results = collection.search(search, read_level=ReadLevel.INDEX_ONLY)
# Just verify the API works and returns a valid response structure
assert results["ids"] is not None
assert len(results["ids"]) == 1
# Results may be empty if data hasn't been compacted yet, which is expected behavior
@pytest.mark.skipif(is_spann_disabled_mode, reason=skip_reason_spann_disabled)
def test_search_default_read_level(
client_factories: ClientFactories,
) -> None:
"""Test search without explicit read_level uses default (INDEX_AND_WAL)."""
collection, _ = _create_test_collection(client_factories)
# Add some data
collection.add(
ids=["doc1", "doc2"],
documents=["hello world", "goodbye world"],
embeddings=[[0.1, 0.2, 0.3, 0.4], [0.5, 0.6, 0.7, 0.8]],
)
# Search without specifying read_level (should use default)
search = Search().rank(Knn(query=[0.1, 0.2, 0.3, 0.4], limit=10))
results = collection.search(search)
# Should return results since default is INDEX_AND_WAL (full consistency)
assert results["ids"] is not None
assert len(results["ids"]) == 1
assert len(results["ids"][0]) > 0

View File

@@ -0,0 +1,180 @@
import pytest
from unittest.mock import MagicMock
from chromadb.api.shared_system_client import SharedSystemClient
from chromadb.api.base_http_client import BaseHTTPClient
from chromadb.config import System
from typing import Optional, Dict, Generator
@pytest.fixture(autouse=True)
def clear_cache() -> Generator[None, None, None]:
"""Automatically clear the system cache before and after each test."""
SharedSystemClient.clear_system_cache()
yield
SharedSystemClient.clear_system_cache()
def create_mock_http_client(
api_url: Optional[str] = None,
headers: Optional[Dict[str, str]] = None,
) -> MagicMock:
"""Create a mock BaseHTTPClient instance with the specified configuration."""
mock_server_api = MagicMock(spec=BaseHTTPClient)
mock_server_api.get_api_url.return_value = api_url or ""
mock_server_api.get_request_headers.return_value = headers or {}
return mock_server_api
def register_mock_system(system_id: str, mock_server_api: MagicMock) -> MagicMock:
"""Register a mock system with the given ID and server API."""
mock_system = MagicMock(spec=System)
mock_system.instance.return_value = mock_server_api
SharedSystemClient._identifier_to_system[system_id] = mock_system
return mock_system
def test_extracts_api_key_from_chroma_cloud_client() -> None:
mock_server_api = create_mock_http_client(
api_url="https://api.trychroma.com/api/v2",
headers={"X-Chroma-Token": "test-api-key-123"},
)
register_mock_system("test-id", mock_server_api)
api_key = SharedSystemClient.get_chroma_cloud_api_key_from_clients()
assert api_key == "test-api-key-123"
def test_extracts_api_key_with_lowercase_header() -> None:
mock_server_api = create_mock_http_client(
api_url="https://api.trychroma.com/api/v2",
headers={"x-chroma-token": "test-api-key-456"},
)
register_mock_system("test-id", mock_server_api)
api_key = SharedSystemClient.get_chroma_cloud_api_key_from_clients()
assert api_key == "test-api-key-456"
def test_extracts_api_key_from_gcp_chroma_cloud_client() -> None:
mock_server_api = create_mock_http_client(
api_url="https://dummy.gcp.trychroma.com/api/v2",
headers={"X-Chroma-Token": "gcp-test-api-key"},
)
register_mock_system("test-id", mock_server_api)
api_key = SharedSystemClient.get_chroma_cloud_api_key_from_clients()
assert api_key == "gcp-test-api-key"
def test_skips_non_chroma_cloud_clients() -> None:
mock_server_api = create_mock_http_client(
api_url="https://localhost:8000/api/v2",
headers={"X-Chroma-Token": "local-api-key"},
)
register_mock_system("test-id", mock_server_api)
api_key = SharedSystemClient.get_chroma_cloud_api_key_from_clients()
assert api_key is None
def test_skips_clients_without_api_url() -> None:
mock_server_api = create_mock_http_client(
api_url=None,
headers={"X-Chroma-Token": "test-api-key"},
)
register_mock_system("test-id", mock_server_api)
api_key = SharedSystemClient.get_chroma_cloud_api_key_from_clients()
assert api_key is None
def test_returns_none_when_no_api_key_in_headers() -> None:
mock_server_api = create_mock_http_client(
api_url="https://api.trychroma.com/api/v2",
headers={},
)
register_mock_system("test-id", mock_server_api)
api_key = SharedSystemClient.get_chroma_cloud_api_key_from_clients()
assert api_key is None
def test_returns_first_api_key_found_from_multiple_clients() -> None:
mock_server_api_1 = create_mock_http_client(
api_url="https://api.trychroma.com/api/v2",
headers={"X-Chroma-Token": "first-key"},
)
mock_server_api_2 = create_mock_http_client(
api_url="https://api.trychroma.com/api/v2",
headers={"X-Chroma-Token": "second-key"},
)
register_mock_system("test-id-1", mock_server_api_1)
register_mock_system("test-id-2", mock_server_api_2)
api_key = SharedSystemClient.get_chroma_cloud_api_key_from_clients()
assert api_key == "first-key"
def test_handles_exception_gracefully() -> None:
mock_system = MagicMock(spec=System)
mock_system.instance.side_effect = Exception("Test exception")
SharedSystemClient._identifier_to_system["test-id"] = mock_system
api_key = SharedSystemClient.get_chroma_cloud_api_key_from_clients()
assert api_key is None
def test_returns_none_when_no_clients_exist() -> None:
api_key = SharedSystemClient.get_chroma_cloud_api_key_from_clients()
assert api_key is None
def test_skips_non_http_clients() -> None:
"""Test that non-BaseHTTPClient instances are skipped."""
mock_server_api = MagicMock() # Not a BaseHTTPClient
register_mock_system("test-id", mock_server_api)
api_key = SharedSystemClient.get_chroma_cloud_api_key_from_clients()
assert api_key is None
def test_extracts_api_key_with_mixed_case_header() -> None:
mock_server_api = create_mock_http_client(
api_url="https://api.trychroma.com/api/v2",
headers={"X-CHROMA-TOKEN": "mixed-case-key"},
)
register_mock_system("test-id", mock_server_api)
api_key = SharedSystemClient.get_chroma_cloud_api_key_from_clients()
assert api_key == "mixed-case-key"
def test_multiple_clients_returns_one_key() -> None:
"""Test that multiple clients return one of the available keys."""
mock_api_1 = create_mock_http_client(
api_url="https://api.trychroma.com/api/v2",
headers={"X-Chroma-Token": "key-1"},
)
mock_api_2 = create_mock_http_client(
api_url="https://api.trychroma.com/api/v2",
headers={"X-Chroma-Token": "key-2"},
)
register_mock_system("id-1", mock_api_1)
register_mock_system("id-2", mock_api_2)
api_key = SharedSystemClient.get_chroma_cloud_api_key_from_clients()
assert api_key in ["key-1", "key-2"]