本文介紹如何將映像位元組直接存入DLF Iceberg表,並使用Daft進行縮圖產生、embedding計算和視覺相似性檢索。
多模態儲存模式簡介
本文採用內聯儲存模式:將映像或音視頻的原始位元組、縮圖和embedding向量與中繼資料存放在同一張Iceberg表中。Daft可以直接從表內解碼映像位元組,無需再通過Object Storage Service路徑取數,寫入、讀取和相似性檢索在一張表內即可完成。
|
資料 |
列類型 |
|
原圖位元組 |
|
|
縮圖(可選,便於輕量瀏覽) |
|
|
embedding向量 |
|
單條媒體檔案較大時,內聯儲存會增大Parquet檔案體積,請根據資料規模權衡選擇。
環境準備
安裝依賴
Python 3.10及以上版本。
下載DLF適配版PyIceberg包pyiceberg-0.10.0.dev1.tar.gz,放到目前的目錄。
安裝PyIceberg及相關依賴。
pip install pyiceberg-0.10.0.dev1.tar.gz "pyarrow>=19.0.0,<22.0.0" "boto3>=1.24.59"安裝Daft。
pip install "daft>=0.7.15"
PyArrow版本必須低於22(pyarrow>=19.0.0,<22.0.0)。PyArrow 22及以上版本內建的AWS SDK在上傳時預設附帶aws-chunked流式校正和,DLF的OSS S3相容介面不支援該編碼,寫入會失敗並報錯aws-chunked encoding is not supported。
配置參數
準備以下資訊:
參數 | 說明 |
| 阿里雲帳號的AccessKey ID。 |
| 阿里雲帳號的AccessKey Secret。 |
| DLF所在地區的ID,例如 |
| DLF中的Catalog名稱(對應Iceberg warehouse)。 |
| 目標資料庫(對應Iceberg namespace)名稱。 |
請妥善保管AccessKey,避免寫入程式碼到代碼或提交到代碼倉庫,建議通過環境變數或Key Management Service讀取。
DLF Iceberg REST服務僅支援VPC內網訪問,因此需在與DLF同地區的VPC環境(如ECS、EMR叢集)中運行本文代碼。各地區的VPC Endpoint請參見Iceberg REST服務存取點。
串連 DLF Catalog
通過Iceberg REST協議,使用PyIceberg的load_catalog串連DLF Catalog。
from pyiceberg.catalog import load_catalog
REGION = "${regionId}"
catalog = load_catalog(
"dlf",
**{
"type": "rest",
"uri": f"http://{REGION}-vpc.dlf.aliyuncs.com/iceberg",
"warehouse": "${catalogName}",
"rest.signing-name": "DlfNext",
"rest.signing-region": REGION,
"rest.sigv4-enabled": "true",
"client.access-key-id": "${accessKeyId}",
"client.secret-access-key": "${accessKeySecret}",
"client.region": REGION,
"s3.endpoint": f"https://oss-{REGION}-internal.aliyuncs.com",
},
)準備範例圖片
準備兩張本地JPEG範例圖片n01.jpg、n02.jpg(RGB格式),放到目前的目錄。可從公開資料集Imagenette(https://github.com/fastai/imagenette)的n01440764(tench)和n02979186(cassette player)類別中各取一張並重新命名。
建立多模態表
使用PyIceberg建立包含映像位元組和embedding向量列的多模態表。
from pyiceberg.schema import Schema
from pyiceberg.types import (
LongType, StringType, BinaryType, ListType, FloatType, NestedField,
)
from pyiceberg.partitioning import UNPARTITIONED_PARTITION_SPEC
schema = Schema(
NestedField(1, "image_id", LongType(), required=True),
NestedField(2, "filename", StringType()),
NestedField(3, "label", StringType()),
NestedField(4, "image", BinaryType()),
NestedField(5, "thumbnail", BinaryType()),
NestedField(6, "embedding",
ListType(element_id=7, element_type=FloatType(), element_required=False)),
)
table = catalog.create_table(
("${database}", "image_catalog"),
schema=schema,
partition_spec=UNPARTITIONED_PARTITION_SPEC,
)
以上建表語句預設建立format-version 2表。DLF支援建立format-version 3表,但當前PyIceberg和Daft尚不支援寫入v3表和VARIANT類型,多模態表請勿通過properties={"format-version": "3"}指定v3。
寫入映像資料
將原圖位元組放入image列,通過Daft的映像函數從同一位元組派生縮圖,一併寫入。Daft 0.7.15及以上版本會自動設定OSS,無需手動傳入io_config。
import daft
from daft.functions import image
df = daft.from_pydict({
"image_id": [1, 2],
"filename": ["n01.jpg", "n02.jpg"],
"label": ["tench", "cassette_player"],
"image": [open("n01.jpg", "rb").read(),
open("n02.jpg", "rb").read()],
})
# 從原圖位元組派生縮圖:解碼 → resize 32×32 → 重新編碼為JPEG
df = df.with_column("thumbnail",
image.encode_image(image.resize(image.decode_image(df["image"]), 32, 32), "JPEG"))
df.write_iceberg(table, mode="append")
讀取與處理映像
讀取後直接解碼image列的內聯位元組,無需再訪問Object Storage Service。
from daft.functions import image
df = daft.read_iceberg(table)
df = df.with_column("img", image.decode_image(df["image"]))
df = df.with_column("thumb", image.resize(df["img"], 64, 64))
df.show()
計算embedding與相似性檢索
-
使用
@daft.func將映像轉換為向量(以下樣本為降採樣描述子,可替換為CLIP或ResNet等模型推理),得到list<float>類型的embedding列。import numpy as np import daft from daft.functions import image @daft.func(return_dtype=daft.DataType.list(daft.DataType.float32())) def embed(img) -> list: v = np.asarray(img).astype("float32").reshape(-1) return (v / (np.linalg.norm(v) or 1.0)).tolist() # 解碼內嵌影像 → 縮放到統一尺寸 → 計算embedding(也可在寫入資料時一併持久化,見完整樣本) df = daft.read_iceberg(table) df = df.with_column("embedding", embed(image.resize(image.decode_image(df["image"]), 8, 8))) -
擷取
embedding列後,通過collect()收集到本地,與查詢圖片的向量計算餘弦相似性並取Top-K,完成視覺檢索。# 收集上一步計算的embedding到本地 d = df.select("image_id", "embedding").collect().to_pydict() vectors = {i: np.asarray(v, "float32") for i, v in zip(d["image_id"], d["embedding"])} # 餘弦相似性 cos = lambda a, b: float(a @ b / ((np.linalg.norm(a) * np.linalg.norm(b)) or 1)) # 以image_id=1為查詢圖片,檢索Top-K近鄰 query_id = 1 results = sorted( ((i, cos(vectors[query_id], v)) for i, v in vectors.items() if i != query_id), key=lambda r: -r[1], ) print(results)
完整樣本
以下樣本展示完整流程:串連DLF → 建表 → 映像位元組入表並產生縮圖和embedding → 瀏覽資料 → 視覺檢索 → 解碼取圖 → 清理。
import uuid
import numpy as np
import daft
from daft.functions import image
from pyiceberg.catalog import load_catalog
from pyiceberg.schema import Schema
from pyiceberg.types import (
LongType, StringType, BinaryType, ListType, FloatType, NestedField)
from pyiceberg.partitioning import UNPARTITIONED_PARTITION_SPEC
REGION, CATALOG, DB = "${regionId}", "${catalogName}", "${database}"
# 1) 串連DLF
catalog = load_catalog("dlf", **{
"type": "rest", "uri": f"http://{REGION}-vpc.dlf.aliyuncs.com/iceberg",
"warehouse": CATALOG, "rest.signing-name": "DlfNext",
"rest.signing-region": REGION, "rest.sigv4-enabled": "true",
"client.access-key-id": "${accessKeyId}",
"client.secret-access-key": "${accessKeySecret}", "client.region": REGION,
"s3.endpoint": f"https://oss-{REGION}-internal.aliyuncs.com"})
# 準備範例圖片位元組(請提前在目前的目錄放置兩張JPEG圖片)
samples = [("n01.jpg", "tench", open("n01.jpg", "rb").read()),
("n02.jpg", "cassette_player", open("n02.jpg", "rb").read())]
# 2) 建多模態表
name = f"image_catalog_{uuid.uuid4().hex[:8]}"
table = catalog.create_table((DB, name), Schema(
NestedField(1, "image_id", LongType(), required=True),
NestedField(2, "filename", StringType()),
NestedField(3, "label", StringType()),
NestedField(4, "image", BinaryType()),
NestedField(5, "thumbnail", BinaryType()),
NestedField(6, "embedding",
ListType(element_id=7, element_type=FloatType(), element_required=False))),
partition_spec=UNPARTITIONED_PARTITION_SPEC)
@daft.func(return_dtype=daft.DataType.list(daft.DataType.float32()))
def embed(img) -> list:
v = np.asarray(img).astype("float32").reshape(-1)
return (v / (np.linalg.norm(v) or 1.0)).tolist()
try:
# 3) 原圖位元組入表,同時派生縮圖和embedding
rows = {"image_id": [], "filename": [], "label": [], "image": []}
for i, (fn, label, jpg) in enumerate(samples, 1):
rows["image_id"].append(i)
rows["filename"].append(fn)
rows["label"].append(label)
rows["image"].append(jpg)
df = daft.from_pydict(rows)
df = df.with_column("thumbnail",
image.encode_image(image.resize(image.decode_image(df["image"]), 32, 32), "JPEG"))
df = df.with_column("embedding",
embed(image.resize(image.decode_image(df["image"]), 8, 8)))
df.select("image_id", "filename", "label", "image", "thumbnail",
"embedding").write_iceberg(table, mode="append")
# 4) 讀回瀏覽(投影下推,不讀像素)
table = catalog.load_table((DB, name))
daft.read_iceberg(table).select("image_id", "label", "filename").sort("image_id").show()
# 5) 視覺檢索:對image_id=1取餘弦近鄰
d = daft.read_iceberg(table).select("image_id", "embedding").collect().to_pydict()
M = {i: np.asarray(v, "float32") for i, v in zip(d["image_id"], d["embedding"])}
cos = lambda a, b: float(a @ b / ((np.linalg.norm(a) * np.linalg.norm(b)) or 1))
print(sorted(((i, cos(M[1], v)) for i, v in M.items() if i != 1), key=lambda r: -r[1]))
# 6) 取出一張圖並解碼
one = daft.read_iceberg(table).where(daft.col("image_id") == 1)
one = one.with_column("img", image.decode_image(one["image"]))
print("decoded shape:", one.select("img").collect().to_pydict()["img"][0].shape)
finally:
catalog.drop_table((DB, name))
樣本中的embedding為降採樣描述子,僅用於示範。實際使用時可替換為CLIP、ResNet等模型推理。