Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/src/distributed-indexing.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Lance-Ray provides distributed index building functionality that leverages Ray's

### Scalar Indexing

`create_scalar_index()` - Distributedly create scalar index using ray. Currently only Inverted/FTS/BTREE/BITMAP/NGRAM/ZONEMAP/BLOOMFILTER/RTREE are supported. Will add more index type support in the future.
`create_scalar_index()` - Distributedly create scalar index using ray. Currently Inverted/FTS/BTREE/BITMAP/LABEL_LIST/NGRAM/ZONEMAP/BLOOMFILTER/RTREE are supported. Will add more index type support in the future.

To construct GeoArrow data for an RTREE index, install the PyLance geo extra:

Expand Down Expand Up @@ -77,7 +77,7 @@ def create_scalar_index(
| `ray_remote_args` | `Dict[str, Any]`, optional | Ray task options (e.g., `num_cpus`, `resources`) |
| `**kwargs` | `Any` | Additional arguments passed to `create_scalar_index` |

**Note:** For distributed scalar indexing, currently only `"INVERTED"`, `"FTS"`, `"BTREE"`, `"BITMAP"`, `"NGRAM"`, `"ZONEMAP"`, `"BLOOMFILTER"` and `"RTREE"` index types are supported.
**Note:** For distributed scalar indexing, currently `"INVERTED"`, `"FTS"`, `"BTREE"`, `"BITMAP"`, `"LABEL_LIST"`, `"NGRAM"`, `"ZONEMAP"`, `"BLOOMFILTER"`, and `"RTREE"` index types are supported.

#### Return Value

Expand Down
12 changes: 10 additions & 2 deletions lance_ray/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ def _build_rabitq_model(*, dimension: int, num_bits: int = 1) -> str:
"RTREE",
]
_SCALAR_INDEX_TYPES = get_args(_ScalarIndexType)
_SCALAR_SEGMENT_INDEX_TYPES = frozenset(_SCALAR_INDEX_TYPES) - {"LABEL_LIST"}
_SCALAR_SEGMENT_INDEX_TYPES = frozenset(_SCALAR_INDEX_TYPES)


