import pytest from requests import Response from .conftest import collection_name as test_collection_name from .helpers.collection_setup import basic_collection_setup, drop_collection, full_collection_setup from .helpers.helpers import request_with_validation @pytest.fixture() def collection_name(test_collection_name): basic_collection_setup(collection_name=test_collection_name) yield test_collection_name drop_collection(collection_name=test_collection_name) @pytest.fixture() def full_collection_name(test_collection_name): coll_name = f"{test_collection_name}_full" full_collection_setup(coll_name) yield coll_name drop_collection(collection_name=coll_name) def set_strict_mode(collection_name, strict_mode_config): request_with_validation( api="/collections/{collection_name}", method="PATCH", path_params={"collection_name": collection_name}, body={ "strict_mode_config": strict_mode_config, }, ).raise_for_status() def get_strict_mode(collection_name): response = request_with_validation( api='/collections/{collection_name}', method="GET", path_params={'collection_name': collection_name}, ) assert response.ok config = response.json()['result']['config'] if "strict_mode_config" not in config: return None else: return config['strict_mode_config'] def strict_mode_enabled(collection_name) -> bool: strict_mode = get_strict_mode(collection_name) return strict_mode is not None and strict_mode['enabled'] def test_patch_collection_full(collection_name): assert not strict_mode_enabled(collection_name) set_strict_mode(collection_name, { "enabled": True, "max_query_limit": 10, "max_timeout": 2, "unindexed_filtering_retrieve": False, "unindexed_filtering_update": False, "search_max_hnsw_ef": 3, "search_allow_exact": False, "search_max_oversampling": 1.5, }) response = request_with_validation( api='/collections/{collection_name}', method="GET", path_params={'collection_name': collection_name}, ) assert response.ok new_strict_mode_config = response.json()['result']['config']['strict_mode_config'] assert new_strict_mode_config['enabled'] assert new_strict_mode_config['max_query_limit'] == 10 assert new_strict_mode_config['max_timeout'] == 2 assert not new_strict_mode_config['unindexed_filtering_retrieve'] assert not new_strict_mode_config['unindexed_filtering_update'] assert new_strict_mode_config['search_max_hnsw_ef'] == 3 assert not new_strict_mode_config['search_allow_exact'] assert new_strict_mode_config['search_max_oversampling'] == 1.5 def test_patch_collection_partially(collection_name): assert not strict_mode_enabled(collection_name) set_strict_mode(collection_name,{ "enabled": True, "max_query_limit": 10, "search_max_oversampling": 1.5, }) response = request_with_validation( api='/collections/{collection_name}', method="GET", path_params={'collection_name': collection_name}, ) assert response.ok new_strict_mode_config = response.json()['result']['config']['strict_mode_config'] assert new_strict_mode_config['enabled'] assert new_strict_mode_config['max_query_limit'] == 10 assert new_strict_mode_config['search_max_oversampling'] == 1.5 def test_strict_mode_query_limit_validation(collection_name): def search_request(): return request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": [0.2, 0.1, 0.9, 0.7], "limit": 4 } ) search_request().raise_for_status() set_strict_mode(collection_name, { "enabled": True, "max_query_limit": 4, }) search_request().raise_for_status() set_strict_mode(collection_name, { "max_query_limit": 3, }) search_fail = search_request() assert "limit" in search_fail.json()['status']['error'] assert not search_fail.ok def test_strict_mode_timeout_validation(collection_name): def search_request_with_timeout(timeout): return request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, query_params={'timeout': timeout}, body={ "query": [0.2, 0.1, 0.9, 0.7], "limit": 3 } ) search_request_with_timeout(3).raise_for_status() set_strict_mode(collection_name, { "enabled": True, "max_timeout": 2, }) search_request_with_timeout(2).raise_for_status() search_fail = search_request_with_timeout(3) assert "timeout" in search_fail.json()['status']['error'] assert not search_fail.ok def test_strict_mode_unindexed_filter_keyword_read_validation(collection_name): def search_request_with_filter(): return request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": [0.2, 0.1, 0.9, 0.7], "limit": 3, "filter": { "must": [ { "key": "city", "match": { "value": "Berlin" } } ] }, } ) search_request_with_filter().raise_for_status() set_strict_mode(collection_name, { "enabled": True, "unindexed_filtering_retrieve": True, }) search_request_with_filter().raise_for_status() set_strict_mode(collection_name, { "unindexed_filtering_retrieve": False, }) search_fail = search_request_with_filter() assert "city" in search_fail.json()['status']['error'] assert not search_fail.ok request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "field_name": "city", "field_schema": "keyword" } ).raise_for_status() # We created an index on this field so it should work now search_request_with_filter().raise_for_status() def test_strict_mode_unindexed_filter_integer_read_validation(collection_name): def search_request_with_filter(): return request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": [0.2, 0.1, 0.9, 0.7], "limit": 3, "filter": { "must": [ { "key": "count", "match": { "value": 1 } } ] }, } ) # works without strict mode search_request_with_filter().raise_for_status() # enable strict mode `unindexed_filtering_retrieve` set_strict_mode(collection_name, { "enabled": True, "unindexed_filtering_retrieve": True, }) # still works search_request_with_filter().raise_for_status() # toggle `unindexed_filtering_retrieve` set_strict_mode(collection_name, { "unindexed_filtering_retrieve": False, }) # search fail because no payload index search_fail = search_request_with_filter() assert "count" in search_fail.json()['status']['error'] assert not search_fail.ok # create payload index request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "field_name": "count", "field_schema": { "type": "integer", "lookup": True, "range": True, } } ).raise_for_status() # works now by leveraging index search_request_with_filter().raise_for_status() # remove lookup capacity on index request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "field_name": "count", "field_schema": { "type": "integer", "lookup": False, "range": True, } } ).raise_for_status() # fails because the integer index does not support `lookup` for our match condition search_fail = search_request_with_filter() assert "count" in search_fail.json()['status']['error'] assert not search_fail.ok def test_strict_mode_unindexed_filter_phrase_read_validation(collection_name): # toggle `unindexed_filtering_retrieve` set_strict_mode(collection_name, { "enabled": True, "unindexed_filtering_retrieve": False, }) # add text index without phrase_matching request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "field_name": "t", "field_schema": { "type": "text" } } ).raise_for_status() def search_request_with_filter(kind: str): return request_with_validation( api='/collections/{collection_name}/points/scroll', method="POST", path_params={'collection_name': collection_name}, body={ "filter": { "must": [ { "key": "t", "match": { kind: "some text" } } ] } }) # works for text matching search_request_with_filter("text").raise_for_status() # does not work if phrase_matching is not enabled search_fail = search_request_with_filter("phrase") assert "Help: Create an index" in search_fail.json()['status']['error'] assert not search_fail.ok request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "field_name": "t", "field_schema": { "type": "text", "phrase_matching": True } } ).raise_for_status() # Now it should work for both search_request_with_filter("text").raise_for_status() search_request_with_filter("phrase").raise_for_status() def test_strict_mode_unindexed_filter_write_validation(collection_name): def update_request_with_filter(): return request_with_validation( api='/collections/{collection_name}/points/delete', method="POST", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "filter": { "must": [ { "key": "city", "match": { "value": "Berlin" } } ] } }) update_request_with_filter().raise_for_status() # Reset any changes basic_collection_setup(collection_name=collection_name) set_strict_mode(collection_name, { "enabled": True, "unindexed_filtering_update": True, }) update_request_with_filter().raise_for_status() set_strict_mode(collection_name, { "unindexed_filtering_update": False, }) search_fail = update_request_with_filter() assert "city" in search_fail.json()['status']['error'] assert not search_fail.ok request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "field_name": "city", "field_schema": "keyword" } ).raise_for_status() # We created an index on this field so it should work now update_request_with_filter().raise_for_status() def test_strict_mode_max_ef_hnsw_validation(collection_name): def search_request(): return request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": [0.2, 0.1, 0.9, 0.7], "limit": 4, "params": { "hnsw_ef": 5, } } ) search_request().raise_for_status() set_strict_mode(collection_name, { "enabled": True, "search_max_hnsw_ef": 5, }) search_request().raise_for_status() set_strict_mode(collection_name, { "search_max_hnsw_ef": 4, }) search_fail = search_request() assert "hnsw_ef" in search_fail.json()['status']['error'] assert not search_fail.ok def test_strict_mode_allow_exact_validation(collection_name): def search_request(): return request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": [0.2, 0.1, 0.9, 0.7], "limit": 4, "params": { "exact": True, } } ) search_request().raise_for_status() set_strict_mode(collection_name, { "enabled": True, "search_allow_exact": True, }) search_request().raise_for_status() set_strict_mode(collection_name, { "search_allow_exact": False, }) search_fail = search_request() assert "exact" in search_fail.json()['status']['error'].lower() assert not search_fail.ok def test_strict_mode_search_max_oversampling_validation(collection_name): def search_request(): return request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": [0.2, 0.1, 0.9, 0.7], "limit": 4, "params": { "quantization": { "oversampling": 2.0, } } } ) search_request().raise_for_status() set_strict_mode(collection_name, { "enabled": True, "search_max_oversampling": 2.0, }) search_request().raise_for_status() set_strict_mode(collection_name, { "enabled": True, "search_max_oversampling": 1.9, }) search_fail = search_request() assert "oversampling" in search_fail.json()['status']['error'] assert not search_fail.ok def test_strict_mode_upsert_max_batch_size(collection_name): def search_request(): return request_with_validation( api='/collections/{collection_name}/points', method="PUT", path_params={'collection_name': collection_name}, body={ "batch": { "ids": [1, 2, 3, 4, 5, 6], "payloads": [{}, {}, {}, {}, {}, {}], "vectors": [ [1, 2, 3, 5], [1, 2, 3, 5], [1, 2, 3, 5], [1, 2, 3, 5], [1, 2, 3, 5], [1, 2, 3, 5] ] } } ) search_request().raise_for_status() set_strict_mode(collection_name, { "enabled": True, "upsert_max_batchsize": 6, }) search_request().raise_for_status() set_strict_mode(collection_name, { "enabled": True, "upsert_max_batchsize": 5, }) search_fail = search_request() assert "upsert" in search_fail.json()['status']['error'] assert not search_fail.ok def test_strict_mode_update_many_upsert_max_batch_size(collection_name): def search_request(): return request_with_validation( api='/collections/{collection_name}/points/batch', method="POST", path_params={'collection_name': collection_name}, body={ "operations": [ { "upsert": { "batch": { "ids": [1, 2, 3, 4, 5, 6], "payloads": [{}, {}, {}, {}, {}, {}], "vectors": [ [1, 2, 3, 5], [1, 2, 3, 5], [1, 2, 3, 5], [1, 2, 3, 5], [1, 2, 3, 5], [1, 2, 3, 5] ] } } } ] } ) search_request().raise_for_status() set_strict_mode(collection_name, { "enabled": True, "upsert_max_batchsize": 6, }) search_request().raise_for_status() set_strict_mode(collection_name, { "enabled": True, "upsert_max_batchsize": 5, }) search_fail = search_request() assert "upsert" in search_fail.json()['status']['error'] assert not search_fail.ok def test_strict_mode_update_vectors_max_batch_size(collection_name): def search_request(): return request_with_validation( api='/collections/{collection_name}/points/vectors', method="PUT", path_params={'collection_name': collection_name}, body={ "points": [ { "id": 1, "vector": [1, 2, 3, 5], }, { "id": 2, "vector": [1, 2, 3, 5], }, { "id": 3, "vector": [1, 2, 3, 5], }, { "id": 4, "vector": [1, 2, 3, 5], }, ] } ) search_request().raise_for_status() set_strict_mode(collection_name, { "enabled": True, "upsert_max_batchsize": 4, }) search_request().raise_for_status() set_strict_mode(collection_name, { "enabled": True, "upsert_max_batchsize": 3, }) search_fail = search_request() assert "update limit" in search_fail.json()['status']['error'] assert not search_fail.ok def test_strict_mode_search_max_batch_size(collection_name): def search_batch_request(n: int): return request_with_validation( api='/collections/{collection_name}/points/query/batch', method="POST", path_params={'collection_name': collection_name}, body={ "searches": [ { "query": [0.2, 0.1, 0.9, 0.7], "limit": 3 } for _ in range(n) ] } ) search_batch_request(3).raise_for_status() set_strict_mode(collection_name, { "enabled": True, "search_max_batchsize": 3, }) search_batch_request(3).raise_for_status() set_strict_mode(collection_name, { "enabled": True, "search_max_batchsize": 2, }) search_fail = search_batch_request(3) assert "search batch size" in search_fail.json()['status']['error'] assert not search_fail.ok def test_strict_mode_max_collection_size_upsert(collection_name): basic_collection_setup(collection_name=collection_name) # Clear collection to not depend on other tests def upsert_points(ids: list[int]): length = len(ids) payloads = [{} for _ in range(length)] vectors = [[1, 2, 3, 5] for _ in range(length)] return request_with_validation( api='/collections/{collection_name}/points', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "batch": { "ids": ids, "payloads": payloads, "vectors": vectors } } ) # Overwriting the same points to trigger cache refreshing for _ in range(32): upsert_points([1, 2, 3, 4, 5]).raise_for_status() set_strict_mode(collection_name, { "enabled": True, "max_collection_vector_size_bytes": 240, }) for _ in range(32): upsert_points([6, 7, 8, 9, 10]).raise_for_status() # Max limit has been reached and one of the next requests must fail. Due to cache it might not be the first call! for _ in range(32): failed_upsert = upsert_points([12, 13, 14, 15, 16]) if failed_upsert.ok: continue assert "Max vector storage size" in failed_upsert.json()['status']['error'] assert not failed_upsert.ok return assert False, "Upserting should have failed but didn't" def test_strict_mode_max_sparse_length_upsert(collection_name): response = request_with_validation( api='/collections/{collection_name}', method="DELETE", path_params={'collection_name': collection_name}, ) assert response.ok response = request_with_validation( api='/collections/{collection_name}', method="PUT", path_params={'collection_name': collection_name}, body={ "sparse_vectors": { "sparse-vector": {} } } ) assert response.ok set_strict_mode(collection_name, { "enabled": True, "sparse_config": { "sparse-vector": { "max_length": 4 } } }) response = request_with_validation( api='/collections/{collection_name}/points', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "points": [ { "id": 1, "vector": { "sparse-vector": { "indices": [1, 2, 3, 4], "values": [0.0, 0.1, 0.2, 0.3] } } } ] } ) assert response.ok failed_upsert = request_with_validation( api='/collections/{collection_name}/points', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "points": [ { "id": 2, "vector": { "sparse-vector": { "indices": [1, 2, 3, 4, 5, 6], "values": [0.1, 0.2, 0.3, 0.4, 0.5, 0.6] } } } ] } ) assert not failed_upsert.ok assert "Sparse vector 'sparse-vector' has a limit of 4 indices" in failed_upsert.json()['status']['error'] def test_strict_mode_max_collection_size_upsert_batch(collection_name): basic_collection_setup(collection_name=collection_name) # Clear collection to not depend on other tests def upsert_points(ids: list[int]): length = len(ids) payloads = [{} for _ in range(length)] vectors = [[1, 2, 3, 5] for _ in range(length)] return request_with_validation( api='/collections/{collection_name}/points/batch', method="POST", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "operations": [ { "upsert": { "batch": { "ids": ids, "payloads": payloads, "vectors": vectors } } } ] } ) for _ in range(32): upsert_points([1, 2, 3, 4, 5]).raise_for_status() set_strict_mode(collection_name, { "enabled": True, "max_collection_vector_size_bytes": 240, }) for _ in range(32): upsert_points([6, 7, 8, 9, 10]).raise_for_status() # Max limit has been reached and one of the next requests must fail. Due to cache it might not be the first call! for _ in range(32): failed_upsert = upsert_points([12, 13, 14, 15, 16]) if failed_upsert.ok: continue assert "Max vector storage size" in failed_upsert.json()['status']['error'] assert not failed_upsert.ok return assert False, "Upserting should have failed but didn't" def test_strict_mode_max_multivector_size_upsert(collection_name): # Clear collection to not depend on other tests response = request_with_validation( api='/collections/{collection_name}', method="DELETE", path_params={'collection_name': collection_name}, ) assert response.ok response = request_with_validation( api='/collections/{collection_name}', method="PUT", path_params={'collection_name': collection_name}, body={ "vectors": { "dense-multi": { "size": 4, "distance": "Dot", "multivector_config": { "comparator": "max_sim" } }, }, } ) assert response.ok set_strict_mode(collection_name, { "enabled": True, "multivector_config": { "dense-multi": { "max_vectors": 5, } } }) response = request_with_validation( api='/collections/{collection_name}/points', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "points": [ { "id": 1, "vector": { "dense-multi": [ [1.05, 1.61, 1.76, 1.74], [2.05, 2.61, 2.76, 2.74], [3.05, 3.61, 3.76, 3.74] ], } } ] } ) assert response.ok # insert multivectors with 6 vectors (points list) failed_upsert = request_with_validation( api='/collections/{collection_name}/points', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "points": [ { "id": 1, "vector": { "dense-multi": [ [1.05, 1.61, 1.76, 1.74], [2.05, 2.61, 2.76, 2.74], [3.05, 3.61, 3.76, 3.74], [3.05, 3.61, 3.76, 3.74], [3.05, 3.61, 3.76, 3.74], [3.05, 3.61, 3.76, 3.74], ], } } ] } ) assert not failed_upsert.ok assert "Multivector 'dense-multi' has a limit of 5 vectors, but 6 were provided!" in failed_upsert.json()['status']['error'] # insert multivectors with 6 vectors (points batch) failed_upsert = request_with_validation( api='/collections/{collection_name}/points', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "batch": { "ids": [1], "vectors": { "dense-multi" : [ [ [1.05, 1.61, 1.76, 1.74], [2.05, 2.61, 2.76, 2.74], [3.05, 3.61, 3.76, 3.74], [3.05, 3.61, 3.76, 3.74], [3.05, 3.61, 3.76, 3.74], [3.05, 3.61, 3.76, 3.74], ] ] } } } ) assert not failed_upsert.ok assert "Multivector 'dense-multi' has a limit of 5 vectors, but 6 were provided!" in failed_upsert.json()['status']['error'] # disable strict mode set_strict_mode(collection_name, { "enabled": False, "multivector_config": { "dense-multi": { "max_vectors": 5, } } }) response = request_with_validation( api='/collections/{collection_name}/points', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "batch": { "ids": [1], "vectors": { "dense-multi" : [ [ [1.05, 1.61, 1.76, 1.74], [2.05, 2.61, 2.76, 2.74], [3.05, 3.61, 3.76, 3.74], [3.05, 3.61, 3.76, 3.74], [3.05, 3.61, 3.76, 3.74], [3.05, 3.61, 3.76, 3.74], ] ] } } } ) assert response.ok def test_strict_mode_read_rate_limiting(collection_name): set_strict_mode(collection_name, { "enabled": True, "read_rate_limit": 1, }) response = request_with_validation( api='/collections/{collection_name}', method="GET", path_params={'collection_name': collection_name}, ) assert response.ok new_strict_mode_config = response.json()['result']['config']['strict_mode_config'] assert new_strict_mode_config['enabled'] assert new_strict_mode_config['read_rate_limit'] == 1 failed_count = 0 for _ in range(10): response = request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": [0.2, 0.1, 0.9, 0.7], "limit": 4 } ) if not response.ok: failed_count += 1 assert response.status_code == 429 assert "Rate limiting exceeded: Read rate limit exceeded" in response.json()['status']['error'] assert response.headers['Retry-After'] is not None # need to wait about 60s for the single token available to be replenished assert 55 < int(response.headers['Retry-After']) <= 60 # loose check, as the rate limiting might not be exact assert failed_count > 5, "Rate limiting did not work" set_strict_mode(collection_name, { "enabled": False, }) for _ in range(10): response = request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": [0.2, 0.1, 0.9, 0.7], "limit": 4 } ) assert response.ok, "Rate limiting should be disabled now" def test_strict_mode_max_collection_payload_size_upsert(collection_name): basic_collection_setup(collection_name=collection_name, on_disk_payload=True) # Clear collection to not depend on other tests def upsert_points(ids: list[int]): length = len(ids) payloads = [{"city": "Berlin"} for _ in range(length)] vectors = [[1, 2, 3, 5] for _ in range(length)] return request_with_validation( api='/collections/{collection_name}/points', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "batch": { "ids": ids, "payloads": payloads, "vectors": vectors } } ) # Overwriting the same points to trigger cache refreshing for _ in range(32): upsert_points([1, 2, 3, 4, 5]).raise_for_status() set_strict_mode(collection_name, { "enabled": True, "max_collection_payload_size_bytes": 45000, }) for _ in range(32): upsert_points([6, 7, 8, 9, 10]).raise_for_status() # Max limit has been reached and one of the next requests must fail. Due to cache it might not be the first call! for i in range(32): failed_upsert = upsert_points([12, 13, 14, 15, 16]) if failed_upsert.ok: continue assert "Max payload storage size" in failed_upsert.json()['status']['error'] assert not failed_upsert.ok return assert False, "Upserting should have failed but didn't" def test_strict_mode_max_collection_payload_size_upsert_batch(collection_name): basic_collection_setup(collection_name=collection_name, on_disk_payload=True) # Clear collection to not depend on other tests def upsert_points(ids: list[int]): length = len(ids) payloads = [{"city": "Berlin"} for _ in range(length)] vectors = [[1, 2, 3, 5] for _ in range(length)] return request_with_validation( api='/collections/{collection_name}/points/batch', method="POST", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "operations": [ { "upsert": { "batch": { "ids": ids, "payloads": payloads, "vectors": vectors } } } ] } ) for _ in range(32): upsert_points([1, 2, 3, 4, 5]).raise_for_status() set_strict_mode(collection_name, { "enabled": True, "max_collection_payload_size_bytes": 45000, }) for i in range(32): upsert_points([6, 7, 8, 9, 10]).raise_for_status() # Max limit has been reached and one of the next requests must fail. Due to cache it might not be the first call! for i in range(32): failed_upsert = upsert_points([12, 13, 14, 15, 16]) if failed_upsert.ok: continue assert "Max payload storage size" in failed_upsert.json()['status']['error'] assert not failed_upsert.ok return assert False, "Upserting should have failed but didn't" def test_strict_mode_max_collection_point_count_upsert_batch(collection_name): basic_collection_setup(collection_name=collection_name, on_disk_payload=True) # Clear collection to not depend on other tests def upsert_points(ids: list[int]): length = len(ids) payloads = [{"city": "Berlin"} for _ in range(length)] vectors = [[1, 2, 3, 5] for _ in range(length)] return request_with_validation( api='/collections/{collection_name}/points/batch', method="POST", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "operations": [ { "upsert": { "batch": { "ids": ids, "payloads": payloads, "vectors": vectors } } } ] } ) for _ in range(32): upsert_points([1, 2]).raise_for_status() set_strict_mode(collection_name, { "enabled": True, "max_points_count": 10, }) for i in range(32): print(upsert_points([3, 4]).json()) # Max limit has been reached and one of the next requests must fail. Due to cache it might not be the first call! for i in range(32): failed_upsert = upsert_points([5, 6, 7, 8, 9, 10]) if failed_upsert.ok: continue assert "Max points count limit of 10 reached!" in failed_upsert.json()['status']['error'] assert not failed_upsert.ok return assert False, "Upserting should have failed but didn't" def test_strict_mode_write_rate_limiting(collection_name): set_strict_mode(collection_name, { "enabled": True, "write_rate_limit": 1, }) response = request_with_validation( api='/collections/{collection_name}', method="GET", path_params={'collection_name': collection_name}, ) assert response.ok new_strict_mode_config = response.json()['result']['config']['strict_mode_config'] assert new_strict_mode_config['enabled'] assert new_strict_mode_config['write_rate_limit'] == 1 failed_count = 0 for _ in range(10): response = request_with_validation( api='/collections/{collection_name}/points', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "points": [ { "id": 1, "vector": [0.05, 0.61, 0.76, 0.74], }, ] } ) if not response.ok: failed_count += 1 assert response.status_code == 429 assert "Rate limiting exceeded: Write rate limit exceeded" in response.json()['status']['error'] assert response.headers['Retry-After'] is not None # need to wait about 60s for the single token available to be replenished assert 55 < int(response.headers['Retry-After']) <= 60 # loose check, as the rate limiting might not be exact assert failed_count > 5, "Rate limiting did not work" # Disable rate limiting set_strict_mode(collection_name, { "enabled": False, }) for _ in range(10): response = request_with_validation( api='/collections/{collection_name}/points', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "points": [ { "id": 1, "vector": [0.05, 0.61, 0.76, 0.74], }, ] } ) assert response.ok, "Rate limiting should be disabled now" def test_strict_mode_write_rate_limiting_filtered_update_op(collection_name): set_strict_mode(collection_name, { "enabled": True, "write_rate_limit": 7, }) request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, body={ "field_name": "city", "field_schema": "keyword", } ).raise_for_status() # This will pass as we still have tokens in the rate limiter. Those will be used by this call. request_with_validation( api='/collections/{collection_name}/points/payload', method="PUT", path_params={'collection_name': collection_name}, body={ "payload": {"City": "Not Berlin"}, "filter": {"must": [{"key": "City", "match": {"value": "Berlin"}}]} } ).raise_for_status() # Not enough tokens left. response = request_with_validation( api='/collections/{collection_name}/points/payload', method="PUT", path_params={'collection_name': collection_name}, body={ "payload": {"City": "Not London"}, "filter": {"must": [{"key": "City", "match": {"value": "London"}}]} } ) assert response.status_code == 429 assert "Rate limiting exceeded: Write rate limit exceeded: Operation requires 5 tokens but only" in response.json()['status']['error'] def test_strict_mode_write_rate_limiting_batch_update_op(collection_name): def upsert_points(ids: list[int]): length = len(ids) payloads = [{} for _ in range(length)] vectors = [[1, 2, 3, 5] for _ in range(length)] return request_with_validation( api='/collections/{collection_name}/points/batch', method="POST", path_params={'collection_name': collection_name}, body={ "operations": [ { "upsert": { "batch": { "ids": ids, "payloads": payloads, "vectors": vectors } } } ] } ) set_strict_mode(collection_name, { "enabled": True, "write_rate_limit": 10, }) # validate that updates with 11 points will never be allowed response = upsert_points([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) assert response.status_code == 429 assert "Rate limiting exceeded: Write rate limit exceeded, request larger than rate limiter capacity, please try to split your request" in response.json()['status']['error'] # validate that updates with 10 points is allowed because there are enough tokens for each point upsert_points([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).raise_for_status() # doing it again fails because we already consumed 10 tokens response = upsert_points([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) assert response.status_code == 429 assert "Rate limiting exceeded: Write rate limit exceeded: Operation requires 10 tokens but only 0.0 were available. Retry after 60s" in response.json()['status']['error'] def test_filter_many_conditions(collection_name): def search_request(condition_count: int): conditions = [] for i in range(condition_count): conditions.append({ "key": "price", "match": { "value": i } }) return request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": [0.2, 0.1, 0.9, 0.7], "limit": 4, "filter": { "must": conditions }, } ) search_request(5).raise_for_status() set_strict_mode(collection_name, { "enabled": True, "filter_max_conditions": 5, }) search_request(5).raise_for_status() search_fail = search_request(6) assert "Filter" in search_fail.json()['status']['error'] assert "limit" in search_fail.json()['status']['error'] assert not search_fail.ok def test_filter_large_condition(collection_name): def search_request(condition_size: int): conditions = [x for x in range(condition_size)] return request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": [0.2, 0.1, 0.9, 0.7], "limit": 4, "filter": { "must": [ { "key": "price", "match": { "any": conditions } } ] }, } ) search_request(5).raise_for_status() set_strict_mode(collection_name, { "enabled": True, "condition_max_size": 5, }) search_request(5).raise_for_status() search_fail = search_request(6) assert "Condition" in search_fail.json()['status']['error'] assert "limit" in search_fail.json()['status']['error'] assert not search_fail.ok def test_filter_nested_condition(collection_name): def search_request(condition_size: int = 2): conditions = [x for x in range(condition_size)] return request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": [0.2, 0.1, 0.9, 0.7], "limit": 4, "filter": { "must": [ { "nested": { "key": "city", "filter": { "must": [ { "key": "key", "match": { "any": conditions, } }, { "key": "key2", "match": { "any": conditions, } } ] } } } ] }, } ) search_request(2).raise_for_status() set_strict_mode(collection_name, { "enabled": True, "condition_max_size": 2, }) search_request(2).raise_for_status() search_fail = search_request(3) assert "Condition" in search_fail.json()['status']['error'] assert "limit" in search_fail.json()['status']['error'] assert not search_fail.ok set_strict_mode(collection_name, { "enabled": True, "condition_max_size": 1000000, # Disabled "filter_max_conditions": 3, }) search_request().raise_for_status() set_strict_mode(collection_name, { "enabled": True, "condition_max_size": 1000000, # Disabled "filter_max_conditions": 1, }) search_fail = search_request() assert "condition" in search_fail.json()['status']['error'] assert "limit" in search_fail.json()['status']['error'] assert not search_fail.ok def test_strict_mode_formula_expression(collection_name): def query_request(): expression = { "sum": [ "discount_price", "$score", ] } return request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "prefetch": { "query": [0.1, 0.2, 0.3, 0.4], }, "query": { "formula": expression, "defaults": { "discount_price": 0 } # Even with default, it should still be restricted } } ) # No restriction, query succeeds query_ok = query_request() assert query_ok.ok set_strict_mode(collection_name, { "enabled": True, "unindexed_filtering_retrieve": False, }) # Now it should fail query_fail = query_request() assert not query_fail.ok assert "discount_price" in query_fail.json()['status']['error'] assert "formula expression" in query_fail.json()['status']['error'] # Create index on `discount_price` request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "field_name": "discount_price", "field_schema": { "type": "integer", "lookup": True, "range": False, } } ).raise_for_status() # Query succeeds with the index query_ok = query_request() assert query_ok.ok def test_strict_mode_read_rate_limiting_small_replenish(collection_name): """ If our read rate limit capacity is larger, test that when exhausting it we're only instructed to wait one more second rather than a full minute. """ set_strict_mode(collection_name, { "enabled": True, "read_rate_limit": 60, }) response = request_with_validation( api='/collections/{collection_name}', method="GET", path_params={'collection_name': collection_name}, ) assert response.ok new_strict_mode_config = response.json()['result']['config']['strict_mode_config'] assert new_strict_mode_config['enabled'] assert new_strict_mode_config['read_rate_limit'] == 60 for _ in range(120): response = request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": [0.2, 0.1, 0.9, 0.7], "limit": 4 } ) if not response.ok: assert response.status_code == 429 assert "Rate limiting exceeded: Read rate limit exceeded" in response.json()['status']['error'] assert response.headers['Retry-After'] is not None # need to wait about a second for one out of 100 tokens to be replenished assert 1 <= int(response.headers['Retry-After']) <= 5 return assert False, "rate limiter was never triggered" def test_strict_mode_unset_rate_limiting_config(collection_name): # set write rate limit set_strict_mode(collection_name, { "enabled": True, "write_rate_limit": 1, }) response = request_with_validation( api='/collections/{collection_name}', method="GET", path_params={'collection_name': collection_name}, ) assert response.ok new_strict_mode_config = response.json()['result']['config']['strict_mode_config'] assert new_strict_mode_config['enabled'] assert new_strict_mode_config['write_rate_limit'] == 1 assert 'read_rate_limit' not in new_strict_mode_config # set read rate limit on top set_strict_mode(collection_name, { "read_rate_limit": 2, }) response = request_with_validation( api='/collections/{collection_name}', method="GET", path_params={'collection_name': collection_name}, ) assert response.ok new_strict_mode_config = response.json()['result']['config']['strict_mode_config'] assert new_strict_mode_config['enabled'] assert new_strict_mode_config['write_rate_limit'] == 1 assert new_strict_mode_config['read_rate_limit'] == 2 # disable only write rate limit on top set_strict_mode(collection_name, { "write_rate_limit": None, }) response = request_with_validation( api='/collections/{collection_name}', method="GET", path_params={'collection_name': collection_name}, ) assert response.ok new_strict_mode_config = response.json()['result']['config']['strict_mode_config'] assert new_strict_mode_config['enabled'] assert new_strict_mode_config['read_rate_limit'] == 2 # assert write rate limit is not unset because it is currently not supported assert new_strict_mode_config['write_rate_limit'] == 1 # Test that examples in recommendations are tracked by rate limiter def test_strict_mode_recommendation_best_score_read_rate_limiting(collection_name): response = request_with_validation( api="/collections/{collection_name}/points/query", method="POST", path_params={"collection_name": collection_name}, body={ "query": { "recommend": { "positive": [1, 2, 3, 4, 5], "strategy": "best_score", }, }, "limit": 10, }, ) assert response.ok, response.text # set read rate limit set_strict_mode(collection_name, { "enabled": True, "read_rate_limit": 4, }) response = request_with_validation( api='/collections/{collection_name}', method="GET", path_params={'collection_name': collection_name}, ) assert response.ok new_strict_mode_config = response.json()['result']['config']['strict_mode_config'] assert new_strict_mode_config['enabled'] assert new_strict_mode_config['read_rate_limit'] == 4 # try max number of examples response = request_with_validation( api="/collections/{collection_name}/points/query", method="POST", path_params={"collection_name": collection_name}, body={ "query": { "recommend": { "positive": [1, 2, 3, 4, 5], "strategy": "best_score", }, }, "limit": 10, }, ) assert response.status_code == 429 assert "Read rate limit exceeded, request larger than rate limiter capacity, please try to split your request" in response.json()['status']['error'] set_strict_mode(collection_name, { "enabled": False, }) response = request_with_validation( api="/collections/{collection_name}/points/query", method="POST", path_params={"collection_name": collection_name}, body={ "query": { "recommend": { "positive": [1, 2, 3, 4, 5], "strategy": "best_score", }, }, "limit": 10, }, ) assert response.ok, response.text def test_strict_mode_retrieve_read_rate_limiting(collection_name): response = request_with_validation( api="/collections/{collection_name}/points", method="POST", path_params={"collection_name": collection_name}, body={ "ids": [1, 2, 3, 4, 5], }, ) assert response.ok, response.text # set read rate limit set_strict_mode(collection_name, { "enabled": True, "read_rate_limit": 4, }) response = request_with_validation( api='/collections/{collection_name}', method="GET", path_params={'collection_name': collection_name}, ) assert response.ok new_strict_mode_config = response.json()['result']['config']['strict_mode_config'] assert new_strict_mode_config['enabled'] assert new_strict_mode_config['read_rate_limit'] == 4 # try max number of ids response = request_with_validation( api="/collections/{collection_name}/points", method="POST", path_params={"collection_name": collection_name}, body={ "ids": [1, 2, 3, 4, 5], }, ) assert response.status_code == 429 assert "Read rate limit exceeded, request larger than rate limiter capacity, please try to split your request" in response.json()['status']['error'] # Check with less examples response = request_with_validation( api="/collections/{collection_name}/points", method="POST", path_params={"collection_name": collection_name}, body={ "ids": [1, 2, 3, 4], }, ) assert response.ok, response.text # Check if tokens are gone response = request_with_validation( api="/collections/{collection_name}/points", method="POST", path_params={"collection_name": collection_name}, body={ "ids": [1, 2, 3, 4], }, ) assert response.status_code == 429 assert "Read rate limit exceeded: Operation requires 4 tokens but only 0.0 were available" in response.json()['status']['error'] def test_scroll_filter_many_conditions(collection_name): def scroll_request(condition_count: int): conditions = [] for i in range(condition_count): conditions.append({ "key": "price", "match": { "value": i } }) return request_with_validation( api='/collections/{collection_name}/points/scroll', method="POST", path_params={'collection_name': collection_name}, body={ "filter": { "must": conditions }, } ) scroll_request(11).raise_for_status() set_strict_mode(collection_name, { "enabled": True, "read_rate_limit": 10, }) response = scroll_request(11) assert response.status_code == 429 assert "Read rate limit exceeded, request larger than rate limiter capacity, please try to split your request" in response.json()['status']['error'] # Less than 11 is fine scroll_request(6).raise_for_status() # Should fail because we already consumed 6 tokens response = scroll_request(6) # Operation requires 7 tokens (1 for the request and one per filter) assert response.status_code == 429 assert "Read rate limit exceeded: Operation requires 7 tokens" in response.json()['status']['error'] def test_strict_mode_group_limits(collection_name): response = request_with_validation( api="/collections/{collection_name}/points/query/groups", method="POST", path_params={"collection_name": collection_name}, body={ "query": [1.0, 0.0, 0.0, 0.0], "limit": 10, "with_payload": True, "group_by": "docId", "group_size": 3, }, ) assert response.ok response = request_with_validation( api="/collections/{collection_name}/points/query/groups", method="POST", path_params={"collection_name": collection_name}, body={ "query": [1.0, 0.0, 0.0, 0.0], "limit": 10, "with_payload": True, "group_by": "docId", "group_size": 3, }, ) assert response.ok set_strict_mode(collection_name, { "enabled": True, "max_query_limit": 15, }) # try again response = request_with_validation( api="/collections/{collection_name}/points/query/groups", method="POST", path_params={"collection_name": collection_name}, body={ "query": [1.0, 0.0, 0.0, 0.0], "limit": 10, "with_payload": True, "group_by": "docId", "group_size": 3, }, ) assert not response.ok assert "Limit exceeded 30 > 15 for \"limit\"" in response.json()['status']['error'] response = request_with_validation( api="/collections/{collection_name}/points/query/groups", method="POST", path_params={"collection_name": collection_name}, body={ "query": [1.0, 0.0, 0.0, 0.0], "limit": 10, "with_payload": True, "group_by": "docId", "group_size": 3, }, ) assert not response.ok assert "Limit exceeded 30 > 15 for \"limit\"" in response.json()['status']['error'] def test_strict_mode_distance_matrix_limits(collection_name): response = request_with_validation( api="/collections/{collection_name}/points/search/matrix/pairs", method="POST", path_params={"collection_name": collection_name}, body={ "sample": 10, "limit": 2, }, ) assert response.ok set_strict_mode(collection_name, { "enabled": True, "max_query_limit": 15, }) # try again response = request_with_validation( api="/collections/{collection_name}/points/search/matrix/pairs", method="POST", path_params={"collection_name": collection_name}, body={ "sample": 10, "limit": 2, }, ) assert not response.ok assert "Limit exceeded 20 > 15 for \"limit\"" in response.json()['status']['error'] def test_read_rate_limiter_many_vectors(full_collection_name): collection_name = full_collection_name def check_response(response: Response, should_succeed: bool): if should_succeed: assert response.ok, response.text else: assert response.status_code == 429 assert "request larger than rate limiter capacity" in response.json()['status']['error'] def check_multivector_query_raw(should_succeed: bool): # query api with vector multivector = [[0.1, 0.2, 0.3, 0.4] for _ in range(3)] search_response = request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": multivector, "using": "dense-multi", "limit": 5 } ) check_response(search_response, should_succeed) def check_multivector_query_id(should_succeed: bool): # query api with id search_response = request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": 2, # this point has a multivector of 3 vectors "using": "dense-multi", "limit": 5 } ) check_response(search_response, should_succeed) # check without strict mode check_multivector_query_raw(should_succeed=True) check_multivector_query_id(should_succeed=True) # Set strict mode with very low read_rate_limit, it should not succeed set_strict_mode(collection_name, { "enabled": True, "read_rate_limit": 2 # multivector has 3 vectors }) check_multivector_query_raw(should_succeed=False) # reset rate limiter for next request set_strict_mode(collection_name, { "enabled": True, "read_rate_limit": 2 # multivector has 3 vectors }) check_multivector_query_id(should_succeed=False) # Set strict mode with just enough read_rate_limit, it should succeed set_strict_mode(collection_name, { "enabled": True, "read_rate_limit": 3 # multivector has 3 vectors }) check_multivector_query_raw(should_succeed=True) # reset rate limiter for next request set_strict_mode(collection_name, { "enabled": True, "read_rate_limit": 5 # multivector has 3 vectors # + 1 of fetching the id # + 1 of the filter for not including the id }) check_multivector_query_id(should_succeed=True) def test_strict_mode_group_by_unindexed(collection_name): response = request_with_validation( api="/collections/{collection_name}/points/query/groups", method="POST", path_params={"collection_name": collection_name}, body={ "query": [1.0, 0.0, 0.0, 0.0], "limit": 10, "with_payload": True, "group_by": "docId", "group_size": 3, }, ) assert response.ok response = request_with_validation( api="/collections/{collection_name}/points/query/groups", method="POST", path_params={"collection_name": collection_name}, body={ "query": [1.0, 0.0, 0.0, 0.0], "limit": 10, "with_payload": True, "group_by": "docId", "group_size": 3, }, ) assert response.ok set_strict_mode(collection_name, { "enabled": True, "unindexed_filtering_retrieve": False, }) # try again response = request_with_validation( api="/collections/{collection_name}/points/query/groups", method="POST", path_params={"collection_name": collection_name}, body={ "query": [1.0, 0.0, 0.0, 0.0], "limit": 10, "with_payload": True, "group_by": "docId", "group_size": 3, }, ) assert not response.ok assert "Index required but not found for \"docId\". Help: Create an index supporting `match` for this key." in response.json()['status']['error'] response = request_with_validation( api="/collections/{collection_name}/points/query/groups", method="POST", path_params={"collection_name": collection_name}, body={ "query": [1.0, 0.0, 0.0, 0.0], "limit": 10, "with_payload": True, "group_by": "docId", "group_size": 3, }, ) assert not response.ok assert "Index required but not found for \"docId\". Help: Create an index supporting `match` for this key." in response.json()['status']['error'] # create geo index request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "field_name": "docId", "field_schema": "geo" } ).raise_for_status() # try again with geo index response = request_with_validation( api="/collections/{collection_name}/points/query/groups", method="POST", path_params={"collection_name": collection_name}, body={ "query": [1.0, 0.0, 0.0, 0.0], "limit": 10, "with_payload": True, "group_by": "docId", "group_size": 3, }, ) assert not response.ok assert "Index of type \"Geo\" found for \"docId\". Help: Create an index supporting `match` for this key." in response.json()['status']['error'] response = request_with_validation( api="/collections/{collection_name}/points/query/groups", method="POST", path_params={"collection_name": collection_name}, body={ "query": [1.0, 0.0, 0.0, 0.0], "limit": 10, "with_payload": True, "group_by": "docId", "group_size": 3, }, ) assert not response.ok assert "Index of type \"Geo\" found for \"docId\". Help: Create an index supporting `match` for this key." in response.json()['status']['error'] # create keyword index (supporting match) request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "field_name": "docId", "field_schema": "keyword" } ).raise_for_status() # now it is allowed response = request_with_validation( api="/collections/{collection_name}/points/query/groups", method="POST", path_params={"collection_name": collection_name}, body={ "query": [1.0, 0.0, 0.0, 0.0], "limit": 10, "with_payload": True, "group_by": "docId", "group_size": 3, }, ) assert response.ok response = request_with_validation( api="/collections/{collection_name}/points/query/groups", method="POST", path_params={"collection_name": collection_name}, body={ "query": [1.0, 0.0, 0.0, 0.0], "limit": 10, "with_payload": True, "group_by": "docId", "group_size": 3, }, ) assert response.ok def test_strict_mode_full_scan(full_collection_name): collection_name = full_collection_name # disable HNSW index response = request_with_validation( api='/collections/{collection_name}', method="PATCH", path_params={'collection_name': collection_name}, body={ "vectors": { "dense-multi": { "hnsw_config": { "m": 0, }, }, } } ) assert response.ok # full scan allowed response = request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": 2, "using": "dense-multi", "limit": 5 } ) assert response.ok # enable strict mode with search_allow_exact set_strict_mode(collection_name, { "enabled": True, "search_allow_exact": False }) # full scan not allowed response = request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": 2, "using": "dense-multi", "limit": 5 } ) assert not response.ok assert "Request is forbidden on 'dense-multi' because global vector indexing is disabled (hnsw_config.m = 0). Help: Use tenant-specific filter, enable global vector indexing or enable strict mode `search_allow_exact` option" in response.json()['status']['error'] # sparse vector still works response = request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": 2, "using": "sparse-text", "limit": 5 } ) assert response.ok # Disabled HNSW is Ok for rescoring response = request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "prefetch": [ { "query": 2, "using": "sparse-text", "limit": 50 } ], "query": 2, "using": "dense-multi", "limit": 5 } ) assert response.ok # Disabled HNSW forbidden prefetch response = request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "prefetch": [ { "query": 2, "using": "dense-multi", "limit": 50 } ], "query": 2, "using": "sparse-text", "limit": 5 } ) assert not response.ok assert "Request is forbidden on 'dense-multi' because global vector indexing is disabled (hnsw_config.m = 0). Help: Use tenant-specific filter, enable global vector indexing or enable strict mode `search_allow_exact` option" in response.json()['status']['error'] def test_strict_mode_full_scan_simple(full_collection_name): collection_name = full_collection_name # Enable strict mode with search_allow_exact response = request_with_validation( api='/collections/{collection_name}', method="PATCH", path_params={'collection_name': collection_name}, body={ "strict_mode_config": { "enabled": True, "search_allow_exact": False }, } ) assert response.ok # full scan allowed response = request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": 2, "using": "dense-text", "limit": 5 } ) assert response.ok # disable HNSW index response = request_with_validation( api='/collections/{collection_name}', method="PATCH", path_params={'collection_name': collection_name}, body={ "hnsw_config": { "m": 0 } } ) assert response.ok # full scan not allowed response = request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": 2, "using": "dense-text", "limit": 5 } ) assert not response.ok assert response.status_code == 400 def test_strict_mode_multitenant_full_scan(full_collection_name): collection_name = full_collection_name def filtered_query(): return request_with_validation( api='/collections/{collection_name}/points/query', method="POST", path_params={'collection_name': collection_name}, body={ "query": 2, "filter": { "must": [ { "key": "city", "match": { "value": "Berlin" } } ] }, "using": "dense-multi", "limit": 5 } ) # disable HNSW index response = request_with_validation( api='/collections/{collection_name}', method="PATCH", path_params={'collection_name': collection_name}, body={ "vectors": { "dense-multi": { "hnsw_config": { "m": 0, "payload_m": 0 }, }, } } ) assert response.ok # filtered search allowed filtered_query().raise_for_status() # enable strict mode with search_allow_exact set_strict_mode(collection_name, { "enabled": True, "search_allow_exact": False }) # filtered search not allowed anymore because no HNSW index response = filtered_query() assert not response.ok assert "Request is forbidden on 'dense-multi'" in response.json()['status']['error'] # add payload index request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "field_name": "city", "field_schema": "keyword" } ).raise_for_status() # still not allowed although we have payload index for the filter response = filtered_query() assert not response.ok assert "Request is forbidden on 'dense-multi'" in response.json()['status']['error'] # add multitenant payload index request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "field_name": "city", "field_schema": { "type": "keyword", "is_tenant": True, } } ).raise_for_status() # still not allowed although we have a multitenant payload index for the filter response = filtered_query() assert not response.ok assert "Request is forbidden on 'dense-multi'" in response.json()['status']['error'] # enabled HNSW payload based index response = request_with_validation( api='/collections/{collection_name}', method="PATCH", path_params={'collection_name': collection_name}, body={ "vectors": { "dense-multi": { "hnsw_config": { "m": 0, "payload_m": 1 }, }, } } ) assert response.ok # finally allowed filtered_query().raise_for_status() def test_strict_mode_payload_index_count(collection_name): # test blocking access to payload indexes set_strict_mode(collection_name, { "enabled": True, "max_payload_index_count": 0, }) response = request_with_validation( api='/collections/{collection_name}', method="GET", path_params={'collection_name': collection_name}, ) assert response.ok new_strict_mode_config = response.json()['result']['config']['strict_mode_config'] assert new_strict_mode_config['enabled'] assert new_strict_mode_config['max_payload_index_count'] == 0 # should fail because zero payload indexes allowed response = request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "field_name": "price", "field_schema": { "type": "float", } } ) assert not response.ok assert response.status_code == 400 assert "Collection already has the maximum number of payload indices (0). Help: Please delete an existing index before creating a new one." in response.json()['status']['error'] set_strict_mode(collection_name, { "enabled": True, "max_payload_index_count": 1, }) response = request_with_validation( api='/collections/{collection_name}', method="GET", path_params={'collection_name': collection_name}, ) assert response.ok new_strict_mode_config = response.json()['result']['config']['strict_mode_config'] assert new_strict_mode_config['enabled'] assert new_strict_mode_config['max_payload_index_count'] == 1 # let's create one index, should work request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "field_name": "city", "field_schema": { "type": "keyword", } } ).raise_for_status() # should fail now with 1 index already response = request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "field_name": "price", "field_schema": { "type": "float", } } ) assert not response.ok assert response.status_code == 400 assert "Collection already has the maximum number of payload indices (1). Help: Please delete an existing index before creating a new one." in response.json()['status']['error'] # let's increase the limit by one set_strict_mode(collection_name, { "enabled": True, "max_payload_index_count": 2, }) # verify config reflects the new limit response = request_with_validation( api='/collections/{collection_name}', method="GET", path_params={'collection_name': collection_name}, ) assert response.ok cfg = response.json()['result']['config']['strict_mode_config'] assert cfg['max_payload_index_count'] == 2 # should work now with 2 indices allowed request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "field_name": "price", "field_schema": { "type": "float", } } ).raise_for_status() # should fail now with 2 indices already response = request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "field_name": "count", "field_schema": { "type": "integer", } } ) assert not response.ok assert response.status_code == 400 assert "Collection already has the maximum number of payload indices (2). Help: Please delete an existing index before creating a new one." in response.json()['status']['error'] set_strict_mode(collection_name, { "enabled": False, }) assert not strict_mode_enabled(collection_name) # should work now without strict mode request_with_validation( api='/collections/{collection_name}/index', method="PUT", path_params={'collection_name': collection_name}, query_params={'wait': 'true'}, body={ "field_name": "count", "field_schema": { "type": "integer", } } ).raise_for_status()