Skip to content

Commit d328f96

Browse files
generalltimvisee
andcommitted
Fix for updating vector-specific config params (qdrant#2328)
* unit-test for updating multivec config * Remove single vector support from vector params update in REST API With this change, a user is always required to specify vector names when updating their parameters. Updating vector parameters in a collection with a single vector is still possible by providing an empty name. This is very reasonable as updating vector parameters on a single vector collection is a bad pattern, since parameters can be set on the collection itself. * Update integration tests to reflect REST API change * Update OpenAPI specification * Remove obsolete VectorsConfigDiff functions, update docs * Update OpenAPI specification * Add name validation to update collection endpoint for vector params --------- Co-authored-by: timvisee <[email protected]>
1 parent 0775e26 commit d328f96

File tree

7 files changed

+207
-64
lines changed

7 files changed

+207
-64
lines changed

docs/redoc/master/openapi.json

+5-12
Original file line numberDiff line numberDiff line change
@@ -5916,18 +5916,11 @@
59165916
}
59175917
},
59185918
"VectorsConfigDiff": {
5919-
"description": "Vector update params separator for single and multiple vector modes\n\nSingle mode:\n\n{ \"hnsw_config\": { \"m\": 8 } }\n\nor multiple mode:\n\n{ \"default\": { \"hnsw_config\": { \"m\": 8 } } }",
5920-
"anyOf": [
5921-
{
5922-
"$ref": "#/components/schemas/VectorParamsDiff"
5923-
},
5924-
{
5925-
"type": "object",
5926-
"additionalProperties": {
5927-
"$ref": "#/components/schemas/VectorParamsDiff"
5928-
}
5929-
}
5930-
]
5919+
"description": "Vector update params for multiple vectors\n\n{ \"vector_name\": { \"hnsw_config\": { \"m\": 8 } } }",
5920+
"type": "object",
5921+
"additionalProperties": {
5922+
"$ref": "#/components/schemas/VectorParamsDiff"
5923+
}
59315924
},
59325925
"VectorParamsDiff": {
59335926
"type": "object",

lib/collection/src/collection.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,7 @@ impl Collection {
11401140
update_vectors_diff: &VectorsConfigDiff,
11411141
) -> CollectionResult<()> {
11421142
let mut config = self.collection_config.write().await;
1143+
update_vectors_diff.check_vector_names(&config.params)?;
11431144
config
11441145
.params
11451146
.update_vectors_from_diff(update_vectors_diff)?;

lib/collection/src/config.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ impl CollectionParams {
183183
&mut self,
184184
update_vectors_diff: &VectorsConfigDiff,
185185
) -> CollectionResult<()> {
186-
for (vector_name, update_params) in update_vectors_diff.params_iter() {
186+
for (vector_name, update_params) in update_vectors_diff.0.iter() {
187187
let vector_params = self.get_vector_params_mut(vector_name)?;
188188

189189
// Update and replace vector params from vector specific diff

lib/collection/src/operations/conversions.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -362,14 +362,15 @@ impl TryFrom<api::grpc::qdrant::vectors_config_diff::Config> for VectorsConfigDi
362362
) -> Result<Self, Self::Error> {
363363
Ok(match value {
364364
api::grpc::qdrant::vectors_config_diff::Config::Params(vector_params) => {
365-
VectorsConfigDiff::Single(vector_params.try_into()?)
365+
let diff: VectorParamsDiff = vector_params.try_into()?;
366+
VectorsConfigDiff::from(diff)
366367
}
367368
api::grpc::qdrant::vectors_config_diff::Config::ParamsMap(vectors_params) => {
368369
let mut params_map = BTreeMap::new();
369370
for (name, params) in vectors_params.map {
370371
params_map.insert(name, params.try_into()?);
371372
}
372-
VectorsConfigDiff::Multi(params_map)
373+
VectorsConfigDiff(params_map)
373374
}
374375
})
375376
}

lib/collection/src/operations/types.rs

+26-41
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ use tonic::codegen::http::uri::InvalidUri;
3232
use validator::{Validate, ValidationErrors};
3333

3434
use super::config_diff;
35-
use crate::config::CollectionConfig;
35+
use crate::config::{CollectionConfig, CollectionParams};
3636
use crate::lookup::types::WithLookupInterface;
3737
use crate::operations::config_diff::HnswConfigDiff;
3838
use crate::save_on_disk;
@@ -995,66 +995,51 @@ pub struct VectorParamsDiff {
995995
pub hnsw_config: Option<HnswConfigDiff>,
996996
}
997997

998-
/// Vector update params separator for single and multiple vector modes
999-
///
1000-
/// Single mode:
1001-
///
1002-
/// { "hnsw_config": { "m": 8 } }
1003-
///
1004-
/// or multiple mode:
998+
/// Vector update params for multiple vectors
1005999
///
10061000
/// {
1007-
/// "default": {
1001+
/// "vector_name": {
10081002
/// "hnsw_config": { "m": 8 }
10091003
/// }
10101004
/// }
10111005
#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone, PartialEq, Hash, Eq)]
1012-
#[serde(rename_all = "snake_case", untagged)]
1013-
pub enum VectorsConfigDiff {
1014-
Single(VectorParamsDiff),
1015-
Multi(BTreeMap<String, VectorParamsDiff>),
1016-
}
1006+
pub struct VectorsConfigDiff(pub BTreeMap<String, VectorParamsDiff>);
10171007

10181008
impl VectorsConfigDiff {
1019-
pub fn get_params(&self, name: &str) -> Option<&VectorParamsDiff> {
1020-
match self {
1021-
VectorsConfigDiff::Single(params) => (name == DEFAULT_VECTOR_NAME).then_some(params),
1022-
VectorsConfigDiff::Multi(params) => params.get(name),
1023-
}
1024-
}
1025-
1026-
/// Iterate over the named vector parameters.
1009+
/// Check that the vector names in this config are part of the given collection.
10271010
///
1028-
/// If this is `Single` it iterates over a single parameter named [`DEFAULT_VECTOR_NAME`].
1029-
pub fn params_iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&str, &VectorParamsDiff)> + 'a> {
1030-
match self {
1031-
VectorsConfigDiff::Single(p) => Box::new(std::iter::once((DEFAULT_VECTOR_NAME, p))),
1032-
VectorsConfigDiff::Multi(p) => Box::new(p.iter().map(|(n, p)| (n.as_str(), p))),
1011+
/// Returns an error if incompatible.
1012+
pub fn check_vector_names(&self, collection: &CollectionParams) -> CollectionResult<()> {
1013+
for vector_name in self.0.keys() {
1014+
collection
1015+
.vectors
1016+
.get_params(vector_name)
1017+
.map(|_| ())
1018+
.ok_or_else(|| OperationError::VectorNameNotExists {
1019+
received_name: vector_name.into(),
1020+
})?;
10331021
}
1022+
Ok(())
10341023
}
10351024
}
10361025

10371026
impl Validate for VectorsConfigDiff {
10381027
fn validate(&self) -> Result<(), ValidationErrors> {
1039-
match self {
1040-
VectorsConfigDiff::Single(single) => single.validate(),
1041-
VectorsConfigDiff::Multi(multi) => {
1042-
let errors = multi
1043-
.values()
1044-
.filter_map(|v| v.validate().err())
1045-
.fold(Err(ValidationErrors::new()), |bag, err| {
1046-
ValidationErrors::merge(bag, "?", Err(err))
1047-
})
1048-
.unwrap_err();
1049-
errors.errors().is_empty().then_some(()).ok_or(errors)
1050-
}
1051-
}
1028+
let errors = self
1029+
.0
1030+
.values()
1031+
.filter_map(|v| v.validate().err())
1032+
.fold(Err(ValidationErrors::new()), |bag, err| {
1033+
ValidationErrors::merge(bag, "?", Err(err))
1034+
})
1035+
.unwrap_err();
1036+
errors.errors().is_empty().then_some(()).ok_or(errors)
10521037
}
10531038
}
10541039

10551040
impl From<VectorParamsDiff> for VectorsConfigDiff {
10561041
fn from(params: VectorParamsDiff) -> Self {
1057-
VectorsConfigDiff::Single(params)
1042+
VectorsConfigDiff(BTreeMap::from([("".into(), params)]))
10581043
}
10591044
}
10601045

openapi/tests/openapi_integration/test_collection_update.py

+15-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from .helpers.helpers import request_with_validation
44
from .helpers.collection_setup import basic_collection_setup, drop_collection
55

6+
default_name = ""
67
collection_name = 'test_collection_uuid'
78

89

@@ -20,9 +21,11 @@ def test_collection_update():
2021
path_params={'collection_name': collection_name},
2122
body={
2223
"vectors": {
23-
"hnsw_config": {
24-
"m": 32,
25-
"ef_construct": 123,
24+
default_name: {
25+
"hnsw_config": {
26+
"m": 32,
27+
"ef_construct": 123,
28+
},
2629
},
2730
},
2831
"optimizers_config": {
@@ -77,8 +80,10 @@ def test_hnsw_update():
7780
path_params={'collection_name': collection_name},
7881
body={
7982
"vectors": {
80-
"hnsw_config": {
81-
"m": 32,
83+
default_name: {
84+
"hnsw_config": {
85+
"m": 32,
86+
},
8287
},
8388
},
8489
"hnsw_config": {
@@ -106,9 +111,11 @@ def test_hnsw_update():
106111
path_params={'collection_name': collection_name},
107112
body={
108113
"vectors": {
109-
"hnsw_config": {
110-
"m": 10,
111-
"ef_construct": 100,
114+
default_name: {
115+
"hnsw_config": {
116+
"m": 10,
117+
"ef_construct": 100,
118+
},
112119
},
113120
},
114121
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import pytest
2+
3+
from .helpers.helpers import request_with_validation
4+
from .helpers.collection_setup import drop_collection, multivec_collection_setup
5+
6+
collection_name = 'test_collection_update_multivec'
7+
8+
9+
@pytest.fixture(autouse=True)
10+
def setup():
11+
multivec_collection_setup(collection_name=collection_name)
12+
yield
13+
drop_collection(collection_name=collection_name)
14+
15+
16+
def test_collection_update_multivec():
17+
response = request_with_validation(
18+
api='/collections/{collection_name}',
19+
method="PATCH",
20+
path_params={'collection_name': collection_name},
21+
body={
22+
"vectors": {
23+
"image": {
24+
"hnsw_config": {
25+
"m": 32,
26+
"ef_construct": 123,
27+
}
28+
}
29+
},
30+
"optimizers_config": {
31+
"default_segment_number": 6,
32+
"indexing_threshold": 10000,
33+
},
34+
}
35+
)
36+
assert response.ok
37+
38+
response = request_with_validation(
39+
api='/collections/{collection_name}/points',
40+
method="PUT",
41+
path_params={'collection_name': collection_name},
42+
query_params={'wait': 'true'},
43+
body={
44+
"points": [
45+
{
46+
"id": 7,
47+
"vector": {
48+
"image": [0.15, 0.31, 0.76, 0.74]
49+
},
50+
"payload": {"city": "Rome"}
51+
}
52+
]
53+
}
54+
)
55+
assert response.ok
56+
57+
response = request_with_validation(
58+
api='/collections/{collection_name}/points/{id}',
59+
method="GET",
60+
path_params={'collection_name': collection_name, 'id': 7},
61+
)
62+
assert response.ok
63+
64+
65+
def test_hnsw_update():
66+
response = request_with_validation(
67+
api='/collections/{collection_name}',
68+
method="GET",
69+
path_params={'collection_name': collection_name},
70+
)
71+
assert response.ok
72+
result = response.json()["result"]
73+
config = result["config"]
74+
assert "hnsw_config" not in config["params"]["vectors"]
75+
assert config["hnsw_config"]["m"] == 16
76+
assert config["hnsw_config"]["ef_construct"] == 100
77+
78+
response = request_with_validation(
79+
api='/collections/{collection_name}',
80+
method="PATCH",
81+
path_params={'collection_name': collection_name},
82+
body={
83+
"vectors": {
84+
"text": {
85+
"hnsw_config": {
86+
"m": 32,
87+
}
88+
}
89+
},
90+
"hnsw_config": {
91+
"ef_construct": 123,
92+
},
93+
}
94+
)
95+
assert response.ok
96+
97+
response = request_with_validation(
98+
api='/collections/{collection_name}',
99+
method="GET",
100+
path_params={'collection_name': collection_name},
101+
)
102+
assert response.ok
103+
result = response.json()["result"]
104+
config = result["config"]
105+
assert config["params"]["vectors"]["text"]["hnsw_config"]["m"] == 32
106+
assert config["hnsw_config"]["m"] == 16
107+
assert config["hnsw_config"]["ef_construct"] == 123
108+
109+
response = request_with_validation(
110+
api='/collections/{collection_name}',
111+
method="PATCH",
112+
path_params={'collection_name': collection_name},
113+
body={
114+
"vectors": {
115+
"text": {
116+
"hnsw_config": {
117+
"m": 10,
118+
"ef_construct": 100,
119+
}
120+
}
121+
},
122+
}
123+
)
124+
assert response.ok
125+
126+
response = request_with_validation(
127+
api='/collections/{collection_name}',
128+
method="GET",
129+
path_params={'collection_name': collection_name},
130+
)
131+
assert response.ok
132+
result = response.json()["result"]
133+
config = result["config"]
134+
assert config["params"]["vectors"]["text"]["hnsw_config"]["m"] == 10
135+
assert config["params"]["vectors"]["text"]["hnsw_config"]["ef_construct"] == 100
136+
137+
138+
def test_invalid_vector_name():
139+
response = request_with_validation(
140+
api='/collections/{collection_name}',
141+
method="PATCH",
142+
path_params={'collection_name': collection_name},
143+
body={
144+
"vectors": {
145+
"i_do_no_exist": {
146+
"hnsw_config": {
147+
"m": 32,
148+
}
149+
}
150+
},
151+
}
152+
)
153+
assert not response.ok
154+
assert response.status_code == 400
155+
error = response.json()["status"]["error"]
156+
assert error == "Wrong input: Not existing vector name error: i_do_no_exist"

0 commit comments

Comments
 (0)