建立外部資料集Compatible with Milvus 3.0.x

外部集合是 Milvus 中的資料集合類型之一,可直接存取來自外部儲存系統或資料庫表(例如 AWS S3 和 Iceberg)的資料,而無需將資料複製到 Milvus 中。它作為資料湖上的查詢層運作,同時維持與 Milvus 查詢介面的相容性。

此功能需搭配 Storage V3 使用。有關啟用說明及相容性考量,請參閱Storage V3。

概述

在典型的 AI 資料管線中,使用者可能已將資料以 Parquet 或其他格式儲存於其儲存系統(例如 AWS S3)中。為了讓 Milvus 能使用這些外部儲存的資料,使用者通常需要透過萃取-轉換-載入(ETL)管線,將資料匯入 Milvus 自身的儲存系統中。

這種「將資料帶入 Milvus」的工作流程會產生難以同步的冗餘資料,並增加工程團隊為確保資料一致性所須承擔的維護負擔。

Bring data to compute workflow 將資料導入運算的工作流程

為解決這些問題,Milvus 提供「外部資料集」功能,讓您能直接從 Milvus 存取外部儲存的資料,無需擔心資料同步與 ETL 管線的問題。

Bring compute to data workflow 「 將運算帶到資料」工作流程

外部集合建立後,即可直接存取您的資料,並將其保留在您原始儲存的位置。在後台,Milvus 會建立清單檔案,用以記錄 Milvus 元資料與外部資料檔案中各列之間的對應關係。待清單檔案準備就緒後,您便能如同在任何受管集合中一樣,於外部集合中建立索引。

當您的資料發生變更時,只需手動觸發不到一秒的刷新操作,即可更新元資料,確保 Milvus 始終保持最新狀態。

步驟 1:建立資料結構

與建立受管集合相同,您在建立外部集合前也需先建立模式。不過,此模式與受管集合的模式略有不同。

from pymilvus import MilvusClient, DataType

schema = MilvusClient.create_schema(
    external_source='s3://s3.<region-id>.amazonaws.com/<bucket>/',
    external_spec='{
        "format": "parquet",
        "extfs": {
            ...
        }
    }'
)
import com.google.gson.JsonObject;
import io.milvus.v2.service.collection.request.CreateCollectionReq;

JsonObject externalSpec = new JsonObject();
externalSpec.addProperty("format", "parquet");
externalSpec.add("extfs", new JsonObject());

CreateCollectionReq.CollectionSchema schema = CreateCollectionReq.CollectionSchema.builder()
        .externalSource("s3://s3.<region-id>.amazonaws.com/<bucket>/")
        .externalSpec(externalSpec)
        .build();
import (
    "github.com/milvus-io/milvus/client/v2/entity"
    client "github.com/milvus-io/milvus/client/v2/milvusclient"
)

schema := entity.NewSchema().
    WithName("product_embeddings").
    WithExternalSource("s3://my-bucket/embeddings/").
    WithExternalSpec(`{"format": "parquet", "extfs": { ... }}`)
// node
export fields='[
        {
            "fieldName": "product_id",
            "dataType": "Int64",
            "isPrimary": true
        },
        {
            "fieldName": "embedding",
            "dataType": "FloatVector",
            "elementTypeParams": {
                "dim": "768"
            }
        },
        {
            "fieldName": "product_name",
            "dataType": "VarChar",
            "elementTypeParams": {
                "max_length": 512
            }
        }
    ]'

要建立外部集合的資料結構,您需要指定來源資料的 URI、資料格式以及驗證設定。

參數名稱

參數說明

範例值

format

目標來源資料檔案的格式。

parquet

snapshot_id

有效的 Iceberg 資料表快照 ID。此參數僅在您將 `format ` 設定為 `iceberg_table` 時適用。

473984310232959286

extfs

以字串化 JSON 結構表示的外部檔案系統設定。

--

您可以透過以下選項設定驗證設定:

