|
7 | 7 | import weaviate |
8 | 8 | import weaviate.classes as wvc |
9 | 9 | from integration.conftest import ( |
| 10 | + AsyncCollectionFactory, |
10 | 11 | CollectionFactory, |
11 | 12 | OpenAICollection, |
12 | 13 | _sanitize_collection_name, |
|
40 | 41 | _NamedVectorConfigCreate, |
41 | 42 | _VectorizerConfigCreate, |
42 | 43 | IndexName, |
| 44 | + PropertyIndexState, |
| 45 | + PropertyIndexTaskStatus, |
43 | 46 | ) |
44 | 47 | from weaviate.collections.classes.tenants import Tenant |
45 | 48 | from weaviate.exceptions import ( |
@@ -2678,3 +2681,224 @@ def test_text_analyzer_roundtrip_from_dict( |
2678 | 2681 | assert config == new |
2679 | 2682 | assert config.to_dict() == new.to_dict() |
2680 | 2683 | 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