Skip to content

Commit 490d81b

Browse files
authored
Merge branch 'main' into feat/runtime-property-reindex
2 parents fbe4a63 + ae327ca commit 490d81b

29 files changed

Lines changed: 1039 additions & 319 deletions

.github/workflows/main.yaml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ env:
2929
WEAVIATE_135: 1.35.18
3030
WEAVIATE_136: 1.36.12
3131
WEAVIATE_137: 1.37.5-e0fe0d5.amd64
32+
WEAVIATE_139: 1.39.0-rc.0-b41225e.amd64
3233

3334
jobs:
3435
lint-and-format:
@@ -162,11 +163,11 @@ jobs:
162163
fail-fast: false
163164
matrix:
164165
versions: [
165-
{ py: "3.10", weaviate: $WEAVIATE_136, grpc: "1.59.0"},
166-
{ py: "3.11", weaviate: $WEAVIATE_136, grpc: "1.66.0"},
167-
{ py: "3.12", weaviate: $WEAVIATE_136, grpc: "1.70.0"},
168-
{ py: "3.13", weaviate: $WEAVIATE_136, grpc: "1.72.1"},
169-
{ py: "3.14", weaviate: $WEAVIATE_136, grpc: "1.76.0"}
166+
{ py: "3.10", weaviate: $WEAVIATE_139, grpc: "1.59.0"},
167+
{ py: "3.11", weaviate: $WEAVIATE_139, grpc: "1.66.0"},
168+
{ py: "3.12", weaviate: $WEAVIATE_139, grpc: "1.70.0"},
169+
{ py: "3.13", weaviate: $WEAVIATE_139, grpc: "1.72.1"},
170+
{ py: "3.14", weaviate: $WEAVIATE_139, grpc: "1.76.0"}
170171
]
171172
optional_dependencies: [false]
172173
steps:
@@ -320,7 +321,8 @@ jobs:
320321
$WEAVIATE_134,
321322
$WEAVIATE_135,
322323
$WEAVIATE_136,
323-
$WEAVIATE_137
324+
$WEAVIATE_137,
325+
$WEAVIATE_139
324326
]
325327
steps:
326328
- name: Checkout

integration/test_collection_config.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,19 @@
5252
WeaviateUnsupportedFeatureError,
5353
)
5454
from integration.conftest import retry_on_http_error
55+
from weaviate.util import _ServerVersion
56+
57+
58+
def _expected_async_enabled(version: _ServerVersion, factor: int) -> bool:
59+
"""Whether the server reports async replication as enabled for a collection.
60+
61+
Up to 1.38 the server stores whatever `async_enabled` the collection was created with. From
62+
1.39 it ignores that and derives the field as `factor > 1 and not globally disabled` instead,
63+
so a collection with a single replica always reports `False`.
64+
"""
65+
if version.is_at_least(1, 39, 0):
66+
return factor > 1
67+
return version.is_at_least(1, 26, 0)
5568

5669

5770
@pytest.fixture(scope="module")
@@ -357,10 +370,9 @@ def test_collection_config_full(collection_factory: CollectionFactory) -> None:
357370
assert config.multi_tenancy_config.auto_tenant_creation is False
358371

359372
assert config.replication_config.factor == 1
360-
if collection._connection._weaviate_version.is_at_least(1, 26, 0):
361-
assert config.replication_config.async_enabled is True
362-
else:
363-
assert config.replication_config.async_enabled is False
373+
assert config.replication_config.async_enabled is _expected_async_enabled(
374+
collection._connection._weaviate_version, factor=1
375+
)
364376

