Skip to content

Commit 762e656

Browse files
committed
test: cover runtime property reindex endpoints (#2097)
Mock tests exercise the exact REST routes against a 1.39-advertising mock server (upsert 202/NO_OP, rebuild, cancel CANCELLED/NO_OP, tenants csv encoding, status parsing incl. coupled task entries) and assert WeaviateUnsupportedFeatureError against a 1.36 mock. Integration tests cover the searchable lifecycle, rangeFilters creation, coupled tokenization changes, multi-tenant selection and the async client, skipping below server 1.39.0.
1 parent 5e7179c commit 762e656

2 files changed

Lines changed: 513 additions & 0 deletions

File tree

integration/test_collection_config.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import weaviate
88
import weaviate.classes as wvc
99
from integration.conftest import (
10+
AsyncCollectionFactory,
1011
CollectionFactory,
1112
OpenAICollection,
1213
_sanitize_collection_name,
@@ -40,6 +41,8 @@
4041
_NamedVectorConfigCreate,
4142
_VectorizerConfigCreate,
4243
IndexName,
44+
PropertyIndexState,
45+
PropertyIndexTaskStatus,
4346
)
4447
from weaviate.collections.classes.tenants import Tenant
4548
from weaviate.exceptions import (
@@ -2678,3 +2681,224 @@ def test_text_analyzer_roundtrip_from_dict(
26782681
assert config == new
26792682
assert config.to_dict() == new.to_dict()
26802683
client.collections.delete(name)
2684+
2685+
2686+
def test_property_reindex_searchable_lifecycle(collection_factory: CollectionFactory) -> None:
2687+
"""Test the full runtime lifecycle of a searchable index: create, no-op, rebuild, cancel, delete."""
2688+
collection_dummy = collection_factory("dummy")
2689+
if collection_dummy._connection._weaviate_version.is_lower_than(1, 39, 0):
2690+
pytest.skip("Runtime property reindex requires Weaviate >= 1.39.0")
2691+
2692+
collection = collection_factory(
2693+
properties=[
2694+
Property(
2695+
name="name",
2696+
data_type=DataType.TEXT,
2697+
index_filterable=True,
2698+
index_searchable=False,
2699+
)
2700+
],
2701+
)
2702+
collection.data.insert_many([{"name": f"object {i}"} for i in range(10)])
2703+
2704+
# create the searchable index declaratively and wait for it to become ready
2705+
status = collection.config.update_property_index(
2706+
"name", "searchable", tokenization=Tokenization.WORD, wait_for_completion=True
2707+
)
2708+
assert status.type == "searchable"
2709+
assert status.status == PropertyIndexState.READY
2710+
assert status.tokenization == Tokenization.WORD
2711+
2712+
# re-putting the matching configuration is a no-op
2713+
task = collection.config.update_property_index(
2714+
"name", "searchable", tokenization=Tokenization.WORD
2715+
)
2716+
assert task.status == PropertyIndexTaskStatus.NO_OP
2717+
assert task.task_id is None
2718+
2719+
# the status endpoint reports the index as ready
2720+
indexes = collection.config.get_property_indexes()
2721+
assert indexes.collection == collection.name
2722+
entry = next(
2723+
index
2724+
for prop in indexes.properties
2725+
if prop.name == "name"
2726+
for index in prop.indexes
2727+
if index.type == "searchable"
2728+
)
2729+
assert entry.status == PropertyIndexState.READY
2730+
2731+
# rebuild the index from scratch
2732+
status = collection.config.rebuild_property_index(
2733+
"name", "searchable", wait_for_completion=True
2734+
)
2735+
assert status.type == "searchable"
2736+
assert status.status == PropertyIndexState.READY
2737+
2738+
# cancelling when no task is live is an idempotent no-op
2739+
task = collection.config.cancel_property_index_task("name", "searchable")
2740+
assert task.status == PropertyIndexTaskStatus.NO_OP
2741+
2742+
# the pre-existing delete API removes the index again
2743+
assert collection.config.delete_property_index("name", "searchable") is True
2744+
2745+
2746+
def test_property_reindex_range_filters(collection_factory: CollectionFactory) -> None:
2747+
"""Test creating a rangeFilters index on an int property via an empty request body."""
2748+
collection_dummy = collection_factory("dummy")
2749+
if collection_dummy._connection._weaviate_version.is_lower_than(1, 39, 0):
2750+
pytest.skip("Runtime property reindex requires Weaviate >= 1.39.0")
2751+
2752+
collection = collection_factory(
2753+
properties=[
2754+
Property(
2755+
name="age",
2756+
data_type=DataType.INT,
2757+
index_filterable=True,
2758+
index_range_filters=False,
2759+
)
2760+
],
2761+
)
2762+
collection.data.insert_many([{"age": i} for i in range(10)])
2763+
2764+
status = collection.config.update_property_index(
2765+
"age", "rangeFilters", wait_for_completion=True
2766+
)
2767+
assert status.type == "rangeFilters"
2768+
assert status.status == PropertyIndexState.READY
2769+
2770+
entry = next(
2771+
index
2772+
for prop in collection.config.get_property_indexes().properties
2773+
if prop.name == "age"
2774+
for index in prop.indexes
2775+
if index.type == "rangeFilters"
2776+
)
2777+
assert entry.status == PropertyIndexState.READY
2778+
2779+
2780+
def test_property_reindex_coupled_tokenization_change(
2781+
collection_factory: CollectionFactory,
2782+
) -> None:
2783+
"""Test that a tokenization change on searchable is coupled with the filterable index as one task."""
2784+
collection_dummy = collection_factory("dummy")
2785+
if collection_dummy._connection._weaviate_version.is_lower_than(1, 39, 0):
2786+
pytest.skip("Runtime property reindex requires Weaviate >= 1.39.0")
2787+
2788+
collection = collection_factory(
2789+
properties=[
2790+
Property(
2791+
name="name",
2792+
data_type=DataType.TEXT,
2793+
index_filterable=True,
2794+
index_searchable=True,
2795+
tokenization=Tokenization.WORD,
2796+
)
2797+
],
2798+
)
2799+
collection.data.insert_many([{"name": f"object {i}"} for i in range(100)])
2800+
2801+
task = collection.config.update_property_index(
2802+
"name", "searchable", tokenization=Tokenization.FIELD
2803+
)
2804+
assert task.status == PropertyIndexTaskStatus.STARTED
2805+
assert task.task_id is not None
2806+
2807+
prop = next(p for p in collection.config.get_property_indexes().properties if p.name == "name")
2808+
searchable = next(i for i in prop.indexes if i.type == "searchable")
2809+
filterable = next(i for i in prop.indexes if i.type == "filterable")
2810+
if searchable.task_id is not None:
2811+
# both entries are driven by the one coupled task while it is in flight
2812+
assert searchable.task_id == task.task_id
2813+
assert filterable.task_id == task.task_id
2814+
assert searchable.target_tokenization == Tokenization.FIELD
2815+
assert filterable.target_tokenization == Tokenization.FIELD
2816+
else:
2817+
# the task already finalized before the first poll
2818+
assert searchable.tokenization == Tokenization.FIELD
2819+
2820+
# poll the status endpoint (via the wait path of a NO_OP upsert) until the migration is done
2821+
status = collection.config.update_property_index(
2822+
"name", "searchable", tokenization=Tokenization.FIELD, wait_for_completion=True
2823+
)
2824+
assert status.status == PropertyIndexState.READY
2825+
assert status.tokenization == Tokenization.FIELD
2826+
2827+
prop = next(p for p in collection.config.get_property_indexes().properties if p.name == "name")
2828+
filterable = next(i for i in prop.indexes if i.type == "filterable")
2829+
assert filterable.status == PropertyIndexState.READY
2830+
assert filterable.tokenization == Tokenization.FIELD
2831+
2832+
2833+
def test_property_reindex_multi_tenant(collection_factory: CollectionFactory) -> None:
2834+
"""Test rangeFilters creation and rebuild with a tenants selection on a multi-tenant collection."""
2835+
collection_dummy = collection_factory("dummy")
2836+
if collection_dummy._connection._weaviate_version.is_lower_than(1, 39, 0):
2837+
pytest.skip("Runtime property reindex requires Weaviate >= 1.39.0")
2838+
2839+
collection = collection_factory(
2840+
properties=[
2841+
Property(
2842+
name="age",
2843+
data_type=DataType.INT,
2844+
index_filterable=True,
2845+
index_range_filters=False,
2846+
)
2847+
],
2848+
multi_tenancy_config=Configure.multi_tenancy(enabled=True),
2849+
)
2850+
collection.tenants.create([Tenant(name="tenant1"), Tenant(name="tenant2")])
2851+
collection.with_tenant("tenant1").data.insert_many([{"age": i} for i in range(5)])
2852+
2853+
status = collection.config.update_property_index(
2854+
"age", "rangeFilters", tenants=["tenant1", "tenant2"], wait_for_completion=True
2855+
)
2856+
assert status.type == "rangeFilters"
2857+
assert status.status == PropertyIndexState.READY
2858+
2859+
status = collection.config.rebuild_property_index(
2860+
"age", "rangeFilters", tenants=["tenant1"], wait_for_completion=True
2861+
)
2862+
assert status.type == "rangeFilters"
2863+
assert status.status == PropertyIndexState.READY
2864+
2865+
2866+
@pytest.mark.asyncio
2867+
async def test_property_reindex_async(async_collection_factory: AsyncCollectionFactory) -> None:
2868+
"""Test the runtime property reindex lifecycle through the async client."""
2869+
collection = await async_collection_factory(
2870+
properties=[
2871+
Property(
2872+
name="name",
2873+
data_type=DataType.TEXT,
2874+
index_filterable=True,
2875+
index_searchable=False,
2876+
)
2877+
],
2878+
)
2879+
if collection._connection._weaviate_version.is_lower_than(1, 39, 0):
2880+
pytest.skip("Runtime property reindex requires Weaviate >= 1.39.0")
2881+
2882+
await collection.data.insert_many([{"name": f"object {i}"} for i in range(10)])
2883+
2884+
status = await collection.config.update_property_index(
2885+
"name", "searchable", tokenization=Tokenization.WORD, wait_for_completion=True
2886+
)
2887+
assert status.type == "searchable"
2888+
assert status.status == PropertyIndexState.READY
2889+
2890+
task = await collection.config.update_property_index(
2891+
"name", "searchable", tokenization=Tokenization.WORD
2892+
)
2893+
assert task.status == PropertyIndexTaskStatus.NO_OP
2894+
2895+
indexes = await collection.config.get_property_indexes()
2896+
assert indexes.collection == collection.name
2897+
2898+
status = await collection.config.rebuild_property_index(
2899+
"name", "searchable", wait_for_completion=True
2900+
)
2901+
assert status.status == PropertyIndexState.READY
2902+
2903+
task = await collection.config.cancel_property_index_task("name", "searchable")
2904+
assert task.status == PropertyIndexTaskStatus.NO_OP

0 commit comments

Comments
 (0)