Skip to content

fix(search): clear orphaned column docs on recursive service hard delete#29549

Merged
mohityadav766 merged 4 commits into
mainfrom
search-cleanup-after-service-delete
Jun 29, 2026
Merged

fix(search): clear orphaned column docs on recursive service hard delete#29549
mohityadav766 merged 4 commits into
mainfrom
search-cleanup-after-service-delete

Conversation

@mohityadav766

@mohityadav766 mohityadav766 commented Jun 27, 2026

Copy link
Copy Markdown
Member

Problem

After #29322 (perf: bound memory + skip ancestor-covered work in bulk recursive hard delete), a recursive hard delete of a databaseService / database / databaseSchema leaves the descendant column docs (column_search_index / tableColumn) orphaned in the search index. The entities are removed from the DB, but their column docs linger and reappear in column-level search / Explore until a full reindex.

Root cause

#29322 set descendantsCoveredByAncestorCascade = true on the database-subtree repos, which makes the bulk hard-delete path skip the per-table deleteFromSearch dispatch and rely on the root service's single deleteFromSearchSearchRepository.deleteOrUpdateChildren cascade (a delete-by-query on service.id).

That cascade targets the service's childAliases[database, databaseSchema, storedProcedure, table, testSuite, …] — which does not include tableColumn. Column docs live in a separate flat column_search_index and were only ever pruned by a table's own per-entity delete (deleteEntityIndex(table)deleteTableColumns, keyed by table.id). Once the per-table dispatch is skipped, nothing clears them.

It slipped through because the #29322 benchmark and IT only counted table_search_index docs, never column_search_index.

Change

SearchRepository.deleteEntityIndex now also clears the flat column_search_index for the database-subtree ancestor types (databaseService / database / databaseSchema), keyed by the service.id / database.id / databaseSchema.id field the column docs carry (verified against ColumnSearchIndex + the column index mapping). This mirrors the existing per-table deleteTableColumns special case.

The shared childAliases-driven cascades (soft-delete, certification, inherited-field propagation) are intentionally left unchanged — the fix is scoped to the hard-delete search path that regressed, not the global alias config.

Tests

  • UnitSearchRepositoryBehaviorTest.deleteEntityIndexRemovesDescendantColumnsForDatabaseSubtreeAncestors: asserts deleteEntityIndex issues the column delete-by-query with the correct parent field for each of the three ancestor types.
  • IntegrationDatabaseServiceResourceIT: the recursive-hard-delete regression now also guards the column_search_index doc (searchable before, gone after) — fails without the fix.
  • Scale (new, @Tag("scale")) — ServiceDeleteSearchCleanupScaleIT: seeds a service with N tables (5 columns each), recursively hard-deletes it, and asserts both the table and column docs scoped to service.id drop to zero. Runs in the nightly scale-it profile.

Scale results (real Testcontainers ES)

tables column docs after recursive hard delete
2,000 10,000 table=0, column=0 (1.1 s)
20,000 100,000 table=0, column=0 (6.7 s)

The delete-by-query is one server-side op, so it scales independently of doc count — demonstrated clearing 100k column docs cleanly.

Fixes #29548

🤖 Generated with Claude Code

Greptile Summary

This PR fixes orphaned column docs in column_search_index after a recursive hard-delete of a databaseService, database, or databaseSchema. PR #29322 introduced an ancestor-cascade optimisation that skips per-table search dispatch, but the resulting deleteOrUpdateChildren cascade only targeted childAliases (which excluded tableColumn), leaving column docs stranded until a full reindex.

  • Core fixSearchRepository.deleteDescendantColumns is wired into the else branch of deleteEntityIndex. It issues a deleteEntityByFields on column_search_index keyed by service.id, database.id, or databaseSchema.id as appropriate, mirroring the existing deleteTableColumns pattern for table-level deletes.
  • Tests — a new unit test asserts the correct parent field is used for each ancestor type; the existing DatabaseServiceResourceIT regression guard is extended to also verify the column doc disappears; a new @scale-tagged test seeds up to 100k tables (5 columns each) and asserts both indexes empty to zero after recursive hard delete.

Confidence Score: 5/5

Safe to merge — the change is a narrowly scoped, additive fix that only runs an extra delete-by-query for three specific entity types during hard delete.

The fix is well-contained: deleteDescendantColumns is a no-op for all entity types except DATABASE_SERVICE, DATABASE, and DATABASE_SCHEMA, so no existing code paths are altered. The parent-field names (service.id, database.id, databaseSchema.id) are confirmed against the column index mapping. Error handling mirrors the established deleteTableColumns pattern. The unit test covers all three ancestor types, the integration test extends the regression guard to the column index, and the scale test demonstrates the cleanup holds at 100k column docs.

No files require special attention.

Important Files Changed

