-
Notifications
You must be signed in to change notification settings - Fork 287
feat(qdrant):support qdrant cloud and add index #522
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9fdbcf3
82883a3
439ed49
2b6dc7e
11e3467
5d434ea
9e48ac2
8ec9bec
dabe6b5
f10887a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| ## Product API smoke tests (local 0.0.0.0:8001) | ||
|
|
||
| Source: https://github.com/MemTensor/MemOS/issues/518 | ||
|
|
||
| ### Prerequisites | ||
| - Service is running: `python -m uvicorn memos.api.server_api:app --host 0.0.0.0 --port 8001` | ||
| - `.env` is configured for Redis, embeddings, and the vector DB (current test setup: Redis reachable, Qdrant Cloud connected). | ||
|
|
||
| ### 1) /product/add | ||
| - Purpose: Write a memory (sync/async). | ||
| - Example request (sync): | ||
|
|
||
| ```bash | ||
| curl -s -X POST http://127.0.0.1:8001/product/add \ | ||
| -H 'Content-Type: application/json' \ | ||
| -d '{ | ||
| "user_id": "tester", | ||
| "mem_cube_id": "default_cube", | ||
| "memory_content": "Apple is a fruit rich in fiber.", | ||
| "async_mode": "sync" | ||
| }' | ||
| ``` | ||
|
|
||
| - Observed result: `200`, message: "Memory added successfully", returns the written `memory_id` and related info. | ||
|
|
||
| ### 2) /product/get_all | ||
| - Purpose: List all memories for the user/type to confirm writes. | ||
| - Example request: | ||
|
|
||
| ```bash | ||
| curl -s -X POST http://127.0.0.1:8001/product/get_all \ | ||
| -H 'Content-Type: application/json' \ | ||
| -d '{ | ||
| "user_id": "tester", | ||
| "memory_type": "text_mem", | ||
| "mem_cube_ids": ["default_cube"] | ||
| }' | ||
| ``` | ||
|
|
||
| - Observed result: `200`, shows the recently written apple memories (WorkingMemory/LongTermMemory/UserMemory present, `vector_sync=success`). | ||
|
|
||
| ### 3) /product/search | ||
| - Purpose: Vector search memories. | ||
| - Example request: | ||
|
|
||
| ```bash | ||
| curl -s -X POST http://127.0.0.1:8001/product/search \ | ||
| -H 'Content-Type: application/json' \ | ||
| -d '{ | ||
| "query": "What fruit is rich in fiber?", | ||
| "user_id": "tester", | ||
| "mem_cube_id": "default_cube", | ||
| "top_k": 5, | ||
| "pref_top_k": 3, | ||
| "include_preference": false | ||
| }' | ||
| ``` | ||
|
|
||
| - Observed result: previously returned 400 because payload indexes (e.g., `vector_sync`) were missing in Qdrant. Index creation is now automatic during Qdrant initialization (memory_type/status/vector_sync/user_name). | ||
| - If results are empty or errors persist, verify indexes exist (auto-created on restart) or recreate/clean the collection. | ||
|
|
||
| ### Notes / Next steps | ||
| - `/product/add` and `/product/get_all` are healthy. | ||
| - `/product/search` still returns empty results even with vectors present; likely related to search filters or vector retrieval. | ||
| - Suggested follow-ups: inspect `SearchHandler` flow, filter conditions (user_id/session/cube_name), and vector DB search calls; capture logs or compare with direct `VecDBFactory.search` calls. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| # memos/reranker/factory.py | ||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| from typing import TYPE_CHECKING, Any | ||
|
|
||
| # Import singleton decorator | ||
|
|
@@ -28,12 +29,19 @@ def from_config(cfg: RerankerConfigFactory | None) -> BaseReranker | None: | |
| backend = (cfg.backend or "").lower() | ||
| c: dict[str, Any] = cfg.config or {} | ||
|
|
||
| headers_extra = c.get("headers_extra") | ||
| if isinstance(headers_extra, str): | ||
| try: | ||
| headers_extra = json.loads(headers_extra) | ||
| except Exception: | ||
| headers_extra = None | ||
|
Comment on lines
+34
to
+37
|
||
|
|
||
| if backend in {"http_bge", "bge"}: | ||
| return HTTPBGEReranker( | ||
| reranker_url=c.get("url") or c.get("endpoint") or c.get("reranker_url"), | ||
| model=c.get("model", "bge-reranker-v2-m3"), | ||
| timeout=int(c.get("timeout", 10)), | ||
| headers_extra=c.get("headers_extra"), | ||
| headers_extra=headers_extra, | ||
| rerank_source=c.get("rerank_source"), | ||
| ) | ||
|
|
||
|
|
@@ -51,7 +59,7 @@ def from_config(cfg: RerankerConfigFactory | None) -> BaseReranker | None: | |
| reranker_url=c.get("url") or c.get("endpoint") or c.get("reranker_url"), | ||
| model=c.get("model", "bge-reranker-v2-m3"), | ||
| timeout=int(c.get("timeout", 10)), | ||
| headers_extra=c.get("headers_extra"), | ||
| headers_extra=headers_extra, | ||
| rerank_source=c.get("rerank_source"), | ||
| reranker_strategy=c.get("reranker_strategy"), | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -23,24 +23,49 @@ def __init__(self, config: QdrantVecDBConfig): | |||||||
| from qdrant_client import QdrantClient | ||||||||
|
|
||||||||
| self.config = config | ||||||||
| # Default payload fields we always index because query filters rely on them | ||||||||
| self._default_payload_index_fields = [ | ||||||||
| "memory_type", | ||||||||
| "status", | ||||||||
| "vector_sync", | ||||||||
| "user_name", | ||||||||
| ] | ||||||||
|
|
||||||||
| # If both host and port are None, we are running in local mode | ||||||||
| if self.config.host is None and self.config.port is None: | ||||||||
| logger.warning( | ||||||||
| "Qdrant is running in local mode (host and port are both None). " | ||||||||
| "In local mode, there may be race conditions during concurrent reads/writes. " | ||||||||
| "It is strongly recommended to deploy a standalone Qdrant server " | ||||||||
| "(e.g., via Docker: https://qdrant.tech/documentation/quickstart/)." | ||||||||
| client_kwargs: dict[str, Any] = {} | ||||||||
| if self.config.url: | ||||||||
| client_kwargs["url"] = self.config.url | ||||||||
| if self.config.api_key: | ||||||||
| client_kwargs["api_key"] = self.config.api_key | ||||||||
| else: | ||||||||
| client_kwargs.update( | ||||||||
| { | ||||||||
| "host": self.config.host, | ||||||||
| "port": self.config.port, | ||||||||
| "path": self.config.path, | ||||||||
| } | ||||||||
| ) | ||||||||
|
|
||||||||
| self.client = QdrantClient( | ||||||||
| host=self.config.host, port=self.config.port, path=self.config.path | ||||||||
| ) | ||||||||
| # If both host and port are None, we are running in local/embedded mode | ||||||||
| if self.config.host is None and self.config.port is None: | ||||||||
| logger.warning( | ||||||||
| "Qdrant is running in local mode (host and port are both None). " | ||||||||
| "In local mode, there may be race conditions during concurrent reads/writes. " | ||||||||
| "It is strongly recommended to deploy a standalone Qdrant server " | ||||||||
| "(e.g., via Docker: https://qdrant.tech/documentation/quickstart/)." | ||||||||
| ) | ||||||||
|
|
||||||||
| self.client = QdrantClient(**client_kwargs) | ||||||||
| self.create_collection() | ||||||||
| # Ensure common payload indexes exist (idempotent) | ||||||||
| try: | ||||||||
| self.ensure_payload_indexes(self._default_payload_index_fields) | ||||||||
| except Exception as e: | ||||||||
| logger.warning(f"Failed to ensure default payload indexes: {e}") | ||||||||
|
|
||||||||
| def create_collection(self) -> None: | ||||||||
| """Create a new collection with specified parameters.""" | ||||||||
| from qdrant_client.http import models | ||||||||
| from qdrant_client.http.exceptions import UnexpectedResponse | ||||||||
|
|
||||||||
| if self.collection_exists(self.config.collection_name): | ||||||||
| collection_info = self.client.get_collection(self.config.collection_name) | ||||||||
|
|
@@ -57,13 +82,25 @@ def create_collection(self) -> None: | |||||||
| "dot": models.Distance.DOT, | ||||||||
| } | ||||||||
|
|
||||||||
| self.client.create_collection( | ||||||||
| collection_name=self.config.collection_name, | ||||||||
| vectors_config=models.VectorParams( | ||||||||
| size=self.config.vector_dimension, | ||||||||
| distance=distance_map[self.config.distance_metric], | ||||||||
| ), | ||||||||
| ) | ||||||||
| try: | ||||||||
| self.client.create_collection( | ||||||||
| collection_name=self.config.collection_name, | ||||||||
| vectors_config=models.VectorParams( | ||||||||
| size=self.config.vector_dimension, | ||||||||
| distance=distance_map[self.config.distance_metric], | ||||||||
| ), | ||||||||
| ) | ||||||||
| except UnexpectedResponse as err: | ||||||||
| # Cloud Qdrant returns 409 when the collection already exists; tolerate and continue. | ||||||||
| if getattr(err, "status_code", None) == 409 or "already exists" in str(err).lower(): | ||||||||
| logger.warning( | ||||||||
| f"Collection '{self.config.collection_name}' already exists. Skipping creation." | ||||||||
| ) | ||||||||
| return | ||||||||
| raise | ||||||||
| except Exception: | ||||||||
| # Bubble up other exceptions so callers can observe failures | ||||||||
| raise | ||||||||
|
Comment on lines
+101
to
+103
|
||||||||
| except Exception: | |
| # Bubble up other exceptions so callers can observe failures | |
| raise |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing
QDRANT_PATHenvironment variable documentation. This variable is used insrc/memos/api/config.py(line 503) but is not documented here. Consider adding it afterQDRANT_PORTfor completeness: