Back to Milvus

Milvus Snapshot as External Table Source

docs/design-docs/design_docs/20260526-milvus-table-external-source.md

3.0.016.3 KB
Original Source

Milvus Snapshot as External Table Source

  • Created: 2026-05-26
  • Status: Implemented
  • Component: External Table, Snapshot, StorageV3, QueryNode, DataNode
  • Related Issue: #45881

Summary

This design adds milvus-table as an external table format. A milvus-table external collection uses a Milvus snapshot metadata JSON file as its source and maps the source StorageV3 segment manifests into target external segments.

Unlike Parquet external tables, a Milvus snapshot is not a set of independent data files. It contains Milvus collection schema, segment manifests, delta logs, and primary-key statistics. The target external collection therefore needs to preserve the source field identity for data fields while still exposing normal Milvus field names and external_field mappings to users.

Implementation Overview

The implementation keeps the public API small and pushes Milvus-specific behavior into the existing external table lifecycle:

  • RootCoord reads snapshot metadata at create time, validates schema identity, aligns target data-field IDs to source field IDs, and rejects external-table chaining.
  • DataCoord builds refresh jobs, pre-allocates ID ranges, and applies DataNode results as kept segments, new segments, or manifest-only updates to existing segments.
  • DataNode reads the Milvus snapshot explore manifest, creates target StorageV3 manifests, runs target functions, copies or translates delete logs, and samples fake-binlog memory size.
  • storagev2/packed parses snapshot metadata, resolves source-relative paths, imports source manifests through Loon FFI, and exposes Milvus-table manifest helpers.
  • QueryNode loads external StorageV3 manifests, splits source and target-owned deltalogs, and accounts eager real-PK and timestamp columns.
  • Segcore resolves physical storage columns, synthesizes virtual PK system fields, loads real PK and source timestamps when needed, and uses take() for output fields.

The main data path is zero-copy: target manifests point at source StorageV3 column-group files after resolving them against external_source. Target-owned files are written only for generated function outputs and delta logs that must live in the target PK space.

Goals

  • Allow a collection snapshot generated by Milvus to be used as an external table source.
  • Keep the public source contract minimal: external_source points to the snapshot metadata JSON file, and external_spec.format is milvus-table.
  • Preserve source field IDs for target data fields so StorageV3 physical column names remain valid.
  • Keep user-facing external_field as the source field name instead of storing source field IDs in the collection schema.
  • Support both real primary key and virtual primary key target collections.
  • Carry source deletes into the target external collection.
  • Reuse source primary-key bloom-filter statistics when the target has a real primary key.
  • Keep Parquet and other external formats unchanged.

Non-Goals

  • Supporting snapshots from non-StorageV3 source collections.
  • Reconstructing a snapshot path from source_collection_id and snapshot_id.
  • Making external tables writable.
  • Supporting schema evolution between refreshes.
  • Reusing source primary-key bloom filters for virtual primary key targets.
  • Supporting dynamic fields, struct fields, partition keys, clustering keys, text match, or auto ID for external collections.
  • Reading target function output fields from external source data. Target function outputs are target-owned fields and are regenerated during refresh.

Public Contract

milvus-table is selected through external_spec:

json
{
  "format": "milvus-table",
  "extfs": {
    "cloud_provider": "aws",
    "region": "us-west-2",
    "access_key_id": "...",
    "access_key_value": "..."
  }
}

The external_source must be the concrete snapshot metadata JSON path:

text
s3://bucket/snapshots/{source_collection_id}/metadata/{snapshot_id}.json

The source snapshot must come from a normal Milvus collection. Creating or refreshing a milvus-table external collection from another external collection snapshot is rejected. Chaining external tables would require refresh and read paths to follow another collection's external source and storage contract, which is outside the milvus-table snapshot contract.

The implementation intentionally does not add a Milvus-table-specific source_collection_id or snapshot_id API. The snapshot metadata JSON is the canonical artifact. This avoids exposing Milvus internal snapshot path layout as part of the external table API.

The target collection schema uses normal field names. For each user data field, external_field must be the source field name:

text
target field name -> external_field source field name -> source field ID

At create time, RootCoord reads the source snapshot metadata and copies the source field ID into the target field. The persisted target schema keeps external_field as the source field name, not as a numeric field ID string.

Schema Alignment

Milvus StorageV3 segment manifests store physical columns by field ID string. For example, source field ID 101 is stored as physical column "101". If the target collection generated different field IDs, data reads would point to missing or incorrect columns.

