fix: add dynamic dimension detection for Ollama embedding models
This fixes dimension mismatch errors when using embedding models with non-standard dimensions (e.g., qwen3-embedding:4b produces 2560-dim vectors instead of the hardcoded 768). Changes: - OllamaEmbeddingProvider: Detect dimensions dynamically by generating test embedding instead of hardcoding to 768 - qdrant_client: Call dimension detection before collection creation - app.py: Initialize Qdrant collection before starting background tasks in streamable-http transport path - tests: Fix integration tests to properly mock EmbeddingService wrapper Fixes dimension mismatch error: "could not broadcast input array from shape (2560,) into shape (768,)" All integration tests passing (6/6). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -418,6 +418,19 @@ async def app_lifespan_basic(server: FastMCP) -> AsyncIterator[AppContext]:
|
||||
"NEXTCLOUD_USERNAME is required for vector sync in BasicAuth mode"
|
||||
)
|
||||
|
||||
# Initialize Qdrant collection before starting background tasks
|
||||
logger.info("Initializing Qdrant collection...")
|
||||
from nextcloud_mcp_server.vector.qdrant_client import get_qdrant_client
|
||||
|
||||
try:
|
||||
await get_qdrant_client() # Triggers collection creation if needed
|
||||
logger.info("Qdrant collection ready")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize Qdrant collection: {e}")
|
||||
raise RuntimeError(
|
||||
f"Cannot start vector sync - Qdrant initialization failed: {e}"
|
||||
) from e
|
||||
|
||||
# Initialize shared state
|
||||
send_stream, receive_stream = anyio.create_memory_object_stream(
|
||||
max_buffer_size=settings.vector_sync_queue_max_size
|
||||
@@ -1086,6 +1099,19 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
|
||||
# Create client since we're outside FastMCP lifespan
|
||||
client = NextcloudClient.from_env()
|
||||
|
||||
# Initialize Qdrant collection before starting background tasks
|
||||
logger.info("Initializing Qdrant collection...")
|
||||
from nextcloud_mcp_server.vector.qdrant_client import get_qdrant_client
|
||||
|
||||
try:
|
||||
await get_qdrant_client() # Triggers collection creation if needed
|
||||
logger.info("Qdrant collection ready")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize Qdrant collection: {e}")
|
||||
raise RuntimeError(
|
||||
f"Cannot start vector sync - Qdrant initialization failed: {e}"
|
||||
) from e
|
||||
|
||||
# Initialize shared state
|
||||
send_stream, receive_stream = anyio_module.create_memory_object_stream(
|
||||
max_buffer_size=settings.vector_sync_queue_max_size
|
||||
|
||||
@@ -17,6 +17,7 @@ class OllamaEmbeddingProvider(EmbeddingProvider):
|
||||
base_url: str,
|
||||
model: str = "nomic-embed-text",
|
||||
verify_ssl: bool = True,
|
||||
timeout=httpx.Timeout(timeout=120, connect=5),
|
||||
):
|
||||
"""
|
||||
Initialize Ollama embedding provider.
|
||||
@@ -29,8 +30,8 @@ class OllamaEmbeddingProvider(EmbeddingProvider):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.model = model
|
||||
self.verify_ssl = verify_ssl
|
||||
self.client = httpx.AsyncClient(verify=verify_ssl, timeout=30.0)
|
||||
self._dimension = 768 # nomic-embed-text default
|
||||
self.client = httpx.AsyncClient(verify=verify_ssl, timeout=timeout)
|
||||
self._dimension: int | None = None # Will be detected dynamically
|
||||
logger.info(
|
||||
f"Initialized Ollama provider: {base_url} (model={model}, verify_ssl={verify_ssl})"
|
||||
)
|
||||
@@ -73,13 +74,36 @@ class OllamaEmbeddingProvider(EmbeddingProvider):
|
||||
embeddings.append(embedding)
|
||||
return embeddings
|
||||
|
||||
async def _detect_dimension(self):
|
||||
"""
|
||||
Detect embedding dimension by generating a test embedding.
|
||||
|
||||
This method queries the model to determine the actual dimension
|
||||
instead of relying on hardcoded values.
|
||||
"""
|
||||
if self._dimension is None:
|
||||
logger.debug(f"Detecting embedding dimension for model {self.model}...")
|
||||
test_embedding = await self.embed("test")
|
||||
self._dimension = len(test_embedding)
|
||||
logger.info(
|
||||
f"Detected embedding dimension: {self._dimension} for model {self.model}"
|
||||
)
|
||||
|
||||
def get_dimension(self) -> int:
|
||||
"""
|
||||
Get embedding dimension.
|
||||
|
||||
Returns:
|
||||
Vector dimension (768 for nomic-embed-text)
|
||||
Vector dimension for the configured model
|
||||
|
||||
Raises:
|
||||
RuntimeError: If dimension not detected yet (call _detect_dimension first)
|
||||
"""
|
||||
if self._dimension is None:
|
||||
raise RuntimeError(
|
||||
f"Embedding dimension not detected yet for model {self.model}. "
|
||||
"Call _detect_dimension() first or generate an embedding."
|
||||
)
|
||||
return self._dimension
|
||||
|
||||
def _check_model_is_loaded(self, autoload: bool = True):
|
||||
|
||||
@@ -66,10 +66,23 @@ async def get_qdrant_client() -> AsyncQdrantClient:
|
||||
from nextcloud_mcp_server.embedding import get_embedding_service
|
||||
|
||||
embedding_service = get_embedding_service()
|
||||
|
||||
# Detect dimension dynamically (for OllamaEmbeddingProvider)
|
||||
if hasattr(embedding_service.provider, "_detect_dimension"):
|
||||
await embedding_service.provider._detect_dimension() # type: ignore[call-non-callable]
|
||||
|
||||
expected_dimension = embedding_service.get_dimension()
|
||||
|
||||
try:
|
||||
# Get existing collection
|
||||
# Explicitly check if collection exists
|
||||
logger.debug(f"Checking if collection '{collection_name}' exists...")
|
||||
collections = await _qdrant_client.get_collections()
|
||||
collection_names = [c.name for c in collections.collections]
|
||||
|
||||
if collection_name in collection_names:
|
||||
# Collection exists - validate dimensions
|
||||
logger.debug(
|
||||
f"Collection '{collection_name}' found, validating dimensions..."
|
||||
)
|
||||
collection_info = await _qdrant_client.get_collection(collection_name)
|
||||
actual_dimension = collection_info.config.params.vectors.size
|
||||
|
||||
@@ -91,12 +104,12 @@ async def get_qdrant_client() -> AsyncQdrantClient:
|
||||
f"(dimension={actual_dimension}, model={settings.ollama_embedding_model})"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Check if it's a dimension mismatch error (re-raise it)
|
||||
if isinstance(e, ValueError) and "Dimension mismatch" in str(e):
|
||||
raise
|
||||
|
||||
# Collection doesn't exist or other error, create it
|
||||
else:
|
||||
# Collection doesn't exist - create it
|
||||
logger.info(
|
||||
f"Collection '{collection_name}' not found, creating with "
|
||||
f"dimension={expected_dimension}, model={settings.ollama_embedding_model}..."
|
||||
)
|
||||
await _qdrant_client.create_collection(
|
||||
collection_name=collection_name,
|
||||
vectors_config=VectorParams(
|
||||
|
||||
Reference in New Issue
Block a user