Skip to content

feat(rag): parent chunk expansion in query pipeline (FEAT-017)#88

Merged
fvadicamo merged 5 commits into
developfrom
feat/feat-017-parent-chunk-expansion
Jul 12, 2026
Merged

feat(rag): parent chunk expansion in query pipeline (FEAT-017)#88
fvadicamo merged 5 commits into
developfrom
feat/feat-017-parent-chunk-expansion

Conversation

@fvadicamo

@fvadicamo fvadicamo commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

What

  • Parent-child chunk linkage actually persisted at ingest time: deterministic uuid5(doc_id, position) ids, ChunkEmbedding.parent_id, parent_id in the Qdrant payload and in the (previously always-NULL) pgvector column
  • Vector search excludes chunk_level=parent in both providers (Qdrant must_not, pgvector IS DISTINCT FROM); parents are context material, not retrieval targets
  • New VectorStoreProvider.retrieve(namespace, chunk_ids) Protocol method (both providers + session-managed adapter)
  • AdvancedQueryPipeline step 6.5 parent_expansion (VEKTRA_PARENT_EXPANSION_ENABLED, default off): replaces child text with the parent chunk text after the retrieval filter and before token budgeting (ARCH-055); children of the same parent collapse into the highest-scored one; trace records children_expanded/siblings_merged/parents_fetched
  • pgvector store() honors caller-provided UUID chunk ids (falls back to random for non-UUID ids)
  • DEBT-025 hermetic fixture replicated in vektra-core and vektra-ingest (ambient VEKTRA_CHUNKING_STRATEGY=dual from a dev .env broke 12 tests there)
  • Eval assets: full-corpus dataset variant (tests/eval/dataset-full.jsonl, ns eval-full) + harness README

Why

  • FEAT-017: retrieved 500-token children often lack surrounding context for the LLM. Expansion swaps in the 1500-token parent section at prompt-build time without touching retrieval precision.
  • The backlog premise ("hierarchy already stored") turned out false: nothing persisted parent_id anywhere, so the plumbing is most of this PR.

Measured (eval-full corpus, Combo D, details in plan Notes)

  • Dual reingest: 105 points = 22 parents + 83 children; parents verified excluded from search
  • A (dual, expansion off) vs fixed baseline: e2e unchanged (grounded 35/55) but retrieval hit 89.1% -> 82.6% (children stop rolling overlap across parent boundaries - 3 boundary questions flip)
  • B (expansion on) vs A: zero per-question flips, avg sources 1.4 -> 1.0 (sibling merge), latency unchanged; expansion verified in traces
  • Root cause of the multi-chunk collapse is upstream of expansion: rerank+threshold funnel wipes all candidates (MC-01: max rerank 0.088 < 0.15 threshold on a 0.61-RRF candidate). Filed as TECH-007 with evidence.

Details

Plan: .s2s/plans/20260712-sprint3-rag-quality.md section 3 (all items checked)

  • Backlog: FEAT-017 completed (premise corrections + honest numbers), TECH-007 filed
  • Changelog: Added (expansion) + Changed (parent exclusion, deterministic pgvector ids)
  • Requirements: ARCH-037, ARCH-055, ARCH-056

Checklist

  • Main feature implemented (default-off, no behavior change unless enabled)
  • Tests written and passing (17 new; suite 658 passed, make lint green)
  • Documentation updated (configuration.md, .env.example, plan, backlog, changelog)
  • Code review ready

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added optional parent-chunk expansion for advanced queries, replacing child excerpts with parent context and consolidating duplicate results.
    • Added parent-child chunk linking during dual-strategy ingestion.
    • Added configuration support for enabling parent expansion.
  • Changed
    • Search results now exclude parent-level chunks during retrieval.
    • Stored chunk identifiers are preserved when valid and deterministic.
  • Documentation
    • Documented configuration, parent expansion behavior, and the RAG evaluation workflow.
  • Tests
    • Added coverage for parent expansion, chunk linking, retrieval, and evaluation datasets.

