diff --git a/.rubocop_todo/search/namespaced_class.yml b/.rubocop_todo/search/namespaced_class.yml
index 452b93880437f14ab7c7008e0299e5268cfeac69..8074e90fc5329b66452012a02444355eefd22592 100644
--- a/.rubocop_todo/search/namespaced_class.yml
+++ b/.rubocop_todo/search/namespaced_class.yml
@@ -93,6 +93,9 @@ Search/NamespacedClass:
     - 'ee/lib/elastic/latest/config.rb'
     - 'ee/lib/elastic/latest/custom_language_analyzers.rb'
     - 'ee/lib/elastic/latest/document_should_be_deleted_from_index_error.rb'
+    - 'ee/lib/elastic/latest/epic_class_proxy.rb'
+    - 'ee/lib/elastic/latest/epic_config.rb'
+    - 'ee/lib/elastic/latest/epic_instance_proxy.rb'
     - 'ee/lib/elastic/latest/git_class_proxy.rb'
     - 'ee/lib/elastic/latest/git_instance_proxy.rb'
     - 'ee/lib/elastic/latest/issue_class_proxy.rb'
@@ -131,6 +134,8 @@ Search/NamespacedClass:
     - 'ee/lib/elastic/v12p1/application_class_proxy.rb'
     - 'ee/lib/elastic/v12p1/application_instance_proxy.rb'
     - 'ee/lib/elastic/v12p1/config.rb'
+    - 'ee/lib/elastic/v12p1/epic_class_proxy.rb'
+    - 'ee/lib/elastic/v12p1/epic_instance_proxy.rb'
     - 'ee/lib/elastic/v12p1/issue_class_proxy.rb'
     - 'ee/lib/elastic/v12p1/issue_instance_proxy.rb'
     - 'ee/lib/elastic/v12p1/merge_request_class_proxy.rb'
diff --git a/config/feature_flags/ops/search_index_curation_epics.yml b/config/feature_flags/ops/search_index_curation_epics.yml
new file mode 100644
index 0000000000000000000000000000000000000000..73eab662cb68abbc01f68fc05058c136ba1a5b5b
--- /dev/null
+++ b/config/feature_flags/ops/search_index_curation_epics.yml
@@ -0,0 +1,8 @@
+---
+name: search_index_curation_epics
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121635
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/413605
+milestone: '16.2'
+type: ops
+group: group::global search
+default_enabled: false
diff --git a/ee/app/models/ee/epic.rb b/ee/app/models/ee/epic.rb
index 52f8eb59e7a03f883ae0d6facfc0eabf5b5a044f..6d00dc59ebfba33f0bd249002734a852283ea44c 100644
--- a/ee/app/models/ee/epic.rb
+++ b/ee/app/models/ee/epic.rb
@@ -23,6 +23,7 @@ module Epic
       include EachBatch
       include ::Exportable
       include Epics::MetadataCacheUpdate
+      include Elastic::ApplicationVersionedSearch
 
       DEFAULT_COLOR = ::Gitlab::Color.of('#1068bf')
       MAX_HIERARCHY_DEPTH = 7
@@ -325,6 +326,17 @@ def search(query)
         fuzzy_search(query, [:title, :description])
       end
 
+      def elasticsearch_available?
+        return false unless ::Feature.enabled?(:elastic_index_epics)
+
+        ::Elastic::DataMigrationService.migration_has_finished?(:create_epic_index)
+      end
+
+      override :use_separate_indices?
+      def use_separate_indices?
+        true
+      end
+
       def ids_for_base_and_decendants(epic_ids)
         ::Gitlab::ObjectHierarchy.new(self.id_in(epic_ids)).base_and_descendants.pluck(:id)
       end
@@ -519,6 +531,12 @@ def validate_parent
       validate_parent_epic
     end
 
+    def use_elasticsearch?
+      return false unless self.class.elasticsearch_available?
+
+      group.use_elasticsearch?
+    end
+
     def issues_readable_by(current_user, preload: nil)
       related_issues = self.class.related_issues(ids: id, preload: preload)
 
