diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/dispatcher/queries.test.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/dispatcher/queries.test.ts index d8a03341f476f..6b462ed469dfe 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/dispatcher/queries.test.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/dispatcher/queries.test.ts @@ -70,7 +70,7 @@ describe('getDispatchableAlertEventsQuery', () => { }); describe('getAlertEpisodeSuppressionsQuery', () => { - it('builds a WHERE clause matching each episode rule_id and group_hash', () => { + it('uses CONCAT + IN to filter by (rule_id, group_hash) pairs', () => { const episodes = [ createAlertEpisode({ rule_id: 'rule-1', group_hash: 'hash-1' }), createAlertEpisode({ rule_id: 'rule-2', group_hash: 'hash-2' }), @@ -78,8 +78,23 @@ describe('getAlertEpisodeSuppressionsQuery', () => { const req = getAlertEpisodeSuppressionsQuery(episodes); - expect(req.query).toContain('rule_id == "rule-1" AND group_hash == "hash-1"'); - expect(req.query).toContain('rule_id == "rule-2" AND group_hash == "hash-2"'); + expect(req.query).toContain('CONCAT(rule_id, "::", group_hash)'); + expect(req.query).toContain('rule-1::hash-1'); + expect(req.query).toContain('rule-2::hash-2'); + }); + + it('deduplicates episodes with the same rule_id and group_hash', () => { + const episodes = [ + createAlertEpisode({ rule_id: 'rule-1', group_hash: 'hash-1', episode_id: 'ep-1' }), + createAlertEpisode({ rule_id: 'rule-1', group_hash: 'hash-1', episode_id: 'ep-2' }), + createAlertEpisode({ rule_id: 'rule-2', group_hash: 'hash-2', episode_id: 'ep-3' }), + ]; + + const req = getAlertEpisodeSuppressionsQuery(episodes); + + const matches = req.query.match(/rule-1::hash-1/g); + expect(matches).toHaveLength(1); + expect(req.query).toContain('rule-2::hash-2'); }); it('queries the alert actions data stream', () => { @@ -148,7 +163,20 @@ describe('getAlertEpisodeSuppressionsQuery', () => { createAlertEpisode({ rule_id: 'only-rule', group_hash: 'only-hash' }), ]); - expect(req.query).toContain('rule_id == "only-rule" AND group_hash == "only-hash"'); + expect(req.query).toContain('only-rule::only-hash'); + }); + + it('builds successfully with a large number of episodes', () => { + const episodes = Array.from({ length: 500 }, (_, i) => + createAlertEpisode({ rule_id: `rule-${i}`, group_hash: `hash-${i}` }) + ); + + const req = getAlertEpisodeSuppressionsQuery(episodes); + + expect(req).toHaveProperty('query'); + expect(req.query).toContain('CONCAT(rule_id, "::", group_hash)'); + expect(req.query).toContain('rule-0::hash-0'); + expect(req.query).toContain('rule-499::hash-499'); }); }); diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/dispatcher/queries.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/dispatcher/queries.ts index 1efb881678c35..4eab7174d5503 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/dispatcher/queries.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/dispatcher/queries.ts @@ -38,6 +38,8 @@ export const getDispatchableAlertEventsQuery = (): EsqlRequest => { | LIMIT 10000`.toRequest(); }; +const PAIR_SEPARATOR = '::'; + export const getAlertEpisodeSuppressionsQuery = (alertEpisodes: AlertEpisode[]): EsqlRequest => { const minLastEventTimestamp = alertEpisodes.reduce((min, ep) => { @@ -50,13 +52,14 @@ export const getAlertEpisodeSuppressionsQuery = (alertEpisodes: AlertEpisode[]): return min === undefined || normalizedTimestamp < min ? normalizedTimestamp : min; }, undefined) ?? new Date(0).toISOString(); - let whereClause = esql.exp`FALSE`; - for (const alertEpisode of alertEpisodes) { - whereClause = esql.exp`${whereClause} OR (rule_id == ${alertEpisode.rule_id} AND group_hash == ${alertEpisode.group_hash})`; - } + const uniquePairKeys = [ + ...new Set(alertEpisodes.map((ep) => `${ep.rule_id}${PAIR_SEPARATOR}${ep.group_hash}`)), + ]; + const pairValues = uniquePairKeys.map((key) => esql.str(key)); return esql`FROM ${ALERT_ACTIONS_DATA_STREAM} - | WHERE ${whereClause} + | EVAL _pair_key = CONCAT(rule_id, ${PAIR_SEPARATOR}, group_hash) + | WHERE _pair_key IN (${pairValues}) | WHERE action_type IN ("ack", "unack", "deactivate", "activate", "snooze", "unsnooze") | WHERE action_type != "snooze" OR expiry > ${minLastEventTimestamp}::datetime | INLINE STATS