mirror of
https://github.com/lancedb/lancedb.git
synced 2026-08-29 01:18:23 +00:00
1f2068b9fe
Carrying over from #2915, this patch introduces: * Single-API call batching support for Gemini embeddings (up to 100 at a time, the API limit) * A versioned user agent header for Gemini API calls * Support for [variable embedding dimension size](https://ai.google.dev/gemini-api/docs/embeddings#control-embedding-size) (Gemini is MRL trained)
184 lines
7.5 KiB
Python
184 lines
7.5 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
|
|
"""Unit tests for GeminiText embedding function."""
|
|
|
|
import sys
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
# Mock google.genai modules before they are imported by gemini_text.py
|
|
mock_google = MagicMock()
|
|
mock_genai = MagicMock()
|
|
mock_types = MagicMock()
|
|
|
|
mock_google.genai = mock_genai
|
|
mock_genai.types = mock_types
|
|
|
|
sys.modules["google"] = mock_google
|
|
sys.modules["google.genai"] = mock_genai
|
|
sys.modules["google.genai.types"] = mock_types
|
|
|
|
import pytest # noqa: E402
|
|
import numpy as np # noqa: E402
|
|
from lancedb.embeddings import get_registry # noqa: E402
|
|
from lancedb import __version__ # noqa: E402
|
|
|
|
|
|
class TestGeminiText:
|
|
"""Tests for GeminiText model registration, configuration, and execution."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup_mocks(self):
|
|
"""Set up standard mocks for google-genai Client and Config."""
|
|
# Reset mocks
|
|
mock_genai.reset_mock()
|
|
mock_types.reset_mock()
|
|
|
|
self.mock_client = MagicMock()
|
|
mock_genai.Client.return_value = self.mock_client
|
|
|
|
# Mock response for embed_content
|
|
self.mock_embedding_1 = MagicMock()
|
|
self.mock_embedding_1.values = [0.1] * 768
|
|
self.mock_embedding_2 = MagicMock()
|
|
self.mock_embedding_2.values = [0.2] * 768
|
|
|
|
self.mock_response = MagicMock()
|
|
self.mock_response.embeddings = [self.mock_embedding_1, self.mock_embedding_2]
|
|
self.mock_client.models.embed_content.return_value = self.mock_response
|
|
|
|
def test_gemini_registered(self):
|
|
"""Test that gemini-text is registered in the embedding function registry."""
|
|
registry = get_registry()
|
|
assert registry.get("gemini-text") is not None
|
|
|
|
def test_client_init_headers(self):
|
|
"""Test that Client is initialized with the partner-attribution header."""
|
|
with patch.dict("os.environ", {"GOOGLE_API_KEY": "test-key"}):
|
|
with patch("lancedb.embeddings.gemini_text.attempt_import_or_raise"):
|
|
registry = get_registry()
|
|
func = registry.get("gemini-text").create()
|
|
|
|
# Access the client property to trigger initialization
|
|
_ = func.client
|
|
|
|
mock_genai.Client.assert_called_once_with(
|
|
api_key="test-key",
|
|
http_options={
|
|
"headers": {
|
|
"x-goog-api-client": f"lancedb/{__version__}",
|
|
}
|
|
},
|
|
)
|
|
|
|
def test_generate_embeddings_batched(self):
|
|
"""Test that multiple texts are sent in a single batched API request."""
|
|
with patch.dict("os.environ", {"GOOGLE_API_KEY": "test-key"}):
|
|
with patch("lancedb.embeddings.gemini_text.attempt_import_or_raise"):
|
|
registry = get_registry()
|
|
func = registry.get("gemini-text").create()
|
|
|
|
texts = ["hello", "world"]
|
|
embeddings = func.generate_embeddings(texts)
|
|
|
|
# Check embed_content was called exactly once
|
|
self.mock_client.models.embed_content.assert_called_once()
|
|
|
|
# Verify call arguments
|
|
call_kwargs = self.mock_client.models.embed_content.call_args.kwargs
|
|
assert call_kwargs["model"] == "gemini-embedding-001"
|
|
assert len(call_kwargs["contents"]) == 2
|
|
assert call_kwargs["contents"][0] == {"parts": [{"text": "hello"}]}
|
|
assert call_kwargs["contents"][1] == {"parts": [{"text": "world"}]}
|
|
|
|
# Verify returns are correct numpy arrays
|
|
assert len(embeddings) == 2
|
|
assert isinstance(embeddings[0], np.ndarray)
|
|
assert embeddings[0].shape == (768,)
|
|
assert np.allclose(embeddings[0], 0.1)
|
|
assert np.allclose(embeddings[1], 0.2)
|
|
|
|
def test_generate_embeddings_retrieval_document(self):
|
|
"""Test that retrieval_document task type prepends the document title part."""
|
|
with patch.dict("os.environ", {"GOOGLE_API_KEY": "test-key"}):
|
|
with patch("lancedb.embeddings.gemini_text.attempt_import_or_raise"):
|
|
registry = get_registry()
|
|
func = registry.get("gemini-text").create(
|
|
source_task_type="retrieval_document"
|
|
)
|
|
|
|
texts = ["doc text"]
|
|
|
|
# We need mock to return only 1 embedding since we only pass 1 text
|
|
mock_embedding = MagicMock()
|
|
mock_embedding.values = [0.3] * 768
|
|
self.mock_response.embeddings = [mock_embedding]
|
|
|
|
embeddings = func.generate_embeddings(
|
|
texts, task_type="retrieval_document"
|
|
)
|
|
|
|
# Check call arguments for retrieval_document
|
|
call_kwargs = self.mock_client.models.embed_content.call_args.kwargs
|
|
assert call_kwargs["contents"][0] == {
|
|
"parts": [{"text": "Embedding of a document"}, {"text": "doc text"}]
|
|
}
|
|
mock_types.EmbedContentConfig.assert_called_once_with(
|
|
output_dimensionality=768, task_type="RETRIEVAL_DOCUMENT"
|
|
)
|
|
|
|
assert len(embeddings) == 1
|
|
assert np.allclose(embeddings[0], 0.3)
|
|
|
|
def test_custom_dimension(self):
|
|
"""Test that custom dimension (dim) can be configured and passed to config."""
|
|
with patch.dict("os.environ", {"GOOGLE_API_KEY": "test-key"}):
|
|
with patch("lancedb.embeddings.gemini_text.attempt_import_or_raise"):
|
|
registry = get_registry()
|
|
func = registry.get("gemini-text").create(dim=3072)
|
|
|
|
assert func.ndims() == 3072
|
|
|
|
texts = ["hello"]
|
|
mock_embedding = MagicMock()
|
|
mock_embedding.values = [0.5] * 3072
|
|
self.mock_response.embeddings = [mock_embedding]
|
|
|
|
_ = func.generate_embeddings(texts)
|
|
|
|
mock_types.EmbedContentConfig.assert_called_once_with(
|
|
output_dimensionality=3072
|
|
)
|
|
|
|
def test_generate_embeddings_chunked(self):
|
|
"""Test that generate_embeddings chunks texts into groups of 100."""
|
|
with patch.dict("os.environ", {"GOOGLE_API_KEY": "test-key"}):
|
|
with patch("lancedb.embeddings.gemini_text.attempt_import_or_raise"):
|
|
registry = get_registry()
|
|
func = registry.get("gemini-text").create()
|
|
|
|
# Passing 250 texts should make 3 calls (100, 100, 50)
|
|
texts = [f"text_{i}" for i in range(250)]
|
|
|
|
# Mock client response to return correct number of embeddings per chunk
|
|
def mock_embed_side_effect(model, contents, config=None):
|
|
mock_resp = MagicMock()
|
|
mock_embeddings = []
|
|
for _ in contents:
|
|
emb = MagicMock()
|
|
# Each embedding is length 768
|
|
emb.values = [0.1] * 768
|
|
mock_embeddings.append(emb)
|
|
mock_resp.embeddings = mock_embeddings
|
|
return mock_resp
|
|
|
|
self.mock_client.models.embed_content.side_effect = (
|
|
mock_embed_side_effect
|
|
)
|
|
|
|
embeddings = func.generate_embeddings(texts)
|
|
|
|
# embed_content should be called 3 times
|
|
assert self.mock_client.models.embed_content.call_count == 3
|
|
assert len(embeddings) == 250
|