diff --git a/ee/config/feature_flags/development/elastic_index_epics.yml b/ee/config/feature_flags/development/elastic_index_epics.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e547ac042de847345b6089e1cf855178704207f2
--- /dev/null
+++ b/ee/config/feature_flags/development/elastic_index_epics.yml
@@ -0,0 +1,8 @@
+---
+name: elastic_index_epics
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121635
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/412108
+milestone: '16.2'
+type: development
+group: group::global search
+default_enabled: false
diff --git a/ee/elastic/migrate/20230615101400_create_epic_index.rb b/ee/elastic/migrate/20230615101400_create_epic_index.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9dd9d3cadf3b3cc5ab42f6f5f9381cf42ba2c269
--- /dev/null
+++ b/ee/elastic/migrate/20230615101400_create_epic_index.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class CreateEpicIndex < Elastic::Migration
+  include Elastic::MigrationCreateIndex
+
+  retry_on_failure
+
+  def document_type
+    :epic
+  end
+
+  def target_class
+    Epic
+  end
+end
diff --git a/ee/lib/elastic/latest/epic_class_proxy.rb b/ee/lib/elastic/latest/epic_class_proxy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5df25fde057b21bacd87edde1690538ed8bb0255
--- /dev/null
+++ b/ee/lib/elastic/latest/epic_class_proxy.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Elastic
+  module Latest
+    class EpicClassProxy < ApplicationClassProxy
+      def elastic_search(query, options: {})
+        raise NotImplementedError
+      end
+
+      def preload_indexing_data(relation)
+        # rubocop: disable CodeReuse/ActiveRecord
+        relation.includes(
+          :author,
+          :labels,
+          :group,
+          :start_date_sourcing_epic,
+          :due_date_sourcing_epic,
+          :start_date_sourcing_milestone,
+          :due_date_sourcing_milestone
+        )
+        # rubocop: enable CodeReuse/ActiveRecord
+      end
+    end
+  end
+end
diff --git a/ee/lib/elastic/latest/epic_config.rb b/ee/lib/elastic/latest/epic_config.rb
new file mode 100644
index 0000000000000000000000000000000000000000..104d02868049992f569c7f6633c2313a3156bfe5
--- /dev/null
+++ b/ee/lib/elastic/latest/epic_config.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Elastic
+  module Latest
+    module EpicConfig
+      extend Elasticsearch::Model::Indexing::ClassMethods
+      extend Elasticsearch::Model::Naming::ClassMethods
+
+      self.index_name = [Rails.application.class.module_parent_name.downcase, Rails.env, 'epics'].join('-')
+
+      settings Elastic::Latest::Config.settings.to_hash.deep_merge(
+        index: Elastic::Latest::Config.separate_index_specific_settings(index_name)
+      )
+
+      mappings dynamic: 'strict' do
+        indexes :id, type: :integer
+        indexes :iid, type: :integer
+        indexes :group_id, type: :integer
+
+        indexes :created_at, type: :date
+        indexes :updated_at, type: :date
+
+        indexes :title, type: :text, index_options: 'positions'
+        indexes :description, type: :text, index_options: 'positions'
+        indexes :state, type: :keyword
+        indexes :confidential, type: :boolean
+        indexes :author_id, type: :integer
+        indexes :label_ids, type: :keyword
+        indexes :start_date, type: :date
+        indexes :due_date, type: :date
+
+        indexes :traversal_ids, type: :keyword
+        indexes :hashed_root_namespace_id, type: :integer
+        indexes :visibility_level, type: :integer
+
+        indexes :schema_version, type: :short
+        indexes :type, type: :keyword
+      end
+    end
+  end
+end
diff --git a/ee/lib/elastic/latest/epic_instance_proxy.rb b/ee/lib/elastic/latest/epic_instance_proxy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..76466f0316751a26cd2022d57cc80ed08a204fec
--- /dev/null
+++ b/ee/lib/elastic/latest/epic_instance_proxy.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Elastic
+  module Latest
+    class EpicInstanceProxy < ApplicationInstanceProxy
+      def as_indexed_json(_options = {})
+        data = {}
+
+        [
+          :id,
+          :iid,
+          :group_id,
+          :created_at,
+          :updated_at,
+          :title,
+          :description,
+          :state,
+          :confidential,
+          :author_id
+        ].each do |attr|
+          data[attr.to_s] = safely_read_attribute_for_elasticsearch(attr)
+        end
+
+        data['label_ids'] = target.label_ids.map(&:to_s)
+        data['start_date'] = target.start_date || target.start_date_from_inherited_source
+        data['due_date'] = target.end_date || target.due_date_from_inherited_source
+
+        data['traversal_ids'] = target.group.elastic_namespace_ancestry
+        data['hashed_root_namespace_id'] = target.group.hashed_root_namespace_id
+        data['visibility_level'] = target.group.visibility_level
+
+        # Schema version. The format is Date.today.strftime('%y_%m')
+        # Please update if you're changing the schema of the document
+        data['schema_version'] = 23_06
+
+        data.merge(generic_attributes)
+      end
+
+      def generic_attributes
+        super.except('join_field')
+      end
+
+      def es_parent
+        "group_#{group.root_ancestor.id}"
+      end
+    end
+  end
+end
diff --git a/ee/lib/elastic/v12p1/epic_class_proxy.rb b/ee/lib/elastic/v12p1/epic_class_proxy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..89a6d9b291983a774e44610776e6f89f4aa13115
--- /dev/null
+++ b/ee/lib/elastic/v12p1/epic_class_proxy.rb
@@ -0,0 +1,10 @@
+# rubocop:disable Naming/FileName
+# frozen_string_literal: true
+
+module Elastic
+  module V12p1
+    EpicClassProxy = Elastic::Latest::EpicClassProxy
+  end
+end
+
+# rubocop:enable Naming/FileName
diff --git a/ee/lib/elastic/v12p1/epic_instance_proxy.rb b/ee/lib/elastic/v12p1/epic_instance_proxy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..461730f177720bb0c1be158f09bf1ff6cbdaa3b8
--- /dev/null
+++ b/ee/lib/elastic/v12p1/epic_instance_proxy.rb
@@ -0,0 +1,10 @@
+# rubocop:disable Naming/FileName
+# frozen_string_literal: true
+
+module Elastic
+  module V12p1
+    EpicInstanceProxy = Elastic::Latest::EpicInstanceProxy
+  end
+end
+
+# rubocop:enable Naming/FileName
diff --git a/ee/lib/gitlab/elastic/helper.rb b/ee/lib/gitlab/elastic/helper.rb
index bcd3059de0a7ae11a05cf3fba23eb081661cf45b..0a8847369c1ae6bba6d75190ef36dbae81e6d4dd 100644
--- a/ee/lib/gitlab/elastic/helper.rb
+++ b/ee/lib/gitlab/elastic/helper.rb
@@ -19,6 +19,7 @@ class Helper
         Note,
         MergeRequest,
         Commit,
