mirror of
https://github.com/qdrant/qdrant.git
synced 2026-07-28 05:31:06 -05:00
* feat: add search_max_batchsize to strict mode config * added test case for search_max_batchsize * Changes for fixing CI issue dure openapi * Modify check_strict_mode_batch --------- Co-authored-by: Arnaud Gourlay <arnaud.gourlay@gmail.com>
2603 lines
81 KiB
Python
2603 lines
81 KiB
Python
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/search',
|
|
method="POST",
|
|
path_params={'collection_name': collection_name},
|
|
body={
|
|
"vector": [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/search',
|
|
method="POST",
|
|
path_params={'collection_name': collection_name},
|
|
query_params={'timeout': timeout},
|
|
body={
|
|
"vector": [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/search',
|
|
method="POST",
|
|
path_params={'collection_name': collection_name},
|
|
body={
|
|
"vector": [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/search',
|
|
method="POST",
|
|
path_params={'collection_name': collection_name},
|
|
body={
|
|
"vector": [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/search',
|
|
method="POST",
|
|
path_params={'collection_name': collection_name},
|
|
body={
|
|
"vector": [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/search',
|
|
method="POST",
|
|
path_params={'collection_name': collection_name},
|
|
body={
|
|
"vector": [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/search',
|
|
method="POST",
|
|
path_params={'collection_name': collection_name},
|
|
body={
|
|
"vector": [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/search/batch',
|
|
method="POST",
|
|
path_params={'collection_name': collection_name},
|
|
body={
|
|
"searches": [
|
|
{
|
|
"vector": [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},
|
|
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},
|
|
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/search',
|
|
method="POST",
|
|
path_params={'collection_name': collection_name},
|
|
body={
|
|
"vector": [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/search',
|
|
method="POST",
|
|
path_params={'collection_name': collection_name},
|
|
body={
|
|
"vector": [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},
|
|
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},
|
|
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/search',
|
|
method="POST",
|
|
path_params={'collection_name': collection_name},
|
|
body={
|
|
"vector": [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/search',
|
|
method="POST",
|
|
path_params={'collection_name': collection_name},
|
|
body={
|
|
"vector": [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/search',
|
|
method="POST",
|
|
path_params={'collection_name': collection_name},
|
|
body={
|
|
"vector": [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/search',
|
|
method="POST",
|
|
path_params={'collection_name': collection_name},
|
|
body={
|
|
"vector": [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/recommend",
|
|
method="POST",
|
|
path_params={"collection_name": collection_name},
|
|
body={
|
|
"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/recommend",
|
|
method="POST",
|
|
path_params={"collection_name": collection_name},
|
|
body={
|
|
"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/recommend",
|
|
method="POST",
|
|
path_params={"collection_name": collection_name},
|
|
body={
|
|
"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/search/groups",
|
|
method="POST",
|
|
path_params={"collection_name": collection_name},
|
|
body={
|
|
"vector": [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/search/groups",
|
|
method="POST",
|
|
path_params={"collection_name": collection_name},
|
|
body={
|
|
"vector": [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/search/groups",
|
|
method="POST",
|
|
path_params={"collection_name": collection_name},
|
|
body={
|
|
"vector": [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/search/groups",
|
|
method="POST",
|
|
path_params={"collection_name": collection_name},
|
|
body={
|
|
"vector": [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/search/groups",
|
|
method="POST",
|
|
path_params={"collection_name": collection_name},
|
|
body={
|
|
"vector": [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/search/groups",
|
|
method="POST",
|
|
path_params={"collection_name": collection_name},
|
|
body={
|
|
"vector": [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() |