使用 AWS 存取金鑰/秘密金鑰

此選項適用於自架設的 MinIO,或您已擁有工作用 AK/SK 的情境。

{
    "format": "...",
    "extfs": {
        "access_key_id":     "AKIA..",
        "access_key_value":  "u4Lh...",
        "region":            "us-west-2",
        "cloud_provider":    "aws",
        "use_ssl":           "true",
        "use_virtual_host":  "true"
    }
}

參數名稱

參數說明

範例值

extfs.access_key_id

存取金鑰 ID

AKIA...

extfs.access_key_value

存取金鑰值

u7LH...

extfs.region

雲端區域 ID

us-west-2

extfs.cloud_provider

雲端供應商 ID

aws

extfs.use_ssl

是否使用 SSL 建立連線。

true

extfs.use_virtual_host

是否使用虛擬主機來存取您的儲存桶。

詳情請參閱這篇文章。

true

使用 AWS IAM

此選項適用於 Milvus 運行於 EC2 執行個體或 EKS 叢集的情境。在此情況下,您無需將 AK/SK 硬編碼。

{
    "format": "...",
    "extfs": {
        "use_iam":           "true",
        "iam_endpoint":      "https://sts.<region>.amazonaws.com",
        "region":            "us-west-2",
        "cloud_provider":    "aws",
        "use_ssl":           "true"
    }
}

參數名稱

參數說明

範例值

extfs.use_iam

是否使用 AWS IAM。

若要使用此選項,請將此參數設定為"true" 。

true

extfs.iam_endpoint

有效的 AWS STS 端點。

詳情請參閱這篇文章。

https:*//*sts.<region>.amazonaws.com

extfs.region

雲端區域 ID

us-west-2

extfs.cloud_provider

雲端服務供應商 ID

aws

extfs.use_ssl

是否使用 SSL 建立連線。

true

使用 Milvus 全局憑證

此選項適用於您將外部資料儲存於 Milvus 儲存桶的情況,此時可直接使用milvus.yaml 中指定的 MinIO 全局設定來存取資料。

{
    "format": "...",
    "extfs": {
        "storage_type": "remote"
    }
}

使用 IAM 角色 ARN

當您的組織使用不同的 AWS 帳戶來管理 Milvus 叢集以及存放目標資料檔案的儲存桶時,此選項適用。

在此情況下,儲存桶擁有者應建立一個 IAM 角色,該角色需

  • 該角色應附加AmazonS3FullAccess 或更細粒度的儲存桶存取政策。

  • 在該角色的「信任政策」的「條件」欄位中,包含一個自定義的sts:ExternalId 。

接著,儲存桶擁有者應向您提供該 IAM 角色的 ARN 及外部 ID,以便您能使用這些值呼叫 `sts:AssumeRole ` 來承接該 IAM 角色。