RootCoord handles this at collection creation:

  1. Validate external_source and external_spec.
  2. Parse the snapshot metadata JSON.
  3. Validate target schema identity against the source snapshot schema.
  4. Build a source-field-name to source-field-schema map.
  5. For each target user data field, set target.field_id to the matching source field ID found through target.external_field.
  6. Mark the create request with preserve_field_ids=true.

System fields and the target virtual primary key field are target-owned fields. They do not map to source data columns.

Target function output fields are also target-owned fields. They are not read from source data and are assigned target-only field IDs during creation. If the source normal Milvus snapshot already contains a function output field, a target ordinary field may still map to that stored source column through external_field; the target collection's own function outputs are recomputed.

During DDL replay, RootCoord skips rereading the external snapshot when preserve_field_ids is already set. This keeps recovery independent from the source bucket and credentials after the schema has been persisted.

Refresh Flow

DataCoord refresh revalidates the external source and spec, reads the snapshot metadata, and writes an explore manifest under the target storage. DataNode consumes that explore manifest for its assigned file range and creates target segment manifests.

For milvus-table, DataCoord exploration reads the snapshot metadata JSON and requires storagev2_manifest_list to be present. If it is missing, refresh fails fast with an explicit error because the source snapshot was not created from a StorageV3 collection.

Each source StorageV3 segment manifest becomes one or more external fragments identified by:

text
source_manifest_path:start_row:end_row

Delete logs are intentionally not part of this fragment identity. If the L1 data fragment is unchanged but snapshot L0 overlays change, DataNode rewrites the target segment manifest and returns the existing segment ID as an updated segment. DataCoord then updates that segment's ManifestPath in place instead of dropping and recreating the segment. If the source L1 manifest path or row range changes, the old target segment is invalidated and a new target segment is created.

L1 fragmentL0 overlayRefresh behavior
ChangedAny stateDrop the old target segment and create a new target segment.
UnchangedUnchangedKeep the target segment unchanged.
UnchangedAdded, removed, or changedKeep the target segment ID and rewrite its manifest.

For milvus-table, DataNode currently creates one target segment per refreshed fragment rather than bin-packing multiple source fragments into one target segment. This keeps row-offset based virtual-PK and delete translation local to one target manifest. The tradeoff is that a source snapshot with many StorageV3 segments can create many target external segments and consume more pre-allocated IDs. Future bin-packing would need an explicit source-row-offset to target-row-offset mapping layer for virtual-PK delete conversion.

Target segment manifest creation is different from generic external files:

  • Parquet and other formats create column groups from file ranges.
  • milvus-table imports source StorageV3 column groups from source segment manifests into a target segment manifest.

This keeps data files zero-copy for the main column data. Target-owned delta logs may still be written when source deletes need to be converted or copied.

DataNode writes manifests directly under final StorageV3 insert-log paths using the ID range pre-allocated by DataCoord. The range is consumed by target segment IDs, fake-binlog log IDs, and fallback deltalog IDs when a source deltalog does not carry an explicit LogID.

Physical Column Resolution

The persisted schema keeps external_field as a source field name. Physical column selection is centralized in StorageColumnResolver on the Go side and Schema::GetPhysicalColumnName / Schema::IsExternalManifestStoredField on the C++ side. The rule depends on whether the caller is reading source data or a target segment manifest:

FormatPhysical column name
Parquet, Lance, Vortex, Icebergexternal_field
Milvus tabletarget field ID string, aligned to source field ID

For milvus-table, source data fields use numeric field ID strings because the source StorageV3 manifests were written by Milvus. Target-only fields, such as virtual PK and target function outputs, are not source data. Function outputs are stored under target numeric field IDs after refresh.

This rule is applied at the actual physical access points:

  • DataNode refresh sampling
  • Storage manifest reader
  • QueryNode segment load
  • Segcore search, query, retrieve, and take
  • DataNode index build
  • External field-size sample

The old approach of rewriting external_field to a numeric string is avoided because it hides user intent in persisted schema and makes future maintenance error-prone.

Primary Key Modes

Real Primary Key

If the target schema contains a user primary key, it must map to the source snapshot primary key. The real primary key column is eagerly loaded during QueryNode segment load so retrieve/take/search output can return real IDs.

For real primary key segments:

  • Source primary-key bloom-filter statistics are imported or read as external stats, so QueryNode can use normal PK pruning.
  • Source segment delta logs are imported into the target manifest as external StorageV3 delta paths.
  • Snapshot L0 delta overlays are copied into target-owned StorageV3 deltalogs under the target segment base path.
  • QueryNode splits source external deltalogs from target-owned deltalogs at load time. Source deltas are decoded with the external StorageV3 reader, target deltas are decoded with normal target storage config.
  • Segcore loads the source insert timestamp column eagerly so a source delete-before-reinsert sequence keeps the same visibility semantics after the snapshot is used as an external table.