+        Epic,
         User,
         Wiki,
         Project
diff --git a/ee/spec/elastic/migrate/20230615101400_create_epic_index_spec.rb b/ee/spec/elastic/migrate/20230615101400_create_epic_index_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b6c60a4adfcf4c58d55916a74e5ffa39838cf4fa
--- /dev/null
+++ b/ee/spec/elastic/migrate/20230615101400_create_epic_index_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative 'migration_shared_examples'
+require File.expand_path('ee/elastic/migrate/20230615101400_create_epic_index.rb')
+
+RSpec.describe CreateEpicIndex, feature_category: :global_search do
+  it_behaves_like 'migration creates a new index', 20230615101400, Epic
+end
diff --git a/ee/spec/elastic_integration/epic_index_spec.rb b/ee/spec/elastic_integration/epic_index_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..73b0227d0f58efb2fa4544021b60f4cabe3937b8
--- /dev/null
+++ b/ee/spec/elastic_integration/epic_index_spec.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Epic index', feature_category: :global_search do
+  let_it_be(:user) { create(:user) }
+  let_it_be(:parent_group) { create(:group) }
+  let_it_be_with_refind(:group) { create(:group, parent: parent_group) }
+  let_it_be_with_refind(:epic) { create(:epic, group: group) }
+  let_it_be(:member) { create(:group_member, :owner, group: group, user: user) }
+  let(:epic_index) { Epic.__elasticsearch__.index_name }
+  let(:helper) { Gitlab::Elastic::Helper.default }
+  let(:client) { helper.client }
+
+  before do
+    stub_feature_flags(elastic_index_epics: true)
+    allow(::Elastic::DataMigrationService).to receive(:migration_has_finished?)
+      .with(:create_epic_index).and_return(true)
+    stub_ee_application_setting(elasticsearch_indexing: true)
+    allow(::Elastic::ProcessBookkeepingService).to receive(:track!)
+  end
+
+  shared_examples 'epics get tracked in Elasticsearch' do
+    it 'use_elasticsearch? is true' do
+      expect(epic).to be_use_elasticsearch
+    end
+
+    context 'when an epic is created' do
+      let(:epic) { build(:epic, group: group) }
+
+      it 'tracks the epic' do
+        expect(::Elastic::ProcessBookkeepingService).to receive(:track!).with(epic).once
+        epic.save!
+      end
+    end
+
+    context 'when an epic is updated' do
+      it 'tracks the epic' do
+        expect(::Elastic::ProcessBookkeepingService).to receive(:track!).with(epic).once
+        epic.update!(title: 'A new title')
+      end
+    end
+
+    context 'when an epic is deleted' do
+      it 'tracks the epic' do
+        expect(::Elastic::ProcessBookkeepingService).to receive(:track!).with(epic).once
+        epic.destroy!
+      end
+
+      it 'deletes the epic from elasticsearch', :elastic_clean do
+        allow(::Elastic::ProcessBookkeepingService).to receive(:track!).and_call_original
+
+        epic = create(:epic, group: group)
+        ensure_elasticsearch_index!
+        expect(epics_in_index).to eq([epic.id])
+
+        epic.destroy!
+
+        ensure_elasticsearch_index!
+        expect(epics_in_index).to be_empty
+      end
+    end
+  end
+
+  shared_examples 'epics do not get tracked in Elasticsearch' do
+    it 'use_elasticsearch? is false' do
+      expect(epic).not_to be_use_elasticsearch
+    end
+
+    context 'when an epic is created' do
+      let(:epic) { build(:epic, group: group) }
+
+      it 'does not track the epic' do
+        expect(::Elastic::ProcessBookkeepingService).not_to receive(:track!).with(epic)
+        epic.save!
+      end
+    end
+
+    context 'when an epic is updated' do
+      it 'does not track the epic' do
+        expect(::Elastic::ProcessBookkeepingService).not_to receive(:track!).with(epic)
+        epic.update!(title: 'A new title')
+      end
+    end
+
+    context 'when an epic is deleted' do
+      it 'does not track the epic' do
+        expect(::Elastic::ProcessBookkeepingService).not_to receive(:track!).with(epic)
+        epic.destroy!
+      end
+    end
+  end
+
+  it_behaves_like 'epics get tracked in Elasticsearch'
+
+  context 'when elasticsearch_limit_indexing? is true' do
+    before do
+      stub_ee_application_setting(elasticsearch_limit_indexing?: true)
+    end
+
+    context 'if the parent group is not in the limited indexes list' do
+      it_behaves_like 'epics do not get tracked in Elasticsearch'
+    end
+
+    context 'if the parent group is in the limited indexes list' do
+      before do
+        create(:elasticsearch_indexed_namespace, namespace: parent_group)
+      end
+
+      it_behaves_like 'epics get tracked in Elasticsearch'
+    end
+  end
+
+  def epics_in_index
+    client.search(index: epic_index).dig('hits', 'hits').map { |hit| hit['_source']['id'] }
+  end
+end
diff --git a/ee/spec/lib/elastic/latest/epic_class_proxy_spec.rb b/ee/spec/lib/elastic/latest/epic_class_proxy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2e72c671645c54e8afc267b03fb2fc4abba78d62
--- /dev/null
+++ b/ee/spec/lib/elastic/latest/epic_class_proxy_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Elastic::Latest::EpicClassProxy, feature_category: :global_search do
+  it 'raises an error when calling elastic_search' do
+    expect { described_class.new(Epic, use_separate_indices: true).elastic_search('*') }
+      .to raise_error(NotImplementedError)
+  end
+end
diff --git a/ee/spec/lib/elastic/latest/epic_config_spec.rb b/ee/spec/lib/elastic/latest/epic_config_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ac1c266a386a4ac3a4b860a1a710124e35255bc2
--- /dev/null
+++ b/ee/spec/lib/elastic/latest/epic_config_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative './config_shared_examples'
+
+RSpec.describe Elastic::Latest::EpicConfig, feature_category: :global_search do
+  describe '.settings' do
+    it_behaves_like 'config settings return correct values'
+  end
+
+  describe '.mappings' do
+    it 'returns config' do
+      expect(described_class.mapping).to be_a(Elasticsearch::Model::Indexing::Mappings)
+    end
+  end
+
+  describe '.index_name' do
+    it 'includes' do
+      expect(described_class.index_name).to include('-epics')
+    end
+  end
+end
diff --git a/ee/spec/lib/elastic/latest/epic_instance_proxy_spec.rb b/ee/spec/lib/elastic/latest/epic_instance_proxy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f5186d869280adec79b1a3c51ba51ace68ebf3a9
--- /dev/null
+++ b/ee/spec/lib/elastic/latest/epic_instance_proxy_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Elastic::Latest::EpicInstanceProxy, feature_category: :global_search do
+  let_it_be(:parent_group) { create(:group) }
+  let_it_be(:group) { create(:group, parent: parent_group) }
+  let_it_be(:label) { create(:group_label, group: group) }
+  let_it_be(:epic) { create(:labeled_epic, :use_fixed_dates, :opened, group: group, labels: [label]) }
+
+  subject { described_class.new(epic) }
+
+  describe '#as_indexed_json' do
+    let(:result) { subject.as_indexed_json.with_indifferent_access }
+
+    it 'serializes the object as a hash' do
+      expect(result).to include(
+        id: epic.id,
+        iid: epic.iid,
+        group_id: group.id,
+        created_at: epic.created_at,
+        updated_at: epic.updated_at,
+        title: epic.title,
+        description: epic.description,
+        state: 'opened',
+        confidential: epic.confidential,
+        author_id: epic.author_id,
+        label_ids: [label.id.to_s],
+        start_date: epic.start_date,
+        due_date: epic.due_date,
+        traversal_ids: "#{parent_group.id}-#{group.id}-",
+        hashed_root_namespace_id: ::Search.hash_namespace_id(parent_group.id),
+        visibility_level: group.visibility_level,
+        schema_version: 2306,
+        type: 'epic'
+      )
+    end
+
+    context 'with start date inherited date from child epic and due date inherited from milestone' do
+      let_it_be(:epic) { create(:epic) }
+      let_it_be(:child_epic) { create(:epic, :use_fixed_dates) }
+      let_it_be(:milestone) { create(:milestone, :with_dates) }
+
+      before do
+        epic.start_date_sourcing_epic = child_epic
+        epic.due_date_sourcing_milestone = milestone
+        epic.save!
+      end
+
+      it 'sets start and due dates to inherited dates' do
+        expect(result[:start_date]).to eq(child_epic.start_date)
+        expect(result[:due_date]).to eq(milestone.due_date)
+      end
+    end
+  end
+
+  describe '#es_parent' do
+    it 'contains group id' do
+      expect(subject.es_parent).to eq("group_#{parent_group.id}")
+    end
+  end
+end
diff --git a/ee/spec/models/epic_spec.rb b/ee/spec/models/epic_spec.rb
index 51c0ee428c03eb2bc927d9a5047c830d71b621ae..34a3f834c362b7f749be0e614362b159cbed0db6 100644
--- a/ee/spec/models/epic_spec.rb
+++ b/ee/spec/models/epic_spec.rb
@@ -1184,6 +1184,84 @@ def as_item(item)
     end
   end
 
