You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(vectorstore): task #61 P1-V vector adapter family — capability + filter Or guard + retrieve defense-in-depth
Closes task #83 per PM @不穷 dispatch (msg=29c9e753). Folds 4 P1-V
items from task #61 spec v1 § 2.3 into a single PR:
P1-V1 — collection init failure contract documentation
------------------------------------------------------
``ensure_collection`` Protocol docstring now spells out the cross-
adapter contract (idempotent / race-safe / fail-loud / cache-not-
poisoned-on-failure). Both adapters already implement these
behaviours; the documentation closes the spec drift gap so future
implementers have a checklist.
P1-V2 — batch upsert atomicity capability declaration
-----------------------------------------------------
New :class:`VectorBackendCapabilities` frozen dataclass on the base
module declares static per-backend behaviour flags. Each
``VectorStoreConnector`` subclass exposes an instance via the
``BACKEND_CAPABILITIES`` class-level attribute:
* ``PgvectorVectorStoreConnector.BACKEND_CAPABILITIES.supports_atomic_batch_upsert = True``
(PGVector wraps bulk INSERT ON CONFLICT in ``engine.begin()`` —
mid-batch failure rolls back the whole batch).
* ``QdrantVectorStoreConnector.BACKEND_CAPABILITIES.supports_atomic_batch_upsert = False``
(Qdrant ``client.upsert(points, wait=True)`` is best-effort
per-point — partial writes possible on mid-batch failure).
``upsert`` Protocol docstring now points at the capability flag so
callers know to chunk + verify on backends that declare ``False``.
P1-V3 — filter Or empty-parts guard
-----------------------------------
``Or.__post_init__`` already rejects empty ``parts`` at DSL
construction. Both adapter translators now also guard at the
translator boundary so a future refactor that bypasses the
constructor (e.g. ``object.__setattr__(or_node, "parts", ())`` on
the frozen dataclass, or a ``dataclasses.replace`` with empty
parts) can't silently degrade to a vacuous "match everything"
disjunction:
* ``aperag/vectorstore/pgvector_connector.py:_SqlFilter._walk`` —
raises ``UnsupportedFilterError`` on empty post-walk parts.
* ``aperag/vectorstore/qdrant_connector.py:_translate_filter`` —
raises ``UnsupportedFilterError`` on empty post-prune subs (so
``rest.Filter(should=[])`` — which Qdrant treats as match-all —
is unreachable).
P1-V4 — Qdrant legacy mode defense-in-depth
-------------------------------------------
``QdrantVectorStoreConnector.retrieve`` now applies the same
``TENANT_PAYLOAD_KEY`` filter in **both** multitenant and legacy
modes, but with a backwards-compatible "no payload key → pass
through" branch so legacy-only rows that don't carry the payload
key keep working:
* In multitenant mode: filter is the primary tenant-isolation
layer (unchanged behaviour).
* In legacy mode: collection-name isolation is the primary layer;
the new payload-level filter is belt-and-braces against tooling
drift / migration mistakes that could plant a stray foreign-tenant
row in a legacy collection.
The new ``BACKEND_CAPABILITIES.supports_legacy_mode`` flag declares
which adapter supports the legacy layout (PGVector ``False``,
Qdrant ``True``) so callers can tell the difference machine-
readably.
Tests
-----
* ``tests/unit_test/vectorstore/test_backend_capabilities.py``
(new) — pins shape + per-flag values for each adapter. Coordinates
with cuiwenbo task #87 P1-D3 collection metadata Pydantic
projection so the static capability matrix stays consistent
across PRs.
* ``tests/unit_test/vectorstore/test_pgvector_translator.py`` and
``test_qdrant_filter_translation.py`` — pin the new Or empty-parts
guard with frozen-dataclass-bypass coverage.
* ``tests/unit_test/vectorstore/test_qdrant_multitenancy_integration.py``
— new ``test_retrieve_legacy_mode_filters_stray_foreign_payload``
exercises the P1-V4 belt-and-braces filter on a real ``:memory:``
Qdrant client: legacy-mode rows without payload key pass through
(backward compat), own-tenant payload passes, foreign-tenant
payload is dropped.
Local: ``uv run pytest tests/unit_test/vectorstore/`` →
**156 passed, 10 skipped, 1 warning**.
Spec / scope alignment
----------------------
* task #61 spec v1 § 2.3 P1-V1 → ensure_collection contract doc ✅
* task #61 spec v1 § 2.3 P1-V2 → BACKEND_CAPABILITIES.supports_atomic_batch_upsert ✅
* task #61 spec v1 § 2.3 P1-V3 → Or empty-parts guard ✅
* task #61 spec v1 § 2.3 P1-V4 → retrieve defense-in-depth + supports_legacy_mode ✅
* Lesson #14 multi-iteration cleanup — legacy mode flagged via
``supports_legacy_mode`` so a future PR can drop the mode
entirely once telemetry confirms zero production usage ✅
* Lesson #17 backend 收敛 contract — capability declaration is the
backend-side contract that lets callers (FE / API / MCP) read a
single source of truth instead of forking on backend type ✅
Follow-ups (NOT in this PR)
---------------------------
* task #84 P1-G1+G2 graph store boundary tests — ziang
* task #85 P1-D1 e2e shape matrix — huangzhangshu
* task #86 P1-D2 Helm Nebula first-class — Planetegg
* task #87 P1-D3 collection metadata vector_backend projection —
cuiwenbo + dongdong (consumes ``BACKEND_CAPABILITIES`` values)
* task #88 P2-S1+S2 batch alias resolution — Bryce after this PR
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0 commit comments