Skip to content

Commit fbe05fb

Browse files
beinanclaude
andcommitted
feat: add distributed zonemap index build with configurable segments
Add zonemap as a new index type in CREATE INDEX DDL with a distributed build path. Fragments are batched into segments built in parallel on Spark executors, then committed as a logical index on the driver. Changes: - ZonemapIndexJob/ZonemapIndexTask for distributed segment builds - num_segments option with validation (positive integer, zonemap only) - commitIndexSegments with pre-commit UUID capture for safe cleanup - Index-based fragment slicing to guarantee requested segment count - Error handling for partial segment failures No scan-side changes: zonemap fragment pruning works via the existing ZonemapFragmentPruner path without needing special scan flags. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8113bce commit fbe05fb

4 files changed

Lines changed: 481 additions & 26 deletions

File tree

docs/src/operations/ddl/create-index.md

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Creates a scalar index on a Lance table to accelerate queries.
77

88
## Overview
99

10-
The `CREATE INDEX` command builds an index on one or more columns of a Lance table. Indexing can improve the performance of queries that filter on the indexed columns. This operation is performed in a distributed manner, building indexes for each data fragment in parallel.
10+
The `CREATE INDEX` command builds an index on one or more columns of a Lance table. Indexing can improve the performance of queries that filter on the indexed columns. Depending on the index method, Lance Spark either uses a fragment-parallel build path or a driver-coordinated commit flow after parallel executor builds.
1111

1212
## Basic Usage
1313

@@ -24,13 +24,23 @@ The following index methods are supported:
2424

2525
| Method | Description |
2626
|---------|-----------------------------------------------------------------------------|
27+
| `zonemap` | Lightweight min/max index for fragment pruning on a scalar column. |
2728
| `btree` | B-tree index for efficient range queries and point lookups on scalar columns. |
2829
| `fts` | Full-text search (inverted) index for text search on string columns. |
2930

3031
## Options
3132

3233
The `CREATE INDEX` command supports options via the `WITH` clause to control index creation. These options are specific to the chosen index method.
3334

35+
### ZoneMap Options
36+
37+
For the `zonemap` method, the following options are supported:
38+
39+
| Option | Type | Description |
40+
|-----------------|------|----------------------------------------------|
41+
| `rows_per_zone` | Long | The approximate number of rows per zonemap zone. |
42+
| `num_segments` | Integer | Number of index segments to create. Each segment covers a batch of fragments. Defaults to `min(fragment_count, spark.default.parallelism)`. |
43+
3444
### BTree Options
3545

3646
For the `btree` method, the following options are supported:
@@ -92,6 +102,15 @@ Create a composite index on multiple columns.
92102
ALTER TABLE lance.db.logs CREATE INDEX idx_ts_level USING btree (timestamp, level);
93103
```
94104

105+
### Lightweight Fragment Pruning
106+
107+
Create a zonemap index when you want lightweight min/max-based fragment pruning:
108+
109+
=== "SQL"
110+
```sql
111+
ALTER TABLE lance.db.users CREATE INDEX idx_id_zonemap USING zonemap (id);
112+
```
113+
95114
### Indexing with Options
96115

97116
Create an index and specify the `zone_size` for the B-tree:
@@ -101,6 +120,15 @@ Create an index and specify the `zone_size` for the B-tree:
101120
ALTER TABLE lance.db.users CREATE INDEX idx_id_zoned USING btree (id) WITH (zone_size = 2048);
102121
```
103122

123+
### Zonemap with Options
124+
125+
Create a zonemap index and specify the approximate number of rows per zone:
126+
127+
=== "SQL"
128+
```sql
129+
ALTER TABLE lance.db.users CREATE INDEX idx_id_zonemap USING zonemap (id) WITH (rows_per_zone = 2048);
130+
```
131+
104132
### Full-Text Search Index
105133

106134
Create an FTS index on a text column:
@@ -133,17 +161,19 @@ The `CREATE INDEX` command returns the following information about the operation
133161
Consider creating an index when:
134162

135163
- You frequently filter a large table on a specific column.
164+
- You want lightweight fragment pruning based on per-zone min/max statistics.
136165
- Your queries involve point lookups or small range scans.
137166

138167
## How It Works
139168

140169
The `CREATE INDEX` command operates as follows:
141170

142-
1. **Distributed Index Building**: For each fragment in the Lance dataset, a separate task is launched to build an index on the specified column(s).
143-
2. **Metadata Merging**: Once all per-fragment indexes are built, their metadata is collected and merged.
171+
1. **Index Build Execution**: Lance Spark chooses an execution path based on the index method. Methods such as `btree`, `fts`, and `zonemap` can build physical index segments in parallel across fragments. `zonemap` publishes those segments directly as one logical index. Range-mode `btree` uses Spark repartitioning and sorted preprocessed data.
172+
2. **Metadata Finalization**: Lance Spark merges or commits the resulting index metadata on the driver so the new logical index becomes visible atomically.
144173
3. **Transactional Commit**: A new table version is committed with the new index information. The operation is atomic and ensures that concurrent reads are not affected.
145174

146175
## Notes and Limitations
147176

148-
- **Index Methods**: The `btree` and `fts` methods are supported for scalar index creation.
149-
- **Index Replacement**: If you create an index with the same name as an existing one, the old index will be replaced by the new one. This is because the underlying implementation uses `replace(true)`.
177+
- **Index Methods**: The `zonemap`, `btree`, and `fts` methods are supported for scalar index creation.
178+
- **Zonemap Column Count**: Zonemap indexes currently support a single column only. The generic `CREATE INDEX` grammar accepts a column list, but Lance rejects multi-column zonemap creation.
179+
- **Index Replacement**: If you create an index with the same name as an existing one, the old index will be replaced by the new one.

integration-tests/test_lance_spark.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,42 @@ def test_create_btree_index_on_string(self, spark):
778778
assert len(query_result) == 3
779779
_assert_lance_index_metadata(spark, "default.employees", "idx_dept", "BTREE")
780780

781+
def test_create_zonemap_index_on_int(self, spark):
782+
"""Test CREATE INDEX with ZoneMap on integer column."""
783+
spark.sql("""
784+
CREATE TABLE default.test_table (
785+
id INT,
786+
name STRING,
787+
value DOUBLE
788+
)
789+
""")
790+
791+
data = [(i, f"Name{i}", float(i * 10)) for i in range(100)]
792+
df = spark.createDataFrame(data, ["id", "name", "value"])
793+
df.writeTo("default.test_table").append()
794+
795+
result = spark.sql("""
796+
ALTER TABLE default.test_table
797+
CREATE INDEX idx_id_zonemap USING zonemap (id)
798+
WITH (rows_per_zone = 8)
799+
""").collect()
800+
801+
assert len(result) == 1
802+
assert result[0][1] == "idx_id_zonemap"
803+
804+
indexes = spark.sql("""
805+
SHOW INDEXES IN default.test_table
806+
""").collect()
807+
zonemap_rows = [row for row in indexes if row["name"] == "idx_id_zonemap"]
808+
assert len(zonemap_rows) >= 1
809+
assert zonemap_rows[0]["index_type"] == "zonemap"
810+
811+
query_result = spark.sql("""
812+
SELECT * FROM default.test_table WHERE id = 50
813+
""").collect()
814+
assert len(query_result) == 1
815+
assert query_result[0].id == 50
816+
781817
def test_create_fts_index(self, spark):
782818
"""Test CREATE INDEX with full-text search (FTS)."""
783819
spark.sql("""

0 commit comments

Comments
 (0)