mirror of
https://github.com/qdrant/qdrant.git
synced 2026-07-26 04:31:02 -05:00
These tests rely on the collection size stats cache being refreshed to detect that a size limit has been exceeded. Without wait=true, upsert operations are written to WAL and acknowledged immediately without being applied to segments. When the cache refreshes, it reads segment data which may not yet reflect the pending WAL operations, causing the size check to see stale values and not reject the request. Adding wait=true ensures operations are applied to segments before the response returns, so the cache refresh sees the correct sizes. Co-authored-by: Cursor Agent <agent@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
2607 lines
81 KiB
Python
2607 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},
|
|
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/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},
|
|
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/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() |