From e23f35114cce7afe366d5ff7eb029ee87b15ca69 Mon Sep 17 00:00:00 2001 From: LLLin000 <809867916@qq.com> Date: Sun, 5 Jul 2026 14:19:37 +0800 Subject: [PATCH] feat: add LanceDB evaluation backend scaffold --- paperforge/embedding/backends/base.py | 6 +++ .../embedding/backends/chroma_backend.py | 7 +++ .../embedding/backends/lance_backend.py | 22 ++++++++++ paperforge/embedding/status.py | 29 ++++++++++++ tests/test_layer4_lance_backend.py | 44 +++++++++++++++++++ 5 files changed, 108 insertions(+) create mode 100644 paperforge/embedding/backends/lance_backend.py create mode 100644 tests/test_layer4_lance_backend.py diff --git a/paperforge/embedding/backends/base.py b/paperforge/embedding/backends/base.py index c904790e..63b0be75 100644 --- a/paperforge/embedding/backends/base.py +++ b/paperforge/embedding/backends/base.py @@ -26,4 +26,10 @@ class VectorBackend(Protocol): def delete_paper(self, paper_id: str) -> int: ... + def capabilities(self) -> dict: + """Return a dict describing what this backend supports. + + Keys: backend, supports_hybrid, supports_multimodal. + """ + ... def health(self) -> dict: ... diff --git a/paperforge/embedding/backends/chroma_backend.py b/paperforge/embedding/backends/chroma_backend.py index c51308b7..400fc5b0 100644 --- a/paperforge/embedding/backends/chroma_backend.py +++ b/paperforge/embedding/backends/chroma_backend.py @@ -100,3 +100,10 @@ class ChromaBackend: "error": str(exc), "corrupted": "hnsw" in err_lower or "corrupt" in err_lower, } + + def capabilities(self) -> dict: + return { + "backend": "chroma", + "supports_hybrid": False, + "supports_multimodal": False, + } diff --git a/paperforge/embedding/backends/lance_backend.py b/paperforge/embedding/backends/lance_backend.py new file mode 100644 index 00000000..c536949d --- /dev/null +++ b/paperforge/embedding/backends/lance_backend.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +import lancedb + + +class LanceBackend: + """LanceDB-based vector backend (evaluation seam, not the default). + + This backend advertises hybrid and multimodal capabilities that the + Chroma backend does not support, making it suitable for evaluation + without switching the production default. + """ + + def __init__(self, dataset_path: str) -> None: + self.db = lancedb.connect(str(dataset_path)) + + def capabilities(self) -> dict: + return { + "backend": "lancedb", + "supports_hybrid": True, + "supports_multimodal": True, + } diff --git a/paperforge/embedding/status.py b/paperforge/embedding/status.py index 9a4af268..aed7cc1f 100644 --- a/paperforge/embedding/status.py +++ b/paperforge/embedding/status.py @@ -9,6 +9,35 @@ from paperforge.embedding.backends import get_vector_backend logger = logging.getLogger(__name__) +def _module_available(name: str) -> bool: + """Check whether *name* can be imported without side effects.""" + import importlib + + try: + importlib.import_module(name) + return True + except ImportError: + return False + + +def get_available_backends() -> dict[str, dict]: + """Return a dict of known backends with installation and selection status.""" + return { + "chroma": { + "installed": True, + "selected": True, + "supports_hybrid": False, + "supports_multimodal": False, + }, + "lancedb": { + "installed": _module_available("lancedb"), + "selected": False, + "supports_hybrid": True, + "supports_multimodal": True, + }, + } + + def get_embed_status(vault: Path) -> dict: """Get vector DB status. API-only mode. diff --git a/tests/test_layer4_lance_backend.py b/tests/test_layer4_lance_backend.py new file mode 100644 index 00000000..8f98bed0 --- /dev/null +++ b/tests/test_layer4_lance_backend.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import pytest + + +class TestLanceBackendCapabilities: + """LanceDB backend capability reporting (optional — skips when lancedb missing).""" + + def test_lance_backend_advertises_file_based_capabilities(self): + pytest.importorskip("lancedb") + from paperforge.embedding.backends.lance_backend import LanceBackend + + backend = LanceBackend("/tmp/lance") + caps = backend.capabilities() + assert caps["backend"] == "lancedb" + assert caps["supports_hybrid"] is True + assert caps["supports_multimodal"] is True + + +class TestAvailableBackends: + """Status-level backend enumeration (always available, no lancedb dependency).""" + + def test_get_available_backends_lists_chroma_and_lance(self): + from paperforge.embedding.status import get_available_backends + + backends = get_available_backends() + + assert "chroma" in backends + assert backends["chroma"]["installed"] is True + assert backends["chroma"]["selected"] is True + + assert "lancedb" in backends + assert backends["lancedb"]["installed"] is False + assert backends["lancedb"]["selected"] is False + assert backends["lancedb"]["supports_hybrid"] is True + assert backends["lancedb"]["supports_multimodal"] is True + + def test_get_available_backends_returns_dict_with_expected_keys(self): + from paperforge.embedding.status import get_available_backends + + backends = get_available_backends() + for name, info in backends.items(): + for key in ("installed", "selected", "supports_hybrid", "supports_multimodal"): + assert key in info, f"{name} missing {key}"