docs: document Blob V2 in the multimodal guide (#4623)

This commit is contained in:
Drew
2026-10-26 09:37:28 -07:00
committed by GitHub
parent 7952ee2a71
commit 436c209580
22 changed files with 3157 additions and 253 deletions
+81 -80
View File
@@ -1,137 +1,134 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
import io
import pytest
import lancedb
import numpy as np
import pyarrow as pa
try:
# --8<-- [start:multimodal_imports]
import io
import lancedb
import numpy as np
import pyarrow as pa
import io
from PIL import Image
# --8<-- [end:multimodal_imports]
except ImportError:
pass
# --8<-- [start:multimodal_imports]
import lancedb
import pyarrow as pa
import pandas as pd
import numpy as np
import io
from PIL import Image
# --8<-- [end:multimodal_imports]
def test_multimodal_ingestion(db_path_factory):
# Ensure dependencies are available
pytest.importorskip("PIL")
pytest.importorskip("lancedb")
pytest.importorskip("numpy")
# --8<-- [start:create_dummy_data]
# Create some dummy images
def create_dummy_image(color):
img = Image.new('RGB', (100, 100), color=color)
img = Image.new("RGB", (100, 100), color=color)
buf = io.BytesIO()
img.save(buf, format='PNG')
img.save(buf, format="PNG")
return buf.getvalue()
# Create dataset with metadata, vectors, and image blobs
data = [
{
"id": 1,
"filename": "red_square.png",
"vector": np.random.rand(128).astype(np.float32),
"image_blob": create_dummy_image('red'),
"label": "red"
"image_blob": create_dummy_image("red"),
"label": "red",
},
{
"id": 2,
"filename": "blue_square.png",
"vector": np.random.rand(128).astype(np.float32),
"image_blob": create_dummy_image('blue'),
"label": "blue"
}
"image_blob": create_dummy_image("blue"),
"label": "blue",
},
]
# --8<-- [end:create_dummy_data]
# --8<-- [start:define_schema]
# Define schema explicitly to ensure image_blob is treated as binary
schema = pa.schema([
pa.field("id", pa.int32()),
pa.field("filename", pa.string()),
pa.field("vector", pa.list_(pa.float32(), 128)),
pa.field("image_blob", pa.binary()), # Important: Use pa.binary() for blobs
pa.field("label", pa.string())
])
schema = pa.schema(
[
pa.field("id", pa.int32()),
pa.field("filename", pa.string()),
pa.field("vector", pa.list_(pa.float32(), 128)),
pa.field("image_blob", pa.binary()),
pa.field("label", pa.string()),
]
)
# --8<-- [end:define_schema]
db_uri = db_path_factory("multimodal_db")
db = lancedb.connect(db_uri)
db = lancedb.connect(db_path_factory("multimodal_db"))
# --8<-- [start:ingest_data]
tbl = db.create_table("images", data=data, schema=schema, mode="overwrite")
# --8<-- [end:ingest_data]
assert len(tbl) == 2
# --8<-- [start:search_data]
# Search for similar images
query_vector = np.random.rand(128).astype(np.float32)
results = tbl.search(query_vector).limit(1).to_pandas()
# --8<-- [end:search_data]
# --8<-- [start:process_results]
# Convert back to PIL Image
for _, row in results.iterrows():
image_bytes = row['image_blob']
image_bytes = row["image_blob"]
image = Image.open(io.BytesIO(image_bytes))
print(f"Retrieved image: {row['filename']}, Size: {image.size}")
# You can now use 'image' with other libraries or display it
# --8<-- [end:process_results]
assert len(results) == 1
def test_blob_api_definition(db_path_factory):
# --8<-- [start:blob_api_schema]
def test_legacy_blob_api_definition(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
# --8<-- [start:legacy_blob_api_schema]
import pyarrow as pa
# Define schema with Blob API metadata for lazy loading
schema = pa.schema([
pa.field("id", pa.int64()),
pa.field(
"video",
pa.large_binary(),
metadata={"lance-encoding:blob": "true"} # Enable Blob API
),
])
# --8<-- [end:blob_api_schema]
schema = pa.schema(
[
pa.field("id", pa.int64()),
pa.field(
"video",
pa.large_binary(),
metadata={"lance-encoding:blob": "true"}, # Enable Blob API
),
]
)
# --8<-- [end:legacy_blob_api_schema]
# --8<-- [start:blob_api_ingest]
# --8<-- [start:legacy_blob_api_ingest]
import lancedb
db = lancedb.connect(db_path_factory("blob_db"))
db = lancedb.connect("./blob_db")
# Create sample data
data = [
{"id": 1, "video": b"fake_video_bytes_1"},
{"id": 2, "video": b"fake_video_bytes_2"}
{"id": 2, "video": b"fake_video_bytes_2"},
]
# Create the table
tbl = db.create_table("videos", data=data, schema=schema)
# --8<-- [end:blob_api_ingest]
# --8<-- [end:legacy_blob_api_ingest]
assert len(tbl) == 2
def test_blob_api_to_pandas(db_path_factory):
db = lancedb.connect(db_path_factory("blob_to_pandas_db"))
schema = pa.schema([
pa.field("id", pa.int64()),
pa.field(
"video",
pa.large_binary(),
metadata={"lance-encoding:blob": "true"},
),
])
def test_legacy_blob_api_to_pandas(db_path_factory):
db = lancedb.connect(db_path_factory("legacy_blob_to_pandas_db"))
schema = pa.schema(
[
pa.field("id", pa.int64()),
pa.field(
"video",
pa.large_binary(),
metadata={"lance-encoding:blob": "true"},
),
]
)
tbl = db.create_table(
"videos",
data=[
@@ -142,7 +139,7 @@ def test_blob_api_to_pandas(db_path_factory):
mode="overwrite",
)
# --8<-- [start:blob_api_to_pandas]
# --8<-- [start:legacy_blob_api_to_pandas]
# Default: blob columns come back lazily
df_lazy = tbl.to_pandas()
@@ -154,7 +151,7 @@ def test_blob_api_to_pandas(db_path_factory):
# Forward extra kwargs to PyArrow's to_pandas
df_typed = tbl.to_pandas(split_blocks=True, self_destruct=True)
# --8<-- [end:blob_api_to_pandas]
# --8<-- [end:legacy_blob_api_to_pandas]
assert len(df_lazy) == 2
assert isinstance(df_bytes["video"].iloc[0], bytes)
@@ -167,16 +164,18 @@ def test_blob_api_to_pandas(db_path_factory):
@pytest.mark.asyncio
async def test_query_to_pandas_kwargs(db_path_factory):
schema = pa.schema([
pa.field("id", pa.int64()),
pa.field("vector", pa.list_(pa.float32(), 128)),
pa.field(
"video",
pa.large_binary(),
metadata={"lance-encoding:blob": "true"},
),
])
async def test_legacy_query_to_pandas_kwargs(db_path_factory):
schema = pa.schema(
[
pa.field("id", pa.int64()),
pa.field("vector", pa.list_(pa.float32(), 128)),
pa.field(
"video",
pa.large_binary(),
metadata={"lance-encoding:blob": "true"},
),
]
)
data = [
{
"id": i,
@@ -186,11 +185,11 @@ async def test_query_to_pandas_kwargs(db_path_factory):
for i in range(10)
]
db = lancedb.connect(db_path_factory("query_to_pandas_db"))
db = lancedb.connect(db_path_factory("legacy_query_to_pandas_db"))
tbl = db.create_table("search_demo", data=data, schema=schema, mode="overwrite")
async_db = await lancedb.connect_async(
str(db_path_factory("query_to_pandas_async_db"))
str(db_path_factory("legacy_query_to_pandas_async_db"))
)
tbl_async = await async_db.create_table(
"search_demo", data=data, schema=schema, mode="overwrite"
@@ -198,7 +197,8 @@ async def test_query_to_pandas_kwargs(db_path_factory):
query_vector = np.random.rand(128).astype(np.float32)
# --8<-- [start:query_to_pandas_kwargs]
# fmt: off
# --8<-- [start:legacy_query_to_pandas_kwargs]
# Plain scan query: blob_mode is supported end to end
df_lazy = (
tbl.search()
@@ -223,7 +223,8 @@ async def test_query_to_pandas_kwargs(db_path_factory):
.select(["id", "vector"])
.to_pandas(split_blocks=True, self_destruct=True)
)
# --8<-- [end:query_to_pandas_kwargs]
# --8<-- [end:legacy_query_to_pandas_kwargs]
# fmt: on
assert len(df_lazy) == 1
assert len(df_bytes) == 1