fvadicamo and others added 4 commits July 12, 2026 21:13
dataset-full.jsonl retargets the 55 questions to the eval-full namespace
(clean full Italian Constitution from Wikisource, 72 chunks + official
OHCHR UDHR PDF, 6 chunks). The excerpt corpus saturates hit rate at 100%;
the full corpus is discriminative (hit 89.1%, MRR 0.806 at baseline).
README documents corpus provenance, the senato.it PDF extraction trap,
and how to run/extend the harness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Store time: DualStrategyChunking parents now carry their own hierarchy id;
run_ingest remaps chunker-local ids to deterministic stored ids
(uuid5(doc_id, position)) and sets ChunkEmbedding.parent_id on children.
Qdrant persists parent_id as a top-level payload key; pgvector fills the
existing (always-NULL until now) parent_id column and honors caller ids.

Search time: both providers exclude chunk_level=parent from search results
(parents are context material, not retrieval targets). New
VectorStoreProvider.retrieve(namespace, chunk_ids) fetches chunks by id.

Query time: AdvancedQueryPipeline step 6.5 (VEKTRA_PARENT_EXPANSION_ENABLED,
default false) replaces child text with the parent chunk text after the
retrieval filter and before token budgeting (ARCH-055); children of the
same parent collapse into the highest-scored one. Trace records
children_expanded, siblings_merged, parents_fetched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…T-017)

- ingest: dual ingest maps children to the parent's deterministic stored
  id (uuid5(doc_id, position)); parents and tables carry no parent_id
- qdrant: parent_id in payload, must_not filter on chunk_level=parent,
  retrieve() namespace isolation and Record (no score) handling
- pgvector: caller-provided uuid ids honored, parent_id column filled,
  dense search excludes parents, retrieve() maps rows and skips bad ids
- advanced pipeline: expansion replaces child text with parent text,
  merges siblings, degrades gracefully on retrieve failure, keeps
  children when the parent is missing, and feeds the token budget with
  the expanded text; default-off leaves the pipeline untouched

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…T-025 fixture

- plan: section 3 complete; Notes with measure A (dual, no expansion:
  retrieval -6.5pp hit from parent-boundary chunking, e2e unchanged) and
  measure B (expansion on: grounding-neutral, zero flips, sources 1.4->1.0
  via sibling merge, expansion verified in traces)
- backlog: FEAT-017 completed with premise corrections and honest numbers;
  TECH-007 filed with the multi-chunk collapse root cause (rerank scores
  for multi-part questions all below min_relevance_score 0.15; MC-01 max
  rerank 0.088 vs raw RRF 0.61 - the funnel wipes candidates before
  expansion can run)
- changelog: FEAT-017 Added entry + Changed entries (parent exclusion in
  search, pgvector deterministic ids)
- tests: DEBT-025 hermetic fixture replicated in vektra-core and
  vektra-ingest (ambient VEKTRA_CHUNKING_STRATEGY=dual from the dev .env
  broke 12 tests there; vektra-shared alone was covered)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@fvadicamo fvadicamo added the enhancement New feature or request label Jul 12, 2026
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Parent-child relationships are persisted during dual ingestion, supported by both vector stores, and optionally expanded in the advanced query pipeline. Configuration, tests, documentation, changelog entries, and evaluation data are added for the feature.

Changes

Parent Chunk Expansion