Filename Overview
openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java Adds deleteDescendantColumns — a targeted delete-by-query on column_search_index keyed by the ancestor's parent-id field — and calls it from the else branch of deleteEntityIndex. Field names verified against the column index mapping. Error handling mirrors the sibling deleteTableColumns.
openmetadata-service/src/test/java/org/openmetadata/service/search/SearchRepositoryBehaviorTest.java New unit test deleteEntityIndexRemovesDescendantColumnsForDatabaseSubtreeAncestors covers all three ancestor types and verifies the correct parent-field/index-name pair is passed to deleteEntityByFields.
openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DatabaseServiceResourceIT.java Extends the existing recursive-hard-delete regression guard to also assert the column doc is absent post-delete, using ColumnSearchIndex.generateColumnId to compute the expected doc ID.
openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/search/scale/ServiceDeleteSearchCleanupScaleIT.java New @scale-tagged test that seeds N tables with 5 columns each, recursively hard-deletes the service, and awaits zero docs in both table_search_index and column_search_index. Excluded from regular PR runs; intended for nightly scale profile.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant SearchRepository
    participant SearchClient
    participant ES as Elasticsearch

    Caller->>SearchRepository: deleteEntityIndex(service/database/schema)
    SearchRepository->>SearchClient: deleteEntity(primary index, entityId)
    SearchClient->>ES: DELETE primary doc

    SearchRepository->>SearchRepository: deleteOrUpdateChildren(entity, indexMapping)
    SearchRepository->>SearchClient: deleteEntityByFields(childAliases)
    note over SearchClient,ES: tableColumn NOT in childAliases

    alt "entityType == TABLE"
        SearchRepository->>SearchRepository: deleteTableColumns(table)
        SearchRepository->>SearchClient: deleteEntityByFields(column_search_index, table.id)
        SearchClient->>ES: DELETE column docs by table.id
    else "entityType == DATABASE_SERVICE / DATABASE / DATABASE_SCHEMA"
        SearchRepository->>SearchRepository: deleteDescendantColumns NEW
        SearchRepository->>SearchClient: deleteEntityByFields(column_search_index, parent_field)
        SearchClient->>ES: DELETE column docs by service.id or database.id or databaseSchema.id
    end
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"}}}%%
sequenceDiagram
    participant Caller
    participant SearchRepository
    participant SearchClient
    participant ES as Elasticsearch

    Caller->>SearchRepository: deleteEntityIndex(service/database/schema)
    SearchRepository->>SearchClient: deleteEntity(primary index, entityId)
    SearchClient->>ES: DELETE primary doc

    SearchRepository->>SearchRepository: deleteOrUpdateChildren(entity, indexMapping)
    SearchRepository->>SearchClient: deleteEntityByFields(childAliases)
    note over SearchClient,ES: tableColumn NOT in childAliases

    alt "entityType == TABLE"
        SearchRepository->>SearchRepository: deleteTableColumns(table)
        SearchRepository->>SearchClient: deleteEntityByFields(column_search_index, table.id)
        SearchClient->>ES: DELETE column docs by table.id
    else "entityType == DATABASE_SERVICE / DATABASE / DATABASE_SCHEMA"
        SearchRepository->>SearchRepository: deleteDescendantColumns NEW
        SearchRepository->>SearchClient: deleteEntityByFields(column_search_index, parent_field)
        SearchClient->>ES: DELETE column docs by service.id or database.id or databaseSchema.id
    end
Loading

Reviews (4): Last reviewed commit: "Merge branch 'main' into search-cleanup-..." | Re-trigger Greptile

PR #29322 made recursive service hard delete fast by setting
descendantsCoveredByAncestorCascade=true on the database-subtree repos, which
skips the per-table deleteFromSearch dispatch and relies on the root service's
deleteOrUpdateChildren cascade (delete-by-query on service.id against the
service childAliases). Those childAliases do not include tableColumn, and column
docs live in a separate flat column_search_index pruned only by the per-table
deleteTableColumns. So once the per-table dispatch is skipped, every descendant
column doc orphans in search (a 20k-table service leaves 100k stale column docs).

Complete the hard-delete search cascade: deleteEntityIndex now also clears the
column index for the database-subtree ancestor types (databaseService / database
/ databaseSchema) by the service.id / database.id / databaseSchema.id field the
column docs carry, mirroring the existing per-table deleteTableColumns case. The
shared childAliases-driven soft-delete / certification / propagation cascades are
intentionally left unchanged.

Tests:
- SearchRepositoryBehaviorTest: deleteEntityIndex issues the column delete-by-query
  with the correct parent field for each of the three ancestor types.
- DatabaseServiceResourceIT: the recursive-hard-delete regression now also guards
  the column_search_index doc (searchable before, gone after).
