chore: 添加虚拟环境到仓库
- 添加 backend_service/venv 虚拟环境 - 包含所有Python依赖包 - 注意:虚拟环境约393MB,包含12655个文件
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from chromadb.utils.embedding_functions.chroma_bm25_embedding_function import (
|
||||
DEFAULT_CHROMA_BM25_STOPWORDS,
|
||||
ChromaBm25EmbeddingFunction,
|
||||
)
|
||||
|
||||
|
||||
def _is_sorted(values: list[int]) -> bool:
|
||||
return all(values[i] >= values[i - 1] for i in range(1, len(values)))
|
||||
|
||||
|
||||
def test_comprehensive_tokenization_matches_reference() -> None:
|
||||
embedder = ChromaBm25EmbeddingFunction()
|
||||
embedding = embedder(
|
||||
[
|
||||
"Usain Bolt's top speed reached ~27.8 mph (44.72 km/h)",
|
||||
]
|
||||
)[0]
|
||||
|
||||
expected_indices = [
|
||||
230246813,
|
||||
395514983,
|
||||
458027949,
|
||||
488165615,
|
||||
729632045,
|
||||
734978415,
|
||||
997512866,
|
||||
1114505193,
|
||||
1381820790,
|
||||
1501587190,
|
||||
1649421877,
|
||||
1837285388,
|
||||
]
|
||||
expected_value = 1.6391153
|
||||
|
||||
assert embedding.indices == expected_indices
|
||||
for value in embedding.values:
|
||||
assert value == pytest.approx(expected_value, abs=1e-5)
|
||||
|
||||
|
||||
def test_matches_rust_reference_values() -> None:
|
||||
embedder = ChromaBm25EmbeddingFunction()
|
||||
embedding = embedder(
|
||||
[
|
||||
"The space-time continuum WARPS near massive objects...",
|
||||
]
|
||||
)[0]
|
||||
|
||||
expected_indices = [
|
||||
90097469,
|
||||
519064992,
|
||||
737893654,
|
||||
1110755108,
|
||||
1950894484,
|
||||
2031641008,
|
||||
2058513491,
|
||||
]
|
||||
expected_value = 1.660867
|
||||
|
||||
assert embedding.indices == expected_indices
|
||||
for value in embedding.values:
|
||||
assert value == pytest.approx(expected_value, abs=1e-5)
|
||||
|
||||
|
||||
def test_generates_embeddings_for_multiple_documents() -> None:
|
||||
embedder = ChromaBm25EmbeddingFunction()
|
||||
texts = [
|
||||
"Usain Bolt's top speed reached ~27.8 mph (44.72 km/h)",
|
||||
"The space-time continuum WARPS near massive objects...",
|
||||
"BM25 is great for sparse retrieval tasks",
|
||||
]
|
||||
|
||||
embeddings = embedder(texts)
|
||||
|
||||
assert len(embeddings) == len(texts)
|
||||
for embedding in embeddings:
|
||||
assert embedding.indices
|
||||
assert len(embedding.indices) == len(embedding.values)
|
||||
assert _is_sorted(embedding.indices)
|
||||
for value in embedding.values:
|
||||
assert value > 0
|
||||
assert math.isfinite(value)
|
||||
|
||||
|
||||
def test_embed_query_matches_call() -> None:
|
||||
embedder = ChromaBm25EmbeddingFunction()
|
||||
query = "retrieve BM25 docs"
|
||||
|
||||
query_embedding = embedder.embed_query([query])[0]
|
||||
doc_embedding = embedder([query])[0]
|
||||
|
||||
assert query_embedding.indices == doc_embedding.indices
|
||||
assert query_embedding.values == doc_embedding.values
|
||||
|
||||
|
||||
def test_config_round_trip() -> None:
|
||||
embedder = ChromaBm25EmbeddingFunction()
|
||||
config = embedder.get_config()
|
||||
|
||||
assert config["k"] == pytest.approx(1.2, abs=1e-9)
|
||||
assert config["b"] == pytest.approx(0.75, abs=1e-9)
|
||||
assert config["avg_doc_length"] == pytest.approx(256.0, abs=1e-9)
|
||||
assert config["token_max_length"] == 40
|
||||
assert "stopwords" not in config
|
||||
|
||||
custom_stopwords = DEFAULT_CHROMA_BM25_STOPWORDS[:10]
|
||||
rebuilt = ChromaBm25EmbeddingFunction.build_from_config(
|
||||
{
|
||||
**config,
|
||||
"stopwords": custom_stopwords,
|
||||
}
|
||||
)
|
||||
|
||||
rebuilt_config = rebuilt.get_config()
|
||||
assert rebuilt_config["stopwords"] == custom_stopwords
|
||||
assert rebuilt_config["token_max_length"] == config["token_max_length"]
|
||||
assert rebuilt_config["k"] == pytest.approx(config["k"], abs=1e-9)
|
||||
assert rebuilt_config["b"] == pytest.approx(config["b"], abs=1e-9)
|
||||
assert rebuilt_config["avg_doc_length"] == pytest.approx(
|
||||
config["avg_doc_length"], abs=1e-9
|
||||
)
|
||||
|
||||
|
||||
def test_validate_config_update_rejects_unknown_keys() -> None:
|
||||
embedder = ChromaBm25EmbeddingFunction()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
embedder.validate_config_update(embedder.get_config(), {"unknown": 123})
|
||||
|
||||
|
||||
def test_validate_config_update_allows_known_keys() -> None:
|
||||
embedder = ChromaBm25EmbeddingFunction()
|
||||
|
||||
embedder.validate_config_update(
|
||||
embedder.get_config(), {"k": 1.1, "stopwords": ["custom"]}
|
||||
)
|
||||
@@ -0,0 +1,95 @@
|
||||
from chromadb.api.types import EmbeddingFunction, Embeddable, Embeddings
|
||||
import numpy as np
|
||||
from typing import cast, Any
|
||||
from chromadb.utils.embedding_functions import (
|
||||
register_embedding_function,
|
||||
known_embedding_functions,
|
||||
)
|
||||
|
||||
|
||||
class LegacyCustomEmbeddingFunction(EmbeddingFunction[Embeddable]):
|
||||
def __call__(self, input: Embeddable) -> Embeddings:
|
||||
return cast(Embeddings, np.array([1, 2, 3]).tolist())
|
||||
|
||||
|
||||
class CustomEmbeddingFunction(EmbeddingFunction[Embeddable]):
|
||||
def __call__(self, input: Embeddable) -> Embeddings:
|
||||
return cast(Embeddings, np.array([1, 2, 3]).tolist())
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "custom_embedding_function"
|
||||
|
||||
@staticmethod
|
||||
def build_from_config(config: dict[str, Any]) -> "CustomEmbeddingFunction":
|
||||
return CustomEmbeddingFunction()
|
||||
|
||||
def get_config(self) -> dict[str, Any]:
|
||||
return {}
|
||||
|
||||
|
||||
@register_embedding_function
|
||||
class CustomEmbeddingFunctionWithRegistration(EmbeddingFunction[Embeddable]):
|
||||
def __call__(self, input: Embeddable) -> Embeddings:
|
||||
return cast(Embeddings, np.array([1, 2, 3]).tolist())
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "custom_embedding_function_with_registration"
|
||||
|
||||
@staticmethod
|
||||
def build_from_config(
|
||||
config: dict[str, Any]
|
||||
) -> "CustomEmbeddingFunctionWithRegistration":
|
||||
return CustomEmbeddingFunctionWithRegistration()
|
||||
|
||||
def get_config(self) -> dict[str, Any]:
|
||||
return {}
|
||||
|
||||
|
||||
def test_legacy_custom_ef() -> None:
|
||||
ef = LegacyCustomEmbeddingFunction()
|
||||
result = ef(["test"])
|
||||
|
||||
# Check the structure: we expect a list with one NumPy array
|
||||
assert isinstance(result, list), "Result should be a list"
|
||||
assert len(result) == 1, "Result should contain exactly one element"
|
||||
assert isinstance(result[0], np.ndarray), "Result element should be a NumPy array"
|
||||
|
||||
# Compare the contents of the array
|
||||
expected = np.array([1, 2, 3], dtype=np.float32)
|
||||
assert np.array_equal(
|
||||
result[0], expected
|
||||
), f"Arrays not equal: {result[0]} vs {expected}"
|
||||
|
||||
|
||||
def test_custom_ef() -> None:
|
||||
ef = CustomEmbeddingFunction()
|
||||
result = ef(["test"])
|
||||
|
||||
# Same checks as above
|
||||
assert isinstance(result, list), "Result should be a list"
|
||||
assert len(result) == 1, "Result should contain exactly one element"
|
||||
assert isinstance(result[0], np.ndarray), "Result element should be a NumPy array"
|
||||
|
||||
expected = np.array([1, 2, 3], dtype=np.float32)
|
||||
assert np.array_equal(
|
||||
result[0], expected
|
||||
), f"Arrays not equal: {result[0]} vs {expected}"
|
||||
|
||||
|
||||
def test_custom_ef_registration() -> None:
|
||||
# check all 4 embedding functions for registration.
|
||||
# LegacyCustomEmbeddingFunction should not be in known_embedding_functions
|
||||
# CustomEmbeddingFunction should not be in known_embedding_functions
|
||||
# CustomEmbeddingFunctionWithRegistration should be in known_embedding_functions
|
||||
|
||||
assert "legacy_custom_embedding_function" not in known_embedding_functions
|
||||
assert "custom_embedding_function" not in known_embedding_functions
|
||||
assert "custom_embedding_function_with_registration" in known_embedding_functions
|
||||
@@ -0,0 +1,90 @@
|
||||
import shutil
|
||||
import os
|
||||
from typing import List, Hashable
|
||||
|
||||
import hypothesis.strategies as st
|
||||
import onnxruntime
|
||||
import pytest
|
||||
from hypothesis import given, settings
|
||||
|
||||
from chromadb.utils.embedding_functions.onnx_mini_lm_l6_v2 import (
|
||||
ONNXMiniLM_L6_V2,
|
||||
)
|
||||
|
||||
from chromadb.utils.embedding_functions.onnx_mini_lm_l6_v2 import _verify_sha256
|
||||
|
||||
|
||||
def unique_by(x: Hashable) -> Hashable:
|
||||
return x
|
||||
|
||||
|
||||
@settings(deadline=None)
|
||||
@given(
|
||||
providers=st.lists(
|
||||
st.sampled_from(onnxruntime.get_all_providers()).filter(
|
||||
lambda x: x not in onnxruntime.get_available_providers()
|
||||
),
|
||||
unique_by=unique_by,
|
||||
min_size=1,
|
||||
)
|
||||
)
|
||||
def test_unavailable_provider_multiple(providers: List[str]) -> None:
|
||||
with pytest.raises(ValueError) as e:
|
||||
ef = ONNXMiniLM_L6_V2(preferred_providers=providers)
|
||||
ef(["test"])
|
||||
assert "Preferred providers must be subset of available providers" in str(e.value)
|
||||
|
||||
|
||||
@given(
|
||||
providers=st.lists(
|
||||
st.sampled_from(onnxruntime.get_available_providers()),
|
||||
min_size=1,
|
||||
unique_by=unique_by,
|
||||
)
|
||||
)
|
||||
def test_available_provider(providers: List[str]) -> None:
|
||||
ef = ONNXMiniLM_L6_V2(preferred_providers=providers)
|
||||
ef(["test"])
|
||||
|
||||
|
||||
def test_warning_no_providers_supplied() -> None:
|
||||
ef = ONNXMiniLM_L6_V2()
|
||||
ef(["test"])
|
||||
|
||||
|
||||
@given(
|
||||
providers=st.lists(
|
||||
st.sampled_from(onnxruntime.get_available_providers()),
|
||||
min_size=1,
|
||||
).filter(lambda x: len(x) > len(set(x)))
|
||||
)
|
||||
def test_provider_repeating(providers: List[str]) -> None:
|
||||
with pytest.raises(ValueError) as e:
|
||||
ef = ONNXMiniLM_L6_V2(preferred_providers=providers)
|
||||
ef(["test"])
|
||||
assert "Preferred providers must be unique" in str(e.value)
|
||||
|
||||
|
||||
def test_invalid_sha256() -> None:
|
||||
ef = ONNXMiniLM_L6_V2()
|
||||
shutil.rmtree(ef.DOWNLOAD_PATH) # clean up any existing models
|
||||
with pytest.raises(ValueError) as e:
|
||||
ef._MODEL_SHA256 = "invalid"
|
||||
ef(["test"])
|
||||
assert "does not match expected SHA256 hash" in str(e.value)
|
||||
|
||||
|
||||
def test_partial_download() -> None:
|
||||
ef = ONNXMiniLM_L6_V2()
|
||||
shutil.rmtree(ef.DOWNLOAD_PATH, ignore_errors=True) # clean up any existing models
|
||||
os.makedirs(ef.DOWNLOAD_PATH, exist_ok=True)
|
||||
path = os.path.join(ef.DOWNLOAD_PATH, ef.ARCHIVE_FILENAME)
|
||||
with open(path, "wb") as f: # create invalid file to simulate partial download
|
||||
f.write(b"invalid")
|
||||
ef._download_model_if_not_exists() # re-download model
|
||||
assert os.path.exists(path)
|
||||
assert _verify_sha256(
|
||||
str(os.path.join(ef.DOWNLOAD_PATH, ef.ARCHIVE_FILENAME)),
|
||||
ef._MODEL_SHA256,
|
||||
)
|
||||
assert len(ef(["test"])) == 1
|
||||
@@ -0,0 +1,118 @@
|
||||
from chromadb.utils import embedding_functions
|
||||
from chromadb.utils.embedding_functions import (
|
||||
EmbeddingFunction,
|
||||
register_embedding_function,
|
||||
)
|
||||
from typing import Dict, Any
|
||||
import pytest
|
||||
from chromadb.api.types import (
|
||||
Embeddings,
|
||||
Space,
|
||||
Embeddable,
|
||||
SparseEmbeddingFunction,
|
||||
)
|
||||
from chromadb.api.models.CollectionCommon import validation_context
|
||||
|
||||
|
||||
def test_get_builtins_holds() -> None:
|
||||
"""
|
||||
Ensure that `get_builtins` is consistent after the ef migration.
|
||||
|
||||
This test is intended to be temporary until the ef migration is complete as
|
||||
these expected builtins are likely to grow as long as users add new
|
||||
embedding functions.
|
||||
|
||||
REMOVE ME ON THE NEXT EF ADDITION
|
||||
"""
|
||||
expected_builtins = {
|
||||
"AmazonBedrockEmbeddingFunction",
|
||||
"BasetenEmbeddingFunction",
|
||||
"CloudflareWorkersAIEmbeddingFunction",
|
||||
"CohereEmbeddingFunction",
|
||||
"VoyageAIEmbeddingFunction",
|
||||
"GoogleGenerativeAiEmbeddingFunction",
|
||||
"GooglePalmEmbeddingFunction",
|
||||
"GoogleVertexEmbeddingFunction",
|
||||
"HuggingFaceEmbeddingFunction",
|
||||
"HuggingFaceEmbeddingServer",
|
||||
"InstructorEmbeddingFunction",
|
||||
"JinaEmbeddingFunction",
|
||||
"MistralEmbeddingFunction",
|
||||
"MorphEmbeddingFunction",
|
||||
"ONNXMiniLM_L6_V2",
|
||||
"OllamaEmbeddingFunction",
|
||||
"OpenAIEmbeddingFunction",
|
||||
"OpenCLIPEmbeddingFunction",
|
||||
"RoboflowEmbeddingFunction",
|
||||
"SentenceTransformerEmbeddingFunction",
|
||||
"Text2VecEmbeddingFunction",
|
||||
"ChromaLangchainEmbeddingFunction",
|
||||
"TogetherAIEmbeddingFunction",
|
||||
"DefaultEmbeddingFunction",
|
||||
"HuggingFaceSparseEmbeddingFunction",
|
||||
"FastembedSparseEmbeddingFunction",
|
||||
"Bm25EmbeddingFunction",
|
||||
"ChromaCloudQwenEmbeddingFunction",
|
||||
"ChromaCloudSpladeEmbeddingFunction",
|
||||
"ChromaBm25EmbeddingFunction",
|
||||
}
|
||||
|
||||
assert expected_builtins == embedding_functions.get_builtins()
|
||||
|
||||
|
||||
def test_default_ef_exists() -> None:
|
||||
assert hasattr(embedding_functions, "DefaultEmbeddingFunction")
|
||||
default_ef = embedding_functions.DefaultEmbeddingFunction()
|
||||
|
||||
assert default_ef is not None
|
||||
assert isinstance(default_ef, EmbeddingFunction) or isinstance(
|
||||
default_ef, SparseEmbeddingFunction
|
||||
)
|
||||
|
||||
|
||||
def test_ef_imports() -> None:
|
||||
for ef in embedding_functions.get_builtins():
|
||||
# Langchain embedding function is a special snowflake
|
||||
if ef == "ChromaLangchainEmbeddingFunction":
|
||||
continue
|
||||
assert hasattr(embedding_functions, ef)
|
||||
assert isinstance(getattr(embedding_functions, ef), type)
|
||||
assert issubclass(
|
||||
getattr(embedding_functions, ef), EmbeddingFunction
|
||||
) or issubclass(getattr(embedding_functions, ef), SparseEmbeddingFunction)
|
||||
|
||||
|
||||
@register_embedding_function
|
||||
class CustomEmbeddingFunction(EmbeddingFunction[Embeddable]):
|
||||
def __init__(self, dim: int = 3):
|
||||
self._dim = dim
|
||||
|
||||
@validation_context("custom_ef_call")
|
||||
def __call__(self, input: Embeddable) -> Embeddings:
|
||||
raise Exception("This is a test exception")
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "custom_ef"
|
||||
|
||||
def get_config(self) -> Dict[str, Any]:
|
||||
return {"dim": self._dim}
|
||||
|
||||
@staticmethod
|
||||
def build_from_config(config: Dict[str, Any]) -> "CustomEmbeddingFunction":
|
||||
return CustomEmbeddingFunction(dim=config["dim"])
|
||||
|
||||
def default_space(self) -> Space:
|
||||
return "cosine"
|
||||
|
||||
|
||||
def test_validation_context_with_custom_ef() -> None:
|
||||
custom_ef = CustomEmbeddingFunction()
|
||||
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
custom_ef(["test data"])
|
||||
|
||||
original_msg = "This is a test exception"
|
||||
expected_msg = f"{original_msg} in custom_ef_call."
|
||||
assert str(excinfo.value) == expected_msg
|
||||
assert excinfo.value.args == (expected_msg,)
|
||||
@@ -0,0 +1,135 @@
|
||||
import os
|
||||
import pytest
|
||||
import numpy as np
|
||||
from chromadb.utils.embedding_functions.morph_embedding_function import (
|
||||
MorphEmbeddingFunction,
|
||||
)
|
||||
|
||||
|
||||
def test_morph_embedding_function_with_api_key() -> None:
|
||||
"""Test Morph embedding function when API key is available."""
|
||||
if os.environ.get("MORPH_API_KEY") is None:
|
||||
pytest.skip("MORPH_API_KEY not set")
|
||||
|
||||
ef = MorphEmbeddingFunction(
|
||||
model_name="morph-embedding-v2"
|
||||
)
|
||||
|
||||
# Test with code snippets (Morph's specialty)
|
||||
code_snippets = [
|
||||
"def hello_world():\n print('Hello, World!')",
|
||||
"class Calculator:\n def add(self, a, b):\n return a + b"
|
||||
]
|
||||
|
||||
embeddings = ef(code_snippets)
|
||||
assert embeddings is not None
|
||||
assert len(embeddings) == 2
|
||||
assert all(isinstance(emb, np.ndarray) for emb in embeddings)
|
||||
assert all(len(emb) > 0 for emb in embeddings)
|
||||
|
||||
|
||||
def test_morph_embedding_function_with_custom_parameters() -> None:
|
||||
"""Test Morph embedding function with custom parameters."""
|
||||
if os.environ.get("MORPH_API_KEY") is None:
|
||||
pytest.skip("MORPH_API_KEY not set")
|
||||
|
||||
ef = MorphEmbeddingFunction(
|
||||
model_name="morph-embedding-v2",
|
||||
api_base="https://api.morphllm.com/v1",
|
||||
encoding_format="float",
|
||||
api_key_env_var="MORPH_API_KEY"
|
||||
)
|
||||
|
||||
# Test with a simple function
|
||||
code_snippet = ["function add(a, b) { return a + b; }"]
|
||||
|
||||
embeddings = ef(code_snippet)
|
||||
assert embeddings is not None
|
||||
assert len(embeddings) == 1
|
||||
assert isinstance(embeddings[0], np.ndarray)
|
||||
assert len(embeddings[0]) > 0
|
||||
|
||||
|
||||
def test_morph_embedding_function_config_roundtrip() -> None:
|
||||
"""Test that Morph embedding function configuration can be saved and restored."""
|
||||
try:
|
||||
import openai
|
||||
except ImportError:
|
||||
pytest.skip("openai package not installed")
|
||||
|
||||
ef = MorphEmbeddingFunction(
|
||||
model_name="morph-embedding-v2",
|
||||
api_base="https://api.morphllm.com/v1",
|
||||
encoding_format="float",
|
||||
api_key_env_var="MORPH_API_KEY"
|
||||
)
|
||||
|
||||
# Get configuration
|
||||
config = ef.get_config()
|
||||
|
||||
# Verify configuration contains expected keys
|
||||
assert "model_name" in config
|
||||
assert "api_base" in config
|
||||
assert "encoding_format" in config
|
||||
assert "api_key_env_var" in config
|
||||
|
||||
# Verify values
|
||||
assert config["model_name"] == "morph-embedding-v2"
|
||||
assert config["api_base"] == "https://api.morphllm.com/v1"
|
||||
assert config["encoding_format"] == "float"
|
||||
assert config["api_key_env_var"] == "MORPH_API_KEY"
|
||||
|
||||
# Test building from config
|
||||
new_ef = MorphEmbeddingFunction.build_from_config(config)
|
||||
new_config = new_ef.get_config()
|
||||
|
||||
# Configurations should match
|
||||
assert config == new_config
|
||||
|
||||
|
||||
def test_morph_embedding_function_name() -> None:
|
||||
"""Test that Morph embedding function returns correct name."""
|
||||
assert MorphEmbeddingFunction.name() == "morph"
|
||||
|
||||
|
||||
def test_morph_embedding_function_spaces() -> None:
|
||||
"""Test that Morph embedding function supports expected spaces."""
|
||||
try:
|
||||
import openai
|
||||
except ImportError:
|
||||
pytest.skip("openai package not installed")
|
||||
|
||||
ef = MorphEmbeddingFunction(
|
||||
model_name="morph-embedding-v2",
|
||||
api_key_env_var="MORPH_API_KEY"
|
||||
)
|
||||
|
||||
# Test default space
|
||||
assert ef.default_space() == "cosine"
|
||||
|
||||
# Test supported spaces
|
||||
supported_spaces = ef.supported_spaces()
|
||||
assert "cosine" in supported_spaces
|
||||
assert "l2" in supported_spaces
|
||||
assert "ip" in supported_spaces
|
||||
|
||||
|
||||
def test_morph_embedding_function_validate_config() -> None:
|
||||
"""Test that Morph embedding function validates configuration correctly."""
|
||||
# Valid configuration
|
||||
valid_config = {
|
||||
"model_name": "morph-embedding-v2",
|
||||
"api_key_env_var": "MORPH_API_KEY"
|
||||
}
|
||||
|
||||
# This should not raise an exception
|
||||
MorphEmbeddingFunction.validate_config(valid_config)
|
||||
|
||||
# Invalid configuration (missing required fields)
|
||||
invalid_config = {
|
||||
"model_name": "morph-embedding-v2"
|
||||
# Missing api_key_env_var
|
||||
}
|
||||
|
||||
with pytest.raises(Exception):
|
||||
MorphEmbeddingFunction.validate_config(invalid_config)
|
||||
@@ -0,0 +1,169 @@
|
||||
import os
|
||||
from typing import Generator, cast
|
||||
import numpy as np
|
||||
import pytest
|
||||
import chromadb
|
||||
from chromadb.api.types import (
|
||||
Embeddable,
|
||||
EmbeddingFunction,
|
||||
Embeddings,
|
||||
Image,
|
||||
Document,
|
||||
)
|
||||
from chromadb.test.property.strategies import hashing_embedding_function
|
||||
from chromadb.test.property.invariants import _exact_distances
|
||||
from chromadb.config import Settings
|
||||
|
||||
|
||||
# A 'standard' multimodal embedding function, which converts inputs to strings
|
||||
# then hashes them to a fixed dimension.
|
||||
class hashing_multimodal_ef(EmbeddingFunction[Embeddable]):
|
||||
def __init__(self) -> None:
|
||||
self._hef = hashing_embedding_function(dim=10, dtype=np.float64)
|
||||
|
||||
def __call__(self, input: Embeddable) -> Embeddings:
|
||||
to_texts = [str(i) for i in input]
|
||||
embeddings = np.array(self._hef(to_texts))
|
||||
# Normalize the embeddings
|
||||
# This is so we can generate random unit vectors and have them be close to the embeddings
|
||||
embeddings /= np.linalg.norm(embeddings, axis=1, keepdims=True) # type: ignore[misc]
|
||||
return cast(Embeddings, embeddings.tolist())
|
||||
|
||||
|
||||
def random_image() -> Image:
|
||||
return np.random.randint(0, 255, size=(10, 10, 3), dtype=np.int64)
|
||||
|
||||
|
||||
def random_document() -> Document:
|
||||
return str(random_image())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def multimodal_collection(
|
||||
default_ef: EmbeddingFunction[Embeddable] = hashing_multimodal_ef(),
|
||||
) -> Generator[chromadb.Collection, None, None]:
|
||||
settings = Settings()
|
||||
if os.environ.get("CHROMA_INTEGRATION_TEST_ONLY"):
|
||||
host = os.environ.get("CHROMA_SERVER_HOST", "localhost")
|
||||
port = int(os.environ.get("CHROMA_SERVER_HTTP_PORT", 0))
|
||||
settings.chroma_api_impl = "chromadb.api.fastapi.FastAPI"
|
||||
settings.chroma_server_http_port = port
|
||||
settings.chroma_server_host = host
|
||||
|
||||
client = chromadb.Client(settings=settings)
|
||||
collection = client.create_collection(
|
||||
name="multimodal_collection", embedding_function=default_ef
|
||||
)
|
||||
yield collection
|
||||
client.clear_system_cache()
|
||||
|
||||
|
||||
# Test adding and querying of a multimodal collection consisting of images and documents
|
||||
def test_multimodal(
|
||||
multimodal_collection: chromadb.Collection,
|
||||
default_ef: EmbeddingFunction[Embeddable] = hashing_multimodal_ef(),
|
||||
n_examples: int = 10,
|
||||
n_query_results: int = 3,
|
||||
) -> None:
|
||||
# Fix numpy's random seed for reproducibility
|
||||
random_state = np.random.get_state()
|
||||
np.random.seed(0)
|
||||
|
||||
image_ids = [str(i) for i in range(n_examples)]
|
||||
images = [random_image() for _ in range(n_examples)]
|
||||
image_embeddings = default_ef(images)
|
||||
|
||||
document_ids = [str(i) for i in range(n_examples, 2 * n_examples)]
|
||||
documents = [random_document() for _ in range(n_examples)]
|
||||
document_embeddings = default_ef(documents)
|
||||
|
||||
# Trying to add a document and an image at the same time should fail
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
# This error string may be in any order
|
||||
match=r"Exactly one of (images|documents|uris)(?:, (images|documents|uris))?(?:, (images|documents|uris))? must be provided in add\.",
|
||||
):
|
||||
multimodal_collection.add(
|
||||
ids=image_ids[0], documents=documents[0], images=images[0]
|
||||
)
|
||||
|
||||
# Add some documents
|
||||
multimodal_collection.add(ids=document_ids, documents=documents)
|
||||
# Add some images
|
||||
multimodal_collection.add(ids=image_ids, images=images)
|
||||
|
||||
# get() should return all the documents and images
|
||||
# ids corresponding to images should not have documents
|
||||
get_result = multimodal_collection.get(include=["documents"])
|
||||
assert len(get_result["ids"]) == len(document_ids) + len(image_ids)
|
||||
for i, id in enumerate(get_result["ids"]):
|
||||
assert id in document_ids or id in image_ids
|
||||
assert get_result["documents"] is not None
|
||||
if id in document_ids:
|
||||
assert get_result["documents"][i] == documents[document_ids.index(id)]
|
||||
if id in image_ids:
|
||||
assert get_result["documents"][i] is None
|
||||
|
||||
# Generate a random query image
|
||||
query_image = random_image()
|
||||
query_image_embedding = default_ef([query_image])
|
||||
|
||||
image_neighbor_indices, _ = _exact_distances(
|
||||
query_image_embedding, image_embeddings + document_embeddings
|
||||
)
|
||||
# Get the ids of the nearest neighbors
|
||||
nearest_image_neighbor_ids = [
|
||||
image_ids[i] if i < n_examples else document_ids[i % n_examples]
|
||||
for i in image_neighbor_indices[0][:n_query_results]
|
||||
]
|
||||
|
||||
# Generate a random query document
|
||||
query_document = random_document()
|
||||
query_document_embedding = default_ef([query_document])
|
||||
document_neighbor_indices, _ = _exact_distances(
|
||||
query_document_embedding, image_embeddings + document_embeddings
|
||||
)
|
||||
nearest_document_neighbor_ids = [
|
||||
image_ids[i] if i < n_examples else document_ids[i % n_examples]
|
||||
for i in document_neighbor_indices[0][:n_query_results]
|
||||
]
|
||||
|
||||
# Querying with both images and documents should fail
|
||||
with pytest.raises(ValueError):
|
||||
multimodal_collection.query(
|
||||
query_images=[query_image], query_texts=[query_document]
|
||||
)
|
||||
|
||||
# Query with images
|
||||
query_result = multimodal_collection.query(
|
||||
query_images=[query_image], n_results=n_query_results, include=["documents"]
|
||||
)
|
||||
|
||||
assert query_result["ids"][0] == nearest_image_neighbor_ids
|
||||
|
||||
# Query with documents
|
||||
query_result = multimodal_collection.query(
|
||||
query_texts=[query_document], n_results=n_query_results, include=["documents"]
|
||||
)
|
||||
|
||||
assert query_result["ids"][0] == nearest_document_neighbor_ids
|
||||
np.random.set_state(random_state)
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_multimodal_update_with_image(
|
||||
multimodal_collection: chromadb.Collection,
|
||||
) -> None:
|
||||
# Updating an entry with an existing document should remove the documentß
|
||||
|
||||
document = random_document()
|
||||
image = random_image()
|
||||
id = "0"
|
||||
|
||||
multimodal_collection.add(ids=id, documents=document)
|
||||
|
||||
multimodal_collection.update(ids=id, images=image)
|
||||
|
||||
get_result = multimodal_collection.get(ids=id, include=["documents"])
|
||||
assert get_result["documents"] is not None
|
||||
assert get_result["documents"][0] is None
|
||||
@@ -0,0 +1,50 @@
|
||||
import pytest
|
||||
|
||||
from chromadb.utils.embedding_functions.ollama_embedding_function import (
|
||||
OllamaEmbeddingFunction,
|
||||
)
|
||||
|
||||
|
||||
def test_ollama_default_model() -> None:
|
||||
pytest.importorskip("ollama", reason="ollama not installed")
|
||||
ef = OllamaEmbeddingFunction()
|
||||
embeddings = ef(["Here is an article about llamas...", "this is another article"])
|
||||
assert embeddings is not None
|
||||
assert len(embeddings) == 2
|
||||
assert all(len(e) == 384 for e in embeddings)
|
||||
|
||||
|
||||
def test_ollama_unknown_model() -> None:
|
||||
pytest.importorskip("ollama", reason="ollama not installed")
|
||||
model_name = "unknown-model"
|
||||
ef = OllamaEmbeddingFunction(model_name=model_name)
|
||||
with pytest.raises(Exception) as e:
|
||||
ef(["Here is an article about llamas...", "this is another article"])
|
||||
assert f'model "{model_name}" not found' in str(e.value)
|
||||
|
||||
|
||||
def test_ollama_backward_compat() -> None:
|
||||
pytest.importorskip("ollama", reason="ollama not installed")
|
||||
ef = OllamaEmbeddingFunction(url="http://localhost:11434/api/embeddings")
|
||||
embeddings = ef(["Here is an article about llamas...", "this is another article"])
|
||||
assert embeddings is not None
|
||||
|
||||
|
||||
def test_wrong_url() -> None:
|
||||
pytest.importorskip("ollama", reason="ollama not installed")
|
||||
ef = OllamaEmbeddingFunction(url="http://localhost:11434/this_is_wrong")
|
||||
with pytest.raises(Exception) as e:
|
||||
ef(["Here is an article about llamas...", "this is another article"])
|
||||
assert "404" in str(e.value)
|
||||
|
||||
|
||||
def test_ollama_ask_user_to_install() -> None:
|
||||
try:
|
||||
from ollama import Client # noqa: F401
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
pytest.skip("ollama python package is installed")
|
||||
with pytest.raises(ValueError) as e:
|
||||
OllamaEmbeddingFunction()
|
||||
assert "The ollama python package is not installed" in str(e.value)
|
||||
@@ -0,0 +1,204 @@
|
||||
import os
|
||||
import tempfile
|
||||
from typing import Dict, Any
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
import pytest
|
||||
import onnxruntime
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from chromadb.utils.embedding_functions import ONNXMiniLM_L6_V2, EmbeddingFunction
|
||||
|
||||
|
||||
class TestONNXMiniLM_L6_V2:
|
||||
"""Test suite for ONNXMiniLM_L6_V2 embedding function."""
|
||||
|
||||
def test_initialization(self) -> None:
|
||||
"""Test that the embedding function initializes correctly."""
|
||||
ef = ONNXMiniLM_L6_V2()
|
||||
assert ef is not None
|
||||
assert isinstance(ef, EmbeddingFunction)
|
||||
|
||||
# Test with valid providers
|
||||
available_providers = onnxruntime.get_available_providers()
|
||||
if available_providers:
|
||||
ef = ONNXMiniLM_L6_V2(preferred_providers=[available_providers[0]])
|
||||
assert ef is not None
|
||||
|
||||
# Test with None providers
|
||||
ef = ONNXMiniLM_L6_V2(preferred_providers=None)
|
||||
assert ef is not None
|
||||
|
||||
def test_embedding_shape_and_normalization(self) -> None:
|
||||
"""Test that embeddings have the correct shape and are normalized."""
|
||||
ef = ONNXMiniLM_L6_V2()
|
||||
|
||||
# Test with a single document
|
||||
docs = ["This is a test document"]
|
||||
embeddings = ef(docs)
|
||||
|
||||
# Check shape and type
|
||||
assert isinstance(embeddings, list)
|
||||
assert len(embeddings) == 1
|
||||
assert (
|
||||
len(embeddings[0]) == 384
|
||||
) # MiniLM-L6-v2 produces 384-dimensional embeddings
|
||||
|
||||
# Check normalization (for cosine similarity)
|
||||
embedding_np = np.array(embeddings[0])
|
||||
norm = np.linalg.norm(embedding_np)
|
||||
assert np.isclose(norm, 1.0, atol=1e-5)
|
||||
|
||||
# Test with multiple documents
|
||||
docs = ["First document", "Second document", "Third document"]
|
||||
embeddings = ef(docs)
|
||||
|
||||
# Check shape
|
||||
assert len(embeddings) == 3
|
||||
assert all(len(emb) == 384 for emb in embeddings)
|
||||
|
||||
def test_batch_processing(self) -> None:
|
||||
"""Test that the embedding function correctly processes batches."""
|
||||
ef = ONNXMiniLM_L6_V2()
|
||||
|
||||
# Create a list of documents larger than the default batch size (32)
|
||||
docs = [f"Document {i}" for i in range(40)]
|
||||
|
||||
# Get embeddings
|
||||
embeddings = ef(docs)
|
||||
|
||||
# Check that all documents were processed
|
||||
assert len(embeddings) == 40
|
||||
assert all(len(emb) == 384 for emb in embeddings)
|
||||
|
||||
def test_config_serialization(self) -> None:
|
||||
"""Test that the embedding function can be serialized and deserialized."""
|
||||
# Create an embedding function with specific providers
|
||||
available_providers = onnxruntime.get_available_providers()
|
||||
providers = available_providers[:1] if available_providers else None
|
||||
ef = ONNXMiniLM_L6_V2(preferred_providers=providers)
|
||||
|
||||
# Get config
|
||||
config = ef.get_config()
|
||||
|
||||
# Check config
|
||||
assert isinstance(config, dict)
|
||||
assert "preferred_providers" in config
|
||||
|
||||
# Build from config
|
||||
ef2 = ONNXMiniLM_L6_V2.build_from_config(config)
|
||||
|
||||
# Check that the new instance works
|
||||
docs = ["Test document"]
|
||||
embeddings = ef2(docs)
|
||||
assert len(embeddings) == 1
|
||||
assert len(embeddings[0]) == 384
|
||||
|
||||
def test_max_tokens(self) -> None:
|
||||
"""Test the max_tokens method."""
|
||||
ef = ONNXMiniLM_L6_V2()
|
||||
assert ef.max_tokens() == 256 # Default for this model
|
||||
|
||||
@patch("httpx.stream")
|
||||
def test_download_functionality(self, mock_stream: MagicMock) -> None:
|
||||
"""Test the model download functionality with mocking."""
|
||||
# Setup mock response
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_response.headers.get.return_value = "1000"
|
||||
mock_response.iter_bytes.return_value = [b"test data"]
|
||||
mock_stream.return_value.__enter__.return_value = mock_response
|
||||
|
||||
# Create a temporary directory for testing
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Patch the download path
|
||||
with patch.object(ONNXMiniLM_L6_V2, "DOWNLOAD_PATH", temp_dir):
|
||||
with patch(
|
||||
"chromadb.utils.embedding_functions.onnx_mini_lm_l6_v2._verify_sha256",
|
||||
return_value=True,
|
||||
):
|
||||
ef = ONNXMiniLM_L6_V2()
|
||||
# Call download method directly
|
||||
ef._download(
|
||||
url="https://test.url",
|
||||
fname=os.path.join(temp_dir, "test_file"),
|
||||
)
|
||||
|
||||
# Check that the file was created
|
||||
assert os.path.exists(os.path.join(temp_dir, "test_file"))
|
||||
|
||||
def test_validate_config(self) -> None:
|
||||
"""Test config validation."""
|
||||
ef = ONNXMiniLM_L6_V2()
|
||||
|
||||
# Test validate_config
|
||||
config: Dict[str, Any] = {"preferred_providers": ["CPUExecutionProvider"]}
|
||||
ef.validate_config(config) # Should not raise
|
||||
|
||||
# Test validate_config_update
|
||||
old_config: Dict[str, Any] = {"preferred_providers": ["CPUExecutionProvider"]}
|
||||
new_config: Dict[str, Any] = {"preferred_providers": ["CUDAExecutionProvider"]}
|
||||
ef.validate_config_update(old_config, new_config) # Should not raise
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"input_text",
|
||||
[
|
||||
"Short text",
|
||||
"A longer text that contains multiple words and should be embedded properly",
|
||||
"", # Empty string
|
||||
"Special characters: !@#$%^&*()",
|
||||
"Numbers: 1234567890",
|
||||
"Unicode: 你好, こんにちは, 안녕하세요",
|
||||
],
|
||||
)
|
||||
def test_various_inputs(self, input_text: str) -> None:
|
||||
"""Test the embedding function with various types of input text."""
|
||||
ef = ONNXMiniLM_L6_V2()
|
||||
|
||||
# Get embeddings
|
||||
embeddings = ef([input_text])
|
||||
|
||||
# Check that embeddings were generated
|
||||
assert len(embeddings) == 1
|
||||
assert len(embeddings[0]) == 384
|
||||
|
||||
def test_consistency(self) -> None:
|
||||
"""Test that the embedding function produces consistent results."""
|
||||
ef = ONNXMiniLM_L6_V2()
|
||||
|
||||
# Get embeddings for the same text twice
|
||||
text = "This is a test document"
|
||||
embeddings1 = ef([text])
|
||||
embeddings2 = ef([text])
|
||||
|
||||
# Check that the embeddings are the same
|
||||
np.testing.assert_allclose(embeddings1[0], embeddings2[0])
|
||||
|
||||
def test_similar_texts_have_similar_embeddings(self) -> None:
|
||||
"""Test that similar texts have similar embeddings."""
|
||||
ef = ONNXMiniLM_L6_V2()
|
||||
|
||||
# Get embeddings for similar texts
|
||||
text1 = "The cat sat on the mat"
|
||||
text2 = "A cat was sitting on a mat"
|
||||
text3 = "Quantum physics is fascinating"
|
||||
|
||||
embeddings = ef([text1, text2, text3])
|
||||
|
||||
# Calculate cosine similarities
|
||||
def cosine_similarity(a: NDArray[np.float32], b: NDArray[np.float32]) -> float:
|
||||
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
|
||||
|
||||
# Similar texts should have higher similarity
|
||||
sim_1_2 = cosine_similarity(
|
||||
np.array(embeddings[0], dtype=np.float32),
|
||||
np.array(embeddings[1], dtype=np.float32),
|
||||
)
|
||||
sim_1_3 = cosine_similarity(
|
||||
np.array(embeddings[0], dtype=np.float32),
|
||||
np.array(embeddings[2], dtype=np.float32),
|
||||
)
|
||||
|
||||
# The similarity between text1 and text2 should be higher than between text1 and text3
|
||||
assert sim_1_2 > sim_1_3
|
||||
@@ -0,0 +1,38 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from chromadb.utils.embedding_functions.openai_embedding_function import (
|
||||
OpenAIEmbeddingFunction,
|
||||
)
|
||||
|
||||
|
||||
def test_with_embedding_dimensions() -> None:
|
||||
if os.environ.get("OPENAI_API_KEY") is None:
|
||||
pytest.skip("OPENAI_API_KEY not set")
|
||||
ef = OpenAIEmbeddingFunction(
|
||||
api_key=os.environ["OPENAI_API_KEY"],
|
||||
model_name="text-embedding-3-small",
|
||||
dimensions=64,
|
||||
)
|
||||
embeddings = ef(["hello world"])
|
||||
assert embeddings is not None
|
||||
assert len(embeddings) == 1
|
||||
assert len(embeddings[0]) == 64
|
||||
|
||||
|
||||
def test_with_embedding_dimensions_not_working_with_old_model() -> None:
|
||||
if os.environ.get("OPENAI_API_KEY") is None:
|
||||
pytest.skip("OPENAI_API_KEY not set")
|
||||
ef = OpenAIEmbeddingFunction(api_key=os.environ["OPENAI_API_KEY"], dimensions=64)
|
||||
with pytest.raises(
|
||||
Exception, match="This model does not support specifying dimensions"
|
||||
):
|
||||
ef(["hello world"])
|
||||
|
||||
|
||||
def test_with_incorrect_api_key() -> None:
|
||||
pytest.importorskip("openai", reason="openai not installed")
|
||||
ef = OpenAIEmbeddingFunction(api_key="incorrect_api_key", dimensions=64)
|
||||
with pytest.raises(Exception, match="Incorrect API key provided"):
|
||||
ef(["hello world"])
|
||||
@@ -0,0 +1,19 @@
|
||||
import os
|
||||
import pytest
|
||||
from chromadb.utils.embedding_functions.voyageai_embedding_function import (
|
||||
VoyageAIEmbeddingFunction,
|
||||
)
|
||||
|
||||
voyageai = pytest.importorskip("voyageai", reason="voyageai not installed")
|
||||
|
||||
|
||||
def test_with_embedding_dimensions() -> None:
|
||||
if os.environ.get("CHROMA_VOYAGE_API_KEY") is None:
|
||||
pytest.skip("CHROMA_VOYAGE_API_KEY not set")
|
||||
ef = VoyageAIEmbeddingFunction(
|
||||
api_key=os.environ["CHROMA_VOYAGE_API_KEY"]
|
||||
)
|
||||
embeddings = ef(["hello world"])
|
||||
assert embeddings is not None
|
||||
assert len(embeddings) == 1
|
||||
assert len(embeddings[0]) == 1536
|
||||
Reference in New Issue
Block a user