Skip to content

perf(search): default-exclude heavy fields (embeddings, upstreamLineage, ...) from search responses - #29517

Draft
mohityadav766 wants to merge 4 commits into
mainfrom
perf/lean-search-source
Draft

perf(search): default-exclude heavy fields (embeddings, upstreamLineage, ...) from search responses#29517
mohityadav766 wants to merge 4 commits into
mainfrom
perf/lean-search-source

Conversation

@mohityadav766

@mohityadav766 mohityadav766 commented Jun 26, 2026

Copy link
Copy Markdown
Member

Fixes #29516

Problem

The main /search/query path (OpenSearchSearchManager / ElasticSearchSearchManager) applies no default _source filter — only caller-provided include/exclude fields. So normal explore/search ships the full _source, including fields the UI doesn't need:

  • embedding — large vectors, used only by the vector-search endpoint
  • upstreamLineage — lineage pages only (and already node.remove(...)'d post-fetch in some paths — fetched, parsed, then discarded)
  • *_suggest, schemaDefinition, customMetrics, lifeCycle, fqnParts

These are already defined as excludable (SOURCE_FIELDS_TO_EXCLUDE) and stripped on lineage / entity-relationship paths — just not on the main search path. They bloat every search response and add memory/GC pressure under load (same class as the column-aggregator OOM, #29501/#29502).

Fix

  • SearchUtils.DEFAULT_SEARCH_SOURCE_EXCLUDES = SOURCE_FIELDS_TO_EXCLUDE + upstreamLineage, and withDefaultSearchSourceExcludes(...) to merge with caller excludes.
  • Both engines now default to that exclude on the main search path; caller opt-in preservedincludeSourceFields (lineage, vector search, etc.) and explicit fetchSource=false behave as before; explicit excludeSourceFields are merged with the defaults.

Safety

Lineage and vector-search endpoints request their own source fields, so they're unaffected. The excluded fields were already the established "exclude from search source" set; this just extends them to the high-traffic explore/search path.

Type of change

  • Performance / non-breaking improvement

Testing

  • SearchUtilsTest green (142/142), incl. a new test asserting the default excludes drop embedding/upstreamLineage/schemaDefinition/customMetrics and that caller excludes are merged (not replaced).
  • openmetadata-service compiles clean.

🤖 Generated with Claude Code

Greptile Summary

This PR applies default _source exclusions (embedding, upstreamLineage, suggest fields, schemaDefinition, customMetrics) to the main /search/query path in both ElasticSearchSearchManager and OpenSearchSearchManager, reducing response payload size and GC pressure without touching lineage or vector-search paths that opt in via includeSourceFields.

  • SearchUtils gains DEFAULT_SEARCH_SOURCE_EXCLUDES and withDefaultSearchSourceExcludes to merge caller excludes with the defaults; the underlying constant is still sourced from ElasticSearchClient.SOURCE_FIELDS_TO_EXCLUDE (pre-existing cross-engine coupling).
  • Both managers restructure the fetch-source branch: fetchSource=false still skips the source entirely, explicit includeSourceFields still bypass the defaults, and only the "no include, no false" path is changed to apply the default excludes.
  • Test: the first assertion (checking specific field names) is correct, but two assertions use Set.of(String[]) which Java resolves to the fixed-arity Set.of(E e1) overload — producing a singleton Set<String[]> rather than a Set<String> — and would fail at runtime.

Confidence Score: 4/5

The production filtering logic is correct and all existing callers are unaffected; the new test has assertion bugs that are likely failing in CI.

The two managers and SearchUtils are correctly implemented and all existing callers that use includeSourceFields or fetchSource=false continue to behave as before. The only concern is in the new test: Set.of(withDefaultSearchSourceExcludes(null)) uses Java's fixed-arity Set.of(E e1) overload (not the varargs overload), creating a singleton Set<String[]> that would not equal the Set of defaults, causing the assertEquals and both assertTrue calls to fail at runtime. The first assertion in the test (checking specific field names via containsAll) is correct. The production change itself is safe to ship once the test assertions are fixed.

openmetadata-service/src/test/java/org/openmetadata/service/search/SearchUtilsTest.java — the two assertions that wrap withDefaultSearchSourceExcludes results in Set.of(...) need to be replaced with new HashSet<>(Arrays.asList(...)).

Important Files Changed

Filename Overview
openmetadata-service/src/main/java/org/openmetadata/service/search/SearchUtils.java Adds DEFAULT_SEARCH_SOURCE_EXCLUDES and withDefaultSearchSourceExcludes utility; imports SOURCE_FIELDS_TO_EXCLUDE from ElasticSearchClient (engine-specific, pre-existing cross-engine coupling).
openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java Refactored fetch-source branching to apply default excludes on the normal search path; fetchSource=false and explicit includeSourceFields behave identically to before.
openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java Mirrors the ElasticSearch change exactly; consistent fetch-source semantics across both engines.
openmetadata-service/src/test/java/org/openmetadata/service/search/SearchUtilsTest.java New test for withDefaultSearchSourceExcludes; the first assertTrue is correct, but two assertions use Set.of(String[]) which in Java produces a singleton Set<String[]> rather than a Set, likely causing runtime assertion failures.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Search Request] --> B{fetchSource == false?}
    B -- Yes --> C[fetchSource false\nno _source returned]
    B -- No --> D{includeSourceFields\nprovided?}
    D -- Yes --> E[fetchSource includeFields, excludeFields\nonly requested fields returned]
    D -- No --> F[fetchSource null,\nwithDefaultSearchSourceExcludes excludeFields\nall fields EXCEPT heavy defaults]
    F --> G[DEFAULT_SEARCH_SOURCE_EXCLUDES\nembedding, upstreamLineage,\nschemaDefinition, customMetrics,\n*_suggest, lifeCycle, fqnParts]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Search Request] --> B{fetchSource == false?}
    B -- Yes --> C[fetchSource false\nno _source returned]
    B -- No --> D{includeSourceFields\nprovided?}
    D -- Yes --> E[fetchSource includeFields, excludeFields\nonly requested fields returned]
    D -- No --> F[fetchSource null,\nwithDefaultSearchSourceExcludes excludeFields\nall fields EXCEPT heavy defaults]
    F --> G[DEFAULT_SEARCH_SOURCE_EXCLUDES\nembedding, upstreamLineage,\nschemaDefinition, customMetrics,\n*_suggest, lifeCycle, fqnParts]
