Create new parallel Vulnerability ES index to solve the primary key issue
## Problem Statement The `Vulnerabilities::Read` model currently [overrides its primary key definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/models/vulnerabilities/read.rb#L26) to use `vulnerability_id` instead of the actual database primary key (`id`). This override exists because the Elasticsearch index uses `vulnerability_id` as the document `_id` for syncing PostgreSQL records to Elasticsearch documents. With the **Vulnerabilities Across Contexts (VAC)** feature, we will have multiple records in the `vulnerability_reads` table for the same vulnerability across different tracked contexts (branches). This means `vulnerability_id` will no longer be unique in the table, which breaks the current ES indexing approach: 1. ES `_id` currently maps to `vulnerability_reads.vulnerability_id` 2. The same vulnerability across two branches will have the same `vulnerability_id`, causing document collisions in ES 3. We cannot rely on `vulnerability_id` as a unique identifier for ES document sync ## Proposed Solution Create a **parallel Elasticsearch index** (`VulnerabilityV2` or `VulnerabilityRead`) that uses `vulnerability_reads.id` as the document `_id`, then gracefully migrate to the new index. ### Why a parallel index? - Changing `_id` within an existing ES index is complex and risky - A parallel index allows for safe rollback if issues arise - Follows the established pattern used for the `epics`/`issues` → `workitems` migration - Avoids compatibility issues with Redis queue backlogs containing older version references ### Implementation Plan #### Phase 1: Create the Parallel Index - Create new ES reference class (e.g., `Search::Elastic::References::VulnerabilityRead` or `VulnerabilityV2`) - The new reference class will use `vulnerability_reads.id` as the document identifier instead of `vulnerability_id` - Create new ES type class with identical mappings to current `Vulnerability` type - The `Vulnerabilities::Read` model will continue to be the source of truth for ES fields - Leverage the existing preloader framework for fields without direct column mappings #### Phase 2: Begin Dual-Writing - Implement dual-write logic to index records to both old and new indices simultaneously - Dual-writing will be achieved by: - Modifying the `elastic_reference` method on `Vulnerabilities::Read` to return an array of reference strings (one for the legacy index, one for the new index) instead of a single reference string - Updating the `track` method in `Elastic::ProcessBookkeepingService` to accept arrays of references and flatten them before processing - Deletes should fail gracefully if a document is not found (to handle race conditions between indices) - **Reads remain on the old index** during this phase - Feature flag to control dual-writing: `vulnerability_read_es_dual_write` #### Phase 3: Backfill New Index - Create ES migration to backfill all existing `vulnerability_reads` records to the new index - Use `MigrationDatabaseBackfillHelper` pattern for batched processing - Ensure `vulnerability_occurrence_id` is indexed for preloads and business logic #### Phase 4: Switch Reads to New Index - Feature flag to switch search queries to the new index: `vulnerability_read_es_new_index` - Update `VulnerabilityIndexHelper` to direct reads to the new index - Update `Search::AdvancedFinders::Security::Vulnerability::SearchFinder` to use `id` as `primary_key` - Update `Search::Elastic::Preloaders::Base#record_key` to use `vulnerability_occurrence_id` for preloads - Validate data consistency between old and new indices - Monitor for any query performance regressions #### Phase 5: Cleanup - Remove dual-write logic - Remove old index via ES migration - Remove feature flags - Update documentation **Note:** Removing the `self.primary_key = :vulnerability_id` override from the `Vulnerabilities::Read` model should be addressed in a follow-up issue, as it may affect other functionality beyond Elasticsearch indexing. ## Related Issues
epic