365377
if collection._connection._weaviate_version.is_at_least(1, 24, 25):
366378
assert (
@@ -1609,7 +1621,9 @@ def test_replication_config_with_async_config(collection_factory: CollectionFact
16091621
)
16101622
config = collection.config.get()
16111623
assert config.replication_config.factor == 1
1612-
assert config.replication_config.async_enabled is True
1624+
assert config.replication_config.async_enabled is _expected_async_enabled(
1625+
collection._connection._weaviate_version, factor=1
1626+
)
16131627
assert config.replication_config.async_config is not None
16141628
ac = config.replication_config.async_config
16151629
assert ac.propagation_concurrency == 4
@@ -1676,7 +1690,9 @@ def test_replication_config_remove_async_config(collection_factory: CollectionFa
16761690
),
16771691
)
16781692
config = collection.config.get()
1679-
assert config.replication_config.async_enabled is True
1693+
assert config.replication_config.async_enabled is _expected_async_enabled(
1694+
collection._connection._weaviate_version, factor=1
1695+
)
16801696
assert config.replication_config.async_config is None
16811697
assert config.replication_config.factor == 1
16821698

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"""Integration tests for hybrid search + MMR diversity selection.
2+
3+
``diversity_selection`` is a hybrid-level argument: the server applies it as a
4+
post-fusion MMR pass (Weaviate >= 1.38.6). These tests assert that ``balance=0``
5+
(pure diversity) produces a different ordering than ``balance=1`` (pure
6+
relevance), and that ``mmr.limit`` caps the result count.
7+
8+
The equivalent ``near_vector`` behaviour is covered in
9+
``test_collection_diversity.py``.
10+
"""
11+
12+
import pytest
13+
14+
from integration.conftest import CollectionFactory
15+
from weaviate.classes.query import Diversity, HybridVector
16+
from weaviate.collections.classes.config import Configure, DataType, Property
17+
from weaviate.collections.classes.data import DataObject
18+
19+
MIN_VERSION = (1, 38, 6)
20+
21+
22+
def _skip_if_unsupported(collection) -> None:
23+
if collection._connection._weaviate_version.is_lower_than(*MIN_VERSION):
24+
pytest.skip("Hybrid diversity selection requires Weaviate >= 1.38.6")
25+
26+
27+
def _create_clustered_collection(collection_factory: CollectionFactory):
28+
"""Create a collection with 3 tight clusters (a, b, c) of vectors in 3D."""
29+
collection = collection_factory(
30+
properties=[Property(name="text", data_type=DataType.TEXT)],
31+
vectorizer_config=Configure.Vectorizer.none(),
32+
)
33+
_skip_if_unsupported(collection)
34+
collection.data.insert_many(
35+
[
36+
DataObject(properties={"text": "a1"}, vector=[1.0, 0.0, 0.0]),
37+
DataObject(properties={"text": "a2"}, vector=[0.95, 0.05, 0.0]),
38+
DataObject(properties={"text": "a3"}, vector=[0.9, 0.1, 0.0]),
39+
DataObject(properties={"text": "b1"}, vector=[0.0, 1.0, 0.0]),
40+
DataObject(properties={"text": "b2"}, vector=[0.05, 0.95, 0.0]),
41+
DataObject(properties={"text": "c1"}, vector=[0.0, 0.0, 1.0]),
42+
]
43+
)
44+
return collection
45+
46+
47+
def _create_large_collection(collection_factory: CollectionFactory, n_items: int = 50):
48+
"""Create a collection with enough items (>25) that a small mmr.limit is distinguishable from the server's default limit."""
49+
collection = collection_factory(
50+
properties=[Property(name="text", data_type=DataType.TEXT)],
51+
vectorizer_config=Configure.Vectorizer.none(),
52+
)
53+
_skip_if_unsupported(collection)
54+
collection.data.insert_many(
55+
[
56+
DataObject(properties={"text": f"t{i}"}, vector=[1.0 - 0.001 * i, 0.0, 0.0])
57+
for i in range(n_items)
58+
]
59+
)
60+
return collection
61+
62+
63+
def test_hybrid_near_vector_balance_0_differs_from_balance_1(
64+
collection_factory: CollectionFactory,
65+
) -> None:
66+
"""Hybrid near-vector: balance=0 (diversity) must reorder vs balance=1 (relevance)."""
67+
collection = _create_clustered_collection(collection_factory)
68+
balance_0 = collection.query.hybrid(
69+
query=None,
70+
vector=HybridVector.near_vector(vector=[1.0, 0.0, 0.0]),
71+
diversity_selection=Diversity.mmr(limit=3, balance=0.0),
72+
limit=3,
73+
).objects
74+
balance_1 = collection.query.hybrid(
75+
query=None,
76+
vector=HybridVector.near_vector(vector=[1.0, 0.0, 0.0]),
77+
diversity_selection=Diversity.mmr(limit=3, balance=1.0),
78+
limit=3,
79+
).objects
80+
assert [o.uuid for o in balance_0] != [o.uuid for o in balance_1]
81+
82+
83+
def test_hybrid_near_vector_balance_1_matches_baseline(
84+
collection_factory: CollectionFactory,
85+
) -> None:
86+
"""Hybrid near-vector with MMR balance=1 (pure relevance) matches the plain baseline."""
87+
collection = _create_clustered_collection(collection_factory)
88+
baseline = collection.query.hybrid(
89+
query=None,
90+
vector=HybridVector.near_vector(vector=[1.0, 0.0, 0.0]),
91+
limit=3,
92+
).objects
93+
mmr_balance_1 = collection.query.hybrid(
94+
query=None,
95+
vector=HybridVector.near_vector(vector=[1.0, 0.0, 0.0]),
96+
diversity_selection=Diversity.mmr(limit=3, balance=1.0),
97+
limit=3,
98+
).objects
99+
assert [o.uuid for o in baseline] == [o.uuid for o in mmr_balance_1]
100+
101+
102+
def test_hybrid_alpha_1_balance_0_differs_from_balance_1(
103+
collection_factory: CollectionFactory,
104+
) -> None:
105+
"""Hybrid with explicit alpha=1.0 (pure vector) applies MMR like near_vector."""
106+
collection = _create_clustered_collection(collection_factory)
107+
balance_0 = collection.query.hybrid(
108+
query="irrelevant",
109+
alpha=1.0,
110+
vector=HybridVector.near_vector(vector=[1.0, 0.0, 0.0]),
111+
diversity_selection=Diversity.mmr(limit=3, balance=0.0),
112+
limit=3,
113+
).objects
114+
balance_1 = collection.query.hybrid(
115+
query="irrelevant",
116+
alpha=1.0,
117+
vector=HybridVector.near_vector(vector=[1.0, 0.0, 0.0]),
118+
diversity_selection=Diversity.mmr(limit=3, balance=1.0),
119+
limit=3,
120+
).objects
121+
assert [o.uuid for o in balance_0] != [o.uuid for o in balance_1]
122+
123+
124+
def test_hybrid_respects_mmr_limit(
125+
collection_factory: CollectionFactory,
126+
) -> None:
127+
"""Hybrid respects mmr.limit as the result-count cap when no outer limit is set."""
128+
mmr_limit = 5
129+
collection = _create_large_collection(collection_factory, n_items=50)
130+
131+
result = collection.query.hybrid(
132+
query=None,
133+
vector=HybridVector.near_vector(vector=[1.0, 0.0, 0.0]),
134+
diversity_selection=Diversity.mmr(limit=mmr_limit, balance=0.5),
135+
).objects
136+
assert len(result) == mmr_limit

