diff --git a/langchain-coordinode/README.md b/langchain-coordinode/README.md index 6a03f58..ef65e1e 100644 --- a/langchain-coordinode/README.md +++ b/langchain-coordinode/README.md @@ -120,7 +120,10 @@ graph = CoordinodeGraph("localhost:7080", timeout=60.0) | `query(query, params)` | Execute Cypher, returns `List[Dict[str, Any]]` | | `refresh_schema()` | Reload node/relationship schema from database | | `add_graph_documents(docs)` | Batch MERGE nodes + relationships from `GraphDocument` list | +| `keyword_search(query, k, label, fuzzy, language)` | Full-text BM25 search — returns `[{"id", "score", "snippet"}, …]` | +| `similarity_search(query_vector, k, label, property)` | Vector nearest-neighbour search — returns `[{"id", "node", "distance"}, …]` | | `schema` | Schema string for LLM context | +| `structured_schema` | Structured schema dict for programmatic access | ## Related Packages diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index b97ce78..d966f5e 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -307,13 +307,22 @@ def keyword_search( # Injected clients (e.g. bare coordinode-embedded LocalClient) may # not implement text_search — return empty rather than AttributeError. return [] - results = self._client.text_search( - label, - query, - limit=k, - fuzzy=fuzzy, - language=language, - ) + try: + results = self._client.text_search( + label, + query, + limit=k, + fuzzy=fuzzy, + language=language, + ) + except Exception: + # Server may return UNIMPLEMENTED (feature not yet available in the + # deployed version) or NOT_FOUND (no text index for *label*). + # We catch broad Exception (rather than grpc.RpcError specifically) + # because langchain-coordinode does not take a hard grpc dependency. + # The exception is logged at DEBUG so it remains observable. + logger.debug("text_search() failed — returning empty list", exc_info=True) + return [] # Use "id" (not "node_id") for consistency with similarity_search() return # format, so callers can write generic code over both methods. return [{"id": r.node_id, "score": r.score, "snippet": r.snippet} for r in results] diff --git a/tests/unit/test_langchain_graph.py b/tests/unit/test_langchain_graph.py index 65442f6..22b6dad 100644 --- a/tests/unit/test_langchain_graph.py +++ b/tests/unit/test_langchain_graph.py @@ -65,6 +65,20 @@ def close(self) -> None: return None +class _ClientWithRaisingTextSearch: + """Fake client whose text_search raises (e.g. gRPC UNIMPLEMENTED).""" + + def cypher(self, query: str, params: dict | None = None) -> list[dict]: + return [] + + def text_search(self, label: str, query: str, **kwargs: Any) -> list[Any]: + raise RuntimeError("StatusCode.UNIMPLEMENTED") + + def close(self) -> None: + # No-op: keeps interface parity with real CoordinodeClient. + return None + + # ── Tests: keyword_search ───────────────────────────────────────────────────── @@ -150,3 +164,12 @@ def test_empty_snippet_preserved(self) -> None: out = graph.keyword_search("test") assert out[0]["snippet"] == "" + + def test_returns_empty_when_text_search_raises(self) -> None: + """Returns [] when text_search raises (e.g. gRPC UNIMPLEMENTED from older server).""" + client = _ClientWithRaisingTextSearch() + graph = CoordinodeGraph(client=client) + + out = graph.keyword_search("query") + + assert out == []