+  describe 'ES related specs' do
+    let_it_be(:epic) { create(:epic, group: group) }
+
+    context 'when create epic index migration is not finished' do
+      before do
+        allow(::Elastic::DataMigrationService).to receive(:migration_has_finished?)
+          .with(:create_epic_index).and_return(false)
+      end
+
+      it 'use_elasticsearch? is false' do
+        expect(epic).not_to be_use_elasticsearch
+      end
+    end
+
+    context 'when create epic index migration is finished' do
+      before do
+        allow(::Elastic::DataMigrationService).to receive(:migration_has_finished?)
+          .with(:create_epic_index).and_return(true)
+      end
+
+      context 'when the group has use_elasticsearch? as true' do
+        before do
+          allow(group).to receive(:use_elasticsearch?).and_return(true)
+        end
+
+        it 'use_elasticsearch? is true' do
+          expect(epic).to be_use_elasticsearch
+        end
+
+        context 'when feature flag is disabled' do
+          before do
+            stub_feature_flags(elastic_index_epics: false)
+          end
+
+          it 'use_elasticsearch? is false' do
+            expect(epic).not_to be_use_elasticsearch
+          end
+        end
+
+        context 'with elasticsearch enabled' do
+          before do
+            allow(Gitlab::CurrentSettings.current_application_settings)
+              .to receive(:elasticsearch_indexing?).and_return(true)
+          end
+
+          it 'calls ::Elastic::ProcessBookkeepingService.track! when the epic is updated' do
+            expect(Elastic::ProcessBookkeepingService).to receive(:track!).with(*epic).once
+
+            epic.update!(title: 'A new title')
+          end
+        end
+      end
+
+      context 'when the group has use_elasticsearch? as false' do
+        before do
+          allow(group).to receive(:use_elasticsearch?).and_return(false)
+        end
+
+        it 'use_elasticsearch? is false' do
+          expect(epic).not_to be_use_elasticsearch
+        end
+
+        context 'with elasticsearch enabled' do
+          before do
+            allow(Gitlab::CurrentSettings.current_application_settings)
+              .to receive(:elasticsearch_indexing?).and_return(true)
+          end
+
+          it 'does not call ::Elastic::ProcessBookkeepingService.track! when the epic is updated' do
+            expect(Elastic::ProcessBookkeepingService).not_to receive(:track!).with(*epic)
+
+            epic.update!(title: 'A new title')
+          end
+        end
+      end
+    end
+  end
+
   describe '#epic_link_type' do
     let_it_be(:source_epic) { create(:epic, group: group) }
     let_it_be(:target_epic) { create(:epic, group: group) }