def _scalar_index_type_name(index_type: str | IndexConfig) -> str | None:
Expand Down Expand Up @@ -473,7 +473,7 @@ def create_scalar_index(

Raises:
ValueError: If input parameters are invalid.
TypeError: If column type is not string.
TypeError: If the column type is incompatible with the index type.
RuntimeError: If index building fails or pylance version is incompatible.
"""
# Check pylance version compatibility
Expand Down Expand Up @@ -596,6 +596,14 @@ def create_scalar_index(
f"Column {column} must be numeric or string type for "
f"{index_type} index, got {value_type}"
)
case "LABEL_LIST":
if not (
pa.types.is_list(field.type) or pa.types.is_large_list(field.type)
):
raise TypeError(
f"Column {column} must be list or large list type for "
f"LABEL_LIST index, got {field.type}"
)
Comment on lines +599 to +606

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newly added LABEL_LIST type validation is only executed when index_type is a string. If the caller passes in an IndexConfig object in accordance with the public contract, this early validation will be skipped. As a result, invalid column types cannot be detected in advance on the Driver side, and failures may be deferred until the Ray worker execution phase. right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that’s right. The existing driver-side checks for the other scalar indexes also only run when index_type is a string; IndexConfig skips them today. This PR follows the existing pattern and keeps the change focused on adding LABEL_LIST. I’m considering aligning validation for string and IndexConfig inputs in lance-format/lance-ray#5250, rather than mixing that broader change into this PR.

case _:
# For other index types, skip strict validation to maintain compatibility
pass
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ classifiers = [

dependencies = [
"ray[data]>=2.41.0",
"pylance>=10.0.0b5",
"pylance>=10.0.0b7",
"lance-namespace",
"packaging",
"pyarrow>=17.0.0",
Expand Down
25 changes: 24 additions & 1 deletion tests/test_distributed_indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ def test_build_distributed_nested_scalar_indexes(self, temp_dir):
indices = {idx.name: idx for idx in updated_dataset.describe_indices()}
assert indices["nested_text_idx"].field_names == ["meta.text"]
assert indices["literal_dot_text_idx"].field_names == ["meta.`a.b`"]
assert indices["hyphen_user_id_idx"].field_names == ["`meta-data`.`user-id`"]
assert indices["hyphen_user_id_idx"].field_names == ["meta-data.user-id"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must we remove the ` character?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for this assertion. This change comes from upstream: lance-format/lance#7503. describe_indices() now uses minimal field-path quoting, so hyphens no longer add backticks. With the updated Lance version, the returned field name is meta-data.user-id, so the test expectation needs to match it.


nested_results = updated_dataset.scanner(
full_text_query="nestedthree",
Expand Down Expand Up @@ -1395,6 +1395,29 @@ class TestDistributedScalarSegmentIndexes:
["value = 4", "value IN (1, 6, 11)"],
id="bloomfilter",
),
pytest.param(
"LABEL_LIST",
"labels",
[
["distributed", "shared"],
["other", None],
None,
[],
["distributed"],
["shared", "other"],
[None],
["other"],
["distributed", "shared"],
["other", None],
None,
[],
],
pa.large_list(pa.string()),
"labels_idx",
"LabelList",
["array_has_any(labels, ['distributed'])"],
id="label-list",
),
],
)
def test_filter_index_matches_baseline(
Expand Down
32 changes: 27 additions & 5 deletions tests/test_vector_index_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def id(self):

class _FakeLanceSchema:
def field(self, column):
if column not in {"value", "text"}:
if column not in {"value", "text", "labels"}:
raise KeyError(column)
return _FakeLanceField()

Expand All @@ -82,6 +82,8 @@ def field(self, column):
return _FakeField(column, index_mod.pa.int64())
if column == "text":
return _FakeField(column, index_mod.pa.string())
if column == "labels":
return _FakeField(column, index_mod.pa.list_(index_mod.pa.string()))
else:
raise KeyError(column)

Expand All @@ -91,6 +93,7 @@ def __iter__(self):
_FakeField("vector"),
_FakeField("value", index_mod.pa.int64()),
_FakeField("text", index_mod.pa.string()),
_FakeField("labels", index_mod.pa.list_(index_mod.pa.string())),
]
)

Expand Down Expand Up @@ -446,10 +449,19 @@ def test_create_index_rejects_invalid_num_segments(monkeypatch):


@pytest.mark.parametrize(
"index_type",
["BTREE", "BITMAP", "INVERTED", "FTS", "NGRAM", "BLOOMFILTER", "RTREE"],
("index_type", "column"),
[
("BTREE", "value"),
("BITMAP", "value"),
("INVERTED", "text"),
("FTS", "text"),
("NGRAM", "text"),
("BLOOMFILTER", "value"),
("RTREE", "value"),
("LABEL_LIST", "labels"),
],
)
def test_create_scalar_index_uses_segment_path(monkeypatch, index_type):
def test_create_scalar_index_uses_segment_path(monkeypatch, index_type, column):
"""Migrated scalar indexes should use Lance's segment workflow."""

captured = {"loads": []}
Expand Down Expand Up @@ -486,7 +498,6 @@ def fake_map_async_with_pool(**kwargs):
)
monkeypatch.setattr(index_mod, "_map_async_with_pool", fake_map_async_with_pool)

column = "text" if index_type in {"INVERTED", "FTS", "NGRAM"} else "value"
updated_dataset = index_mod.create_scalar_index(
uri="memory://fake",
column=column,
Expand All @@ -502,6 +513,17 @@ def fake_map_async_with_pool(**kwargs):
assert fake_dataset.commit_kwargs["segments"] == ["segment"]


def test_create_label_list_index_rejects_non_list_column():
"""LABEL_LIST should reject invalid columns before Ray workers start."""

with pytest.raises(TypeError, match="must be list or large list type"):
index_mod.create_scalar_index(
uri=_FakeDataset(),
column="value",
index_type="LABEL_LIST",
)


def test_create_index_passes_block_size_to_loads_and_handler(monkeypatch):
"""The vector index path should use block_size for driver and worker loads."""

Expand Down
16 changes: 8 additions & 8 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading