Skip to content

Commit 1066074

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

2 files changed

Lines changed: 80 additions & 8 deletions

File tree

python/python/lance/dataset.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3074,6 +3074,14 @@ def _prepare_scalar_index_request(
30743074
else:
30753075
raise Exception("index_type must be str or IndexConfig")
30763076

3077+
@staticmethod
3078+
def _is_segment_native_scalar_index_type(
3079+
index_type: Union[str, IndexConfig],
3080+
) -> bool:
3081+
if isinstance(index_type, IndexConfig):
3082+
index_type = index_type.index_type
3083+
return index_type.upper() in {"BTREE", "BITMAP", "ZONEMAP"}
3084+
30773085
def create_scalar_index(
30783086
self,
30793087
column: str,
@@ -3291,7 +3299,9 @@ def create_scalar_index(
32913299
column, index_type, kwargs
32923300
)
32933301

3294-
if fragment_ids is not None and logical_index_type in {"BTREE", "BITMAP"}:
3302+
if fragment_ids is not None and self._is_segment_native_scalar_index_type(
3303+
logical_index_type
3304+
):
32953305
raise ValueError(
32963306
f"{logical_index_type} distributed indexing uses "
32973307
"create_index_uncommitted(..., "
@@ -4003,8 +4013,8 @@ def create_index_uncommitted(
40034013
"""
40044014
Create one segment without publishing it and return its metadata.
40054015
4006-
This is the public distributed-build API for vector, BTREE scalar,
4007-
and canonical bitmap scalar index construction. Unlike
4016+
This is the public distributed-build API for vector index construction
4017+
and BTREE, canonical BITMAP, and ZONEMAP scalar segment construction. Unlike
40084018
:meth:`create_index`, this method does not publish the index into the
40094019
dataset manifest. Instead, it writes one segment under
40104020
``_indices/<segment_uuid>/`` and returns the resulting
@@ -4049,11 +4059,8 @@ def create_index_uncommitted(
40494059
Index
40504060
Metadata for the segment that was written by this call.
40514061
"""
4052-
is_scalar_segment_request = (
4053-
isinstance(index_type, str) and index_type.upper() in {"BTREE", "BITMAP"}
4054-
) or (
4055-
isinstance(index_type, IndexConfig)
4056-
and index_type.index_type.upper() in {"BTREE", "BITMAP"}
4062+
is_scalar_segment_request = self._is_segment_native_scalar_index_type(
4063+
index_type
40574064
)
40584065
if is_scalar_segment_request:
40594066
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_segment_merge_and_commit_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)