Goal: Fix birdwatcher's show manifest / scan-binlog access to external collection data files. The ARN information required by external collections (role_arn, external_id, session_name, AK/SK, etc.) does not come from Milvus component configuration; it is attached to the collection schema (external_source URI + the extfs field of the external_spec JSON). This design unifies how these credentials are passed through and supports the mixed scenario where a single manifest contains both external data files and internal function-output files (e.g. sparse vectors generated from a varchar column).
Architecture: Add a shared layer states/ossutil/external.go encapsulating external spec/source parsing, external object store client construction, and a per-file ManifestPathResolver. The three commands show manifest, scan-binlog, and inspect-parquet --external reuse this layer: they read the ARN credentials from the collection to build the external store, then route each file in the manifest to the internal or external store through the resolver.
Tech stack: Go, minio-go/v7, Arrow/Parquet, Birdwatcher states/ossutil, Milvus schemapb / etcdpb.
show manifest(states/show_manifest.go) andscan-binlog(states/scan_binlog.go) originally both created their object store client vias.GetObjectStore(...)(states/instance.go:151), which ultimately goes throughossutil.GetObjectStoreFromCfg(states/ossutil/minio_cfg.go:116). That pulls component configuration over gRPC and reads keys such asminio.rolearn/minio.externalidfrom Milvus's own minio configuration.- However, external collection data files live in the user's external bucket, and the credentials to access them (
role_arn,external_id,session_name, AK/SK,use_iam, etc.) exist only in the collection schema:schema.ExternalSource: the external source URI, e.g.oss://oss-cn-hangzhou.aliyuncs.com/bucket/root/...schema.ExternalSpec: a JSON string, e.g.{"format":"parquet","extfs":{"role_arn":"...","external_id":"...",...}}
- Consequently the original implementation cannot access the real data files of an external collection with the correct credentials.
External collections may declare functions (BM25 / embedding). Their output columns (e.g. sparse vectors) are computed by DataNode ExecuteFunctionsForSegment (milvus internal/datanode/external/function_executor.go) and written to Milvus internal storage ({rootPath}/files/insert_log/.../_data/...), then appended to the same manifest as a new column group. The result is a single manifest that contains both:
- External files: absolute URIs (e.g.
s3://bucket/data.parquet,oss://endpoint/bucket/...), requiring external bucket credentials; - Internal files: relative paths (e.g.
abc.parquet,bm25.3/0), requiring Milvus configuration credentials.
A design that routes "everything through the external store" will necessarily fail on mixed manifests.
-
The manifest file itself always lives in Milvus internal storage.
createManifestForSegment(milvusinternal/datanode/external/task_update.go:1062) writes{rootPath}/files/insert_log/{coll}/{part}/{seg}/_metadata/manifest-{ver}.avro;MarshalManifestPath(internal/storagev2/packed/ffi_common.go:355) stores that base path into thebase_pathfield of the segment'sManifestPathJSON. -
Path storage/restore rules inside the manifest (milvus-storage
cpp/src/manifest.cpp)- On write,
toRelativePaths(base_path): internal files (prefixed by{base_path}/_data|_delta|_stats|_index|../lobs) are trimmed to relative paths; external files keep their absolute URI as-is. - On read,
ToAbsolutePaths(base_path): relative paths without a scheme are re-joined to{base_path}/<dir>; absolute URIs with a scheme are returned unchanged.
- On write,
-
milvus-storage routes the storage backend per file (
cpp/src/filesystem/fs.cpp,FilesystemCache::resolve_config)- Absolute URI (has a scheme) → matched against
extfs.*properties by address+bucket → external FS (carrying ARN/external_id etc.); - Path without a scheme (relative) → default
fs.*→ internal Milvus storage. - In other words: mixed manifests are natively supported; the discriminator is "is it an absolute URI".
- Absolute URI (has a scheme) → matched against
-
Function output columns are written to internal storage.
ExecuteFunctionsForSegmentwrites output packed files to{rootPath}/files/insert_log/.../_data/..., and BM25 stats to{base_path}/_stats/bm25.{id}/0(function_executor.go:454).
Path contains a scheme (
://) → external; path is relative (no scheme) → internal.
This matches milvus-storage's native discriminator and does not depend on schema field mapping.
Public API:
| Symbol | Description |
|---|---|
ExternalSourceSpec |
Parsed external spec; extfs keys aligned with milvus specutil.ExtfsKey*: cloud_provider/region/role_arn/session_name/external_id/access_key_id/access_key_value/use_iam/iam_endpoint/bucket_name/storage_type/use_ssl/load_frequency/anonymous/aliyun_role_auth_mode |
ParseExternalSpec(raw) |
Parses the schema.ExternalSpec JSON; only the parquet format is supported |
ExternalSourceLocation |
Parsed external source URI: Scheme/Host/Bucket/RootPath |
ParseExternalSource(raw) |
Parses the schema.ExternalSource URI |
InferCloudProviderFromScheme(scheme) |
Infers oss/aws/gcp/tencent/huawei |
NewResolvedExternalObjectStore(ctx, source, spec, skipBucketCheck) |
Builds *oss.ResolvedObjectStore + location from external credentials; RoleARN/ExternalID etc. come from the collection, not configuration |
NewResolvedExternalObjectStoreFromSchema(ctx, externalSource, externalSpec, skipBucketCheck) |
Convenience wrapper |
NewResolvedExternalObjectStoreFromCollection(ctx, collection, skipBucketCheck) |
Builds from a collection model; errors if the collection is not external |
FileBackend |
Enum FileBackendInternal / FileBackendExternal |
ManifestPathResolver |
Per-file routing: Resolve(filePath, dirPrefix) (store, key, backend, err) |
ResolveExternalObjectKey(location, externalFile) |
External file reference (absolute URI / ROOT_PATH / relative path) → object key |
ManifestPathResolver.Resolve routing logic:
- Contains
://→ResolveExternalObjectKey→ external store; - Contains
ROOT_PATH→ replaced withlocation.RootPath→ external store; - Otherwise, relative path →
path.Join(manifestBasePath, dirPrefix, rel)→ internal store. dirPrefixis taken per section as_data/_delta/_stats/_index/../lobs.
- Resolve the collection: from
--collection, or by looking upCollectionIDfrom the first manifest segment. - If
schema.GetExternalSource() != "": build the external store viaNewResolvedExternalObjectStoreFromCollection; the manifest file itself is still read from the internal store (s.GetObjectStore). - When printing, annotate every column group / delta / stats / index / lob file with
Backend: internal|externaland the final object key viaManifestPathResolver.Resolve(printManifestWithResolver). - JSON output mode and non-external collection behavior remain unchanged.
- Detect an external collection (
isExternalCollection); if so, build the external store + location. - In
getObject, for delta logs: when the path is external (isExternalPath: contains://orROOT_PATH), route throughResolveExternalObjectKeyto the external store; otherwise use the internal store. - In
workFn, external segments that have a manifest go throughscanExternalSegment:- Read the manifest from the internal store;
- Iterate column groups, selecting the store per file via the resolver;
- Read parquet (Arrow), mapping manifest column names to field IDs (
external_field, or the field ID string / field name); - Deserialize cells, set the PK, apply the filters (
loEntryFilter/deltalogFilter/exprFilter), and feed thescanTask. - Segments that are not external / have no manifest keep the original binlog iteration path.
- Remove the local duplicated external parsing / client-construction implementations and delegate to
ossutil(parseExternalSpec/parseExternalSource/newExternalMinioClient/resolveExternalObjectKeybecome thin wrappers or aliases, keeping existing call sites and tests unchanged). inspectExternalManifestParquetswitches toManifestPathResolver: external column groups follow the original logic; internal column groups (function outputs) are read from the internal store, supporting per-file display of mixed column groups.
New files:
states/ossutil/external.gostates/ossutil/external_test.gostates/scan_binlog_external.go
Modified files:
states/show_manifest.gostates/scan_binlog.gostates/inspect_parquet.go
states/ossutil/external_test.go:ParseExternalSpecwith allextfskeys, empty spec, and rejection of an unsupported format;ParseExternalSourceURI parsing;ResolveExternalObjectKeyfor relative path / already-rooted / full URI;InferCloudProviderFromScheme;ManifestPathResolvermixed manifest: external absolute URI, externalROOT_PATH, internal function-output file (_data), internal bm25 stats (_stats).
- The existing external parsing tests in the
statespackage'sinspect_parquet_test.goremain green (preserved via thin wrappers).
go build ./...
go test ./states/ossutil/ ./states/
gofmt -l states/ossutil/external.go states/ossutil/external_test.go states/scan_binlog_external.go states/show_manifest.go states/scan_binlog.go states/inspect_parquet.go
go vet ./states/ ./states/ossutil/Note: the repository .golangci.yml is in v2 format and cannot be parsed by the golangci-lint binary currently available in this environment (a tool-version mismatch, not a code issue); lint is therefore validated via go vet + gofmt plus a temporary v1 config. In go test ./..., the build failures in states/etcd/show and states/milvusctl are caused by untracked test files referencing nonexistent fields and are unrelated to this change.
- Non-external collection behavior is unchanged: the resolver is only enabled when
ExternalSource != ""; all other paths fully preserve the original logic. - milvus-table format: its column group files may reference internal source manifests (property
milvusTableSourceManifestPathProperty). The current resolver covers most cases via "no scheme = internal"; recognizing the milvus-table-specific property is left for follow-up. LoadFrequency/ auth mode: whenuse_iam,anonymous, and AK/SK are all absent in the external spec, the resolver falls back to the IAM chain; Aliyunrole_arndefaults tooidc(consistent with the existing inspect-parquet behavior).- LOB files: the directory prefix is
../lobs; the relative path up-traversal is normalized bypath.Join. - Real cloud dependencies: unit tests only cover parsing and routing and never issue real object store requests; real connectivity is left to manual verification.