以下是一個範例權限政策,可附加至 IAM 角色並包含允許的權限。您可以根據需求調整此政策。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
                "s3:GetBucketLocation"
            ],
            "Resource": "arn:aws:s3:::SOURCE-DATA-BUCKET"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::SOURCE-DATA-BUCKET/*"
        }
    ]
}

此外,與該 IAM 角色相關聯的信任政策會定義哪些使用者被允許承接該角色。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::ACCOUNT_RUNNING_MILVUS:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "YOUR_UNIQUE_EXTERNAL_ID"
        }
      }
    }
  ]
}

取得 IAM 角色 ARN 和外部 ID 後,您可以按照以下方式設定external_spec 參數:

{
    "format": "...",
    "extfs": {
        "cloud_provider": "aws",
        "region": "us-west-2",
        "storage_type": "remote",
        "use_ssl": "true",
        "use_iam": "true",
        "role_arn": "arn:aws:iam::306787000000:role/lentitude-bucket-role",
        "external_id": "YOUR_UNIQUE_EXTERNAL_ID",
        "load_frequency": "900"
    }
}

參數名稱

參數說明

範例值

extfs.cloud_provider

雲端供應商 ID

aws

extfs.region

雲端區域 ID

us-west-2

extfs.use_ssl

是否使用 SSL 建立連線。

true

extfs.use_iam

是否使用 AWS IAM。

請將此選項設定為"true" 。

true

extfs.role_arn

從儲存桶擁有者處取得的 IAM 角色 ARN。

arn:aws:iam::306787000000:role/...

extfs.external_id

從儲存桶擁有者處取得的外部 ID。

--

extfs.load_frequency

Milvus 擷取臨時認證憑證的間隔(單位:秒)。

900

步驟 2:新增欄位

當資料結構準備就緒後,您可以依照以下方式新增欄位:

schema.add_field(
    field_name="product_id",
    datatype=DataType.INT64,
    external_field="id" # field name in the external data file
)
schema.add_field(
    field_name="product_name",
    datatype=DataType.VARCHAR,
    max_length=512,
    external_field="name"
)
schema.add_field(
    field_name="embedding",
    datatype=DataType.FLOAT_VECTOR,
    dim=768,
    external_field="vector"
)
import io.milvus.v2.common.DataType;
import io.milvus.v2.service.collection.request.AddFieldReq;

schema.addField(AddFieldReq.builder()
        .fieldName("product_id")
        .dataType(DataType.Int64)
        .externalField("id")
        .build());
schema.addField(AddFieldReq.builder()
        .fieldName("product_name")
        .dataType(DataType.VarChar)
        .maxLength(512)
        .externalField("name")
        .build());
schema.addField(AddFieldReq.builder()
        .fieldName("embedding")
        .dataType(DataType.FloatVector)
        .dimension(768)
        .externalField("vector")
        .build());
import (
    "github.com/milvus-io/milvus/client/v2/entity"
    client "github.com/milvus-io/milvus/client/v2/milvusclient"
)

schema = schema.
    WithField(
        entity.NewField().
            WithName("product_id").
            WithDataType(entity.FieldTypeInt64).
            WithExternalField("id"),
    ).
    WithField(
        entity.NewField().
            WithName("product_name").
            WithDataType(entity.FieldTypeVarChar).
            WithMaxLength(512).
            WithExternalField("name"),
    ).
    WithField(
        entity.NewField().
            WithName("embedding").
            WithDataType(entity.FieldTypeFloatVector).
            WithDim(768).
            WithExternalField("vector"),
    )
// node
export schema="{
    \"externalSource\": \"volume://my_volume/path/to/a/folder\",
    \"externalSpec\": \"{\\\"format\\\": \\\"parquet\\\"}\",
    \"fields\": $fields
}"

步驟 3:建立集合

將所有欄位新增至資料結構後,即可建立外部集合。

client = MilvusClient(
    uri="http://localhost:19530",
    token="root:Milvus"
)

client.create_collection(
    collection_name="test_collection",
    schema=schema
)
import io.milvus.v2.client.ConnectConfig;
import io.milvus.v2.client.MilvusClientV2;

ConnectConfig connectConfig = ConnectConfig.builder()
        .uri("http://localhost:19530")
        .token("root:Milvus")
        .build();

MilvusClientV2 client = new MilvusClientV2(connectConfig);

CreateCollectionReq createReq = CreateCollectionReq.builder()
        .collectionName("test_collection")
        .collectionSchema(schema)
        .build();
client.createCollection(createReq);
import (
    "github.com/milvus-io/milvus/client/v2/entity"
    client "github.com/milvus-io/milvus/client/v2/milvusclient"
)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

milvusAddr := "http://localhost:19530"
token := "root:Milvus"

client, err := milvusclient.New(ctx, &milvusclient.ClientConfig{
    Address: milvusAddr,
    APIKey: token
})

err = client.CreateCollection(ctx, milvusclient.NewCreateCollectionOption("test_collection", schema))

if err != nil {
    fmt.Println(err.Error())
    // handle error
}
// node
curl --request POST \
--url "${PROJECT_ENDPOINT}/v2/vectordb/collections/create" \
--header "Authorization: Bearer ${TOKEN}" \
--header "Content-Type: application/json" \
--header "Request-Timeout: 10" \
-d "{
    \"dbName\": \"my_database\",
    \"collectionName\": \"test_collection\",
    \"schema\": $schema
}"

步驟 4:建立索引

您可以像在受管集合中一樣,為外部集合的欄位建立索引。

index_params = client.prepare_index_params()
# Add indexes
index_params.add_index(
    field_name="embedding",
    index_type="AUTOINDEX",
    metric_type="COSINE"
)
index_params.add_index(
    field_name="product_name",
    index_type="AUTOINDEX"
)
client.create_index(
    db_name="my_database",
    collection_name="test_collection",
    index_params=index_params
)
import io.milvus.v2.common.IndexParam;
import io.milvus.v2.service.index.request.CreateIndexReq;
import java.util.*;

IndexParam indexParamForIdField = IndexParam.builder()
        .fieldName("product_name")
        .indexType(IndexParam.IndexType.AUTOINDEX)
        .build();
IndexParam indexParamForVectorField = IndexParam.builder()
        .fieldName("embedding")
        .indexType(IndexParam.IndexType.AUTOINDEX)
        .metricType(IndexParam.MetricType.COSINE)
        .build();
List<IndexParam> indexParams = new ArrayList<>();
indexParams.add(indexParamForIdField);
indexParams.add(indexParamForVectorField);
CreateIndexReq createIndexReq = CreateIndexReq.builder()
        .dbName("my_database")
        .collectionName("test_collection")
        .indexParams(indexParams)
        .build();
client.createIndex(createIndexReq);
import (
    "github.com/milvus-io/milvus/client/v2/entity"
    "github.com/milvus-io/milvus/client/v2/index"
    "github.com/milvus-io/milvus/client/v2/milvusclient"
)

collectionName := "test_collection"
indexOptions := []milvusclient.CreateIndexOption{
    milvusclient.NewCreateIndexOption(collectionName, "embedding", index.NewAutoIndex(entity.COSINE)),
    milvusclient.NewCreateIndexOption(collectionName, "product_name", index.NewAutoIndex(index.AUTOINDEX)),
}
indexTask, err := client.CreateIndex(ctx, indexOptions)
if err != nil {
    // handler err
}
err = indexTask.Await(ctx)
if err != nil {
    // handler err
}
client.createIndex({
    db_name: "my_database",
    collection_name: "test_collection",
    field_name: "product_name",
    index_type: "AUTOINDEX"
})
client.createIndex({
    db_name: "my_database",
    collection_name: "test_collection",
    field_name: "embedding",
    index_type: "AUTOINDEX",
    metric_type: "COSINE"
})
export indexParams='[
        {
            "fieldName": "embedding",
            "indexName": "my_vector",
            "indexType": "AUTOINDEX"
        },
        {
            "fieldName": "product_name",
            "indexName": "my_id",
            "indexType": "AUTOINDEX"
        }
    ]'

curl --request POST \
--url "${PROJECT_ENDPOINT}/v2/vectordb/indexes/create" \
--header "Authorization: Bearer ${TOKEN}" \
--header "Content-Type: application/json" \
--header "Request-Timeout: 10" \
-d "{
    \"dbName\": \"my_database\",
    \"collectionName\": \"test_collection\",
    \"indexParams\": $indexParams
}"

步驟 5:刷新資料

當集合準備就緒後,請刷新該集合以建立資料的元資料和索引。

job_id = client.refresh_external_collection(
    db_name="my_database",
    collection_name="test_collection"
)
while True:
    progress = client.get_refresh_external_collection_progress(job_id=job_id)
    print(f"  {progress.state}: {progress.progress}%")
    if progress.state == "RefreshCompleted":
        elapsed = progress.end_time - progress.start_time
        print(f"  Completed in {elapsed}ms")
        break
    elif progress.state == "RefreshFailed":
        print(f"  Failed: {progress.reason}")
        break
    time.sleep(2)
import io.milvus.v2.service.utility.request.GetRefreshExternalCollectionProgressReq;
import io.milvus.v2.service.utility.request.ListRefreshExternalCollectionJobsReq;
import io.milvus.v2.service.utility.request.RefreshExternalCollectionReq;
import io.milvus.v2.service.utility.response.GetRefreshExternalCollectionProgressResp;
import io.milvus.v2.service.utility.response.ListRefreshExternalCollectionJobsResp;
import io.milvus.v2.service.utility.response.RefreshExternalCollectionJobInfo;
import io.milvus.v2.service.utility.response.RefreshExternalCollectionResp;

while (true) {
    GetRefreshExternalCollectionProgressResp resp = client.getRefreshExternalCollectionProgress(
            GetRefreshExternalCollectionProgressReq.builder()
                    .jobId(jobId)
                    .build());
    RefreshExternalCollectionJobInfo jobInfo = resp.getJobInfo();
    if ("RefreshCompleted".equals(jobInfo.getState())) {
        long elapsed = jobInfo.getEndTime() - jobInfo.getStartTime();
        System.out.printf("  Refresh completed in %dms%n", elapsed);
        break;
    } else if ("RefreshFailed".equals(jobInfo.getState())) {
        System.out.printf("  Refresh failed: %s%n", jobInfo.getReason());
    }
    TimeUnit.SECONDS.sleep(2);
}
refreshResult, err := client.RefreshExternalCollection(ctx,
    client.NewRefreshExternalCollectionOption("test_collection"))
jobID := refreshResult.JobID
for {
    progress, _ := client.GetRefreshExternalCollectionProgress(ctx,
        client.NewGetRefreshExternalCollectionProgressOption(jobID))
    fmt.Printf("State: %s\n", progress.State)
    if progress.State == entity.RefreshStateCompleted {
        fmt.Println("Refresh completed!")
        break
    }
    if progress.State == entity.RefreshStateFailed {
        fmt.Printf("Refresh failed: %s\n", progress.Reason)
        break
    }
    time.Sleep(2 * time.Second)
}
// node
curl --request POST \
--url "${PROJECT_ENDPOINT}/v2/vectordb/jobs/external_collection/refresh" \
--header "Authorization: Bearer ${TOKEN}" \
--header "Content-Type: application/json" \
--header "Request-Timeout: 10" \
-d "{
    \"dbName\": \"my_database\",
    \"collectionName\": \"test_collection\",
    \"externalSource\": \"volume://my_volume/path/to/a/folder\",
    \"externalSpec\": \"{\\\"format\\\": \\\"parquet\\\"}\"
}"

刷新操作為非同步,因此您需要設定一個迴圈來監控其進度。

  • 刷新操作會掃描資料檔案的元資料,並據此產生清單檔案。此過程通常需時 150 至 250 毫秒。

  • 清單檔案會記錄 Milvus 中的元資料與外部檔案中各列之間的對應關係。

  • 若來源資料有更新,您需要手動再次呼叫刷新操作,以確保 Milvus 保持最新狀態。

  • 若刷新操作需移除所有活躍的元資料且不進行任何新增,系統將拒絕該請求。

下一步

刷新外部集合後,您可以載入並釋放該集合,並在外部集合中執行相似度搜尋和查詢,操作方式與任何受管集合相同,惟用於隨需運算的資料庫中的集合,必須附加至隨需運算叢集才能進行搜尋和查詢。

在執行 DQL 操作(例如搜尋、查詢、取得及混合搜尋)之前,您需要建立一個工作階段,以附加隨需運算叢集的運算資源。

若外部資料來源日後包含您希望在 Milvus 中公開的另一個欄位,請在外部集合的資料結構中新增該欄位,並再次刷新外部集合。詳細資訊請參閱《變更外部集合資料結構》。