test/collection/test_classes_generative.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,31 @@ def test_generative_parameters_images_parsing(
182182
),
183183
),
184184
),
185+
(
186+
GenerativeConfig.deepseek(
187+
base_url="http://localhost:8080",
188+
model="deepseek-chat",
189+
temperature=0.5,
190+
max_tokens=100,
191+
frequency_penalty=0.1,
192+
presence_penalty=0.2,
193+
top_p=0.9,
194+
stop=["\n"],
195+
)._to_grpc(_GenerativeConfigRuntimeOptions(return_metadata=True)),
196+
generative_pb2.GenerativeProvider(
197+
return_metadata=True,
198+
deepseek=generative_pb2.GenerativeDeepseek(
199+
base_url="http://localhost:8080",
200+
model="deepseek-chat",
201+
temperature=0.5,
202+
max_tokens=100,
203+
frequency_penalty=0.1,
204+
presence_penalty=0.2,
205+
top_p=0.9,
206+
stop=base_pb2.TextArray(values=["\n"]),
207+
),
208+
),
209+
),
185210
(
186211
GenerativeConfig.dummy()._to_grpc(
187212
_GenerativeConfigRuntimeOptions(return_metadata=True)

test/collection/test_config.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,21 @@ def test_basic_config():
319319
}
320320
},
321321
),
322+
(
323+
Configure.Vectorizer.text2vec_openai(
324+
vectorize_collection_name=False,
325+
model="ada",
326+
endpoint="/api/v3/embeddings",
327+
),
328+
{
329+
"text2vec-openai": {
330+
"vectorizeClassName": False,
331+
"model": "ada",
332+
"endpoint": "/api/v3/embeddings",
333+
"isAzure": False,
334+
}
335+
},
336+
),
322337
(
323338
Configure.Vectorizer.text2vec_mistral(
324339
vectorize_collection_name=False,
@@ -1104,6 +1119,30 @@ def test_config_with_vectorizer_and_properties(
11041119
}
11051120
},
11061121
),
1122+
(
1123+
Configure.Generative.deepseek(
1124+
model="deepseek-chat",
1125+
max_tokens=100,
1126+
temperature=0.5,
1127+
frequency_penalty=0.1,
1128+
presence_penalty=0.2,
1129+
top_p=0.9,
1130+
base_url="https://api.deepseek.com",
1131+
stop=["\n"],
1132+
),
1133+
{
1134+
"generative-deepseek": {
1135+
"model": "deepseek-chat",
1136+
"maxTokens": 100,
1137+
"temperature": 0.5,
1138+
"frequencyPenalty": 0.1,
1139+
"presencePenalty": 0.2,
1140+
"topP": 0.9,
1141+
"baseURL": "https://api.deepseek.com",
1142+
"stop": ["\n"],
1143+
}
1144+
},
1145+
),
11071146
(
11081147
Configure.Generative.xai(
11091148
model="grok-2-latest",
@@ -1759,6 +1798,28 @@ def test_vector_config_flat_pq() -> None:
17591798
}
17601799
},
17611800
),
1801+
(
1802+
[
1803+
Configure.NamedVectors.text2vec_openai(
1804+
name="test",
1805+
source_properties=["prop"],
1806+
endpoint="/api/v3/embeddings",
1807+
)
1808+
],
1809+
{
1810+
"test": {
1811+
"vectorizer": {
1812+
"text2vec-openai": {
1813+
"properties": ["prop"],
1814+
"vectorizeClassName": True,
1815+
"endpoint": "/api/v3/embeddings",
1816+
"isAzure": False,
1817+
}
1818+
},
1819+
"vectorIndexType": "hnsw",
1820+
}
1821+
},
1822+
),
17621823
(
17631824
[Configure.NamedVectors.text2vec_mistral(name="test", source_properties=["prop"])],
17641825
{
@@ -2361,6 +2422,28 @@ def test_config_with_named_vectors(
23612422
}
23622423
},
23632424
),
2425+
(
2426+
[
2427+
Configure.Vectors.text2vec_openai(
2428+
name="test",
2429+
source_properties=["prop"],
2430+
endpoint="/api/v3/embeddings",
2431+
)
2432+
],
2433+
{
2434+
"test": {
2435+
"vectorizer": {
2436+
"text2vec-openai": {
2437+
"properties": ["prop"],
2438+
"vectorizeClassName": True,
2439+
"endpoint": "/api/v3/embeddings",
2440+
"isAzure": False,
2441+
}
2442+
},
2443+
"vectorIndexType": "hnsw",
2444+
}
2445+
},
2446+
),
23642447
(
23652448
[Configure.Vectors.text2vec_mistral(name="test", source_properties=["prop"])],
23662449
{
@@ -2408,6 +2491,27 @@ def test_config_with_named_vectors(
24082491
}
24092492
},
24102493
),
2494+
(
2495+
[
2496+
Configure.Vectors.text2vec_morph(
2497+
name="test",
2498+
source_properties=["prop"],
2499+
endpoint="/api/v3/embeddings",
2500+
)
2501+
],
2502+
{
2503+
"test": {
2504+
"vectorizer": {
2505+
"text2vec-morph": {
2506+
"vectorizeClassName": True,
2507+
"properties": ["prop"],
2508+
"endpoint": "/api/v3/embeddings",
2509+
}
2510+
},
2511+
"vectorIndexType": "hnsw",
2512+
}
2513+
},
2514+
),
24112515
(
24122516
[
24132517
Configure.Vectors.text2vec_google(

0 commit comments

Comments
 (0)