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:
George
2024-11-04 08:37:53 +01:00
committed by George Panchuk
parent 152f7dee4f
commit bf1f8ef1aa
10 changed files with 450 additions and 34 deletions

View File

@@ -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")

View 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)

View File

@@ -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))

View File

@@ -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"]

View File

@@ -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

View File

@@ -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):

View File

@@ -1,2 +1,3 @@
from qdrant_client.http.models import *
from qdrant_client.fastembed_common import *
from qdrant_client.embed.models import *

View File

@@ -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")

File diff suppressed because one or more lines are too long

View File

@@ -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)