Skip to content

Commit 9b63bd7

Browse files
author
wangzheyan
committed
feat(python): expose zonemap segment builds
1 parent cc657c5 commit 9b63bd7

2 files changed

Lines changed: 78 additions & 8 deletions

File tree

python/python/lance/dataset.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3291,7 +3291,11 @@ def create_scalar_index(
32913291
column, index_type, kwargs
32923292
)
32933293

3294-
if fragment_ids is not None and logical_index_type in {"BTREE", "BITMAP"}:
3294+
if fragment_ids is not None and logical_index_type in {
3295+
"BTREE",
3296+
"BITMAP",
3297+
"ZONEMAP",
3298+
}:
32953299
raise ValueError(
32963300
f"{logical_index_type} distributed indexing uses "
32973301
"create_index_uncommitted(..., "
@@ -4003,8 +4007,8 @@ def create_index_uncommitted(
40034007
"""
40044008
Create one segment without publishing it and return its metadata.
40054009
4006-
This is the public distributed-build API for vector, BTREE scalar,
4007-
and canonical bitmap scalar index construction. Unlike
4010+
This is the public distributed-build API for vector, BTREE, BITMAP,
4011+
and ZONEMAP scalar index construction. Unlike
40084012
:meth:`create_index`, this method does not publish the index into the
40094013
dataset manifest. Instead, it writes one segment under
40104014
``_indices/<segment_uuid>/`` and returns the resulting
@@ -4020,9 +4024,9 @@ def create_index_uncommitted(
40204024
4. commit the final segment list with
40214025
:meth:`commit_existing_index_segments`
40224026
4023-
BTREE segments do not yet support merging; collect the returned
4024-
segments and pass them straight to
4025-
:meth:`commit_existing_index_segments`.
4027+
BTREE, BITMAP, and ZONEMAP segments can be passed to
4028+
:meth:`merge_existing_index_segments` when consolidation is desired, or
4029+
committed directly with :meth:`commit_existing_index_segments`.
40264030
40274031
Parameters are the same as :meth:`create_index`, with one additional
40284032
requirement:
@@ -4050,10 +4054,11 @@ def create_index_uncommitted(
40504054
Metadata for the segment that was written by this call.
40514055
"""
40524056
is_scalar_segment_request = (
4053-
isinstance(index_type, str) and index_type.upper() in {"BTREE", "BITMAP"}
4057+
isinstance(index_type, str)
4058+
and index_type.upper() in {"BTREE", "BITMAP", "ZONEMAP"}
40544059
) or (
40554060
isinstance(index_type, IndexConfig)
4056-
and index_type.index_type.upper() in {"BTREE", "BITMAP"}
4061+
and index_type.index_type.upper() in {"BTREE", "BITMAP", "ZONEMAP"}
40574062
)
40584063
if is_scalar_segment_request:
40594064
if fragment_ids is None:

python/python/tests/test_scalar_index.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4008,6 +4008,71 @@ def test_bitmap_uncommitted_segments_can_be_committed_from_python(tmp_path):
40084008
)
40094009

40104010

4011+
def test_zonemap_uncommitted_segments_can_be_merged_from_python(tmp_path):
4012+
ds = generate_multi_fragment_dataset(
4013+
tmp_path, num_fragments=4, rows_per_fragment=40
4014+
)
4015+
4016+
index_name = "id_zonemap_segments"
4017+
fragment_ids = [fragment.fragment_id for fragment in ds.get_fragments()]
4018+
with pytest.raises(ValueError, match="create_index_uncommitted"):
4019+
ds.create_scalar_index(
4020+
column="id",
4021+
index_type="ZONEMAP",
4022+
fragment_ids=[fragment_ids[0]],
4023+
)
4024+
4025+
staged_segments = [
4026+
ds.create_index_uncommitted(
4027+
column="id",
4028+
index_type="ZONEMAP",
4029+
name=index_name,
4030+
fragment_ids=[fragment_id],
4031+
)
4032+
for fragment_id in fragment_ids
4033+
]
4034+
4035+
assert len({segment.uuid for segment in staged_segments}) == len(staged_segments)
4036+
for segment, fragment_id in zip(staged_segments, fragment_ids):
4037+
files = segment.files
4038+
assert files is not None
4039+
assert segment.fragment_ids == {fragment_id}
4040+
assert any(file.path == "zonemap.lance" for file in files)
4041+
assert all(not file.path.startswith("part_") for file in files)
4042+
4043+
merged_segment = ds.merge_existing_index_segments(staged_segments)
4044+
merged_files = merged_segment.files
4045+
assert merged_files is not None
4046+
assert merged_segment.uuid not in {segment.uuid for segment in staged_segments}
4047+
assert merged_segment.fragment_ids == set(fragment_ids)
4048+
assert any(file.path == "zonemap.lance" for file in merged_files)
4049+
assert all(not file.path.startswith("part_") for file in merged_files)
4050+
4051+
ds = ds.commit_existing_index_segments(index_name, "id", [merged_segment])
4052+
descriptions = {index.name: index for index in ds.describe_indices()}
4053+
assert descriptions[index_name].index_type == "ZoneMap"
4054+
assert len(descriptions[index_name].segments) == 1
4055+
4056+
filter_expr = "id >= 20 AND id < 90"
4057+
without_index = ds.scanner(
4058+
filter=filter_expr,
4059+
columns=["id", "text"],
4060+
use_scalar_index=False,
4061+
).to_table()
4062+
with_index = ds.scanner(
4063+
filter=filter_expr,
4064+
columns=["id", "text"],
4065+
use_scalar_index=True,
4066+
).to_table()
4067+
4068+
assert with_index.num_rows == without_index.num_rows
4069+
assert with_index["id"].to_pylist() == without_index["id"].to_pylist()
4070+
assert (
4071+
"ScalarIndexQuery"
4072+
in ds.scanner(filter=filter_expr, use_scalar_index=True).explain_plan()
4073+
)
4074+
4075+
40114076
def test_merge_index_metadata_btree_soft_break(tmp_path):
40124077
ds = generate_multi_fragment_dataset(
40134078
tmp_path, num_fragments=2, rows_per_fragment=100

0 commit comments

Comments
 (0)