- ServiceDeleteSearchCleanupScaleIT (new, @tag("scale")): seeds a service with N
  tables (5 columns each), recursively hard-deletes it, and asserts both the table
  and column docs scoped to service.id drop to zero. Verified clearing 100k column
  docs (20k tables) in ~7s.

Fixes #29548

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 27, 2026
@mohityadav766 mohityadav766 self-assigned this Jun 27, 2026
Review feedback: deleteDescendantColumns let the IOException propagate silently
to the generic outer catch in deleteEntityIndex, so a column-cleanup failure was
indistinguishable from a primary-document delete failure in the logs. Wrap the
delete-by-query in a try/catch that logs the entityType + FQN and rethrows
(preserving the retry path), matching the sibling deleteTableColumns.

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

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

🔴 Playwright Results — 3 failure(s), 51 flaky

✅ 4422 passed · ❌ 3 failed · 🟡 51 flaky · ⏭️ 38 skipped

Shard Passed Failed Flaky Skipped
🔴 Shard 1 317 1 7 4
🔴 Shard 2 818 1 10 9
🟡 Shard 3 823 0 6 7
🟡 Shard 4 828 0 3 10
🟡 Shard 5 863 0 11 0
🔴 Shard 6 773 1 14 8

Genuine Failures (failed on all attempts)

