mirror of
https://github.com/qdrant/qdrant-client.git
synced 2026-07-23 11:11:01 -05:00
deprecate: completely replace fastembed bm25 with qdrant core bm25 in hosted qdrant (#1166)
* deprecate: completely replace fastembed bm25 with qdrant core bm25 * fix: fix param name in embedder, accept lower case bm25 model name * tests: enable skipped tests, add bm25 core usage tests * fix: fix ci, run fastembed tests in integration tests * fix: fix pip uninstall prompt * tests: log message when running tests wo fastembed
This commit is contained in:
6
.github/workflows/integration-tests.yml
vendored
6
.github/workflows/integration-tests.yml
vendored
@@ -69,12 +69,6 @@ jobs:
|
||||
run: |
|
||||
QDRANT_VERSION='v1.16.3' ./tests/integration-tests.sh
|
||||
shell: bash
|
||||
- name: Run fastembed tests without fastembed
|
||||
run: |
|
||||
pip3 uninstall fastembed -y
|
||||
pytest -x tests/test_fastembed.py
|
||||
pytest -x tests/embed_tests/test_local_inference.py
|
||||
shell: bash
|
||||
- name: Upload failed snapshot if tests fail
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from qdrant_client.http import models
|
||||
from qdrant_client.embed.models import NumericVector
|
||||
|
||||
|
||||
class BuiltinEmbedder:
|
||||
_SUPPORTED_MODELS = ("Qdrant/Bm25",)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
def embed(
|
||||
self,
|
||||
model_name: str,
|
||||
texts: list[str] | None = None,
|
||||
options: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> NumericVector:
|
||||
if texts is None:
|
||||
if "images" in kwargs:
|
||||
raise ValueError(
|
||||
"Image processing is only available with cloud inference of FastEmbed"
|
||||
)
|
||||
|
||||
raise ValueError("Texts must be provided for the inference")
|
||||
|
||||
if not self.is_supported_sparse_model(model_name):
|
||||
raise ValueError(
|
||||
f"Model {model_name} is not supported in {self.__class__.__name__}. "
|
||||
f"Did you forget to enable cloud inference or install FastEmbed for local inference?"
|
||||
)
|
||||
|
||||
return [models.Document(text=text, options=options, model=model_name) for text in texts]
|
||||
|
||||
@classmethod
|
||||
def is_supported_text_model(cls, model_name: str) -> bool:
|
||||
"""Mock embedder interface, only sparse text model Qdrant/Bm25 is supported
|
||||
|
||||
Args:
|
||||
model_name (str): The name of the model to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the model is supported, False otherwise.
|
||||
"""
|
||||
return False # currently only Qdrant/Bm25 is supported
|
||||
|
||||
@classmethod
|
||||
def is_supported_image_model(cls, model_name: str) -> bool:
|
||||
"""Mock embedder interface, only sparse text model Qdrant/Bm25 is supported
|
||||
|
||||
Args:
|
||||
model_name (str): The name of the model to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the model is supported, False otherwise.
|
||||
"""
|
||||
return False # currently only Qdrant/Bm25 is supported
|
||||
|
||||
@classmethod
|
||||
def is_supported_late_interaction_text_model(cls, model_name: str) -> bool:
|
||||
"""Mock embedder interface, only sparse text model Qdrant/Bm25 is supported
|
||||
|
||||
Args:
|
||||
model_name (str): The name of the model to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the model is supported, False otherwise.
|
||||
"""
|
||||
return False # currently only Qdrant/Bm25 is supported
|
||||
|
||||
@classmethod
|
||||
def is_supported_late_interaction_multimodal_model(cls, model_name: str) -> bool:
|
||||
"""Mock embedder interface, only sparse text model Qdrant/Bm25 is supported
|
||||
|
||||
Args:
|
||||
model_name (str): The name of the model to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the model is supported, False otherwise.
|
||||
"""
|
||||
return False # currently only Qdrant/Bm25 is supported
|
||||
|
||||
@classmethod
|
||||
def is_supported_sparse_model(cls, model_name: str) -> bool:
|
||||
"""Checks if the model is supported. Only `Qdrant/Bm25` is supported
|
||||
|
||||
Args:
|
||||
model_name (str): The name of the model to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the model is supported, False otherwise.
|
||||
"""
|
||||
return model_name.lower() in [model.lower() for model in cls._SUPPORTED_MODELS]
|
||||
@@ -27,7 +27,9 @@ class ModelInstance(BaseModel, Generic[T], arbitrary_types_allowed=True): # typ
|
||||
|
||||
|
||||
class Embedder:
|
||||
def __init__(self, threads: int | None = None, **kwargs: Any) -> None:
|
||||
def __init__(
|
||||
self, threads: int | None = None, use_core_bm25: bool = True, **kwargs: Any
|
||||
) -> None:
|
||||
self.embedding_models: dict[str, list[ModelInstance[TextEmbedding]]] = defaultdict(list)
|
||||
self.sparse_embedding_models: dict[str, list[ModelInstance[SparseTextEmbedding]]] = (
|
||||
defaultdict(list)
|
||||
@@ -42,6 +44,7 @@ class Embedder:
|
||||
str, list[ModelInstance[LateInteractionMultimodalEmbedding]]
|
||||
] = defaultdict(list)
|
||||
self._threads = threads
|
||||
self._use_core_bm25 = use_core_bm25
|
||||
|
||||
def get_or_init_model(
|
||||
self,
|
||||
@@ -227,7 +230,7 @@ class Embedder:
|
||||
options: dict[str, Any] | None = None,
|
||||
is_query: bool = False,
|
||||
batch_size: int = 8,
|
||||
) -> NumericVector:
|
||||
) -> NumericVector | list[models.Document]:
|
||||
if (texts is None) is (images is None):
|
||||
raise ValueError("Either documents or images should be provided")
|
||||
|
||||
@@ -237,7 +240,7 @@ class Embedder:
|
||||
embeddings = self._embed_dense_text(
|
||||
texts, model_name, options, is_query, batch_size
|
||||
)
|
||||
elif FastEmbedMisc.is_supported_sparse_model(model_name):
|
||||
elif self.is_supported_sparse_model(model_name):
|
||||
embeddings = self._embed_sparse_text(
|
||||
texts, model_name, options, is_query, batch_size
|
||||
)
|
||||
@@ -294,7 +297,12 @@ class Embedder:
|
||||
options: dict[str, Any] | None,
|
||||
is_query: bool,
|
||||
batch_size: int,
|
||||
) -> list[models.SparseVector]:
|
||||
) -> list[models.SparseVector] | list[models.Document]:
|
||||
if self._use_core_bm25 and model_name.lower() == "Qdrant/bm25".lower():
|
||||
return [
|
||||
models.Document(text=text, model=model_name, options=options) for text in texts
|
||||
]
|
||||
|
||||
embedding_model_inst = self.get_or_init_sparse_model(
|
||||
model_name=model_name, **options or {}
|
||||
)
|
||||
@@ -434,8 +442,7 @@ class Embedder:
|
||||
"""
|
||||
return FastEmbedMisc.is_supported_late_interaction_multimodal_model(model_name)
|
||||
|
||||
@classmethod
|
||||
def is_supported_sparse_model(cls, model_name: str) -> bool:
|
||||
def is_supported_sparse_model(self, model_name: str) -> bool:
|
||||
"""Check if model is supported by fastembed
|
||||
|
||||
Args:
|
||||
@@ -444,4 +451,6 @@ class Embedder:
|
||||
Returns:
|
||||
bool: True if the model is supported, False otherwise.
|
||||
"""
|
||||
if self._use_core_bm25 and model_name.lower() == "Qdrant/bm25".lower():
|
||||
return True
|
||||
return FastEmbedMisc.is_supported_sparse_model(model_name)
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import Iterable, Any, Type, get_args
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from qdrant_client.embed.builtin_embedder import BuiltinEmbedder
|
||||
from qdrant_client.http import models
|
||||
from qdrant_client.embed.common import INFERENCE_OBJECT_TYPES
|
||||
from qdrant_client.embed.embed_inspector import InspectorEmbed
|
||||
@@ -51,14 +50,13 @@ class ModelEmbedder:
|
||||
**kwargs: Any,
|
||||
):
|
||||
self._batch_accumulator: dict[str, list[INFERENCE_OBJECT_TYPES]] = {}
|
||||
self._embed_storage: dict[str, list[NumericVector]] = {}
|
||||
self._embed_storage: dict[str, list[NumericVector] | list[models.Document]] = {}
|
||||
self._embed_inspector = InspectorEmbed(parser=parser)
|
||||
self._is_builtin_embedder_available = self._check_builtin_embedder_availability(
|
||||
is_local_mode, server_version
|
||||
)
|
||||
self.embedder = (
|
||||
Embedder(**kwargs) if FastEmbedMisc.is_installed() else BuiltinEmbedder(**kwargs)
|
||||
)
|
||||
is_local_mode=is_local_mode, server_version=server_version
|
||||
) # check if bm25 is available in Qdrant core
|
||||
self._fastembed_available = FastEmbedMisc.is_installed()
|
||||
self.embedder = Embedder(use_core_bm25=self._is_builtin_embedder_available, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _check_builtin_embedder_availability(
|
||||
@@ -139,12 +137,7 @@ class ModelEmbedder:
|
||||
if len(raw_models) < batch_size:
|
||||
is_small = True
|
||||
|
||||
if (
|
||||
isinstance(self.embedder, BuiltinEmbedder)
|
||||
or parallel is None
|
||||
or parallel == 1
|
||||
or is_small
|
||||
):
|
||||
if parallel is None or parallel == 1 or is_small:
|
||||
for batch in iter_batch(raw_models, batch_size):
|
||||
yield from self.embed_models_batch(batch, inference_batch_size=batch_size)
|
||||
else:
|
||||
@@ -211,7 +204,13 @@ class ModelEmbedder:
|
||||
is_query: bool = False,
|
||||
accumulating: bool = False,
|
||||
inference_batch_size: int | None = None,
|
||||
) -> dict[str, BaseModel] | dict[str, NumericVector] | BaseModel | NumericVector:
|
||||
) -> (
|
||||
dict[str, BaseModel]
|
||||
| dict[str, NumericVector]
|
||||
| BaseModel
|
||||
| NumericVector
|
||||
| models.Document
|
||||
):
|
||||
"""Embed model's fields requiring inference
|
||||
|
||||
Args:
|
||||
@@ -326,7 +325,7 @@ class ModelEmbedder:
|
||||
|
||||
def _drain_accumulator(
|
||||
self, data: models.VectorStruct, is_query: bool, inference_batch_size: int = 8
|
||||
) -> NumericVectorStruct:
|
||||
) -> NumericVectorStruct | models.Document:
|
||||
"""Drain accumulator and replaces inference objects with computed embeddings
|
||||
It is assumed objects are traversed in the same order as they were added to the accumulator
|
||||
|
||||
@@ -348,7 +347,7 @@ class ModelEmbedder:
|
||||
|
||||
if isinstance(data, list):
|
||||
for i, value in enumerate(data):
|
||||
if not isinstance(value, get_args(INFERENCE_OBJECT_TYPES)): # if value is vector
|
||||
if not isinstance(value, get_args(INFERENCE_OBJECT_TYPES)): # if value is a vector
|
||||
return data
|
||||
|
||||
data[i] = self._drain_accumulator(
|
||||
@@ -378,9 +377,11 @@ class ModelEmbedder:
|
||||
|
||||
def embed(
|
||||
objects: list[INFERENCE_OBJECT_TYPES], model_name: str, batch_size: int
|
||||
) -> list[NumericVector]:
|
||||
) -> list[NumericVector] | list[models.Document]:
|
||||
"""
|
||||
Assemble batches by options and data type based groups, embeds and return embeddings in the original order
|
||||
Assemble batches by options and data type based groups, embeds and return embeddings in the original order.
|
||||
If models.Document model is bm25 and Qdrant version is 1.15.3 or higher, return the document without changes
|
||||
to be processed by Qdrant itself.
|
||||
"""
|
||||
unique_options: list[dict[str, Any]] = []
|
||||
unique_options_is_text: list[bool] = [] # multimodal models can have both text
|
||||
@@ -436,13 +437,10 @@ class ModelEmbedder:
|
||||
self.embedder.is_supported_late_interaction_multimodal_model(model),
|
||||
)
|
||||
):
|
||||
if isinstance(self.embedder, BuiltinEmbedder):
|
||||
raise ValueError(
|
||||
f"{model} is not among supported models. "
|
||||
f"Have you forgotten to set `cloud_inference` or install `fastembed` for local inference?"
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"{model} is not among supported models")
|
||||
raise ValueError(
|
||||
f"{model} is not found among supported models."
|
||||
f"Check if `cloud_inference` is set to True or `fastembed` is installed (for local inference)?"
|
||||
)
|
||||
|
||||
for model, data in self._batch_accumulator.items():
|
||||
self._embed_storage[model] = embed(
|
||||
@@ -450,14 +448,14 @@ class ModelEmbedder:
|
||||
)
|
||||
self._batch_accumulator.clear()
|
||||
|
||||
def _next_embed(self, model_name: str) -> NumericVector:
|
||||
def _next_embed(self, model_name: str) -> NumericVector | models.Document:
|
||||
"""Get next computed embedding from embedded batch
|
||||
|
||||
Args:
|
||||
model_name: str - retrieve embedding from the storage by this model name
|
||||
|
||||
Returns:
|
||||
NumericVector: computed embedding
|
||||
NumericVector | models.Document : computed embedding
|
||||
"""
|
||||
return self._embed_storage[model_name].pop(0)
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ class FastEmbedMisc:
|
||||
# If it's not, ask the user to install it
|
||||
raise ImportError(
|
||||
"fastembed is not installed."
|
||||
" Please install it to enable fast vector indexing with `pip install fastembed`."
|
||||
" Please install it to compute embedding for document implicitly with `pip install fastembed`."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -20,7 +20,6 @@ from qdrant_client.fastembed_common import (
|
||||
ImageEmbedding,
|
||||
)
|
||||
|
||||
|
||||
COLLECTION_NAME = "inference_collection"
|
||||
DENSE_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
|
||||
DENSE_DIM = 384
|
||||
@@ -1516,7 +1515,6 @@ def test_upload_mixed_batches_upload_collection(parallel, cached_embeddings):
|
||||
# endregion
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_upsert_batch_with_different_options():
|
||||
bm25_name = "Qdrant/bm25"
|
||||
local_client = QdrantClient(":memory:")
|
||||
@@ -1555,7 +1553,6 @@ def test_upsert_batch_with_different_options():
|
||||
vector={
|
||||
"sparse-text-en": sparse_doc_1,
|
||||
"sparse-text-de": sparse_doc_2,
|
||||
**download_options,
|
||||
},
|
||||
),
|
||||
models.PointStruct(id=1, vector={"sparse-text-en": sparse_doc_3}),
|
||||
@@ -1580,7 +1577,6 @@ def test_upsert_batch_with_different_options():
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_batch_size_propagation():
|
||||
def mock(func, kw_param_storage):
|
||||
def decorated(*args, **kwargs):
|
||||
@@ -1671,7 +1667,7 @@ def mock_late_interaction_multimodal_embedding():
|
||||
qdrant_client.embed.embedder.LateInteractionMultimodalEmbedding = original_class
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
@pytest.mark.skip(reason="colpali is a humongous model")
|
||||
def test_embed_multimodal(mock_late_interaction_multimodal_embedding):
|
||||
mock_cls = mock_late_interaction_multimodal_embedding
|
||||
|
||||
@@ -1735,3 +1731,49 @@ def test_embed_multimodal(mock_late_interaction_multimodal_embedding):
|
||||
assert np.allclose(np_text_vectors[0], mock_cls.text_embedding)
|
||||
assert np.allclose(np_text_vectors[1], mock_cls.text_embedding)
|
||||
assert np.allclose(np_image_vector, mock_cls.image_embedding)
|
||||
|
||||
|
||||
def test_bm25_core():
|
||||
bm25_name = "Qdrant/bm25"
|
||||
local_client = QdrantClient(":memory:")
|
||||
remote_client = QdrantClient()
|
||||
if remote_client.collection_exists(COLLECTION_NAME):
|
||||
remote_client.delete_collection(COLLECTION_NAME)
|
||||
|
||||
remote_kwargs = {}
|
||||
remote_client._client.upsert = arg_interceptor(remote_client._client.upsert, remote_kwargs)
|
||||
|
||||
sparse_doc_1 = models.Document(text="a sunny day", model=bm25_name)
|
||||
sparse_doc_2 = models.Document(
|
||||
text="a rainy day",
|
||||
model=bm25_name,
|
||||
)
|
||||
|
||||
sparse_vectors_config = {
|
||||
"sparse": models.SparseVectorParams(modifier=models.Modifier.IDF),
|
||||
}
|
||||
for client in (local_client, remote_client):
|
||||
client.create_collection(
|
||||
COLLECTION_NAME, vectors_config={}, sparse_vectors_config=sparse_vectors_config
|
||||
)
|
||||
|
||||
points = [
|
||||
models.PointStruct(
|
||||
id=0,
|
||||
vector={
|
||||
"sparse": sparse_doc_1,
|
||||
},
|
||||
),
|
||||
models.PointStruct(id=1, vector={"sparse": sparse_doc_2}),
|
||||
]
|
||||
|
||||
remote_client.upsert(COLLECTION_NAME, points)
|
||||
|
||||
for point in remote_kwargs["points"]:
|
||||
assert isinstance(point.vector["sparse"], models.Document)
|
||||
|
||||
if not local_client._FASTEMBED_INSTALLED:
|
||||
with pytest.raises(ImportError):
|
||||
local_client.upsert(COLLECTION_NAME, points)
|
||||
else:
|
||||
local_client.upsert(COLLECTION_NAME, points)
|
||||
|
||||
@@ -68,6 +68,12 @@ if [[ $PYTEST_EXIT_CODE -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running inference tests without fastembed"
|
||||
|
||||
pip3 uninstall fastembed -y
|
||||
|
||||
pytest -x tests/test_fastembed.py
|
||||
pytest -x tests/embed_tests/test_local_inference.py
|
||||
|
||||
echo "Ok, that is enough"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user