241 lines
9.8 KiB
Python
241 lines
9.8 KiB
Python
# Copyright 2025-present the zvec project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
from __future__ import annotations
|
|
|
|
from functools import lru_cache
|
|
from typing import Optional
|
|
|
|
from ..common.constants import TEXT, DenseVectorType
|
|
from .embedding_function import DenseEmbeddingFunction
|
|
from .jina_function import JinaFunctionBase
|
|
|
|
|
|
class JinaDenseEmbedding(JinaFunctionBase, DenseEmbeddingFunction[TEXT]):
|
|
"""Dense text embedding function using Jina AI API.
|
|
|
|
This class provides text-to-vector embedding capabilities using Jina AI's
|
|
embedding models. It inherits from ``DenseEmbeddingFunction`` and implements
|
|
dense text embedding via the Jina Embeddings API (OpenAI-compatible).
|
|
|
|
Jina Embeddings v5 models support task-specific embedding through the
|
|
``task`` parameter, which optimizes the embedding for different use cases
|
|
such as retrieval, text matching, or classification. They also support
|
|
Matryoshka Representation Learning, allowing flexible output dimensions.
|
|
|
|
Args:
|
|
model (str, optional): Jina embedding model identifier.
|
|
Defaults to ``"jina-embeddings-v5-text-nano"``. Available models:
|
|
- ``"jina-embeddings-v5-text-nano"``: 768 dims, 239M params, 8K context
|
|
- ``"jina-embeddings-v5-text-small"``: 1024 dims, 677M params, 32K context
|
|
dimension (Optional[int], optional): Desired output embedding dimension.
|
|
If ``None``, uses model's default dimension. Supports Matryoshka
|
|
dimensions: 32, 64, 128, 256, 512, 768 (nano) / 1024 (small).
|
|
Defaults to ``None``.
|
|
api_key (Optional[str], optional): Jina API authentication key.
|
|
If ``None``, reads from ``JINA_API_KEY`` environment variable.
|
|
Obtain your key from: https://jina.ai/api-dashboard
|
|
task (Optional[str], optional): Task type to optimize embeddings for.
|
|
Defaults to ``None``. Valid values:
|
|
- ``"retrieval.query"``: For search queries
|
|
- ``"retrieval.passage"``: For documents/passages to be searched
|
|
- ``"text-matching"``: For symmetric text similarity
|
|
- ``"classification"``: For text classification
|
|
- ``"separation"``: For clustering/separation tasks
|
|
|
|
Attributes:
|
|
dimension (int): The embedding vector dimension.
|
|
data_type (DataType): Always ``DataType.VECTOR_FP32`` for this implementation.
|
|
model (str): The Jina model name being used.
|
|
task (Optional[str]): The task type for embedding optimization.
|
|
|
|
Raises:
|
|
ValueError: If API key is not provided and not found in environment,
|
|
if task is not a valid task type, or if API returns an error response.
|
|
TypeError: If input to ``embed()`` is not a string.
|
|
RuntimeError: If network error or Jina service error occurs.
|
|
|
|
Note:
|
|
- Requires Python 3.10, 3.11, or 3.12
|
|
- Requires the ``openai`` package: ``pip install openai``
|
|
- Jina API is OpenAI-compatible, so it uses the ``openai`` Python client
|
|
- Embedding results are cached (LRU cache, maxsize=10) to reduce API calls
|
|
- For retrieval tasks, use ``"retrieval.query"`` for queries and
|
|
``"retrieval.passage"`` for documents
|
|
- API usage requires a Jina API key from https://jina.ai/api-dashboard
|
|
|
|
Examples:
|
|
>>> # Basic usage with default model
|
|
>>> from zvec.extension import JinaDenseEmbedding
|
|
>>> import os
|
|
>>> os.environ["JINA_API_KEY"] = "jina_..."
|
|
>>>
|
|
>>> emb_func = JinaDenseEmbedding()
|
|
>>> vector = emb_func.embed("Hello, world!")
|
|
>>> len(vector)
|
|
768
|
|
|
|
>>> # Retrieval use case: embed queries and documents differently
|
|
>>> query_emb = JinaDenseEmbedding(task="retrieval.query")
|
|
>>> doc_emb = JinaDenseEmbedding(task="retrieval.passage")
|
|
>>>
|
|
>>> query_vector = query_emb.embed("What is machine learning?")
|
|
>>> doc_vector = doc_emb.embed("Machine learning is a subset of AI...")
|
|
|
|
>>> # Using larger model with custom dimension (Matryoshka)
|
|
>>> emb_func = JinaDenseEmbedding(
|
|
... model="jina-embeddings-v5-text-small",
|
|
... dimension=256,
|
|
... api_key="jina_...",
|
|
... task="text-matching",
|
|
... )
|
|
>>> vector = emb_func.embed("Semantic similarity comparison")
|
|
>>> len(vector)
|
|
256
|
|
|
|
>>> # Using with zvec collection
|
|
>>> import zvec
|
|
>>> emb_func = JinaDenseEmbedding(task="retrieval.passage")
|
|
>>> schema = zvec.CollectionSchema(
|
|
... name="docs",
|
|
... vectors=zvec.VectorSchema(
|
|
... "embedding", zvec.DataType.VECTOR_FP32, emb_func.dimension
|
|
... ),
|
|
... )
|
|
>>> collection = zvec.create_and_open(path="./my_docs", schema=schema)
|
|
|
|
See Also:
|
|
- ``DenseEmbeddingFunction``: Base class for dense embeddings
|
|
- ``OpenAIDenseEmbedding``: Alternative using OpenAI API
|
|
- ``QwenDenseEmbedding``: Alternative using Qwen/DashScope API
|
|
- ``DefaultLocalDenseEmbedding``: Local model without API calls
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
model: str = "jina-embeddings-v5-text-nano",
|
|
dimension: Optional[int] = None,
|
|
api_key: Optional[str] = None,
|
|
task: Optional[str] = None,
|
|
**kwargs,
|
|
):
|
|
"""Initialize the Jina dense embedding function.
|
|
|
|
Args:
|
|
model (str): Jina model name. Defaults to "jina-embeddings-v5-text-nano".
|
|
dimension (Optional[int]): Target embedding dimension or None for default.
|
|
api_key (Optional[str]): API key or None to use environment variable.
|
|
task (Optional[str]): Task type for embedding optimization or None.
|
|
**kwargs: Additional parameters for API calls.
|
|
|
|
Raises:
|
|
ValueError: If API key is not provided and not in environment,
|
|
or if task is not a valid task type.
|
|
"""
|
|
# Initialize base class for API connection
|
|
JinaFunctionBase.__init__(self, model=model, api_key=api_key, task=task)
|
|
|
|
# Store dimension configuration
|
|
self._custom_dimension = dimension
|
|
|
|
# Determine actual dimension
|
|
if dimension is None:
|
|
self._dimension = self._MODEL_DIMENSIONS.get(model, 768)
|
|
else:
|
|
self._dimension = dimension
|
|
|
|
# Store extra attributes
|
|
self._extra_params = kwargs
|
|
|
|
@property
|
|
def dimension(self) -> int:
|
|
"""int: The expected dimensionality of the embedding vector."""
|
|
return self._dimension
|
|
|
|
@property
|
|
def extra_params(self) -> dict:
|
|
"""dict: Extra parameters for model-specific customization."""
|
|
return self._extra_params
|
|
|
|
def __call__(self, input: TEXT) -> DenseVectorType:
|
|
"""Make the embedding function callable."""
|
|
return self.embed(input)
|
|
|
|
@lru_cache(maxsize=10)
|
|
def embed(self, input: TEXT) -> DenseVectorType:
|
|
"""Generate dense embedding vector for the input text.
|
|
|
|
This method calls the Jina Embeddings API to convert input text
|
|
into a dense vector representation. Results are cached to improve
|
|
performance for repeated inputs.
|
|
|
|
Args:
|
|
input (TEXT): Input text string to embed. Must be non-empty after
|
|
stripping whitespace. Maximum length depends on model:
|
|
8192 tokens for v5-nano, 32768 tokens for v5-small.
|
|
|
|
Returns:
|
|
DenseVectorType: A list of floats representing the embedding vector.
|
|
Length equals ``self.dimension``. Example:
|
|
``[0.123, -0.456, 0.789, ...]``
|
|
|
|
Raises:
|
|
TypeError: If ``input`` is not a string.
|
|
ValueError: If input is empty/whitespace-only, or if the API returns
|
|
an error or malformed response.
|
|
RuntimeError: If network connectivity issues or Jina service
|
|
errors occur.
|
|
|
|
Examples:
|
|
>>> emb = JinaDenseEmbedding(task="retrieval.query")
|
|
>>> vector = emb.embed("What is deep learning?")
|
|
>>> len(vector)
|
|
768
|
|
>>> isinstance(vector[0], float)
|
|
True
|
|
|
|
>>> # Error: empty input
|
|
>>> emb.embed(" ")
|
|
ValueError: Input text cannot be empty or whitespace only
|
|
|
|
>>> # Error: non-string input
|
|
>>> emb.embed(123)
|
|
TypeError: Expected 'input' to be str, got int
|
|
|
|
Note:
|
|
- This method is cached (maxsize=10). Identical inputs return cached results.
|
|
- The cache is based on exact string match (case-sensitive).
|
|
- Task type affects embedding optimization but not caching behavior.
|
|
"""
|
|
if not isinstance(input, TEXT):
|
|
raise TypeError(f"Expected 'input' to be str, got {type(input).__name__}")
|
|
|
|
input = input.strip()
|
|
if not input:
|
|
raise ValueError("Input text cannot be empty or whitespace only")
|
|
|
|
# Call API
|
|
embedding_vector = self._call_text_embedding_api(
|
|
input=input,
|
|
dimension=self._custom_dimension,
|
|
)
|
|
|
|
# Verify dimension
|
|
if len(embedding_vector) != self.dimension:
|
|
raise ValueError(
|
|
f"Dimension mismatch: expected {self.dimension}, "
|
|
f"got {len(embedding_vector)}"
|
|
)
|
|
|
|
return embedding_vector
|