diff --git a/ee/spec/services/elastic/cluster_reindexing_service_spec.rb b/ee/spec/services/elastic/cluster_reindexing_service_spec.rb
index a7e28a9ff675821e08f07fd004a46b0d892432e7..f242d622cea76bd3a5a84df6b31b3ea25225580e 100644
--- a/ee/spec/services/elastic/cluster_reindexing_service_spec.rb
+++ b/ee/spec/services/elastic/cluster_reindexing_service_spec.rb
@@ -78,7 +78,7 @@
         expect { cluster_reindexing_service.execute }.to change { task.reload.state }.from('indexing_paused').to('reindexing')
 
         subtasks = task.subtasks
-        expect(subtasks.count).to eq(8)
+        expect(subtasks.count).to eq(helper.standalone_indices_proxies.count + 1) # +1 for main index
 
         subtask_1 = subtasks.find { |subtask| subtask.alias_name == main_alias }
         slice_1 = subtask_1.slices.first
diff --git a/ee/spec/services/elastic/process_bookkeeping_service_spec.rb b/ee/spec/services/elastic/process_bookkeeping_service_spec.rb
index 70375aacf4d4c23a19ee205674221f6a62f5ad69..75d4f4a30542d0de69055add362cb5214ece3194 100644
--- a/ee/spec/services/elastic/process_bookkeeping_service_spec.rb
+++ b/ee/spec/services/elastic/process_bookkeeping_service_spec.rb
@@ -463,6 +463,64 @@
           expect { described_class.new.execute }.not_to exceed_all_query_limit(control)
         end
       end