Features/TagsSuggestion.spec.ts › should decline suggested tags for a container column (shard 1)
Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m(�[22m�[2m)�[22m failed

Locator: locator('[data-testid="task-feed-card"]').filter({ hasText: '#23' }).first()
Expected: visible
Timeout: 15000ms
Error: element(s) not found

Call log:
�[2m  - Expect "toBeVisible" with timeout 15000ms�[22m
�[2m  - waiting for locator('[data-testid="task-feed-card"]').filter({ hasText: '#23' }).first()�[22m

Features/GlobalPageSize.spec.ts › Page size should persist across different pages (shard 2)
�[31mTest timeout of 60000ms exceeded.�[39m
Pages/Lineage/DataAssetLineage.spec.ts › Column lineage for container -> mlModel (shard 6)
�[31mTest timeout of 180000ms exceeded.�[39m
🟡 51 flaky test(s) (passed on retry)
  • Features/DataAssetRulesEnabled.spec.ts › Verify the Topic Entity Action items after rules is Enabled (shard 1, 1 retry)
  • Features/DataAssetRulesDisabled.spec.ts › Verify the Store Procedure entity item action after rules disabled (shard 1, 1 retry)
  • Features/Pagination.spec.ts › should test Database Schemas complete flow with search (shard 1, 1 retry)
  • Features/TagsSuggestion.spec.ts › should decline requested tags for an api endpoint request schema field (shard 1, 2 retries)
  • Flow/Metric.spec.ts › Verify Granularity Update (shard 1, 1 retry)
  • Flow/SearchRBAC.spec.ts › User without permission (shard 1, 1 retry)
  • Flow/SearchRBAC.spec.ts › the browse tree only shows the asset-type categories a user can access (shard 1, 2 retries)
  • Features/BulkEditEntity.spec.ts › Glossary (shard 2, 2 retries)
  • Features/BulkEditOperationBadges.spec.ts › Glossary bulk edit search filters rows and clear restores them (shard 2, 1 retry)
  • Features/BulkImport.spec.ts › Table (shard 2, 2 retries)
  • Features/Container.spec.ts › Container page should show Schema and Children count (shard 2, 1 retry)
  • Features/Container.spec.ts › Container page children pagination (shard 2, 1 retry)
  • Features/Container.spec.ts › Copy column link button should copy the column URL to clipboard (shard 2, 1 retry)
  • Features/DataQuality/ProfilerIngestionForm.spec.ts › switching DYNAMIC → STATIC does not leak smartSampling or thresholds (shard 2, 1 retry)
  • Features/DataQuality/TestCaseImportExportE2eFlow.spec.ts › Admin: Complete export-import-validate flow (shard 2, 2 retries)
  • Features/GlobalSearchSuggestions.spec.ts › Navigate to column from column suggestion (shard 2, 1 retry)
  • Features/Glossary/GlossaryHierarchy.spec.ts › should move term with children to different glossary (shard 2, 1 retry)
  • Features/IncidentManager.spec.ts › Complete Incident lifecycle with table owner (shard 3, 1 retry)
  • Features/LandingPageWidgets/FollowingWidget.spec.ts › Check followed entity present in following widget (shard 3, 1 retry)
  • Features/Permissions/EntityPermissions.spec.ts › SearchIndex deny entity-specific permission operations (shard 3, 1 retry)
  • Features/Permissions/EntityPermissions.spec.ts › Metric allow 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 › source URL button links to the configured source (shard 3, 1 retry)
  • Flow/NestedChildrenUpdates.spec.ts › should add and remove tags to nested column immediately without refresh (shard 4, 1 retry)
  • Flow/PersonaFlow.spec.ts › Set default persona for team should work properly (shard 4, 1 retry)
  • Pages/DataContractsSemanticRules.spec.ts › Validate Description Rule Is_Set (shard 4, 1 retry)
  • Pages/Entity.spec.ts › Description Add, Update and Remove for child entities (shard 5, 1 retry)
  • Pages/Entity.spec.ts › Announcement create, edit & delete (shard 5, 1 retry)
  • Pages/Entity.spec.ts › Tag and Glossary Selector should close vice versa (shard 5, 1 retry)
  • Pages/Entity.spec.ts › Glossary Term Add, Update and Remove for child entities (shard 5, 1 retry)
  • ... and 21 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

@mohityadav766 mohityadav766 merged commit fc85a54 into main Jun 29, 2026
65 of 72 checks passed
@mohityadav766 mohityadav766 deleted the search-cleanup-after-service-delete branch June 29, 2026 13:51
@gitar-bot

gitar-bot Bot commented Jun 29, 2026

Copy link
Copy Markdown
Code Review ✅ Approved

Clears orphaned column documents in the search index during recursive hard deletes of database-related services by introducing a targeted descendant cleanup. The fix is verified through new unit, integration, and scale tests.

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

sonika-shah pushed a commit that referenced this pull request Jun 30, 2026
…ardDelete IT

DatabaseServiceResourceIT.recursiveHardDelete_serviceSubtree_leavesNoOrphansAndSearchClean
fails fleet-wide (every PR, including unrelated ones) on the "column_search_index doc
should be present in search before delete" precondition. It passes in isolation and
passed on its introducing PR (#29549, 3 engines), but flakes under the full concurrent
IT suite: no backend indexing code changed after #29549.

Root cause: #29549 added deleteDescendantColumns, a delete-by-query against the shared
column_search_index that now fires on every databaseService/database/databaseSchema
hard delete (SearchRepository.deleteEntityIndex). Under the concurrent suite hundreds of
these run together, contending on that one index and delaying visibility of a freshly
indexed column doc past the 60s precondition timeout.

Drop the column_search_index SearchDoc from this concurrent base-test subtree, keeping the
table_search_index and relationship-orphan assertions. Column-cleanup correctness stays
covered by SearchRepositoryBehaviorTest (unit) and ServiceDeleteSearchCleanupScaleIT
(@tag scale), neither of which runs under this contention.
sonika-shah added a commit that referenced this pull request Jun 30, 2026
…ardDelete IT

DatabaseServiceResourceIT.recursiveHardDelete_serviceSubtree_leavesNoOrphansAndSearchClean
fails fleet-wide (every PR, including unrelated ones) on the "column_search_index doc
should be present in search before delete" precondition. It passes in isolation and
passed on its introducing PR (#29549, 3 engines), but flakes under the full concurrent
IT suite; no backend indexing code changed after #29549.

Root cause: #29549 added deleteDescendantColumns, a delete-by-query against the shared
column_search_index that now fires on every databaseService/database/databaseSchema
hard delete (SearchRepository.deleteEntityIndex). Under the concurrent suite hundreds of
these run together, contending on that one index and delaying visibility of a freshly
indexed column doc past the 60s precondition timeout.

Drop the column_search_index SearchDoc from this concurrent base-test subtree, keeping the
table_search_index and relationship-orphan assertions. Column-cleanup correctness stays
covered by SearchRepositoryBehaviorTest (unit) and ServiceDeleteSearchCleanupScaleIT
(@tag scale), neither of which runs under this contention.
chirag-madlani pushed a commit that referenced this pull request Jul 1, 2026
…ardDelete IT (#29626)

DatabaseServiceResourceIT.recursiveHardDelete_serviceSubtree_leavesNoOrphansAndSearchClean
fails fleet-wide (every PR, including unrelated ones) on the "column_search_index doc
should be present in search before delete" precondition. It passes in isolation and
passed on its introducing PR (#29549, 3 engines), but flakes under the full concurrent
IT suite; no backend indexing code changed after #29549.

Root cause: #29549 added deleteDescendantColumns, a delete-by-query against the shared
column_search_index that now fires on every databaseService/database/databaseSchema
hard delete (SearchRepository.deleteEntityIndex). Under the concurrent suite hundreds of
these run together, contending on that one index and delaying visibility of a freshly
indexed column doc past the 60s precondition timeout.

Drop the column_search_index SearchDoc from this concurrent base-test subtree, keeping the
table_search_index and relationship-orphan assertions. Column-cleanup correctness stays
covered by SearchRepositoryBehaviorTest (unit) and ServiceDeleteSearchCleanupScaleIT
(@tag scale), neither of which runs under this contention.
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.

Recursive service hard delete leaves orphaned column docs in search (regression from #29322)

3 participants