Issue #3586315: Avoid full catalog scan when agent has no scope subscriptions.
Description
AiContextSelector::getPrefilteredPublishedItemIds() returned NULL when
$scopeSubscriptions was empty, causing the chunk loop to fall back to a
full catalog scan on every AI request.
For SELECTION_MODE_MINIMAL with no subscriptions, only global items and
auto-include candidates can ever be selected. Subscription-scoped items are
always discarded during scoring, so loading every published context item is
unnecessary. On sites with many context items, this results in dozens of
extra chunked queries per request, affecting the common "global items only"
agent configuration.
This MR replaces the full scan with a targeted prefilter when using
SELECTION_MODE_MINIMAL with no subscriptions.
The prefilter candidate set is the union of:
- Global items — retrieved from the scope index (
scope_id = 'global'). - Unindexed items — published items with no scope index rows (target entity-only items, items with no scope, etc.). Looked up via a LEFT JOIN anti-join on the entity data table, intersected with access-checked published IDs.
- Current target-entity items — items that explicitly reference the current target entity via the DER field. Covers the edge case where an item has both a subscription scope (indexed) and a target entity reference.
- Custom non-subscription scope items — items indexed under enabled scope
plugins where
supportsSubscriptions()isFALSE(excludingglobalandtarget_entity, which are handled separately).
SELECTION_MODE_MATCH_ALL is unchanged and continues to scan the full
catalog.
Files changed
AiContextScopeIndexService- Added
getPublishedItemIdsWithoutScopeIndex()— anti-join lookup for published default-translation items absent from the scope index. Falls back to all published IDs when the index table is missing. - Added
getItemIdsByScopeIds()— indexed lookup by scope plugin ID. - Injects
@entity_type.managerto resolve the entity data table name.
- Added
AiContextScopeResolver- Added
getUnindexedItemIds()— intersects anti-join results with access-checked published IDs. - Added
getNonSubscriptionIndexedItemIds()— collects items indexed under custom non-subscription scope plugins.
- Added
AiContextSelector- Added
buildNoSubscriptionCandidateIds()andloadTargetEntityCandidateIds(). - Updated
getPrefilteredPublishedItemIds()to accept$selectionMode(before$debugLogging) and apply the targeted prefilter forSELECTION_MODE_MINIMALwhen no subscriptions are present. - Debug log uses
@target_entityplaceholder (not@te).
- Added
AiContextSelectorNoSubscriptionPrefilterTest- New kernel test class (8 tests) covering global selection,
MATCH_ALL,neverInclude,alwaysInclude, target-entity auto-include, custom non-subscription scopes, and debug-log prefilter narrowing.
- New kernel test class (8 tests) covering global selection,
ai_context_test_scope_auto_include- Test module with a non-subscription scope plugin for extensibility coverage.
Testing instructions
-
Enable the module and create several context items:
- at least one with Global scope
- one or more with a non-global scope (for example, Language or Tag)
-
Create an agent at
/admin/config/ai/agentsand leave its scope subscriptions empty. -
Enable debug logging at Configuration → AI Context → Settings → General → Enable debug logging.
-
Trigger an AI request using that agent (for example, through the AI Agents Debugger).
-
Open the Drupal log (
/admin/reports/dblog) and verify a message similar to:AiContextSelector: no-subscription prefilter selected X of Y published item IDs (global: …, unindexed: …, target-entity: …)where X is significantly smaller than Y (only prefilter candidates are selected rather than the full catalog).
-
Verify that the agent response still includes the global context item.
-
Repeat with an agent that has scope subscriptions and verify behavior remains unchanged.
-
Run kernel tests:
vendor/bin/phpunit --filter AiContextSelectorNoSubscriptionPrefilterTest
Checklist
- I have updated the MR title to use format:
Issue #1234: My issue title. - I have updated the MR description to include:
Closes #1234 - I have performed a self-review of my own code
- I have added or updated tests, or explained in the description why this change is not covered by tests
- I have updated documentation for any new or changed functionality
- I have written testing instructions and verified them locally
- I have noted any required post-merge steps (config imports, cache rebuilds, manual changes)
- This MR contains no breaking API or hook changes, or they are explicitly documented in the description
AI Compliance
Note
Check the one that best describes your usage, or leave all unchecked if AI was not significantly used.
-
AI Assisted Code
Mainly written by a human; AI used for autocomplete or partial generation under full human supervision. -
AI Generated Code
Mainly generated by AI, reviewed and approved by a human before this MR was created. -
Vibe Coded
Generated by AI and only functionally reviewed before this MR was created.
Closes #3586315 (closed)