CCC: Refactor to remove N+1 patterns: batch children & term loads
>>> [!note] Migrated issue
<!-- Drupal.org comment -->
<!-- Migrated from issue #3574904. -->
Reported by: [kristen pol](https://www.drupal.org/user/8389)
Related to !80
>>>
<p>[Tracker]<br>
<strong>Update Summary: </strong>[One-line status update for stakeholders]<br>
<strong>Check-in Date: </strong>MM/DD/YYYY<br>
<strong>Blocked by: </strong>[#XXXXXX] (New issues on new lines)<br>
<strong>Additional Collaborators: </strong> @username1, @username2<br>
<em>Metadata is used by the <a href="https://www.drupalstarforge.ai/" title="AI Tracker">AI Tracker.</a> Docs and additional fields <a href="https://www.drupalstarforge.ai/ai-dashboard/docs" title="AI Issue Tracker Documentation">here</a>.</em><br>
[/Tracker]</p>
<h3 id="summary-problem-motivation">Problem/Motivation</h3>
<p>Follow-up to:</p>
<p><span class="drupalorg-gitlab-issue-link project-issue-status-info project-issue-status-7"><a href="https://www.drupal.org/project/ai_context/issues/3573713" title="Status: Closed (fixed)">#3573713: Full architecture review of CCC in prep for 1.0</a></span></p>
<p>Replace per-entity child and taxonomy-term loads with batched queries or precomputed maps so selection and rendering flows perform O(1) queries per request rather than O(N).</p>
<p>Why this matters</p>
<p>Current code issues:</p>
<ul>
<li>AiContextItem::getChildren() / hasChildren() run an entityQuery per item and call loadMultiple inside loops (src/Entity/AiContextItem.php).</li>
<li>AiContextRouter::rankWithKeyword() loads taxonomy terms per item (src/Service/AiContextRouter.php).</li>
</ul>
<p>These cause N+1 DB queries and high CPU/memory under scale. Fixing this is the highest-impact perf improvement.</p>
<p>Acceptance criteria (measurable)</p>
<p>For a selection/render of N candidate items:</p>
<ul>
<li>Child lookup and term label resolution complete using 1–2 batched queries (not N queries).</li>
<li>End-to-end selection + render request reduces DB query count by ≥ 80% vs current baseline in a representative test fixture (e.g., N=200).</li>
<li>Unit/integration tests assert that getChildren/hasChildren callers use batched paths and tests show query count improvement.</li>
<li>No behavioral regressions: child relationships, subcontext inclusion, and term-based ranking results unchanged.</li>
</ul>
<h3 id="summary-proposed-resolution">Proposed resolution</h3>
<p><strong>Files to inspect/change</strong></p>
<ul>
<li>src/Entity/AiContextItem.php (getChildren, hasChildren, related helpers)</li>
<li>src/Service/AiContextRouter.php (rankWithKeyword term loading + ranking loop)</li>
<li>src/Service/AiContextRenderer.php (load/translate flows)</li>
<li>src/Form/AiContextItemForm.php (countGlobalItems() uses loads)</li>
<li>ai_context.module (hooks that load children on delete)</li>
<li>tests/ (add/update tests to measure DB queries)</li>
</ul>
<p><strong>Suggested implementation approach (recommended, non-prescriptive)</strong></p>
<p><strong>1. Add a ChildrenLookup service (e.g., AiContextChildrenService) responsible for:</strong></p>
<ul>
<li>A single query to get all children IDs for a list of parent IDs and returning a map parent_id => [child_entities].</li>
<li>A cached short-lived in-request map for repeated lookups.</li>
<li>A batched hasChildren(parent_ids) helper that uses COUNT(...) with GROUP BY or an IN query.</li>
</ul>
<p><strong>2. Update consumers:</strong></p>
<ul>
<li>Selector, renderer, and list builders should call ChildrenLookup once with the batch of parent IDs and use the map rather than per-entity calls.</li>
<li>Replace direct calls to AiContextItem::getChildren()/hasChildren() in hot paths with service usage; keep methods on entity as convenience but mark as thin wrappers that delegate.</li>
</ul>
<p><strong>3. Taxonomy term batching:</strong></p>
<ul>
<li>In Router/Renderer, when you have a batch of entities, collect unique term IDs across the batch and call taxonomy_term_storage->loadMultiple(term_ids) once; build a term-label map used in ranking.</li>
</ul>
<p><strong>4. Refactor AiContextItem::getTagIds/getTargetMarkup/getDescriptionMarkup to be data-only or thin wrappers to avoid extra loads.</strong></p>
<p><strong>5. Update ai_context.module child-delete hook to use the service for batch clearing instead of loading and saving each child one-by-one; consider direct field updates if safe.<br>
</strong></p>
<p><strong>6. Add instrumentation (temporary) to measure query counts and time before/after.<br>
</strong></p>
<p><strong>Subtasks</strong></p>
<ul>
<li>[Investigation] Add a small benchmark script or test fixture and record baseline DB queries for representative flows.</li>
<li>[Service] Implement AiContextChildrenService with methods loadChildrenForParents(array $parentIds) and hasChildrenForParents(array $parentIds).</li>
<li>[Integration] Replace hot-path callers: Selector, Renderer, list builder, form countGlobalItems, hook implementations.</li>
<li>[Terms] Batch taxonomy term loads in Router ranking and renderer.</li>
<li>[Tests] Add unit tests for the new service and kernel tests measuring query counts/regression for selection/render flows.</li>
<li>[Docs] Document the selector→renderer preload contract and deprecate direct per-entity container calls where applicable.</li>
<li>[Cleanup] Remove/mark deprecated entity-level static loads or ensure they delegate to the service.</li>
</ul>
<p><strong>Testing strategy</strong></p>
<ul>
<li>Add a kernel test that creates a fixture of ~200 context items with mixed parent relationships and tags; execute select+render path and assert DB query count before/after refactor.</li>
<li>Unit tests for ChildrenLookup service logic and term batching logic (mock storage).</li>
<li>Functional tests to ensure UI features (subcontext, child inclusion, global counts) behave the same.</li>
</ul>
<p><strong>Risks & mitigations</strong></p>
<ul>
<li>Risk: behavioral changes if child order or filtering semantics change. Mitigate by keeping same ordering and writing regression tests that compare selected IDs/text before/after.</li>
<li>Risk: introducing a service changes DI surfaces. Mitigate by leaving entity convenience methods delegating to the service and adding deprecation notes.</li>
<li>Risk: large schema changes unnecessary — avoid schema work; aim for query/logic changes only.</li>
</ul>
<h3 id="summary-remaining-tasks">Target date or deadline</h3>
<p>February 2026</p>
<h3 id="summary-remaining-tasks">Remaining tasks</h3>
<ul>
<li>Review AI findings</li>
<li>Decide if they are correct</li>
<li>Decide if the proposed approach is best or propose alternative</li>
<li>Update code</li>
<li>Review and test</li>
</ul>
<h3 id="summary-ai-usage">AI usage (if applicable)</h3>
<p>[x] AI Assisted Issue<br>
This issue was generated with AI assistance, but was reviewed and refined by the creator.</p>
<p>[ ] AI Assisted Code<br>
This code was mainly generated by a human, with AI autocompleting or parts AI generated, but under full human supervision.</p>
<p>[x] AI Generated Code<br>
This code was mainly generated by an AI with human guidance, and reviewed, tested, and refined by a human.</p>
<p>[ ] Vibe Coded<br>
This code was generated by an AI and has only been functionally tested.</p>
> Related issue: [Issue #3573713](https://www.drupal.org/node/3573713)
issue