Virtual Primary Key

If the target schema has no user primary key, Milvus injects the external virtual primary key:

text
virtual_pk = (target_segment_id & 0xffffffff) << 32 | row_offset

For virtual primary key segments:

  • Source primary-key bloom filters cannot be reused because they are keyed by source primary key, not target virtual primary key.
  • QueryNode uses a conservative PK candidate for routing.
  • DataNode converts source-PK delete records from both source segment deltalogs and snapshot L0 overlays into target virtual-PK delete records during refresh.

The conversion first reads source delete keys, then scans the source primary key column only for matching keys. This keeps memory proportional to delete key count plus matched rows, not to total source rows.

Delete Handling

Milvus snapshots can contain two delete sources:

  1. Delta logs attached to source segment manifests.
  2. L0 delta overlays referenced by snapshot segment manifests.

Real primary key targets keep delete semantics in source PK space:

  1. The Loon FFI manifest builder imports source segment deltalogs from source manifests when the target has an external primary key.
  2. DataNode copies snapshot L0 overlays into target-owned StorageV3 deltalogs.
  3. QueryNode loads source deltas through the external StorageV3 deltalog reader and target-owned deltas through the normal StorageV3 deltalog reader.
  4. Segcore uses source insert timestamps for real-PK milvus-table rows so delete timestamps are compared against the original insert timestamps.

Virtual primary key targets must convert delete semantics:

  1. Combine snapshot L0 overlays with source segment deltalogs.
  2. Read source deltalogs and collect deleted source PKs.
  3. Scan source PK and timestamp columns for affected fragments only.
  4. Map each matching source row offset to the target virtual PK.
  5. Write target-owned virtual-PK deltalogs.
  6. Add those deltalogs to the target StorageV3 manifest.

Duplicate source primary keys are handled by mapping one source delete key to all matching target row offsets. Timestamp ordering follows existing Milvus delete semantics: a delete only removes rows with insert timestamp smaller than the delete timestamp.

QueryNode Load and Read Path

QueryNode loads milvus-table external segments as normal sealed external segments with StorageV3 manifests.

During load:

  • Physical column names are resolved through the schema.
  • Real primary key fields are loaded eagerly.
  • Real primary key milvus-table segments also load source timestamps eagerly.
  • Virtual primary key fields use the existing synthetic virtual-PK column.
  • Deltalogs are loaded for external segments instead of being skipped. For real-PK milvus-table segments, QueryNode separates source external delta paths from target-owned delta paths before decoding them.
  • Real primary key bloom-filter stats are read from external-aware storage.

During search/query/retrieve/take:

  • Segcore requests physical columns using Schema::GetPhysicalColumnName.
  • For milvus-table, this returns the field ID string.
  • For other external formats, this returns external_field.
  • Output field IDs and result IDs remain target Milvus field IDs and target PKs.

Index Build

DataNode passes external_source and external_spec through BuildIndexInfo. The C++ index build path reads external_spec and resolves manifest columns with the same rule as the query path:

  • milvus-table: field ID string.
  • Other external formats: external_field.

This keeps index build consistent with load/search/query/take and avoids a separate schema mutation path for indexes.

Compatibility and Failure Behavior

  • If external_source is not a JSON path for milvus-table, create or refresh fails with a parameter error.
  • If the snapshot metadata lacks storagev2_manifest_list, refresh fails fast.
  • If a source deltalog is not a StorageV3 _delta path, refresh fails fast.
  • If a source deltalog needs to be materialized into the target manifest and DataNode cannot derive or allocate a target deltalog ID, refresh fails instead of writing an unstable target path.
  • If target schema does not match the source snapshot schema, create or refresh fails instead of reading data with mismatched field IDs.
  • If a later refresh points to a snapshot with a different schema, refresh fails schema identity validation.
  • Existing Parquet, Lance, Vortex, and Iceberg external tables keep their existing external_field physical-column behavior.

Validation

The implementation adds coverage for:

  • RootCoord schema alignment tests.
  • DataCoord refresh schema validation tests.
  • DataNode manifest creation, deltalog copy, and virtual-PK deltalog translation tests.
  • QueryNode external real-PK bloom filter and deltalog load tests.
  • StorageV3 packed reader and Milvus-table snapshot metadata tests.
  • Segcore tests for field-ID physical column resolution.
  • Go client E2E tests for Milvus-table snapshot refresh, query, take, real PK, virtual PK, and delete behavior.