Layer / File(s) Summary
Contracts and deterministic ingestion
vektra-shared/src/vektra_shared/*, vektra-ingest/src/vektra_ingest/*, vektra-ingest/tests/*
Chunk and search result types expose parent_id; configuration enables parent expansion; dual ingestion maps local parent identifiers to deterministic stored UUIDs.
Vector-store persistence and retrieval
vektra-index/src/vektra_index/*, vektra-index/tests/*
Pgvector and Qdrant persist parent links, exclude parent-level chunks from search, and retrieve parent records by ID with namespace filtering.
Advanced pipeline expansion
vektra-core/src/vektra_core/advanced_pipeline.py, vektra-core/tests/*
The advanced pipeline optionally replaces child snippets with parent text, merges siblings, records trace metadata, and handles retrieval failures or missing parents.
Configuration, evaluation, and release support
.env.example, docs/reference/configuration.md, CHANGELOG.md, tests/eval/*
Configuration and changelog documentation are updated, and a full multilingual RAG evaluation dataset and workflow are added.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant QueryClient
  participant AdvancedQueryPipeline
  participant VectorStoreProvider
  participant LLM
  QueryClient->>AdvancedQueryPipeline: submit advanced query
  AdvancedQueryPipeline->>VectorStoreProvider: search child chunks
  VectorStoreProvider-->>AdvancedQueryPipeline: filtered child results
  AdvancedQueryPipeline->>VectorStoreProvider: retrieve parent chunk IDs
  VectorStoreProvider-->>AdvancedQueryPipeline: parent chunk text
  AdvancedQueryPipeline->>LLM: construct prompt with expanded context
  LLM-->>QueryClient: generated answer
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: optional parent chunk expansion in the query pipeline.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/feat-017-parent-chunk-expansion

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added documentation Improvements or additions to documentation component:shared vektra-shared component component:core vektra-core component component:ingest vektra-ingest component component:index vektra-index component labels Jul 12, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements parent chunk expansion (FEAT-017) in the advanced query pipeline, allowing retrieved child chunks to be replaced with their parent chunk's text before token budgeting. To support this, parent-child linkage is now persisted using deterministic UUIDs, parent chunks are excluded from default search results, and a new retrieve method is added to the vector store providers. The review feedback highlights a potential crash in Qdrant when retrieving invalid UUIDs, recommends optimizing parent ID deduplication from O(N^2) to O(N) using dict.fromkeys, and suggests updating logging in pgvector to use structlog with keyword-only arguments to align with the repository style guide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread vektra-index/src/vektra_index/providers/qdrant.py Outdated
Comment thread vektra-core/src/vektra_core/advanced_pipeline.py Outdated
Comment thread vektra-index/src/vektra_index/providers/pgvector.py
….fromkeys

Addresses review comments 3567194789 (invalid point ids would fail the
whole retrieve batch server-side; now filtered and logged like pgvector)
and 3567194791 (O(N^2) unique-preserving scan). +2 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@fvadicamo fvadicamo left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review comments addressed: id validation in 3eb2523 (with note on the actual failure mode), dict.fromkeys dedup in 3eb2523, structlog suggestion declined for file-level consistency (see thread). Suite 660 passed.

@fvadicamo
fvadicamo merged commit 639f20b into develop Jul 12, 2026
19 checks passed
@fvadicamo
fvadicamo deleted the feat/feat-017-parent-chunk-expansion branch July 12, 2026 22:02

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
vektra-ingest/src/vektra_ingest/chunking.py (1)

268-280: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale docstring: parent_id = None no longer accurate.

The class docstring (lines 137-141) still states parent chunks have parent_id = None, but this change now sets parent_id to the chunk's own hierarchy id. Please update the docstring to match the new FEAT-017 behavior described in the inline comment at Line 268.

📝 Proposed docstring fix
     Parent-child hierarchy (2 levels):
     - Level 0 (parent): every parent_chunk_size tokens, a parent chunk is
-      emitted with the full accumulated text. parent_id = None.
+      emitted with the full accumulated text. parent_id = its own
+      hierarchy id (FEAT-017), later remapped to a stored id by the
+      ingest pipeline.
     - Level 1 (child): normal fixed-size chunks with overlap.
       parent_id = ID of the enclosing parent chunk.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vektra-ingest/src/vektra_ingest/chunking.py` around lines 268 - 280, The
class docstring incorrectly says parent chunks use parent_id = None. Update the
docstring for the chunking class to state that parent chunks set parent_id to
their own hierarchy ID, matching the FEAT-017 behavior implemented by the parent
DocumentChunk construction and its inline comment.
🧹 Nitpick comments (3)
vektra-shared/src/vektra_shared/config.py (1)

501-503: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the Phase-2 flag out of VektraSettings.

Line 501 duplicates configuration already owned by QueryPipelineConfig, violating the root model’s Phase-1-only boundary. Let the consuming advanced pipeline instantiate QueryPipelineConfig directly.

Based on learnings, VektraSettings must remain limited to Phase 1 variables while Phase 2 sub-configs validate independently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vektra-shared/src/vektra_shared/config.py` around lines 501 - 503, Remove
parent_expansion_enabled from VektraSettings so the root model remains limited
to Phase 1 configuration. Keep this setting owned and validated by
QueryPipelineConfig, and update the advanced pipeline consumer to instantiate
QueryPipelineConfig directly.

Source: Learnings

vektra-index/tests/test_qdrant_provider.py (1)

282-297: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assertion only checks the exclusion key, not its match value.

condition_keys collects the key kwarg from every FieldCondition call (including the must conditions), so the test would still pass if the must_not condition's match value were changed from "parent" to something else. Consider asserting on the specific call that has both key="metadata.chunk_level" and match built with value="parent".

As per path instructions, "Check that tests are genuinely testing behavior, not just mocking everything."

♻️ Proposed stronger assertion
         filter_kwargs = _mock_qdrant_models.Filter.call_args.kwargs
         assert "must_not" in filter_kwargs
         assert len(filter_kwargs["must_not"]) == 1
-        condition_keys = [
-            c.kwargs.get("key")
-            for c in _mock_qdrant_models.FieldCondition.call_args_list
-        ]
-        assert "metadata.chunk_level" in condition_keys
+        chunk_level_calls = [
+            c
+            for c in _mock_qdrant_models.FieldCondition.call_args_list
+            if c.kwargs.get("key") == "metadata.chunk_level"
+        ]
+        assert len(chunk_level_calls) == 1
+        _mock_qdrant_models.MatchValue.assert_any_call(value="parent")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vektra-index/tests/test_qdrant_provider.py` around lines 282 - 297,
Strengthen test_build_filter_excludes_parent_chunks by inspecting the
FieldCondition call corresponding to the must_not filter and asserting it uses
key "metadata.chunk_level" with a match value of "parent". Do not rely on
condition_keys alone, since it includes unrelated must conditions and does not
verify the excluded value.

Source: Path instructions

vektra-index/src/vektra_index/providers/pgvector.py (1)

338-352: 🚀 Performance & Scalability | 🔵 Trivial

Consider indexing the chunk_level JSONB predicate.

This filter now runs on every dense/sparse search call. As document_chunks grows, an unindexed chunk_metadata->>'chunk_level' extraction on every query could become a scan bottleneck. Consider a functional/expression index (e.g., CREATE INDEX ON document_chunks ((chunk_metadata->>'chunk_level'))) or, longer-term, promoting chunk_level to a dedicated indexed column.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vektra-index/src/vektra_index/providers/pgvector.py` around lines 338 - 352,
Add an index supporting the JSONB chunk_level extraction used by
_exclude_parent_chunks, preferably a functional index on document_chunks for
chunk_metadata->>'chunk_level', and ensure it is created through the project’s
existing schema or migration mechanism. Keep the current filtering behavior
unchanged, including rows without chunk_level.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@vektra-core/src/vektra_core/advanced_pipeline.py`:
- Around line 414-419: Move the _expand_parents call to immediately follow the
retrieval filter, before the existing post_retrieval safeguard. Run the
SafeguardHook/post-retrieval processing on the expanded results before prompt
construction, while preserving the original child IDs as the filter identifiers
used by that safeguard.

In `@vektra-index/tests/test_pgvector_unit.py`:
- Around line 179-239: Update
test_store_honors_deterministic_ids_and_parent_linkage and
test_store_falls_back_to_random_id_on_non_uuid to stop patching
DocumentChunkOrm. Intercept objects passed to session.add, allow the real
SQLAlchemy ORM instances to be constructed, and assert their id and parent_id
attributes; retain the existing deterministic-ID, fallback-ID, and
parent-linkage expectations.

---

Outside diff comments:
In `@vektra-ingest/src/vektra_ingest/chunking.py`:
- Around line 268-280: The class docstring incorrectly says parent chunks use
parent_id = None. Update the docstring for the chunking class to state that
parent chunks set parent_id to their own hierarchy ID, matching the FEAT-017
behavior implemented by the parent DocumentChunk construction and its inline
comment.

---

Nitpick comments:
In `@vektra-index/src/vektra_index/providers/pgvector.py`:
- Around line 338-352: Add an index supporting the JSONB chunk_level extraction
used by _exclude_parent_chunks, preferably a functional index on document_chunks
for chunk_metadata->>'chunk_level', and ensure it is created through the
project’s existing schema or migration mechanism. Keep the current filtering
behavior unchanged, including rows without chunk_level.

In `@vektra-index/tests/test_qdrant_provider.py`:
- Around line 282-297: Strengthen test_build_filter_excludes_parent_chunks by
inspecting the FieldCondition call corresponding to the must_not filter and
asserting it uses key "metadata.chunk_level" with a match value of "parent". Do
not rely on condition_keys alone, since it includes unrelated must conditions
and does not verify the excluded value.

In `@vektra-shared/src/vektra_shared/config.py`:
- Around line 501-503: Remove parent_expansion_enabled from VektraSettings so
the root model remains limited to Phase 1 configuration. Keep this setting owned
and validated by QueryPipelineConfig, and update the advanced pipeline consumer
to instantiate QueryPipelineConfig directly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e5df3b9f-97cf-4939-ac49-4ea088f08fe4

📥 Commits

Reviewing files that changed from the base of the PR and between 71586f6 and 3eb2523.

⛔ Files ignored due to path filters (2)
  • .s2s/BACKLOG.md is excluded by !.s2s/**
  • .s2s/plans/20260712-sprint3-rag-quality.md is excluded by !.s2s/**
📒 Files selected for processing (21)
  • .env.example
  • CHANGELOG.md
  • docs/reference/configuration.md
  • tests/eval/README.md
  • tests/eval/dataset-full.jsonl
  • vektra-core/src/vektra_core/advanced_pipeline.py
  • vektra-core/tests/conftest.py
  • vektra-core/tests/test_advanced_pipeline.py
  • vektra-index/src/vektra_index/adapters.py
  • vektra-index/src/vektra_index/providers/pgvector.py
  • vektra-index/src/vektra_index/providers/qdrant.py
  • vektra-index/tests/test_pgvector_unit.py
  • vektra-index/tests/test_qdrant_provider.py
  • vektra-ingest/src/vektra_ingest/chunking.py
  • vektra-ingest/src/vektra_ingest/pipeline.py
  • vektra-ingest/tests/conftest.py
  • vektra-ingest/tests/test_dual_chunking.py
  • vektra-ingest/tests/test_pipeline.py
  • vektra-shared/src/vektra_shared/config.py
  • vektra-shared/src/vektra_shared/protocols.py
  • vektra-shared/src/vektra_shared/types.py

Comment on lines +414 to +419
# Step 6.5: Parent chunk expansion (FEAT-017)
if self._parent_expansion and filtered:
t0 = time.monotonic()
filtered, expansion_meta = await self._expand_parents(
query.namespace, filtered
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Safeguard the expanded context before prompt construction.

Line 380 runs post_retrieval on child snippets, then this block injects uninspected parent text into the prompt. Move expansion immediately after the retrieval filter and run the post-retrieval safeguard on the expanded results, retaining child IDs as filter identifiers.

As per path instructions, vektra-core/** requires SafeguardHook integration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vektra-core/src/vektra_core/advanced_pipeline.py` around lines 414 - 419,
Move the _expand_parents call to immediately follow the retrieval filter, before
the existing post_retrieval safeguard. Run the SafeguardHook/post-retrieval
processing on the expanded results before prompt construction, while preserving
the original child IDs as the filter identifiers used by that safeguard.

Source: Path instructions

Comment on lines +179 to +239
async def test_store_honors_deterministic_ids_and_parent_linkage(self):
"""store() keeps caller-provided uuid ids and fills the parent_id column."""
from vektra_index.providers.pgvector import PgvectorProvider

session = self._make_session()
provider = PgvectorProvider()

parent_uuid = uuid4()
child_uuid = uuid4()
chunks = [
ChunkEmbedding(
chunk_id=str(parent_uuid),
text="parent",
dense=[0.1] * 384,
metadata={"chunk_level": "parent"},
),
ChunkEmbedding(
chunk_id=str(child_uuid),
text="child",
dense=[0.1] * 384,
metadata={"chunk_level": "child"},
parent_id=str(parent_uuid),
),
]

with patch("vektra_index.models.DocumentChunkOrm") as MockOrm:
MockOrm.return_value = MagicMock()
result = await provider.store(session, "default", uuid4(), chunks)

assert result == [str(parent_uuid), str(child_uuid)]
orm_kwargs = [c.kwargs for c in MockOrm.call_args_list]
assert orm_kwargs[0]["id"] == parent_uuid
assert orm_kwargs[0]["parent_id"] is None
assert orm_kwargs[1]["id"] == child_uuid
assert orm_kwargs[1]["parent_id"] == parent_uuid

@pytest.mark.asyncio
async def test_store_falls_back_to_random_id_on_non_uuid(self):
"""Non-UUID caller ids (external store-chunks callers) get random uuids."""
from vektra_index.providers.pgvector import PgvectorProvider

session = self._make_session()
provider = PgvectorProvider()

chunks = [
ChunkEmbedding(
chunk_id="not-a-uuid",
text="x",
dense=[0.1] * 384,
metadata={},
parent_id="also-not-a-uuid",
),
]

with patch("vektra_index.models.DocumentChunkOrm") as MockOrm:
MockOrm.return_value = MagicMock()
result = await provider.store(session, "default", uuid4(), chunks)

UUID(result[0]) # random but valid
assert MockOrm.call_args.kwargs["parent_id"] is None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Don't mock the SQLAlchemy ORM class — use the session.add intercept pattern.

Both test_store_honors_deterministic_ids_and_parent_linkage and test_store_falls_back_to_random_id_on_non_uuid patch DocumentChunkOrm itself with a MagicMock, then assert on the mock's constructor kwargs. This only verifies what arguments were passed to the (fake) constructor, not that a real DocumentChunkOrm instance was correctly built. As per path instructions, "Prefer real objects over mocks. Check that SQLAlchemy ORM classes are never mocked (use session.add intercept pattern instead)."

♻️ Proposed fix using the intercept pattern
-        with patch("vektra_index.models.DocumentChunkOrm") as MockOrm:
-            MockOrm.return_value = MagicMock()
-            result = await provider.store(session, "default", uuid4(), chunks)
-
-        assert result == [str(parent_uuid), str(child_uuid)]
-        orm_kwargs = [c.kwargs for c in MockOrm.call_args_list]
-        assert orm_kwargs[0]["id"] == parent_uuid
-        assert orm_kwargs[0]["parent_id"] is None
-        assert orm_kwargs[1]["id"] == child_uuid
-        assert orm_kwargs[1]["parent_id"] == parent_uuid
+        added: list = []
+        session.add = lambda obj: added.append(obj)
+
+        result = await provider.store(session, "default", uuid4(), chunks)
+
+        assert result == [str(parent_uuid), str(child_uuid)]
+        assert added[0].id == parent_uuid
+        assert added[0].parent_id is None
+        assert added[1].id == child_uuid
+        assert added[1].parent_id == parent_uuid

The same fix applies to test_store_falls_back_to_random_id_on_non_uuid.

As per path instructions for tests/**: "Check that SQLAlchemy ORM classes are never mocked (use session.add intercept pattern instead)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vektra-index/tests/test_pgvector_unit.py` around lines 179 - 239, Update
test_store_honors_deterministic_ids_and_parent_linkage and
test_store_falls_back_to_random_id_on_non_uuid to stop patching
DocumentChunkOrm. Intercept objects passed to session.add, allow the real
SQLAlchemy ORM instances to be constructed, and assert their id and parent_id
attributes; retain the existing deterministic-ID, fallback-ID, and
parent-linkage expectations.

Source: Path instructions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component:core vektra-core component component:index vektra-index component component:ingest vektra-ingest component component:shared vektra-shared component documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant