diff --git a/ee/elastic/docs/20240130154724_add_fields_to_projects_index.yml b/ee/elastic/docs/20240130154724_add_fields_to_projects_index.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4b4f9ed96784dedcbc42d2fa5bda3b92cd10b262
--- /dev/null
+++ b/ee/elastic/docs/20240130154724_add_fields_to_projects_index.yml
@@ -0,0 +1,10 @@
+---
+name: AddFieldsToProjectsIndex
+version: '20240130154724'
+description: 'Adds repository_languages, forked, owner_id and mirror fields to the projects index'
+group: group::global search
+milestone: '16.9'
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/143771
+obsolete: false
+marked_obsolete_by_url:
+marked_obsolete_in_milestone:
diff --git a/ee/elastic/migrate/20240130154724_add_fields_to_projects_index.rb b/ee/elastic/migrate/20240130154724_add_fields_to_projects_index.rb
new file mode 100644
index 0000000000000000000000000000000000000000..868547981795f0ff286f9544f03cbb87b0b318cc
--- /dev/null
+++ b/ee/elastic/migrate/20240130154724_add_fields_to_projects_index.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class AddFieldsToProjectsIndex < Elastic::Migration
+  include Elastic::MigrationUpdateMappingsHelper
+
+  DOCUMENT_TYPE = Project
+
+  private
+
+  def new_mappings
+    {
+      mirror: {
+        type: 'boolean'
+      },
+      owner_id: {
+        type: 'integer'
+      },
+      forked: {
+        type: 'boolean'
+      },
+      repository_languages: {
+        type: 'keyword'
+      }
+    }
+  end
+end
diff --git a/ee/lib/elastic/latest/project_class_proxy.rb b/ee/lib/elastic/latest/project_class_proxy.rb
index e16e0c3c5625f99286846c1c1d25788ef30fa4ec..5b9bc5e042d4a00172801de5b5ba991b78a3542e 100644
--- a/ee/lib/elastic/latest/project_class_proxy.rb
+++ b/ee/lib/elastic/latest/project_class_proxy.rb
@@ -98,7 +98,7 @@ def rejected_project_filter(namespace, options)
 
       # rubocop: disable CodeReuse/ActiveRecord
       def preload_indexing_data(relation)
-        relation.includes(:project_feature, :route, :namespace, :catalog_resource)
+        relation.includes(:project_feature, :route, :catalog_resource, :fork_network, :mirror_user, :repository_languages, :group, namespace: :owner)
       end
       # rubocop: enable CodeReuse/ActiveRecord
     end
diff --git a/ee/lib/elastic/latest/project_config.rb b/ee/lib/elastic/latest/project_config.rb
index f0dde5689193bdb8be1a3974807213449cc83c16..a7e0a4072ea7dd665a08f8cdad6aec0cc2c90797 100644
--- a/ee/lib/elastic/latest/project_config.rb
+++ b/ee/lib/elastic/latest/project_config.rb
@@ -37,6 +37,11 @@ module ProjectConfig
 
         indexes :ci_catalog, type: :boolean
         indexes :readme_content, type: :text
+
+        indexes :mirror, type: :boolean
+        indexes :forked, type: :boolean
+        indexes :owner_id, type: :integer
+        indexes :repository_languages, type: :keyword
       end
     end
   end
diff --git a/ee/lib/elastic/latest/project_instance_proxy.rb b/ee/lib/elastic/latest/project_instance_proxy.rb
index 7d3e4121ebfe4fef10bb80354afa15934852efa0..9b75292a0d8b48b11d6c051edae9d9249c74b97f 100644
--- a/ee/lib/elastic/latest/project_instance_proxy.rb
+++ b/ee/lib/elastic/latest/project_instance_proxy.rb
@@ -38,7 +38,7 @@ def as_indexed_json(_options = {})
 
         # 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['schema_version'] = 24_02
 
         data['traversal_ids'] = target.elastic_namespace_ancestry
 
@@ -46,6 +46,13 @@ def as_indexed_json(_options = {})
           data['ci_catalog'] = target.catalog_resource.present?
         end
 
+        if ::Elastic::DataMigrationService.migration_has_finished?(:add_fields_to_projects_index)
+          data['mirror'] = target.mirror?
+          data['forked'] = target.forked? || false
+          data['owner_id'] = target.owner.id
+          data['repository_languages'] = target.repository_languages.map(&:name)
+        end
+
         unless ::Elastic::DataMigrationService.migration_has_finished?(:migrate_projects_to_separate_index)
           # Set it as a parent in our `project => child` JOIN field
           data['join_field'] = es_type
diff --git a/ee/spec/elastic/migrate/20240130154724_add_fields_to_projects_index_spec.rb b/ee/spec/elastic/migrate/20240130154724_add_fields_to_projects_index_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..137e4acec5056fda8e9f9efd8a78d14d6676c2a5
--- /dev/null
+++ b/ee/spec/elastic/migrate/20240130154724_add_fields_to_projects_index_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative 'migration_shared_examples'
+require File.expand_path('ee/elastic/migrate/20240130154724_add_fields_to_projects_index.rb')
+
+RSpec.describe AddFieldsToProjectsIndex, :elastic, feature_category: :global_search do
+  let(:version) { 20240130154724 }
+
+  include_examples 'migration adds mapping'
+end
diff --git a/ee/spec/lib/ee/gitlab/elastic/helper_spec.rb b/ee/spec/lib/ee/gitlab/elastic/helper_spec.rb
index 4245a15931a472efcbf267037b5519cf7d869c27..37a825c787e387fee68484237334776d4e62447e 100644
--- a/ee/spec/lib/ee/gitlab/elastic/helper_spec.rb
+++ b/ee/spec/lib/ee/gitlab/elastic/helper_spec.rb
@@ -518,7 +518,8 @@
           assignee_id: :all,
           hashed_root_namespace_id: :all,
           work_item_type_id: :all,
-          noteable_id: :all
+          noteable_id: :all,
+          owner_id: :all
         }
       end
 
diff --git a/ee/spec/lib/elastic/latest/project_instance_proxy_spec.rb b/ee/spec/lib/elastic/latest/project_instance_proxy_spec.rb
index 5c680e319ef108142772920c61ac82e2ad041f9d..08d177248b22ecbbc310b6be748467cd3c2f03f9 100644
--- a/ee/spec/lib/elastic/latest/project_instance_proxy_spec.rb
+++ b/ee/spec/lib/elastic/latest/project_instance_proxy_spec.rb
@@ -5,7 +5,7 @@
 RSpec.describe Elastic::Latest::ProjectInstanceProxy, :elastic_helpers, feature_category: :global_search do
   let_it_be(:project) { create(:project) }
 
-  let(:schema_version) { 2306 }
+  let(:schema_version) { 2402 }
 
   subject(:proxy) { described_class.new(project) }
 
@@ -85,6 +85,43 @@
           ci_catalog: project.catalog_resource.present?
         )
       end
+    end
+  end
+
+  describe 'when add_fields_to_projects_index migration is completed' do
+    before do
+      stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
+      set_elasticsearch_migration_to(:add_fields_to_projects_index, including: true)
+      ensure_elasticsearch_index! # ensure objects are indexed
+    end
+
+    describe '#as_indexed_json' do
+      it 'serializes project as hash' do
+        result = proxy.as_indexed_json.with_indifferent_access
+
+        expect(result).to include(
+          id: project.id,
+          name: project.name,
+          path: project.path,
+          description: project.description,
+          namespace_id: project.namespace_id,
+          created_at: project.created_at,
+          updated_at: project.updated_at,
+          archived: project.archived,
+          last_activity_at: project.last_activity_at,
+          name_with_namespace: project.name_with_namespace,
+          path_with_namespace: project.path_with_namespace,
+          traversal_ids: project.elastic_namespace_ancestry,
+          type: 'project',
+          visibility_level: project.visibility_level,
+          schema_version: schema_version,
+          ci_catalog: project.catalog_resource.present?,
+          mirror: project.mirror?,
+          forked: project.forked? || false,
+          owner_id: project.owner.id,
+          repository_languages: project.repository_languages.map(&:name)
+        )
+      end
 
       it 'contains the expected mappings' do
         result = proxy.as_indexed_json.with_indifferent_access.keys
diff --git a/ee/spec/models/concerns/elastic/project_spec.rb b/ee/spec/models/concerns/elastic/project_spec.rb
index 32a580656a2f4ea2e14e4d42e5e89f81bf14efa4..1410ff4b3df438738d5b2b8347fa837d39476efa 100644
--- a/ee/spec/models/concerns/elastic/project_spec.rb
+++ b/ee/spec/models/concerns/elastic/project_spec.rb
@@ -7,7 +7,7 @@
     stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
   end
 
-  let(:schema_version) { 2306 }
+  let(:schema_version) { 2402 }
 
   context 'when limited indexing is on' do
     let_it_be(:project) { create(:project, :empty_repo, name: 'main_project') }
diff --git a/ee/spec/services/elastic/process_bookkeeping_service_spec.rb b/ee/spec/services/elastic/process_bookkeeping_service_spec.rb
index 57f2e76d97e0db2e1aee0e556f0ea065aa38a832..e909069a658ba4caaa28bf15a58e9d747598480b 100644
--- a/ee/spec/services/elastic/process_bookkeeping_service_spec.rb
+++ b/ee/spec/services/elastic/process_bookkeeping_service_spec.rb
@@ -6,6 +6,7 @@
   :clean_gitlab_redis_shared_state,
   :elastic,
   feature_category: :global_search do
+  include ProjectForksHelper
   let(:ref_class) { ::Gitlab::Elastic::DocumentReference }
 
   let(:fake_refs) { Array.new(10) { |i| ref_class.new(Issue, i, "issue_#{i}", 'project_1') } }
@@ -403,13 +404,18 @@
 
     context 'N+1 queries' do
       it 'does not have N+1 queries for projects' do
-        projects = create_list(:project, 2)
+        project = create(:project)
+        projects = [create(:project, group: create(:group))]
+        projects << fork_project(project)
+        projects << create(:project, :mirror)
 
         described_class.track!(*projects)
 
         control = ActiveRecord::QueryRecorder.new(skip_cached: false) { described_class.new.execute }
 
-        projects += create_list(:project, 3)
+        projects << create(:project, group: create(:group))
+        projects << fork_project(project)
+        projects << create(:project, :mirror)
 
         described_class.track!(*projects)