mirror of
https://github.com/qdrant/qdrant-client.git
synced 2026-07-23 11:11:01 -05:00
Local inference image support (#836)
* new: add backbone for image support * new: convert b64 to pil, embed images, add test * tests: add test file * refactor: replace 3 different inference object vars with a common one * fix: fix type hints * fix: fix type hints * tests: add tests * fix: remove redundant imports * new: propagate image options * Custom inference object (#837) * new: add inference object support * new: add inference object support * fix: remove redundant import * refactor: return newline * fix: fix propagate options test
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
#
|
||||
# ****** WARNING: THIS FILE IS AUTOGENERATED ******
|
||||
|
||||
import base64
|
||||
import io
|
||||
import uuid
|
||||
import warnings
|
||||
from itertools import tee
|
||||
@@ -19,6 +21,7 @@ from pydantic import BaseModel
|
||||
from qdrant_client.async_client_base import AsyncQdrantBase
|
||||
from qdrant_client.conversions import common_types as types
|
||||
from qdrant_client.conversions.conversion import GrpcToRest
|
||||
from qdrant_client.embed.common import INFERENCE_OBJECT_TYPES
|
||||
from qdrant_client.embed.embed_inspector import InspectorEmbed
|
||||
from qdrant_client.embed.models import NumericVector, NumericVectorStruct
|
||||
from qdrant_client.embed.schema_parser import ModelSchemaParser
|
||||
@@ -29,13 +32,21 @@ from qdrant_client.hybrid.fusion import reciprocal_rank_fusion
|
||||
from qdrant_client import grpc
|
||||
|
||||
try:
|
||||
from fastembed import SparseTextEmbedding, TextEmbedding, LateInteractionTextEmbedding
|
||||
from fastembed import (
|
||||
SparseTextEmbedding,
|
||||
TextEmbedding,
|
||||
LateInteractionTextEmbedding,
|
||||
ImageEmbedding,
|
||||
)
|
||||
from fastembed.common import OnnxProvider
|
||||
from PIL import Image as PilImage
|
||||
except ImportError:
|
||||
TextEmbedding = None
|
||||
SparseTextEmbedding = None
|
||||
OnnxProvider = None
|
||||
LateInteractionTextEmbedding = None
|
||||
ImageEmbedding = None
|
||||
PilImage = None
|
||||
SUPPORTED_EMBEDDING_MODELS: Dict[str, Tuple[int, models.Distance]] = (
|
||||
{
|
||||
model["model"]: (model["dim"], models.Distance.COSINE)
|
||||
@@ -63,6 +74,11 @@ _LATE_INTERACTION_EMBEDDING_MODELS: Dict[str, Tuple[int, models.Distance]] = (
|
||||
if LateInteractionTextEmbedding
|
||||
else {}
|
||||
)
|
||||
_IMAGE_EMBEDDING_MODELS: Dict[str, Tuple[int, models.Distance]] = (
|
||||
{model["model"]: model for model in ImageEmbedding.list_supported_models()}
|
||||
if ImageEmbedding
|
||||
else {}
|
||||
)
|
||||
|
||||
|
||||
class AsyncQdrantFastembedMixin(AsyncQdrantBase):
|
||||
@@ -70,6 +86,7 @@ class AsyncQdrantFastembedMixin(AsyncQdrantBase):
|
||||
embedding_models: Dict[str, "TextEmbedding"] = {}
|
||||
sparse_embedding_models: Dict[str, "SparseTextEmbedding"] = {}
|
||||
late_interaction_embedding_models: Dict[str, "LateInteractionTextEmbedding"] = {}
|
||||
image_embedding_models: Dict[str, "ImageEmbedding"] = {}
|
||||
_FASTEMBED_INSTALLED: bool
|
||||
|
||||
def __init__(self, parser: ModelSchemaParser, **kwargs: Any):
|
||||
@@ -294,6 +311,31 @@ class AsyncQdrantFastembedMixin(AsyncQdrantBase):
|
||||
)
|
||||
return cls.late_interaction_embedding_models[model_name]
|
||||
|
||||
@classmethod
|
||||
def _get_or_init_image_model(
|
||||
cls,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
providers: Optional[Sequence["OnnxProvider"]] = None,
|
||||
**kwargs: Any,
|
||||
) -> "ImageEmbedding":
|
||||
if model_name in cls.image_embedding_models:
|
||||
return cls.image_embedding_models[model_name]
|
||||
cls._import_fastembed()
|
||||
if model_name not in _IMAGE_EMBEDDING_MODELS:
|
||||
raise ValueError(
|
||||
f"Unsupported embedding model: {model_name}. Supported models: {_IMAGE_EMBEDDING_MODELS}"
|
||||
)
|
||||
cls.image_embedding_models[model_name] = ImageEmbedding(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=threads,
|
||||
providers=providers,
|
||||
**kwargs,
|
||||
)
|
||||
return cls.image_embedding_models[model_name]
|
||||
|
||||
def _embed_documents(
|
||||
self,
|
||||
documents: Iterable[str],
|
||||
@@ -727,8 +769,9 @@ class AsyncQdrantFastembedMixin(AsyncQdrantBase):
|
||||
]
|
||||
return [self._scored_points_to_query_responses(response) for response in responses]
|
||||
|
||||
@staticmethod
|
||||
@classmethod
|
||||
def _resolve_query(
|
||||
cls,
|
||||
query: Union[
|
||||
types.PointId,
|
||||
List[float],
|
||||
@@ -764,10 +807,10 @@ class AsyncQdrantFastembedMixin(AsyncQdrantBase):
|
||||
GrpcToRest.convert_point_id(query) if isinstance(query, grpc.PointId) else query
|
||||
)
|
||||
return models.NearestQuery(nearest=query)
|
||||
if isinstance(query, models.Document):
|
||||
if isinstance(query, INFERENCE_OBJECT_TYPES):
|
||||
model_name = query.model
|
||||
if model_name is None:
|
||||
raise ValueError("`model` field has to be set explicitly in the `Document`")
|
||||
raise ValueError(f"`model` field has to be set explicitly in the {type(query)}")
|
||||
return models.NearestQuery(nearest=query)
|
||||
if query is None:
|
||||
return None
|
||||
@@ -813,7 +856,7 @@ class AsyncQdrantFastembedMixin(AsyncQdrantBase):
|
||||
A deepcopy of the method with embedded fields
|
||||
"""
|
||||
if paths is None:
|
||||
if isinstance(model, models.Document):
|
||||
if isinstance(model, INFERENCE_OBJECT_TYPES):
|
||||
return self._embed_raw_data(model, is_query=is_query)
|
||||
model = deepcopy(model)
|
||||
paths = self._embed_inspector.inspect(model)
|
||||
@@ -839,6 +882,32 @@ class AsyncQdrantFastembedMixin(AsyncQdrantBase):
|
||||
setattr(item, path.current, embeddings[0])
|
||||
return model
|
||||
|
||||
@staticmethod
|
||||
def _resolve_inference_object(data: models.VectorStruct) -> models.VectorStruct:
|
||||
"""Resolve inference object into a model
|
||||
|
||||
Args:
|
||||
data: models.VectorStruct - data to resolve, if it's an inference object, convert it to a proper type,
|
||||
otherwise - keep unchanged
|
||||
|
||||
Returns:
|
||||
models.VectorStruct: resolved data
|
||||
"""
|
||||
if not isinstance(data, models.InferenceObject):
|
||||
return data
|
||||
model_name = data.model
|
||||
value = data.object
|
||||
options = data.options
|
||||
if model_name in (
|
||||
*SUPPORTED_EMBEDDING_MODELS.keys(),
|
||||
*SUPPORTED_SPARSE_EMBEDDING_MODELS.keys(),
|
||||
*_LATE_INTERACTION_EMBEDDING_MODELS.keys(),
|
||||
):
|
||||
return models.Document(model=model_name, text=value, options=options)
|
||||
if model_name in _IMAGE_EMBEDDING_MODELS:
|
||||
return models.Image(model=model_name, image=value, options=options)
|
||||
raise ValueError(f"{model_name} is not among supported models")
|
||||
|
||||
def _embed_raw_data(
|
||||
self, data: models.VectorStruct, is_query: bool = False
|
||||
) -> NumericVectorStruct:
|
||||
@@ -851,8 +920,11 @@ class AsyncQdrantFastembedMixin(AsyncQdrantBase):
|
||||
Returns:
|
||||
NumericVectorStruct: Embedded data
|
||||
"""
|
||||
data = self._resolve_inference_object(data)
|
||||
if isinstance(data, models.Document):
|
||||
return self._embed_document(data, is_query=is_query)
|
||||
elif isinstance(data, models.Image):
|
||||
return self._embed_image(data)
|
||||
elif isinstance(data, dict):
|
||||
return {
|
||||
key: self._embed_raw_data(value, is_query=is_query)
|
||||
@@ -879,10 +951,9 @@ class AsyncQdrantFastembedMixin(AsyncQdrantBase):
|
||||
"""
|
||||
model_name = document.model
|
||||
text = document.text
|
||||
options = document.options or {}
|
||||
if model_name in SUPPORTED_EMBEDDING_MODELS:
|
||||
embedding_model_inst = self._get_or_init_model(
|
||||
model_name=model_name, **document.options or {}
|
||||
)
|
||||
embedding_model_inst = self._get_or_init_model(model_name=model_name, **options)
|
||||
if not is_query:
|
||||
embedding = list(embedding_model_inst.embed(documents=[text]))[0].tolist()
|
||||
else:
|
||||
@@ -890,7 +961,7 @@ class AsyncQdrantFastembedMixin(AsyncQdrantBase):
|
||||
return embedding
|
||||
elif model_name in SUPPORTED_SPARSE_EMBEDDING_MODELS:
|
||||
sparse_embedding_model_inst = self._get_or_init_sparse_model(
|
||||
model_name=model_name, **document.options or {}
|
||||
model_name=model_name, **options
|
||||
)
|
||||
if not is_query:
|
||||
sparse_embedding = list(sparse_embedding_model_inst.embed(documents=[text]))[0]
|
||||
@@ -901,7 +972,7 @@ class AsyncQdrantFastembedMixin(AsyncQdrantBase):
|
||||
)
|
||||
elif model_name in _LATE_INTERACTION_EMBEDDING_MODELS:
|
||||
li_embedding_model_inst = self._get_or_init_late_interaction_model(
|
||||
model_name=model_name, **document.options or {}
|
||||
model_name=model_name, **options
|
||||
)
|
||||
if not is_query:
|
||||
embedding = list(li_embedding_model_inst.embed(documents=[text]))[0].tolist()
|
||||
@@ -910,3 +981,27 @@ class AsyncQdrantFastembedMixin(AsyncQdrantBase):
|
||||
return embedding
|
||||
else:
|
||||
raise ValueError(f"{model_name} is not among supported models")
|
||||
|
||||
def _embed_image(self, image: models.Image) -> NumericVector:
|
||||
"""Embed an image using the specified embedding model
|
||||
|
||||
Args:
|
||||
image: Image to embed
|
||||
|
||||
Returns:
|
||||
NumericVector: Image's embedding
|
||||
|
||||
Raises:
|
||||
ValueError: If model is not supported
|
||||
"""
|
||||
model_name = image.model
|
||||
if model_name in _IMAGE_EMBEDDING_MODELS:
|
||||
embedding_model_inst = self._get_or_init_image_model(
|
||||
model_name=model_name, **image.options or {}
|
||||
)
|
||||
image_data = base64.b64decode(image.image)
|
||||
with io.BytesIO(image_data) as buffer:
|
||||
with PilImage.open(buffer) as image:
|
||||
embedding = list(embedding_model_inst.embed(images=[image]))[0].tolist()
|
||||
return embedding
|
||||
raise ValueError(f"{model_name} is not among supported models")
|
||||
|
||||
8
qdrant_client/embed/common.py
Normal file
8
qdrant_client/embed/common.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from typing import Set, Type, Tuple
|
||||
|
||||
from qdrant_client.http import models
|
||||
|
||||
INFERENCE_OBJECT_NAMES: Set[str] = {"Document", "Image", "InferenceObject"}
|
||||
INFERENCE_OBJECT_TYPES: Tuple[
|
||||
Type[models.Document], Type[models.Image], Type[models.InferenceObject]
|
||||
] = (models.Document, models.Image, models.InferenceObject)
|
||||
@@ -4,6 +4,7 @@ from typing import Union, List, Optional, Iterable
|
||||
from pydantic import BaseModel
|
||||
|
||||
from qdrant_client._pydantic_compat import model_fields_set
|
||||
from qdrant_client.embed.common import INFERENCE_OBJECT_TYPES
|
||||
from qdrant_client.embed.schema_parser import ModelSchemaParser
|
||||
|
||||
from qdrant_client.embed.utils import convert_paths, Path
|
||||
@@ -112,7 +113,7 @@ class InspectorEmbed:
|
||||
if model is None:
|
||||
return []
|
||||
|
||||
if isinstance(model, models.Document):
|
||||
if isinstance(model, INFERENCE_OBJECT_TYPES):
|
||||
return [accum]
|
||||
|
||||
if isinstance(model, BaseModel):
|
||||
@@ -132,7 +133,7 @@ class InspectorEmbed:
|
||||
if not isinstance(current_model, BaseModel):
|
||||
continue
|
||||
|
||||
if isinstance(current_model, models.Document):
|
||||
if isinstance(current_model, INFERENCE_OBJECT_TYPES):
|
||||
found_paths.append(accum)
|
||||
|
||||
found_paths.extend(inspect_recursive(current_model, accum))
|
||||
@@ -157,7 +158,7 @@ class InspectorEmbed:
|
||||
if not isinstance(current_model, BaseModel):
|
||||
continue
|
||||
|
||||
if isinstance(current_model, models.Document):
|
||||
if isinstance(current_model, INFERENCE_OBJECT_TYPES):
|
||||
found_paths.append(accum)
|
||||
|
||||
found_paths.extend(inspect_recursive(current_model, accum))
|
||||
|
||||
@@ -2,9 +2,7 @@ from typing import Union, List, Dict
|
||||
|
||||
from pydantic import StrictFloat, StrictStr
|
||||
|
||||
from qdrant_client.grpc import SparseVector
|
||||
from qdrant_client.http.models import ExtendedPointId
|
||||
from qdrant_client.models import Document # type: ignore[attr-defined]
|
||||
from qdrant_client.http.models import ExtendedPointId, SparseVector
|
||||
|
||||
|
||||
NumericVector = Union[
|
||||
@@ -24,4 +22,4 @@ NumericVectorStruct = Union[
|
||||
Dict[StrictStr, NumericVector],
|
||||
]
|
||||
|
||||
__all__ = ["Document", "NumericVector", "NumericVectorInput", "NumericVectorStruct"]
|
||||
__all__ = ["NumericVector", "NumericVectorInput", "NumericVectorStruct"]
|
||||
|
||||
@@ -67,6 +67,7 @@ class ModelSchemaParser:
|
||||
"""
|
||||
|
||||
CACHE_PATH = "_inspection_cache.py"
|
||||
INFERENCE_OBJECT_NAMES = {"Document", "Image"}
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._defs: Dict[str, Union[Dict[str, Any], List[Dict[str, Any]]]] = deepcopy(DEFS) # type: ignore[arg-type]
|
||||
@@ -159,7 +160,7 @@ class ModelSchemaParser:
|
||||
if not isinstance(schema, dict):
|
||||
return document_paths
|
||||
|
||||
if "title" in schema and schema["title"] == "Document":
|
||||
if "title" in schema and schema["title"] in self.INFERENCE_OBJECT_NAMES:
|
||||
document_paths.append(current_path)
|
||||
return document_paths
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from typing import Union, List, Optional, Iterable
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from qdrant_client.embed.common import INFERENCE_OBJECT_TYPES
|
||||
from qdrant_client.embed.schema_parser import ModelSchemaParser
|
||||
from qdrant_client.embed.utils import Path
|
||||
from qdrant_client.http import models
|
||||
@@ -41,7 +42,7 @@ class Inspector:
|
||||
return False
|
||||
|
||||
def _inspect_model(self, model: BaseModel, paths: Optional[List[Path]] = None) -> bool:
|
||||
if isinstance(model, models.Document):
|
||||
if isinstance(model, INFERENCE_OBJECT_TYPES):
|
||||
return True
|
||||
|
||||
paths = (
|
||||
@@ -80,7 +81,7 @@ class Inspector:
|
||||
if model is None:
|
||||
return False
|
||||
|
||||
if isinstance(model, models.Document):
|
||||
if isinstance(model, INFERENCE_OBJECT_TYPES):
|
||||
return True
|
||||
|
||||
if isinstance(model, BaseModel):
|
||||
@@ -98,7 +99,7 @@ class Inspector:
|
||||
|
||||
elif isinstance(model, list):
|
||||
for current_model in model:
|
||||
if isinstance(current_model, models.Document):
|
||||
if isinstance(current_model, INFERENCE_OBJECT_TYPES):
|
||||
return True
|
||||
|
||||
if not isinstance(current_model, BaseModel):
|
||||
@@ -121,7 +122,7 @@ class Inspector:
|
||||
for key, values in model.items():
|
||||
values = [values] if not isinstance(values, list) else values
|
||||
for current_model in values:
|
||||
if isinstance(current_model, models.Document):
|
||||
if isinstance(current_model, INFERENCE_OBJECT_TYPES):
|
||||
return True
|
||||
|
||||
if not isinstance(current_model, BaseModel):
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
from qdrant_client.http.models import *
|
||||
from qdrant_client.fastembed_common import *
|
||||
from qdrant_client.embed.models import *
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import base64
|
||||
import io
|
||||
import uuid
|
||||
import warnings
|
||||
from itertools import tee
|
||||
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union, Set, get_args
|
||||
from copy import deepcopy
|
||||
|
||||
|
||||
import numpy as np
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from qdrant_client.client_base import QdrantBase
|
||||
from qdrant_client.conversions import common_types as types
|
||||
from qdrant_client.conversions.conversion import GrpcToRest
|
||||
from qdrant_client.embed.common import INFERENCE_OBJECT_TYPES
|
||||
from qdrant_client.embed.embed_inspector import InspectorEmbed
|
||||
from qdrant_client.embed.models import NumericVector, NumericVectorStruct
|
||||
from qdrant_client.embed.schema_parser import ModelSchemaParser
|
||||
@@ -20,13 +25,21 @@ from qdrant_client.hybrid.fusion import reciprocal_rank_fusion
|
||||
from qdrant_client import grpc
|
||||
|
||||
try:
|
||||
from fastembed import SparseTextEmbedding, TextEmbedding, LateInteractionTextEmbedding
|
||||
from fastembed import (
|
||||
SparseTextEmbedding,
|
||||
TextEmbedding,
|
||||
LateInteractionTextEmbedding,
|
||||
ImageEmbedding,
|
||||
)
|
||||
from fastembed.common import OnnxProvider
|
||||
from PIL import Image as PilImage
|
||||
except ImportError:
|
||||
TextEmbedding = None
|
||||
SparseTextEmbedding = None
|
||||
OnnxProvider = None
|
||||
LateInteractionTextEmbedding = None
|
||||
ImageEmbedding = None
|
||||
PilImage = None
|
||||
|
||||
|
||||
SUPPORTED_EMBEDDING_MODELS: Dict[str, Tuple[int, models.Distance]] = (
|
||||
@@ -60,6 +73,12 @@ _LATE_INTERACTION_EMBEDDING_MODELS: Dict[str, Tuple[int, models.Distance]] = (
|
||||
else {}
|
||||
)
|
||||
|
||||
_IMAGE_EMBEDDING_MODELS: Dict[str, Tuple[int, models.Distance]] = (
|
||||
{model["model"]: model for model in ImageEmbedding.list_supported_models()}
|
||||
if ImageEmbedding
|
||||
else {}
|
||||
)
|
||||
|
||||
|
||||
class QdrantFastembedMixin(QdrantBase):
|
||||
DEFAULT_EMBEDDING_MODEL = "BAAI/bge-small-en"
|
||||
@@ -67,6 +86,7 @@ class QdrantFastembedMixin(QdrantBase):
|
||||
embedding_models: Dict[str, "TextEmbedding"] = {}
|
||||
sparse_embedding_models: Dict[str, "SparseTextEmbedding"] = {}
|
||||
late_interaction_embedding_models: Dict[str, "LateInteractionTextEmbedding"] = {}
|
||||
image_embedding_models: Dict[str, "ImageEmbedding"] = {}
|
||||
_FASTEMBED_INSTALLED: bool
|
||||
|
||||
def __init__(self, parser: ModelSchemaParser, **kwargs: Any):
|
||||
@@ -310,6 +330,34 @@ class QdrantFastembedMixin(QdrantBase):
|
||||
)
|
||||
return cls.late_interaction_embedding_models[model_name]
|
||||
|
||||
@classmethod
|
||||
def _get_or_init_image_model(
|
||||
cls,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
providers: Optional[Sequence["OnnxProvider"]] = None,
|
||||
**kwargs: Any,
|
||||
) -> "ImageEmbedding":
|
||||
if model_name in cls.image_embedding_models:
|
||||
return cls.image_embedding_models[model_name]
|
||||
|
||||
cls._import_fastembed()
|
||||
|
||||
if model_name not in _IMAGE_EMBEDDING_MODELS:
|
||||
raise ValueError(
|
||||
f"Unsupported embedding model: {model_name}. Supported models: {_IMAGE_EMBEDDING_MODELS}"
|
||||
)
|
||||
|
||||
cls.image_embedding_models[model_name] = ImageEmbedding(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=threads,
|
||||
providers=providers,
|
||||
**kwargs,
|
||||
)
|
||||
return cls.image_embedding_models[model_name]
|
||||
|
||||
def _embed_documents(
|
||||
self,
|
||||
documents: Iterable[str],
|
||||
@@ -803,8 +851,9 @@ class QdrantFastembedMixin(QdrantBase):
|
||||
|
||||
return [self._scored_points_to_query_responses(response) for response in responses]
|
||||
|
||||
@staticmethod
|
||||
@classmethod
|
||||
def _resolve_query(
|
||||
cls,
|
||||
query: Union[
|
||||
types.PointId,
|
||||
List[float],
|
||||
@@ -844,10 +893,10 @@ class QdrantFastembedMixin(QdrantBase):
|
||||
)
|
||||
return models.NearestQuery(nearest=query)
|
||||
|
||||
if isinstance(query, models.Document):
|
||||
if isinstance(query, INFERENCE_OBJECT_TYPES):
|
||||
model_name = query.model
|
||||
if model_name is None:
|
||||
raise ValueError("`model` field has to be set explicitly in the `Document`")
|
||||
raise ValueError(f"`model` field has to be set explicitly in the {type(query)}")
|
||||
return models.NearestQuery(nearest=query)
|
||||
|
||||
if query is None:
|
||||
@@ -898,7 +947,7 @@ class QdrantFastembedMixin(QdrantBase):
|
||||
A deepcopy of the method with embedded fields
|
||||
"""
|
||||
if paths is None:
|
||||
if isinstance(model, models.Document):
|
||||
if isinstance(model, INFERENCE_OBJECT_TYPES):
|
||||
return self._embed_raw_data(model, is_query=is_query)
|
||||
model = deepcopy(model)
|
||||
paths = self._embed_inspector.inspect(model)
|
||||
@@ -924,6 +973,35 @@ class QdrantFastembedMixin(QdrantBase):
|
||||
setattr(item, path.current, embeddings[0])
|
||||
return model
|
||||
|
||||
@staticmethod
|
||||
def _resolve_inference_object(data: models.VectorStruct) -> models.VectorStruct:
|
||||
"""Resolve inference object into a model
|
||||
|
||||
Args:
|
||||
data: models.VectorStruct - data to resolve, if it's an inference object, convert it to a proper type,
|
||||
otherwise - keep unchanged
|
||||
|
||||
Returns:
|
||||
models.VectorStruct: resolved data
|
||||
"""
|
||||
|
||||
if not isinstance(data, models.InferenceObject):
|
||||
return data
|
||||
|
||||
model_name = data.model
|
||||
value = data.object
|
||||
options = data.options
|
||||
if model_name in (
|
||||
*SUPPORTED_EMBEDDING_MODELS.keys(),
|
||||
*SUPPORTED_SPARSE_EMBEDDING_MODELS.keys(),
|
||||
*_LATE_INTERACTION_EMBEDDING_MODELS.keys(),
|
||||
):
|
||||
return models.Document(model=model_name, text=value, options=options)
|
||||
if model_name in _IMAGE_EMBEDDING_MODELS:
|
||||
return models.Image(model=model_name, image=value, options=options)
|
||||
|
||||
raise ValueError(f"{model_name} is not among supported models")
|
||||
|
||||
def _embed_raw_data(
|
||||
self,
|
||||
data: models.VectorStruct,
|
||||
@@ -938,8 +1016,12 @@ class QdrantFastembedMixin(QdrantBase):
|
||||
Returns:
|
||||
NumericVectorStruct: Embedded data
|
||||
"""
|
||||
data = self._resolve_inference_object(data)
|
||||
|
||||
if isinstance(data, models.Document):
|
||||
return self._embed_document(data, is_query=is_query)
|
||||
elif isinstance(data, models.Image):
|
||||
return self._embed_image(data)
|
||||
elif isinstance(data, dict):
|
||||
return {
|
||||
key: self._embed_raw_data(value, is_query=is_query) for key, value in data.items()
|
||||
@@ -966,10 +1048,9 @@ class QdrantFastembedMixin(QdrantBase):
|
||||
"""
|
||||
model_name = document.model
|
||||
text = document.text
|
||||
options = document.options or {}
|
||||
if model_name in SUPPORTED_EMBEDDING_MODELS:
|
||||
embedding_model_inst = self._get_or_init_model(
|
||||
model_name=model_name, **(document.options or {})
|
||||
)
|
||||
embedding_model_inst = self._get_or_init_model(model_name=model_name, **options)
|
||||
if not is_query:
|
||||
embedding = list(embedding_model_inst.embed(documents=[text]))[0].tolist()
|
||||
else:
|
||||
@@ -977,7 +1058,7 @@ class QdrantFastembedMixin(QdrantBase):
|
||||
return embedding
|
||||
elif model_name in SUPPORTED_SPARSE_EMBEDDING_MODELS:
|
||||
sparse_embedding_model_inst = self._get_or_init_sparse_model(
|
||||
model_name=model_name, **(document.options or {})
|
||||
model_name=model_name, **options
|
||||
)
|
||||
if not is_query:
|
||||
sparse_embedding = list(sparse_embedding_model_inst.embed(documents=[text]))[0]
|
||||
@@ -989,7 +1070,7 @@ class QdrantFastembedMixin(QdrantBase):
|
||||
)
|
||||
elif model_name in _LATE_INTERACTION_EMBEDDING_MODELS:
|
||||
li_embedding_model_inst = self._get_or_init_late_interaction_model(
|
||||
model_name=model_name, **(document.options or {})
|
||||
model_name=model_name, **options
|
||||
)
|
||||
if not is_query:
|
||||
embedding = list(li_embedding_model_inst.embed(documents=[text]))[0].tolist()
|
||||
@@ -998,3 +1079,28 @@ class QdrantFastembedMixin(QdrantBase):
|
||||
return embedding
|
||||
else:
|
||||
raise ValueError(f"{model_name} is not among supported models")
|
||||
|
||||
def _embed_image(self, image: models.Image) -> NumericVector:
|
||||
"""Embed an image using the specified embedding model
|
||||
|
||||
Args:
|
||||
image: Image to embed
|
||||
|
||||
Returns:
|
||||
NumericVector: Image's embedding
|
||||
|
||||
Raises:
|
||||
ValueError: If model is not supported
|
||||
"""
|
||||
model_name = image.model
|
||||
if model_name in _IMAGE_EMBEDDING_MODELS:
|
||||
embedding_model_inst = self._get_or_init_image_model(
|
||||
model_name=model_name, **(image.options or {})
|
||||
)
|
||||
image_data = base64.b64decode(image.image)
|
||||
with io.BytesIO(image_data) as buffer:
|
||||
with PilImage.open(buffer) as image:
|
||||
embedding = list(embedding_model_inst.embed(images=[image]))[0].tolist()
|
||||
return embedding
|
||||
|
||||
raise ValueError(f"{model_name} is not among supported models")
|
||||
|
||||
1
tests/embed_tests/misc/test_image.txt
Normal file
1
tests/embed_tests/misc/test_image.txt
Normal file
File diff suppressed because one or more lines are too long
@@ -1,4 +1,5 @@
|
||||
from typing import Optional, List
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
@@ -17,6 +18,10 @@ DENSE_DIM = 384
|
||||
SPARSE_MODEL_NAME = "Qdrant/bm42-all-minilm-l6-v2-attentions"
|
||||
COLBERT_MODEL_NAME = "colbert-ir/colbertv2.0"
|
||||
COLBERT_DIM = 128
|
||||
DENSE_IMAGE_MODEL_NAME = "Qdrant/resnet50-onnx"
|
||||
DENSE_IMAGE_DIM = 2048
|
||||
|
||||
TEST_IMAGE_PATH = Path(__file__).parent / "misc" / "test_image.txt"
|
||||
|
||||
|
||||
# todo: remove once we don't store models in class variables
|
||||
@@ -716,17 +721,21 @@ def test_propagate_options(prefer_grpc):
|
||||
if not local_client._FASTEMBED_INSTALLED:
|
||||
pytest.skip("FastEmbed is not installed, skipping")
|
||||
remote_client = QdrantClient(prefer_grpc=prefer_grpc)
|
||||
|
||||
dense_doc_1 = models.Document(
|
||||
text="hello world", model=DENSE_MODEL_NAME, options={"lazy_load": True}
|
||||
)
|
||||
sparse_doc_1 = models.Document(
|
||||
text="hello world", model=SPARSE_MODEL_NAME, options={"lazy_load": True}
|
||||
)
|
||||
|
||||
multi_doc_1 = models.Document(
|
||||
text="hello world", model=COLBERT_MODEL_NAME, options={"lazy_load": True}
|
||||
)
|
||||
with open(TEST_IMAGE_PATH, "r") as f:
|
||||
base64_string = f.read()
|
||||
|
||||
dense_image_1 = models.Image(
|
||||
image=base64_string, model=DENSE_IMAGE_MODEL_NAME, options={"lazy_load": True}
|
||||
)
|
||||
|
||||
points = [
|
||||
models.PointStruct(
|
||||
@@ -735,6 +744,7 @@ def test_propagate_options(prefer_grpc):
|
||||
"text": dense_doc_1,
|
||||
"multi-text": multi_doc_1,
|
||||
"sparse-text": sparse_doc_1,
|
||||
"image": dense_image_1,
|
||||
},
|
||||
)
|
||||
]
|
||||
@@ -748,6 +758,7 @@ def test_propagate_options(prefer_grpc):
|
||||
comparator=models.MultiVectorComparator.MAX_SIM
|
||||
),
|
||||
),
|
||||
"image": models.VectorParams(size=DENSE_IMAGE_DIM, distance=models.Distance.COSINE),
|
||||
}
|
||||
sparse_vectors_config = {
|
||||
"sparse-text": models.SparseVectorParams(modifier=models.Modifier.IDF)
|
||||
@@ -772,3 +783,196 @@ def test_propagate_options(prefer_grpc):
|
||||
assert local_client.embedding_models[DENSE_MODEL_NAME].model.lazy_load
|
||||
assert local_client.sparse_embedding_models[SPARSE_MODEL_NAME].model.lazy_load
|
||||
assert local_client.late_interaction_embedding_models[COLBERT_MODEL_NAME].model.lazy_load
|
||||
assert local_client.image_embedding_models[DENSE_IMAGE_MODEL_NAME].model.lazy_load
|
||||
|
||||
local_client.embedding_models.clear()
|
||||
local_client.sparse_embedding_models.clear()
|
||||
local_client.late_interaction_embedding_models.clear()
|
||||
local_client.image_embedding_models.clear()
|
||||
|
||||
inference_object_dense_doc_1 = models.InferenceObject(
|
||||
object="hello world",
|
||||
model=DENSE_MODEL_NAME,
|
||||
options={"lazy_load": True},
|
||||
)
|
||||
|
||||
inference_object_sparse_doc_1 = models.InferenceObject(
|
||||
object="hello world",
|
||||
model=SPARSE_MODEL_NAME,
|
||||
options={"lazy_load": True},
|
||||
)
|
||||
|
||||
inference_object_multi_doc_1 = models.InferenceObject(
|
||||
object="hello world",
|
||||
model=COLBERT_MODEL_NAME,
|
||||
options={"lazy_load": True},
|
||||
)
|
||||
|
||||
inference_object_dense_image_1 = models.InferenceObject(
|
||||
object=base64_string,
|
||||
model=DENSE_IMAGE_MODEL_NAME,
|
||||
options={"lazy_load": True},
|
||||
)
|
||||
|
||||
points = [
|
||||
models.PointStruct(
|
||||
id=2,
|
||||
vector={
|
||||
"text": inference_object_dense_doc_1,
|
||||
"multi-text": inference_object_multi_doc_1,
|
||||
"sparse-text": inference_object_sparse_doc_1,
|
||||
"image": inference_object_dense_image_1,
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
local_client.upsert(COLLECTION_NAME, points)
|
||||
remote_client.upsert(COLLECTION_NAME, points)
|
||||
|
||||
assert local_client.embedding_models[DENSE_MODEL_NAME].model.lazy_load
|
||||
assert local_client.sparse_embedding_models[SPARSE_MODEL_NAME].model.lazy_load
|
||||
assert local_client.late_interaction_embedding_models[COLBERT_MODEL_NAME].model.lazy_load
|
||||
assert local_client.image_embedding_models[DENSE_IMAGE_MODEL_NAME].model.lazy_load
|
||||
|
||||
|
||||
@pytest.mark.parametrize("prefer_grpc", [True, False])
|
||||
def test_image(prefer_grpc):
|
||||
local_client = QdrantClient(":memory:")
|
||||
if not local_client._FASTEMBED_INSTALLED:
|
||||
pytest.skip("FastEmbed is not installed, skipping")
|
||||
remote_client = QdrantClient(prefer_grpc=prefer_grpc)
|
||||
local_kwargs = {}
|
||||
local_client._client.upsert = arg_interceptor(local_client._client.upsert, local_kwargs)
|
||||
|
||||
with open(TEST_IMAGE_PATH, "r") as f:
|
||||
base64_string = f.read()
|
||||
|
||||
dense_image_1 = models.Image(image=base64_string, model=DENSE_IMAGE_MODEL_NAME)
|
||||
points = [
|
||||
models.PointStruct(id=i, vector=dense_img) for i, dense_img in enumerate([dense_image_1])
|
||||
]
|
||||
|
||||
for client in local_client, remote_client:
|
||||
if client.collection_exists(COLLECTION_NAME):
|
||||
client.delete_collection(COLLECTION_NAME)
|
||||
vector_params = models.VectorParams(size=DENSE_IMAGE_DIM, distance=models.Distance.COSINE)
|
||||
client.create_collection(COLLECTION_NAME, vectors_config=vector_params)
|
||||
client.upsert(COLLECTION_NAME, points)
|
||||
|
||||
vec_points = local_kwargs["points"]
|
||||
assert all([isinstance(vec_point.vector, list) for vec_point in vec_points])
|
||||
assert local_client.scroll(COLLECTION_NAME, limit=1, with_vectors=True)[0]
|
||||
compare_collections(
|
||||
local_client,
|
||||
remote_client,
|
||||
num_vectors=10,
|
||||
collection_name=COLLECTION_NAME,
|
||||
)
|
||||
|
||||
local_client.query_points(COLLECTION_NAME, dense_image_1)
|
||||
remote_client.query_points(COLLECTION_NAME, dense_image_1)
|
||||
|
||||
local_client.delete_collection(COLLECTION_NAME)
|
||||
remote_client.delete_collection(COLLECTION_NAME)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("prefer_grpc", [True, False])
|
||||
def test_inference_object(prefer_grpc):
|
||||
local_client = QdrantClient(":memory:")
|
||||
if not local_client._FASTEMBED_INSTALLED:
|
||||
pytest.skip("FastEmbed is not installed, skipping")
|
||||
remote_client = QdrantClient(prefer_grpc=prefer_grpc)
|
||||
local_kwargs = {}
|
||||
local_client._client.upsert = arg_interceptor(local_client._client.upsert, local_kwargs)
|
||||
|
||||
with open(TEST_IMAGE_PATH, "r") as f:
|
||||
base64_string = f.read()
|
||||
|
||||
inference_object_dense_doc_1 = models.InferenceObject(
|
||||
object="hello world",
|
||||
model=DENSE_MODEL_NAME,
|
||||
options={"lazy_load": True},
|
||||
)
|
||||
|
||||
inference_object_sparse_doc_1 = models.InferenceObject(
|
||||
object="hello world",
|
||||
model=SPARSE_MODEL_NAME,
|
||||
options={"lazy_load": True},
|
||||
)
|
||||
|
||||
inference_object_multi_doc_1 = models.InferenceObject(
|
||||
object="hello world",
|
||||
model=COLBERT_MODEL_NAME,
|
||||
options={"lazy_load": True},
|
||||
)
|
||||
|
||||
inference_object_dense_image_1 = models.InferenceObject(
|
||||
object=base64_string,
|
||||
model=DENSE_IMAGE_MODEL_NAME,
|
||||
options={"lazy_load": True},
|
||||
)
|
||||
|
||||
points = [
|
||||
models.PointStruct(
|
||||
id=1,
|
||||
vector={
|
||||
"text": inference_object_dense_doc_1,
|
||||
"multi-text": inference_object_multi_doc_1,
|
||||
"sparse-text": inference_object_sparse_doc_1,
|
||||
"image": inference_object_dense_image_1,
|
||||
},
|
||||
)
|
||||
]
|
||||
vectors_config = {
|
||||
"text": models.VectorParams(size=DENSE_DIM, distance=models.Distance.COSINE),
|
||||
"multi-text": models.VectorParams(
|
||||
size=COLBERT_DIM,
|
||||
distance=models.Distance.COSINE,
|
||||
multivector_config=models.MultiVectorConfig(
|
||||
comparator=models.MultiVectorComparator.MAX_SIM
|
||||
),
|
||||
),
|
||||
"image": models.VectorParams(size=DENSE_IMAGE_DIM, distance=models.Distance.COSINE),
|
||||
}
|
||||
sparse_vectors_config = {
|
||||
"sparse-text": models.SparseVectorParams(modifier=models.Modifier.IDF)
|
||||
}
|
||||
|
||||
for client in local_client, remote_client:
|
||||
if client.collection_exists(COLLECTION_NAME):
|
||||
client.delete_collection(COLLECTION_NAME)
|
||||
client.create_collection(
|
||||
COLLECTION_NAME,
|
||||
vectors_config=vectors_config,
|
||||
sparse_vectors_config=sparse_vectors_config,
|
||||
)
|
||||
client.upsert(COLLECTION_NAME, points)
|
||||
|
||||
vec_points = local_kwargs["points"]
|
||||
vector = vec_points[0].vector
|
||||
assert isinstance(vector["text"], list)
|
||||
assert isinstance(vector["multi-text"], list)
|
||||
assert isinstance(vector["sparse-text"], models.SparseVector)
|
||||
assert isinstance(vector["image"], list)
|
||||
assert local_client.scroll(COLLECTION_NAME, limit=1, with_vectors=True)[0]
|
||||
compare_collections(
|
||||
local_client,
|
||||
remote_client,
|
||||
num_vectors=10,
|
||||
collection_name=COLLECTION_NAME,
|
||||
)
|
||||
|
||||
local_client.query_points(COLLECTION_NAME, inference_object_dense_doc_1, using="text")
|
||||
remote_client.query_points(COLLECTION_NAME, inference_object_dense_doc_1, using="text")
|
||||
|
||||
local_client.query_points(COLLECTION_NAME, inference_object_sparse_doc_1, using="sparse-text")
|
||||
remote_client.query_points(COLLECTION_NAME, inference_object_sparse_doc_1, using="sparse-text")
|
||||
|
||||
local_client.query_points(COLLECTION_NAME, inference_object_multi_doc_1, using="multi-text")
|
||||
remote_client.query_points(COLLECTION_NAME, inference_object_multi_doc_1, using="multi-text")
|
||||
|
||||
local_client.query_points(COLLECTION_NAME, inference_object_dense_image_1, using="image")
|
||||
remote_client.query_points(COLLECTION_NAME, inference_object_dense_image_1, using="image")
|
||||
|
||||
local_client.delete_collection(COLLECTION_NAME)
|
||||
remote_client.delete_collection(COLLECTION_NAME)
|
||||
|
||||
Reference in New Issue
Block a user