Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions test/collection/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3529,3 +3529,98 @@ def test_reconfigure_inverted_index_without_stopword_presets_leaves_existing(sel
}
merged = rc.merge_with_existing(existing)
assert merged["stopwordPresets"] == {"fr": ["le", "la"]}


class TestSharedVectorIndexConfig:
"""A config object reused for several vectors must not be shared or mutated."""

def test_quantizer_does_not_leak_between_vectors(self) -> None:
shared = Configure.VectorIndex.hnsw(ef_construction=128)
config = _CollectionConfigCreate(
name="test",
vector_config=[
Configure.Vectors.self_provided(name="plain", vector_index_config=shared),
Configure.Vectors.self_provided(
name="quantized",
vector_index_config=shared,
quantizer=Configure.VectorIndex.Quantizer.pq(),
),
],
)

vectors = config._to_dict()["vectorConfig"]
assert vectors["plain"]["vectorIndexConfig"] == {"efConstruction": 128}
assert vectors["quantized"]["vectorIndexConfig"] == {
"efConstruction": 128,
"pq": {"enabled": True, "encoder": {}},
}
assert shared._to_dict() == {"efConstruction": 128}, "the caller's object was mutated"

def test_quantizer_does_not_leak_between_vectors_with_dynamic_index(self) -> None:
shared = Configure.VectorIndex.dynamic()
config = _CollectionConfigCreate(
name="test",
vector_config=[
Configure.Vectors.self_provided(name="plain", vector_index_config=shared),
Configure.Vectors.self_provided(
name="quantized",
vector_index_config=shared,
quantizer=Configure.VectorIndex.Quantizer.bq(),
),
],
)

vectors = config._to_dict()["vectorConfig"]
assert vectors["plain"]["vectorIndexConfig"] == {}
assert vectors["quantized"]["vectorIndexConfig"] == {
"hnsw": {"bq": {"enabled": True}},
"flat": {"bq": {"enabled": True}},
}
assert shared._to_dict() == {}, "the caller's object was mutated"

def test_quantizer_does_not_leak_between_multi_vectors(self) -> None:
shared = Configure.VectorIndex.hnsw(ef_construction=128)
config = _CollectionConfigCreate(
name="test",
vector_config=[
Configure.MultiVectors.self_provided(name="plain", vector_index_config=shared),
Configure.MultiVectors.self_provided(
name="quantized",
vector_index_config=shared,
quantizer=Configure.VectorIndex.Quantizer.pq(),
),
],
)

vectors = config._to_dict()["vectorConfig"]
assert vectors["plain"]["vectorIndexConfig"] == {
"efConstruction": 128,
"multivector": {"enabled": True},
}
assert vectors["quantized"]["vectorIndexConfig"] == {
"efConstruction": 128,
"multivector": {"enabled": True},
"pq": {"enabled": True, "encoder": {}},
}
assert shared._to_dict() == {"efConstruction": 128}, "the caller's object was mutated"

def test_encoding_does_not_leak_between_multi_vectors(self) -> None:
shared = Configure.VectorIndex.MultiVector.multi_vector()
config = _CollectionConfigCreate(
name="test",
vector_config=[
Configure.MultiVectors.self_provided(name="plain", multi_vector_config=shared),
Configure.MultiVectors.self_provided(
name="muvera",
multi_vector_config=shared,
encoding=Configure.VectorIndex.MultiVector.Encoding.muvera(),
),
],
)

vectors = config._to_dict()["vectorConfig"]
assert vectors["plain"]["vectorIndexConfig"] == {"multivector": {"enabled": True}}
assert vectors["muvera"]["vectorIndexConfig"] == {
"multivector": {"enabled": True, "muvera": {"enabled": True}}
}
assert shared.encoding is None, "the caller's object was mutated"
8 changes: 8 additions & 0 deletions weaviate/collections/classes/config_vectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ def single(
vector_index_config: Optional[VectorIndexConfigCreate],
quantizer: Optional[_QuantizerConfigCreate],
) -> Optional[VectorIndexConfigCreate]:
# copy so that a config object reused for several vectors is never mutated in place
if vector_index_config is not None:
vector_index_config = vector_index_config.model_copy(deep=True)
if quantizer is not None:
if vector_index_config is None:
vector_index_config = _IndexWrappers.__hnsw(quantizer=quantizer)
Expand All @@ -182,8 +185,13 @@ def multi(
multi_vector_config: Optional[_MultiVectorConfigCreate],
encoding: Optional[_MultiVectorEncodingConfigCreate],
) -> Optional[VectorIndexConfigCreate]:
# copy so that config objects reused for several vectors are never mutated in place
if vector_index_config is not None:
vector_index_config = vector_index_config.model_copy(deep=True)
if multi_vector_config is None:
multi_vector_config = _MultiVectorConfigCreate(aggregation=None, encoding=None)
else:
multi_vector_config = multi_vector_config.model_copy(deep=True)
if encoding is not None:
multi_vector_config.encoding = encoding
if vector_index_config is None:
Expand Down
Loading