Loading

Comments Outside Diff (1)

  1. openmetadata-service/src/main/java/org/openmetadata/service/search/SearchUtils.java, line 7 (link)

    P2 DEFAULT_SEARCH_SOURCE_EXCLUDES and withDefaultSearchSourceExcludes are shared utilities consumed by both ElasticSearchSearchManager and OpenSearchSearchManager, but they are built from ElasticSearchClient.SOURCE_FIELDS_TO_EXCLUDE — an engine-specific constant. If OpenSearch ever needs a different exclusion set (e.g. a field renamed or split in one engine's mapping), this cross-engine import will silently apply the wrong list to OpenSearch. Moving SOURCE_FIELDS_TO_EXCLUDE (and FIELDS_TO_REMOVE) to SearchClient or SearchUtils itself would remove the coupling.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (4): Last reviewed commit: "Merge branch 'main' into perf/lean-searc..." | Re-trigger Greptile

The main /search/query path applied no default _source filter — it only honored
caller-provided include/exclude fields — so normal explore/search shipped full
_source including embeddings (vector search only), upstreamLineage (lineage pages
only, and already stripped post-fetch), the *_suggest fields, schemaDefinition and
customMetrics. These bloat every search response and drive memory/GC pressure under
concurrent load.

Apply SearchUtils.DEFAULT_SEARCH_SOURCE_EXCLUDES (the existing SOURCE_FIELDS_TO_EXCLUDE
set + upstreamLineage) as the default _source exclude on the OS and ES search paths,
merged with any caller-provided excludes. Callers that need a field opt in via
includeSourceFields, so lineage and vector-search paths are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions github-actions Bot added backend safe to test Add this label to run secure Github workflows on PRs labels Jun 26, 2026
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 63%
63.42% (70688/111446) 45.9% (40625/88504) 47.81% (12369/25866)

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

🔴 Playwright Results — 14 failure(s), 218 flaky

✅ 4251 passed · ❌ 14 failed · 🟡 218 flaky · ⏭️ 38 skipped

Shard Passed Failed Flaky Skipped
🔴 Shard 1 367 8 11 11
🟡 Shard 2 811 0 5 9
🔴 Shard 3 813 1 6 7
🟡 Shard 4 810 0 3 10
🔴 Shard 5 678 4 182 0
🔴 Shard 6 772 1 11 1

Genuine Failures (failed on all attempts)

Pages/Lineage/LineageInteraction.spec.ts › Verify edge click opens edge drawer (shard 1)
Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m(�[22m�[2m)�[22m failed

Locator: getByTestId('edge-pw-database-service-2463a2eb.pw-database-823e5e47.pw-database-schema-8a160a82.pw-table-a2532d02-ef8d-45d4-a465-52aaed7ba3e8-undefined')
Expected: visible
Timeout: 15000ms
Error: element(s) not found

Call log:
�[2m  - Expect "toBeVisible" with timeout 15000ms�[22m
�[2m  - waiting for getByTestId('edge-pw-database-service-2463a2eb.pw-database-823e5e47.pw-database-schema-8a160a82.pw-table-a2532d02-ef8d-45d4-a465-52aaed7ba3e8-undefined')�[22m

Pages/Lineage/LineageInteraction.spec.ts › Verify edge delete button in drawer (shard 1)
Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m(�[22m�[2m)�[22m failed

Locator: getByTestId('edge-pw-database-service-9692a341.pw-database-59589c1b.pw-database-schema-13f764e1.pw-table-8bcc70ca-ad2d-4e12-9db5-e87fc9c10e01-undefined')
Expected: visible
Timeout: 15000ms
Error: element(s) not found

Call log:
�[2m  - Expect "toBeVisible" with timeout 15000ms�[22m
�[2m  - waiting for getByTestId('edge-pw-database-service-9692a341.pw-database-59589c1b.pw-database-schema-13f764e1.pw-table-8bcc70ca-ad2d-4e12-9db5-e87fc9c10e01-undefined')�[22m

Pages/Lineage/LineageInteraction.spec.ts › Verify node panel opens on click (shard 1)
�[31mTest timeout of 60000ms exceeded.�[39m
Pages/Lineage/LineageInteraction.spec.ts › Verify edit mode with edge operations (shard 1)
Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m(�[22m�[2m)�[22m failed

Locator: getByTestId('edge-pw-database-service-eece7d9d.pw-database-09b294d2.pw-database-schema-42a957ff.pw-table-fd31d6b4-e4bb-42f9-b2bf-310357566f7d-undefined')
Expected: visible
Timeout: 15000ms
Error: element(s) not found

Call log:
�[2m  - Expect "toBeVisible" with timeout 15000ms�[22m
�[2m  - waiting for getByTestId('edge-pw-database-service-eece7d9d.pw-database-09b294d2.pw-database-schema-42a957ff.pw-table-fd31d6b4-e4bb-42f9-b2bf-310357566f7d-undefined')�[22m

Pages/Lineage/PlatformLineage.spec.ts › Verify table search with special characters as handled (shard 1)
TimeoutError: page.waitForResponse: Timeout 30000ms exceeded while waiting for event "response"
Pages/Lineage/PlatformLineage.spec.ts › Verify service platform view (shard 1)
TimeoutError: page.waitForResponse: Timeout 30000ms exceeded while waiting for event "response"
Pages/Lineage/PlatformLineage.spec.ts › Verify domain platform view (shard 1)
TimeoutError: page.waitForResponse: Timeout 30000ms exceeded while waiting for event "response"
Pages/Lineage/PlatformLineage.spec.ts › Verify platform view switching (shard 1)
TimeoutError: page.waitForResponse: Timeout 30000ms exceeded while waiting for event "response"
Features/SearchIndexNestedColumns.spec.ts › 25-level oversized nested column indexes and is searchable by its deep column name (shard 3)
Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m(�[22m�[2m)�[22m failed

Locator: locator('[data-testid="global-search-suggestion-box"]')
Expected: visible
Timeout: 15000ms
Error: element(s) not found

Call log:
�[2m  - Expect "toBeVisible" with timeout 15000ms�[22m
�[2m  - waiting for locator('[data-testid="global-search-suggestion-box"]')�[22m

Pages/Entity.spec.ts › User should be denied access to edit description when deny policy rule is applied on an entity (shard 5)
TimeoutError: page.waitForResponse: Timeout 30000ms exceeded while waiting for event "response"
Pages/Entity.spec.ts › User should be denied access to edit description when deny policy rule is applied on an entity (shard 5)
TimeoutError: page.waitForResponse: Timeout 30000ms exceeded while waiting for event "response"
Pages/Entity.spec.ts › User should be denied access to edit description when deny policy rule is applied on an entity (shard 5)
TimeoutError: page.waitForResponse: Timeout 30000ms exceeded while waiting for event "response"
Pages/ExploreBrowse.spec.ts › service type drill-down disables unrelated roots and query-panel Clear resets it (shard 5)
�[31mTest timeout of 180000ms exceeded.�[39m
Pages/Lineage/DataAssetLineage.spec.ts › Column lineage for searchIndex -> apiEndpoint (shard 6)
TimeoutError: page.waitForResponse: Timeout 30000ms exceeded while waiting for event "response"
🟡 218 flaky test(s) (passed on retry)
  • Features/DataAssetRulesEnabled.spec.ts › Verify the MlModel Entity Action items after rules is Enabled (shard 1, 1 retry)
  • Features/DataAssetRulesEnabled.spec.ts › Verify the Container Entity Action items after rules is Enabled (shard 1, 1 retry)
  • Features/DataAssetRulesEnabled.spec.ts › Verify the DashboardDataModel Entity Action items after rules is Enabled (shard 1, 1 retry)
  • Features/DataAssetRulesEnabled.spec.ts › Verify the Chart Entity Action items after rules is Enabled (shard 1, 1 retry)
  • Features/DataAssetRulesDisabled.spec.ts › Verify the SearchIndex entity item action after rules disabled (shard 1, 1 retry)
  • Features/DescriptionSuggestion.spec.ts › should add and accept a requested topic schema field description (shard 1, 1 retry)
  • Features/Glossary/GlossaryPagination.spec.ts › should filter by InReview status (shard 1, 1 retry)
  • Pages/Lineage/DataAssetLineage.spec.ts › verify create lineage for entity - Mlmodel (shard 1, 1 retry)
  • Pages/Lineage/DataAssetLineage.spec.ts › verify create lineage for entity - File (shard 1, 1 retry)
  • Flow/SearchRBAC.spec.ts › a table-scoped user sees tables but never dashboards (shard 1, 1 retry)
  • Flow/SearchRBAC.spec.ts › a dashboard-scoped user sees dashboards but never tables (shard 1, 1 retry)
  • Features/Announcements/AnnouncementEntity.spec.ts › edits an existing announcement on a domain (shard 2, 1 retry)
  • Features/BulkEditEntity.spec.ts › Glossary (shard 2, 1 retry)
  • Features/BulkImport.spec.ts › Database Schema (shard 2, 1 retry)
  • Features/Container.spec.ts › Copy column link button should copy the column URL to clipboard (shard 2, 1 retry)
  • Features/GlobalPageSize.spec.ts › Page size should persist across different pages (shard 2, 1 retry)
  • Features/LandingPageWidgets/FollowingWidget.spec.ts › Check followed entity present in following widget (shard 3, 1 retry)
  • Features/NestedColumnsExpandCollapse.spec.ts › should not duplicate rows when expanding and collapsing nested columns with same names in Version History (shard 3, 1 retry)
  • Features/Permissions/EntityPermissions.spec.ts › Spreadsheet deny common operations permissions (shard 3, 1 retry)
  • Features/RestoreEntityInheritedFields.spec.ts › Validate restore with Inherited domain and data products assigned (shard 3, 1 retry)
  • Features/Table.spec.ts › Table pagination with sorting should works (shard 3, 1 retry)
  • Features/Topic.spec.ts › Copy nested field link should include full hierarchical path (shard 3, 2 retries)
  • Flow/NestedChildrenUpdates.spec.ts › should update nested column description immediately without page refresh (shard 4, 1 retry)
  • Flow/PersonaFlow.spec.ts › Set default persona for team should work properly (shard 4, 1 retry)
  • Pages/CustomProperties.spec.ts › Should clear search and show all properties for apiCollection in right panel (shard 4, 1 retry)
  • Pages/Entity.spec.ts › Delete Container (shard 5, 1 retry)
  • Pages/Entity.spec.ts › Copy entity URL from header (shard 5, 1 retry)
  • Pages/Entity.spec.ts › Announcement create, edit & delete (shard 5, 1 retry)
  • Pages/Entity.spec.ts › Inactive Announcement create & delete (shard 5, 1 retry)
  • Pages/Entity.spec.ts › UpVote & DownVote entity (shard 5, 1 retry)
  • ... and 188 more

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

@sonarqubecloud

Copy link
Copy Markdown

@gitar-bot

gitar-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 1 resolved / 3 findings

Optimizes search response payloads by excluding heavyweight fields, though the implementation includes an incorrect usage of Set.of in SearchUtilsTest that fails to validate the new filtering logic.

⚠️ Security: NLQ success path skips ContextMemory visibility filter

📄 openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java:593-607 📄 openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java:576-590

This PR adds applyContextMemoryVisibility(...) as a per-memory privacy guarantee and wires it into every search path that returns documents — applyRbacCondition/applyRbacQueryWithCaching, searchWithDirectQuery, buildSearchRequestBuilder, listWithOffset(subjectContext), and fallbackToBasicSearch. However, the successful NLQ branch of searchWithNLQ in both engines builds and executes the search directly (ES lines 593-623, OS lines 576-602) without applying RBAC or the new visibility filter. Only the NLQ fallback path got the call.

Because buildVisibilityFilter restricts contextMemory documents to those owned by / shared with the requesting non-admin user and leaves all other documents untouched, omitting it here means a non-admin user issuing a natural-language query (which can target the contextMemory / global index) can receive private context memories the filter is meant to hide. The class doc-comment explicitly states this filter is a privacy guarantee that must apply even when RBAC is disabled, so the gap contradicts the stated invariant and is engine-symmetric (present in both ES and OS).

Apply applyContextMemoryVisibility(subjectContext, requestBuilder) (and ideally RBAC, consistent with the fallback path) before executing the transformed NLQ query in both engines.

ElasticSearchSearchManager: apply visibility before building the NLQ request
requestBuilder.from(request.getFrom());
requestBuilder.size(request.getSize());

applyContextMemoryVisibility(subjectContext, requestBuilder);

// Add aggregations for NLQ query
addAggregationsToNLQQuery(requestBuilder, request.getIndex());

SearchRequest searchRequest = requestBuilder.build(request.getIndex());
OpenSearchSearchManager: apply visibility before building the NLQ request
requestBuilder.from(request.getFrom());
requestBuilder.size(request.getSize());

applyContextMemoryVisibility(subjectContext, requestBuilder);

// Add aggregations for NLQ query
addAggregationsToNLQQuery(requestBuilder, request.getIndex());

SearchResponse<JsonData> response =
    client.search(requestBuilder.build(request.getIndex()), JsonData.class);
💡 Edge Case: Username-based search preference breaks for '_'-prefixed names

📄 openmetadata-service/src/main/java/org/openmetadata/service/search/SearchUtils.java:70-84 📄 openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java:1143 📄 openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java:1184

SearchUtils.searchPreferenceFor() returns the raw user name as the Elasticsearch/OpenSearch preference routing value, which is now applied to every search via requestBuilder.preference(...) (ElasticSearchSearchManager L1143, OpenSearchSearchManager L1184) and to the raw vector-search URL via appendPreferenceParam().

Elasticsearch/OpenSearch treat any preference string that begins with _ as a reserved/special token (e.g. _local, _shards, _only_nodes). A custom value starting with _ that is not one of the recognized tokens causes the search to fail server-side (IllegalArgumentException: no Preference for [_xxx]). If a user name can begin with an underscore (e.g. certain SSO/SCIM-provisioned or system accounts), all of that user's searches would error out rather than degrade gracefully.

This is speculative on whether _-prefixed user names are reachable in practice, hence minor, but it is cheap to harden by prefixing the routing key with a constant so it is always a plain custom value.

Prefix the routing key with a constant so it is always treated as a plain custom preference value, never a reserved '_' token.
public static String searchPreferenceFor(SubjectContext subjectContext) {
  String preference = null;
  if (subjectContext != null && subjectContext.user() != null) {
    // Prefix with a constant so the routing key is never interpreted as an
    // ES/OS reserved '_'-prefixed preference token.
    preference = "user:" + subjectContext.user().getName();
  }
  return preference;
}
✅ 1 resolved
Edge Case: fetchSource=true no longer returns full _source

📄 openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java:1241-1247 📄 openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java:1285-1291
The refactored branch logic in buildSearchRequestBuilder changes the semantics of fetchSource=true. Previously Boolean.TRUE.equals(request.getFetchSource()) with no include/exclude fields produced requestBuilder.fetchSource(null, null) — i.e. the FULL _source. Now fetchSource=true with no includeSourceFields falls through to the final else branch and applies withDefaultSearchSourceExcludes(...), silently stripping embedding, upstreamLineage, schemaDefinition, customMetrics, lifeCycle, fqnParts, and the *_suggest fields.

I verified the existing internal callers that set withFetchSource(true) without includeSourceFields (GlossaryTermRepository.getGlossaryUsageFromES, SearchRepository.getEntitiesContainingFQNFromES, ReindexingUtil.findReferenceInElasticSearchAcrossAllIndexes, MCP SearchMetadataTool) only read id/fullyQualifiedName/entityType (or already trim these heavy fields), so they are functionally unaffected today. However, this is an undocumented contract change: the PR description only claims fetchSource=false is preserved and never mentions that fetchSource=true no longer guarantees the full source. Any future or external caller that sets fetchSource=true expecting the complete document (e.g. to read schemaDefinition or customMetrics) will silently lose those fields. Consider preserving full-source semantics for an explicit fetchSource=true, or documenting this behavior change in the schema/Javadoc.

🤖 Prompt for agents
Code Review: Optimizes search response payloads by excluding heavyweight fields, though the implementation includes an incorrect usage of `Set.of` in `SearchUtilsTest` that fails to validate the new filtering logic.

1. ⚠️ Security: NLQ success path skips ContextMemory visibility filter
   Files: openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java:593-607, openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java:576-590

   This PR adds `applyContextMemoryVisibility(...)` as a per-memory privacy guarantee and wires it into every search path that returns documents — `applyRbacCondition`/`applyRbacQueryWithCaching`, `searchWithDirectQuery`, `buildSearchRequestBuilder`, `listWithOffset(subjectContext)`, and `fallbackToBasicSearch`. However, the *successful* NLQ branch of `searchWithNLQ` in both engines builds and executes the search directly (ES lines 593-623, OS lines 576-602) without applying RBAC or the new visibility filter. Only the NLQ fallback path got the call.
   
   Because `buildVisibilityFilter` restricts `contextMemory` documents to those owned by / shared with the requesting non-admin user and leaves all other documents untouched, omitting it here means a non-admin user issuing a natural-language query (which can target the contextMemory / global index) can receive private context memories the filter is meant to hide. The class doc-comment explicitly states this filter is a privacy guarantee that must apply even when RBAC is disabled, so the gap contradicts the stated invariant and is engine-symmetric (present in both ES and OS).
   
   Apply `applyContextMemoryVisibility(subjectContext, requestBuilder)` (and ideally RBAC, consistent with the fallback path) before executing the transformed NLQ query in both engines.

   Fix (ElasticSearchSearchManager: apply visibility before building the NLQ request):
   requestBuilder.from(request.getFrom());
   requestBuilder.size(request.getSize());
   
   applyContextMemoryVisibility(subjectContext, requestBuilder);
   
   // Add aggregations for NLQ query
   addAggregationsToNLQQuery(requestBuilder, request.getIndex());
   
   SearchRequest searchRequest = requestBuilder.build(request.getIndex());

   Fix (OpenSearchSearchManager: apply visibility before building the NLQ request):
   requestBuilder.from(request.getFrom());
   requestBuilder.size(request.getSize());
   
   applyContextMemoryVisibility(subjectContext, requestBuilder);
   
   // Add aggregations for NLQ query
   addAggregationsToNLQQuery(requestBuilder, request.getIndex());
   
   SearchResponse<JsonData> response =
       client.search(requestBuilder.build(request.getIndex()), JsonData.class);

2. 💡 Edge Case: Username-based search preference breaks for '_'-prefixed names
   Files: openmetadata-service/src/main/java/org/openmetadata/service/search/SearchUtils.java:70-84, openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java:1143, openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java:1184

   `SearchUtils.searchPreferenceFor()` returns the raw user name as the Elasticsearch/OpenSearch `preference` routing value, which is now applied to every search via `requestBuilder.preference(...)` (ElasticSearchSearchManager L1143, OpenSearchSearchManager L1184) and to the raw vector-search URL via `appendPreferenceParam()`.
   
   Elasticsearch/OpenSearch treat any `preference` string that begins with `_` as a reserved/special token (e.g. `_local`, `_shards`, `_only_nodes`). A custom value starting with `_` that is not one of the recognized tokens causes the search to fail server-side (`IllegalArgumentException: no Preference for [_xxx]`). If a user name can begin with an underscore (e.g. certain SSO/SCIM-provisioned or system accounts), all of that user's searches would error out rather than degrade gracefully.
   
   This is speculative on whether `_`-prefixed user names are reachable in practice, hence minor, but it is cheap to harden by prefixing the routing key with a constant so it is always a plain custom value.

   Fix (Prefix the routing key with a constant so it is always treated as a plain custom preference value, never a reserved '_' token.):
   public static String searchPreferenceFor(SubjectContext subjectContext) {
     String preference = null;
     if (subjectContext != null && subjectContext.user() != null) {
       // Prefix with a constant so the routing key is never interpreted as an
       // ES/OS reserved '_'-prefixed preference token.
       preference = "user:" + subjectContext.user().getName();
     }
     return preference;
   }

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@sonarqubecloud

Copy link
Copy Markdown

@mohityadav766
mohityadav766 marked this pull request as draft July 28, 2026 12:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Search responses ship heavy unused fields by default (embeddings, upstreamLineage, ...)

1 participant