Store image bytes directly in a DLF Iceberg table and use Daft for thumbnail generation, embedding computation, and visual similarity search.
Multimodal storage overview
In the inline storage approach, raw image or audio/video bytes, thumbnails, and embedding vectors are stored alongside metadata in the same Iceberg table. Daft decodes image bytes directly from the table without accessing object storage paths, so writes, reads, and similarity searches all operate on a single table.
|
Data |
Column type |
|
Raw image bytes |
|
|
Thumbnail (optional; for lightweight browsing) |
|
|
Embedding vector |
|
When individual media files are large, inline storage increases Parquet file size. Choose your storage approach based on your data scale.
Environment setup
Install dependencies
Python 3.10 or later.
Install the DLF-compatible PyIceberg package (
pyiceberg-dlf).python3 -m venv venv source venv/bin/activate pip install -U pip # Uninstall pyiceberg, which cannot coexist with pyiceberg-dlf pip uninstall -y pyiceberg # rest-sigv4 is required (installs boto3 for REST sigv4 signing) pip install "pyiceberg-dlf[rest-sigv4,pyarrow,pandas]"Install Daft.
pip install "daft>=0.7.17"
Configure parameters
Prepare the following information:
Parameter | Description |
| Your AccessKey ID. |
| Your AccessKey secret. |
| Region ID where DLF is deployed, for example, |
| Catalog name in DLF (corresponds to the Iceberg warehouse). |
| Name of the target database (Iceberg namespace). |
Protect your AccessKey credentials. Do not hardcode them in your code or commit them to code repositories. We recommend retrieving them from environment variables or a secret management service.
Connect to DLF catalog
Connect to the DLF catalog via the Iceberg REST protocol by using PyIceberg's load_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",
},
)Prepare sample images
Prepare two local JPEG sample images named n01.jpg and n02.jpg (RGB format) and place them in the current directory. You can download one image each from the n01440764 (tench) and n02979186 (cassette player) classes in the public Imagenette dataset and rename them.
Create a multimodal table
Use PyIceberg to create a multimodal table with columns for image bytes and embedding vectors:
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,
)
This statement creates a format-version 2 table by default. DLF supports format-version 3 tables, but PyIceberg and Daft do not yet support writing to v3 tables or the VARIANT type. Do not set properties={"format-version": "3"} for multimodal tables.
Write image data
Store raw image bytes in the image column and use Daft image functions to derive thumbnails. Daft 0.7.15 and later configures OSS automatically, so you do not need to pass io_config manually.
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()],
})
# Derive thumbnails from raw image bytes: decode -> resize to 32x32 -> re-encode as 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")
Read and process images
After reading the table, decode the inline bytes in the image column directly -- no object storage access required.
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()
Compute embeddings and similarity search
-
Use
@daft.functo convert images into vectors and produce alist<float>embedding column. The following example uses a downsampled descriptor; you can replace it with CLIP, ResNet, or another model.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() # Decode inline images, resize to uniform dimensions, and compute embeddings (can also be persisted at write time; see the complete example) df = daft.read_iceberg(table) df = df.with_column("embedding", embed(image.resize(image.decode_image(df["image"]), 8, 8))) -
With the
embeddingcolumn ready, callcollect()to gather the data locally, compute cosine similarity against a query image vector, and retrieve the top-K results.# Collect the embeddings computed in the previous step 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"])} # Cosine similarity cos = lambda a, b: float(a @ b / ((np.linalg.norm(a) * np.linalg.norm(b)) or 1)) # Use image_id=1 as the query image and retrieve Top-K nearest neighbors 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)
Complete example
The following example walks through the end-to-end workflow: connect to DLF, create a table, write image bytes with thumbnails and embeddings, browse data, run a visual search, decode an image, and clean up.
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) Connect to 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"})
# Prepare sample image bytes (place two JPEG images in the current directory beforehand)
samples = [("n01.jpg", "tench", open("n01.jpg", "rb").read()),
("n02.jpg", "cassette_player", open("n02.jpg", "rb").read())]
# 2) Create a multimodal table
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) Write image bytes with derived thumbnails and embeddings
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) Browse data (projection pushdown, no pixel columns)
table = catalog.load_table((DB, name))
daft.read_iceberg(table).select("image_id", "label", "filename").sort("image_id").show()
# 5) Visual search: find cosine neighbors for 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) Retrieve and decode an image
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))
The embeddings in this example are downsampled descriptors for demonstration only. In production, replace them with CLIP, ResNet, or another model for inference.