+
+      it 'does not have N+1 queries for epics' do
+        epics = create_list(:epic, 2, :use_fixed_dates)
+
+        described_class.track!(*epics)
+
+        control = ActiveRecord::QueryRecorder.new(skip_cached: false) { described_class.new.execute }
+
+        epics += create_list(:epic, 3, :use_fixed_dates)
+
+        described_class.track!(*epics)
+
+        expect { described_class.new.execute }.not_to exceed_all_query_limit(control)
+      end
+
+      it 'does not have N+1 queries for epics with inherited dates' do
+        child_epic = create(:epic, :use_fixed_dates)
+        milestone = create(:milestone, :with_dates)
+
+        epics = create_list(:epic, 2)
+        epics.each do |epic|
+          epic.start_date_sourcing_epic = child_epic
+          epic.due_date_sourcing_milestone = milestone
+          epic.save!
+        end
+
+        described_class.track!(*epics)
+
+        control = ActiveRecord::QueryRecorder.new(skip_cached: false) { described_class.new.execute }
+
+        epics += create_list(:epic, 3)
+        epics.each do |epic|
+          epic.start_date_sourcing_epic = child_epic
+          epic.due_date_sourcing_milestone = milestone
+          epic.save!
+        end
+
+        described_class.track!(*epics)
+
+        expect { described_class.new.execute }.not_to exceed_all_query_limit(control)
+      end
+
+      it 'does not have N+1 queries for epics in a group with multiple parents' do
+        parent_group = create(:group)
+        group = create(:group, parent: parent_group)
+
+        epics = create_list(:epic, 2, group: group)
+
+        described_class.track!(*epics)
+
+        control = ActiveRecord::QueryRecorder.new(skip_cached: false) { described_class.new.execute }
+
+        epics += create_list(:epic, 3, group: group)
+
+        described_class.track!(*epics)
+
+        expect { described_class.new.execute }.not_to exceed_all_query_limit(control)
+      end
     end
 
     def expect_processing(*refs, failures: [])
diff --git a/ee/spec/workers/search/index_curation_worker_spec.rb b/ee/spec/workers/search/index_curation_worker_spec.rb
index a32c5c2c9aa078f502fc1348e5a62b7e3fc4fbe3..d8ea79a8c415806df4a793337af047f2307f1b7e 100644
--- a/ee/spec/workers/search/index_curation_worker_spec.rb
+++ b/ee/spec/workers/search/index_curation_worker_spec.rb
@@ -11,7 +11,7 @@
   let(:logger) { ::Gitlab::Elasticsearch::Logger.build }
 
   describe '#curator_settings' do
-    let(:standalone_index_types) { %w[commits issues merge_requests notes users wikis projects] }
+    let(:standalone_index_types) { %w[commits issues merge_requests notes users wikis projects epics] }
     let(:curation_include_patterns) { [main_index_pattern] + standalone_index_types.map { |x| /#{x}/ } }
     let(:main_index_target_name) { "gitlab-test" }
     let(:main_index_name) { "gitlab-test-20220923-1517" }