Feature Proposal: Delete Event Support for ElasticGraph #825
Replies: 2 comments 5 replies
-
|
I'm a fan of this approach and generally the design looks good to me. Few small questions. Does the size of the field key have any noticeable impact on the document sizes? Since every document needs this new field is there any noticeable size difference between something like I'm also curious if you know why must_not performs so poorly with a small number of deleted documents? It's not clear to me what features would make that query difficult. |
Beta Was this translation helpful? Give feedback.
-
|
Great write-up! Two questions:
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Summary
This proposal outlines adding support for
deleteevents to ElasticGraph, enabling applications to properly handle document deletions while maintaining ElasticGraph's core guarantee of safe out-of-order event processing. The feature will use a tombstoning approach with opt-in configuration to ensure backward compatibility and optimal performance.Context
Current State
ElasticGraph currently supports only
upsertevents in its event stream specification:{ "op": "upsert", "id": "1", "type": "Widget", "version": 1, "record": { ... } }This schema was designed to also support delete events, but they are not currently supported:
{ "op": "delete", "id": "1", "type": "Widget", "version": 2 }The Challenge
Supporting delete events while maintaining ElasticGraph's core guarantee of safe out-of-order event processing presents significant challenges:
Version Ordering: ElasticGraph uses Elasticsearch's external versioning to handle duplicate and out-of-order events safely. When a document is deleted from Elasticsearch, the version information is lost, potentially allowing older upsert events to "resurrect" deleted documents.
Performance Impact: Any solution must not significantly degrade query performance for the majority of use cases that don't require delete support.
Data Integrity: The solution must handle edge cases like rollover indices and custom routing correctly.
The Version Ordering Problem Explained
To understand why delete events are challenging, consider how ElasticGraph currently handles out-of-order events with upserts:
Normal Order (Works Correctly)
ElasticGraph processes Event 1 first, storing Widget 1 with name "A". When Event 2 arrives, Elasticsearch's external versioning recognizes that version 2 is newer than version 1, so it updates the document to have name "B". ✅
Out-of-Order (Still Works Correctly)
ElasticGraph processes Event 2 first, storing Widget 1 with name "B". When Event 1 arrives later, Elasticsearch recognizes that version 1 is older than the current version 2, so it ignores the event. The document correctly remains with name "B". ✅
The Delete Problem - Normal Order
This should work as: create widget → delete widget → recreate widget. The final state should be a widget with name "C". ✅
The Delete Problem - Out-of-Order (Broken with a naive solution)
Naive approach:
Proposed Solution
Tombstoning Approach
We propose implementing delete support using a "tombstoning" approach:
When a
deleteevent is processed, instead of removing the document from Elasticsearch, we:idandversion__deleted: truefield to mark the document as deletedAll queries automatically filter out deleted documents by adding
__deleted: falseto the query filter when neededQuery Performance Analysis
To determine the optimal approach for filtering deleted documents, we conducted extensive performance testing comparing different query patterns and field types. The analysis examined two key variables:
Query Patterns Tested
mustqueries: Explicitly include only non-deleted documents{ "query": { "bool": { "must": { "term": { "__deleted": false } } } } }must_notqueries: Exclude deleted documents{ "query": { "bool": { "must_not": { "term": { "__deleted": true } } } } }Field Types and Query Operators
termqueries: Direct boolean value matchingexistsoperator: Checking for field presence/absencePerformance Results
We did a benchmark which revealed significant performance differences based on the query pattern and result set size:
must_nottermmusttermmust_nottermmusttermmust_notexistsmustexistsmust_notexistsmustexistsKey Findings
Dramatic Performance Difference with Small Result Sets: When queries return few results,
must_notqueries perform approximately 250x worse thanmustqueries (7,229ms vs 26ms average).Similar Performance with Large Result Sets: When queries return many results, both approaches perform similarly (~37ms average).
Field Type Impact: Boolean vs existence-based queries showed minimal performance differences within the same query pattern.
early all documents in the index (common case), this intersection becomes expensive.
Decision: Use
mustQueriesBased on these results, we will use
mustqueries with__deleted: falseto ensure optimal performance across all scenarios, particularly for queries that return small result sets.Opt-in Configuration
Delete support will be opt-in at the index level to ensure backward compatibility:
Smart Query Filtering
To minimize performance impact, we'll use intelligent query filtering:
__deleted: falsefilterThis approach ensures optimal performance while maintaining correctness.
Required Fields for Delete Events
For indices using rollover or custom routing, delete events must include the necessary routing information:
Rollover indices - must include the rollover key:
{ "op": "delete", "id": "1", "type": "Widget", "version": 2, "record": { "createdAt": "2023-03-30T00:30:00Z" } }Custom routing - must include the routing key:
{ "op": "delete", "id": "1", "type": "Widget", "version": 2, "record": { "workspaceId": "ABCD1234" } }Validation and Error Handling
<ObjectTypeName>Deletiontype will be generated for validationImplementation Details
Schema Changes
For indices with delete support enabled:
__deletedboolean field in the index mappingWidgetDeletion)__deleted: falseto all non-delete events during indexingQuery Modification
The query engine will be enhanced to:
__deleted: falsefilter is neededPerformance Considerations
Based on our benchmarking analysis:
mustqueries with__deleted: falseprovides the best performance across all scenariosMigration Path
For New Indices
Simply add
index.support_deletes!to the schema definition before indexing any documents.Safety Measures
Why Delete Support Must Be Enabled Before Indexing Data
The requirement that
support_deletes!be enabled only on empty indices is critical for data consistency. Here's why:The Problem: Our tombstoning approach relies on every document having a
__deleted: falsefield to distinguish between:__deleted: trueWhat Happens with Existing Data: If we enabled delete support on an index that already contains documents:
__deletedfield entirely__deleted: false__deleted: false, which would exclude all existing documents!Example Scenario:
The Solution: By requiring delete support to be enabled before any data is indexed:
__deleted: falsefield from the startEnforcement: The admin tooling will compare the current Elasticsearch mapping against the desired schema. If the mapping lacks the
__deletedfield but the schema configuration includessupport_deletes!, it will throw a clear error explaining that delete support cannot be added to indices with existing data.Migration Path for Existing Indices: If users need delete support on existing indices, they would need to:
support_deletes!enabled__deleted: false)Benefits
Alternatives Considered
Elasticsearch's
index.gc_deletesSettingWe investigated using Elasticsearch's built-in deleted document version retention, but this approach:
External Version Tracking
Maintaining deleted document versions in a separate datastore was considered but rejected due to:
No Delete Support
Forcing applications to handle deletions themselves was considered but rejected because:
Beta Was this translation helpful? Give feedback.
All reactions