Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@ def __init__(
dense_vector_name=dense_vector_name,
sparse_vector_name=sparse_vector_name,
)
# Track if the user provided their own sparse functions. This is to prevent
# them from being overwritten by the lazy-init correction for async clients.
self._user_provided_sparse_doc_fn = sparse_doc_fn is not None
self._user_provided_sparse_query_fn = sparse_query_fn is not None

if (
client is None
Expand Down Expand Up @@ -1545,12 +1549,13 @@ def get_default_sparse_doc_encoder(
) -> SparseEncoderCallable:
"""
Get the default sparse document encoder.
Use old format for backward compatibility if detected.
For async-only clients, assumes new format initially.
Will be auto-corrected on first async operation if collection uses old format.
"""
if self.use_old_sparse_encoder(collection_name):
# Update the sparse vector name to use the old format
self.sparse_vector_name = DEFAULT_SPARSE_VECTOR_NAME_OLD
return default_sparse_encoder("naver/efficient-splade-VI-BT-large-doc")
if self._client is not None:
if self.use_old_sparse_encoder(collection_name):
self.sparse_vector_name = DEFAULT_SPARSE_VECTOR_NAME_OLD
return default_sparse_encoder("naver/efficient-splade-VI-BT-large-doc")

if fastembed_sparse_model is not None:
return fastembed_sparse_encoder(model_name=fastembed_sparse_model)
Expand All @@ -1564,12 +1569,16 @@ def get_default_sparse_query_encoder(
) -> SparseEncoderCallable:
"""
Get the default sparse query encoder.
Use old format for backward compatibility if detected.
For async-only clients, assumes new format initially.
Will be auto-corrected on first async operation if collection uses old format.
"""
if self.use_old_sparse_encoder(collection_name):
# Update the sparse vector name to use the old format
self.sparse_vector_name = DEFAULT_SPARSE_VECTOR_NAME_OLD
return default_sparse_encoder("naver/efficient-splade-VI-BT-large-query")
if self._client is not None:
if self.use_old_sparse_encoder(collection_name):
# Update the sparse vector name to use the old format
self.sparse_vector_name = DEFAULT_SPARSE_VECTOR_NAME_OLD
return default_sparse_encoder(
"naver/efficient-splade-VI-BT-large-query"
)

if fastembed_sparse_model is not None:
return fastembed_sparse_encoder(model_name=fastembed_sparse_model)
Expand Down Expand Up @@ -1613,10 +1622,10 @@ def _detect_vector_format(self, collection_name: str) -> None:
async def _adetect_vector_format(self, collection_name: str) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method no longer mirrors the sync version -- maybe an issue?

"""
Asynchronous method to detect and handle old vector formats from existing collections.
- named vs non-named vectors
- new sparse vector field name vs old sparse vector field name
"""
try:
old_sparse_name = self.sparse_vector_name # Store state before detection

collection_info = await self._aclient.get_collection(collection_name)
vectors_config = collection_info.config.params.vectors
sparse_vectors = collection_info.config.params.sparse_vectors or {}
Expand All @@ -1632,18 +1641,49 @@ async def _adetect_vector_format(self, collection_name: str) -> None:
self._legacy_vector_format = True
self.dense_vector_name = LEGACY_UNNAMED_VECTOR

# Detect sparse vector name if any sparse vectors configured
# Detect sparse vector name and correct if necessary
if isinstance(sparse_vectors, dict) and len(sparse_vectors) > 0:
if self.sparse_vector_name in sparse_vectors:
pass
elif DEFAULT_SPARSE_VECTOR_NAME_OLD in sparse_vectors:
self.sparse_vector_name = DEFAULT_SPARSE_VECTOR_NAME_OLD

# If the name changed, our initial assumption was wrong. Correct it.
if self.enable_hybrid and old_sparse_name != self.sparse_vector_name:
self._reinitialize_sparse_encoders()

except Exception as e:
logger.warning(
f"Could not detect vector format for collection {collection_name}: {e}"
)

def _reinitialize_sparse_encoders(self) -> None:
"""Recreate default sparse encoders after vector format detection, respecting user-provided functions."""
if not self.enable_hybrid:
return

# Only override the doc function if the user did NOT provide one
if not self._user_provided_sparse_doc_fn:
if self.sparse_vector_name == DEFAULT_SPARSE_VECTOR_NAME_OLD:
self._sparse_doc_fn = default_sparse_encoder(
"naver/efficient-splade-VI-BT-large-doc"
)
else:
self._sparse_doc_fn = fastembed_sparse_encoder(
model_name=self.fastembed_sparse_model
)

# Only override the query function if the user did NOT provide one
if not self._user_provided_sparse_query_fn:
if self.sparse_vector_name == DEFAULT_SPARSE_VECTOR_NAME_OLD:
self._sparse_query_fn = default_sparse_encoder(
"naver/efficient-splade-VI-BT-large-query"
)
else:
self._sparse_query_fn = fastembed_sparse_encoder(
model_name=self.fastembed_sparse_model
)

def _validate_custom_sharding(
self,
):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ dev = [

[project]
name = "llama-index-vector-stores-qdrant"
version = "0.8.5"
version = "0.8.6"
description = "llama-index vector_stores qdrant integration"
authors = [{name = "Your Name", email = "[email protected]"}]
requires-python = ">=3.9,<3.14"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,21 @@ async def collection_initialized_payload_indexed_vector_store() -> AsyncGenerato
client.close()
except Exception:
pass


@pytest_asyncio.fixture
async def async_only_hybrid_vector_store() -> QdrantVectorStore:
"""
Fixture to test initialization with only an async client and hybrid search enabled.
This specifically reproduces the bug conditions.
"""
# Note: Each in-memory client has its own isolated storage.
aclient = qdrant_client.AsyncQdrantClient(":memory:")

# The key part: initialize with *only* the aclient.
return QdrantVectorStore(
"test_async_only",
aclient=aclient,
enable_hybrid=True,
fastembed_sparse_model="Qdrant/bm25",
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from llama_index.core.vector_stores.types import BasePydanticVectorStore
from llama_index.vector_stores.qdrant import QdrantVectorStore
from llama_index.core.schema import TextNode
from llama_index.core.vector_stores.types import (
VectorStoreQuery,
VectorStoreQueryMode,
Expand Down Expand Up @@ -818,3 +819,53 @@ async def test_async_query_initializes_with_async_client_only() -> None:
assert result is not None
assert len(result.nodes) == 1
assert getattr(result.nodes[0], "text", None) == "hello"


# --- Test async-only initialization with hybrid search enabled ---
@pytest.mark.asyncio
async def test_init_with_async_client_only_and_hybrid_succeeds(
async_only_hybrid_vector_store: QdrantVectorStore,
) -> None:
"""
Tests that QdrantVectorStore initializes without errors when only
an async client is provided and hybrid search is enabled.
"""
# The test passes if the fixture is created successfully without raising an exception.
# We add a simple assertion to confirm the object is of the correct type.
assert isinstance(async_only_hybrid_vector_store, QdrantVectorStore)
assert async_only_hybrid_vector_store.enable_hybrid is True


# --- Test for async-only legacy collection correction ---
@pytest.mark.asyncio
async def test_async_only_hybrid_legacy_collection() -> None:
"""Test that async-only mode correctly handles legacy sparse vector format."""
collection_name = "test_legacy_async"
aclient = AsyncQdrantClient(":memory:")

# Create collection with OLD sparse vector name
await aclient.create_collection(
collection_name=collection_name,
vectors_config={
"text-dense": qmodels.VectorParams(size=2, distance=qmodels.Distance.COSINE)
},
sparse_vectors_config={"text-sparse": qmodels.SparseVectorParams()}, # OLD name
)

# Initialize with async client only
store = QdrantVectorStore(
collection_name=collection_name,
aclient=aclient,
enable_hybrid=True,
fastembed_sparse_model="Qdrant/bm25",
)

# Initially assumes new format
assert store.sparse_vector_name == "text-sparse-new"

# After first async operation, should detect and correct to old format
node = TextNode(text="test", embedding=[1.0, 0.0])
await store.async_add([node])

# Should now be corrected
assert store.sparse_vector_name == "text-sparse"