-
Notifications
You must be signed in to change notification settings - Fork 732
feat(graph_db): introduce LanceDB graph backend with native compaction and FTS auto-indexing #1457
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
base: main
Are you sure you want to change the base?
Changes from all commits
ee39e29
3ba47a6
d6fe9be
5abd660
d6d59ce
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 |
|---|---|---|
|
|
@@ -41,6 +41,7 @@ def build_graph_db_config(user_id: str = "default") -> dict[str, Any]: | |
| "neo4j": APIConfig.get_neo4j_config(user_id=user_id), | ||
| "polardb": APIConfig.get_polardb_config(user_id=user_id), | ||
| "postgres": APIConfig.get_postgres_config(user_id=user_id), | ||
| "lance": APIConfig.get_lance_graph_config(user_id=user_id), | ||
| } | ||
|
|
||
| # Support both GRAPH_DB_BACKEND and legacy NEO4J_BACKEND env vars | ||
|
|
@@ -62,10 +63,27 @@ def build_vec_db_config() -> dict[str, Any]: | |
| Returns: | ||
| Validated vector database configuration dictionary | ||
| """ | ||
| vec_db_backend = os.getenv("MOS_VEC_DB_BACKEND", "milvus").lower() | ||
|
|
||
| config = {} | ||
| if vec_db_backend == "milvus": | ||
| config = APIConfig.get_milvus_config() | ||
| elif vec_db_backend == "lance": | ||
| base_uri = os.getenv("LANCE_URI", "./data/lance_db") | ||
| config = { | ||
| "uri": base_uri, | ||
| "collection_name": ["memories"], | ||
| "embedding_dimension": int(os.getenv("EMBEDDING_DIMENSION", 2048)), | ||
| } | ||
| elif vec_db_backend == "qdrant": | ||
| config = APIConfig.get_qdrant_config() | ||
| else: | ||
| raise ValueError(f"Unsupported vector DB backend: {vec_db_backend}") | ||
|
|
||
| return VectorDBConfigFactory.model_validate( | ||
| { | ||
| "backend": "milvus", | ||
| "config": APIConfig.get_milvus_config(), | ||
| "backend": vec_db_backend, | ||
| "config": config, | ||
| } | ||
| ) | ||
|
Comment on lines
+66
to
88
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import copy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import math | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Any | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -71,7 +72,40 @@ def handle_search_memories(self, search_req: APISearchRequest) -> SearchResponse | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| results = cube_view.search_memories(search_req_local) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not search_req_local.relativity: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| search_req_local.relativity = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.logger.info(f"[SearchHandler] Relativity filter: {search_req_local.relativity}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Extract and log scores for visibility before filtering | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self.logger.isEnabledFor(logging.DEBUG): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| score_details = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for key in ("text_mem", "pref_mem"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| buckets = results.get(key) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance(buckets, list): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for bucket in buckets: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| memories = bucket.get("memories") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance(memories, list): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for mem in memories: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance(mem, dict): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mem_text = mem.get("memory", "").replace("\n", " ") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Truncate to 100 chars to avoid log flooding | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(mem_text) > 100: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mem_text = mem_text[:100] + "..." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| meta = mem.get("metadata", {}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| score = meta.get("relativity", 1.0) if isinstance(meta, dict) else 1.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| score_val = float(score) if score is not None else 1.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except (TypeError, ValueError): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| score_val = 1.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| score_details.append(f"[{score_val:.4f}] {mem_text}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if score_details: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.logger.debug( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"[SearchHandler] Reranker scores before threshold ({search_req_local.relativity}): \n" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+78
to
+106
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Extract and log scores for visibility before filtering | |
| if self.logger.isEnabledFor(logging.DEBUG): | |
| score_details = [] | |
| for key in ("text_mem", "pref_mem"): | |
| buckets = results.get(key) | |
| if not isinstance(buckets, list): | |
| continue | |
| for bucket in buckets: | |
| memories = bucket.get("memories") | |
| if not isinstance(memories, list): | |
| continue | |
| for mem in memories: | |
| if not isinstance(mem, dict): | |
| continue | |
| mem_text = mem.get("memory", "").replace("\n", " ") | |
| # Truncate to 100 chars to avoid log flooding | |
| if len(mem_text) > 100: | |
| mem_text = mem_text[:100] + "..." | |
| meta = mem.get("metadata", {}) | |
| score = meta.get("relativity", 1.0) if isinstance(meta, dict) else 1.0 | |
| try: | |
| score_val = float(score) if score is not None else 1.0 | |
| except (TypeError, ValueError): | |
| score_val = 1.0 | |
| score_details.append(f"[{score_val:.4f}] {mem_text}") | |
| if score_details: | |
| self.logger.debug( | |
| f"[SearchHandler] Reranker scores before threshold ({search_req_local.relativity}): \n" | |
| # Extract and log scores for visibility before filtering without logging raw memory text | |
| if self.logger.isEnabledFor(logging.DEBUG): | |
| score_details = [] | |
| for key in ("text_mem", "pref_mem"): | |
| buckets = results.get(key) | |
| if not isinstance(buckets, list): | |
| continue | |
| for bucket_index, bucket in enumerate(buckets): | |
| memories = bucket.get("memories") | |
| if not isinstance(memories, list): | |
| continue | |
| for mem_index, mem in enumerate(memories): | |
| if not isinstance(mem, dict): | |
| continue | |
| meta = mem.get("metadata", {}) | |
| score = meta.get("relativity", 1.0) if isinstance(meta, dict) else 1.0 | |
| try: | |
| score_val = float(score) if score is not None else 1.0 | |
| except (TypeError, ValueError): | |
| score_val = 1.0 | |
| mem_id = mem.get("id") or mem.get("memory_id") | |
| if mem_id is None and isinstance(meta, dict): | |
| mem_id = meta.get("id") or meta.get("memory_id") | |
| mem_ref = ( | |
| f"id={mem_id}" | |
| if mem_id is not None | |
| else f"bucket={bucket_index},index={mem_index}" | |
| ) | |
| score_details.append(f"[{score_val:.4f}] {key} {mem_ref}") | |
| if score_details: | |
| self.logger.debug( | |
| f"[SearchHandler] Reranker scores before threshold ({search_req_local.relativity}):\n" |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -302,3 +302,7 @@ def add_nodes_batch(self, nodes: list[dict[str, Any]], user_name: str | None = N | |||||||||||||||||||||||||||
| - metadata: dict[str, Any] - Node metadata | ||||||||||||||||||||||||||||
| user_name: Optional user name (will use config default if not provided) | ||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @abstractmethod | ||||||||||||||||||||||||||||
| def node_not_exist(self, scope: str, user_name: str | None = None) -> bool: | ||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||
|
Comment on lines
+306
to
+308
|
||||||||||||||||||||||||||||
| @abstractmethod | |
| def node_not_exist(self, scope: str, user_name: str | None = None) -> bool: | |
| pass | |
| def node_not_exist(self, scope: str, user_name: str | None = None) -> bool: | |
| """ | |
| Return True when the given scope has no memory items, otherwise False. | |
| This default implementation derives the result from ``get_all_memory_items``. | |
| The ``user_name`` argument is accepted for API compatibility but is not used | |
| here because the base retrieval API is scoped only by ``scope``. Backends | |
| that need user-specific existence checks can override this method. | |
| """ | |
| return len(self.get_all_memory_items(scope)) == 0 |
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.
lancedbis declared with two different minimum versions across extras (tree-memrequires >=0.30.1 whilelance-mem/allallow >=0.17.0). Mixed constraints like this can lead to confusing resolver behavior and makes it unclear which version the code is targeting. It would be better to align these constraints (and include any required companion deps liketantivyin the same extra if FTS is considered part of the feature set).