Skip to content

Commit 4913df6

Browse files
committed
[TST] add proptest for config & schema reconciliation
1 parent a021c46 commit 4913df6

File tree

5 files changed

+1299
-0
lines changed

5 files changed

+1299
-0
lines changed

rust/types/src/collection_configuration.rs

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,4 +1079,365 @@ mod tests {
10791079
))
10801080
);
10811081
}
1082+
1083+
#[cfg(feature = "testing")]
1084+
mod proptests {
1085+
use super::*;
1086+
use crate::hnsw_configuration::Space;
1087+
use crate::proptest_utils::strategies::{
1088+
embedding_function_strategy, internal_collection_configuration_strategy,
1089+
internal_hnsw_configuration_strategy, internal_spann_configuration_strategy,
1090+
knn_index_strategy,
1091+
};
1092+
use crate::{HnswConfiguration, MetadataValue, SpannConfiguration};
1093+
use proptest::prelude::*;
1094+
use proptest::test_runner::TestCaseError;
1095+
1096+
fn space_to_metadata_str(space: &Space) -> &'static str {
1097+
match space {
1098+
Space::L2 => "l2",
1099+
Space::Cosine => "cosine",
1100+
Space::Ip => "ip",
1101+
}
1102+
}
1103+
1104+
fn metadata_from_hnsw_config(config: &InternalHnswConfiguration) -> Metadata {
1105+
let mut metadata = Metadata::new();
1106+
metadata.insert(
1107+
"hnsw:space".to_string(),
1108+
MetadataValue::Str(space_to_metadata_str(&config.space).to_string()),
1109+
);
1110+
metadata.insert(
1111+
"hnsw:construction_ef".to_string(),
1112+
MetadataValue::Int(config.ef_construction as i64),
1113+
);
1114+
metadata.insert(
1115+
"hnsw:search_ef".to_string(),
1116+
MetadataValue::Int(config.ef_search as i64),
1117+
);
1118+
metadata.insert(
1119+
"hnsw:M".to_string(),
1120+
MetadataValue::Int(config.max_neighbors as i64),
1121+
);
1122+
metadata.insert(
1123+
"hnsw:num_threads".to_string(),
1124+
MetadataValue::Int(config.num_threads as i64),
1125+
);
1126+
metadata.insert(
1127+
"hnsw:resize_factor".to_string(),
1128+
MetadataValue::Float(config.resize_factor),
1129+
);
1130+
metadata.insert(
1131+
"hnsw:sync_threshold".to_string(),
1132+
MetadataValue::Int(config.sync_threshold as i64),
1133+
);
1134+
metadata.insert(
1135+
"hnsw:batch_size".to_string(),
1136+
MetadataValue::Int(config.batch_size as i64),
1137+
);
1138+
metadata
1139+
}
1140+
1141+
fn metadata_hnsw_strategy() -> impl Strategy<Value = (InternalHnswConfiguration, Metadata)>
1142+
{
1143+
internal_hnsw_configuration_strategy().prop_map(|config| {
1144+
let metadata = metadata_from_hnsw_config(&config);
1145+
(config, metadata)
1146+
})
1147+
}
1148+
1149+
fn assert_spann_is_default_with_space(
1150+
config: &InternalSpannConfiguration,
1151+
expected_space: Space,
1152+
) -> Result<(), TestCaseError> {
1153+
let default_config = InternalSpannConfiguration {
1154+
space: expected_space,
1155+
..InternalSpannConfiguration::default()
1156+
};
1157+
prop_assert_eq!(config, &default_config);
1158+
Ok(())
1159+
}
1160+
1161+
fn assert_hnsw_is_default_with_space(
1162+
config: &InternalHnswConfiguration,
1163+
expected_space: Space,
1164+
) -> Result<(), TestCaseError> {
1165+
let default_config = InternalHnswConfiguration {
1166+
space: expected_space,
1167+
..InternalHnswConfiguration::default()
1168+
};
1169+
prop_assert_eq!(config, &default_config);
1170+
Ok(())
1171+
}
1172+
1173+
proptest! {
1174+
#[test]
1175+
fn try_from_config_roundtrip_internal(
1176+
internal_config in internal_collection_configuration_strategy()
1177+
) {
1178+
let collection_config: CollectionConfiguration = internal_config.clone().into();
1179+
let default_knn = match &internal_config.vector_index {
1180+
VectorIndexConfiguration::Hnsw(_) => KnnIndex::Hnsw,
1181+
VectorIndexConfiguration::Spann(_) => KnnIndex::Spann,
1182+
};
1183+
1184+
let result = InternalCollectionConfiguration::try_from_config(
1185+
collection_config.clone(),
1186+
default_knn,
1187+
None,
1188+
)
1189+
.expect("conversion should succeed");
1190+
1191+
let embedding_function = internal_config.embedding_function.clone();
1192+
let expected_vector_index = match &internal_config.vector_index {
1193+
VectorIndexConfiguration::Hnsw(original) => {
1194+
let external: HnswConfiguration = original.clone().into();
1195+
let expected_internal: InternalHnswConfiguration = external.clone().into();
1196+
match &result.vector_index {
1197+
VectorIndexConfiguration::Hnsw(converted) => {
1198+
prop_assert_eq!(converted, &expected_internal);
1199+
}
1200+
_ => prop_assert!(false, "expected HNSW configuration"),
1201+
}
1202+
VectorIndexConfiguration::Hnsw(expected_internal)
1203+
}
1204+
VectorIndexConfiguration::Spann(original) => {
1205+
let external: SpannConfiguration = original.clone().into();
1206+
let expected_internal: InternalSpannConfiguration = external.clone().into();
1207+
match &result.vector_index {
1208+
VectorIndexConfiguration::Spann(converted) => {
1209+
prop_assert_eq!(converted, &expected_internal);
1210+
}
1211+
_ => prop_assert!(false, "expected SPANN configuration"),
1212+
}
1213+
VectorIndexConfiguration::Spann(expected_internal)
1214+
}
1215+
};
1216+
1217+
prop_assert_eq!(
1218+
result.embedding_function.clone(),
1219+
embedding_function.clone()
1220+
);
1221+
let expected = InternalCollectionConfiguration {
1222+
vector_index: expected_vector_index,
1223+
embedding_function: embedding_function.clone(),
1224+
};
1225+
prop_assert_eq!(result, expected);
1226+
1227+
let opposite_knn = match &internal_config.vector_index {
1228+
VectorIndexConfiguration::Hnsw(_) => KnnIndex::Spann,
1229+
VectorIndexConfiguration::Spann(_) => KnnIndex::Hnsw,
1230+
};
1231+
let opposite_result = InternalCollectionConfiguration::try_from_config(
1232+
collection_config,
1233+
opposite_knn,
1234+
None,
1235+
)
1236+
.expect("conversion for opposite default should succeed");
1237+
1238+
prop_assert_eq!(
1239+
opposite_result.embedding_function.clone(),
1240+
internal_config.embedding_function.clone()
1241+
);
1242+
1243+
match (&internal_config.vector_index, &opposite_result.vector_index) {
1244+
(VectorIndexConfiguration::Hnsw(original), VectorIndexConfiguration::Spann(spann)) => {
1245+
let expected_space = original.space.clone();
1246+
assert_spann_is_default_with_space(spann, expected_space)?;
1247+
}
1248+
(VectorIndexConfiguration::Spann(original), VectorIndexConfiguration::Hnsw(hnsw)) => {
1249+
let expected_space = original.space.clone();
1250+
assert_hnsw_is_default_with_space(hnsw, expected_space)?;
1251+
}
1252+
_ => prop_assert!(false, "unexpected opposite conversion result"),
1253+
}
1254+
}
1255+
}
1256+
1257+
proptest! {
1258+
#[test]
1259+
fn try_from_config_uses_metadata_when_configs_absent(
1260+
(expected_hnsw, metadata) in metadata_hnsw_strategy(),
1261+
embedding in embedding_function_strategy(),
1262+
knn in knn_index_strategy(),
1263+
) {
1264+
let collection_config = CollectionConfiguration {
1265+
hnsw: None,
1266+
spann: None,
1267+
embedding_function: embedding.clone(),
1268+
};
1269+
1270+
let result = InternalCollectionConfiguration::try_from_config(
1271+
collection_config,
1272+
knn,
1273+
Some(metadata.clone()),
1274+
)
1275+
.expect("conversion should succeed");
1276+
1277+
match (knn, &result.vector_index) {
1278+
(KnnIndex::Hnsw, VectorIndexConfiguration::Hnsw(hnsw)) => {
1279+
prop_assert_eq!(hnsw, &expected_hnsw);
1280+
}
1281+
(KnnIndex::Spann, VectorIndexConfiguration::Spann(spann)) => {
1282+
prop_assert_eq!(spann.space.clone(), expected_hnsw.space.clone());
1283+
assert_spann_is_default_with_space(spann, expected_hnsw.space.clone())?;
1284+
}
1285+
_ => prop_assert!(false, "unexpected vector index variant"),
1286+
}
1287+
prop_assert_eq!(result.embedding_function.clone(), embedding);
1288+
}
1289+
}
1290+
1291+
proptest! {
1292+
#[test]
1293+
fn try_from_config_uses_metadata_when_hnsw_config_is_default_values(
1294+
(expected_hnsw, metadata) in metadata_hnsw_strategy(),
1295+
embedding in embedding_function_strategy(),
1296+
) {
1297+
let collection_config = CollectionConfiguration {
1298+
hnsw: Some(HnswConfiguration::default()),
1299+
spann: None,
1300+
embedding_function: embedding.clone(),
1301+
};
1302+
1303+
let result = InternalCollectionConfiguration::try_from_config(
1304+
collection_config,
1305+
KnnIndex::Hnsw,
1306+
Some(metadata.clone()),
1307+
)
1308+
.expect("conversion should succeed");
1309+
1310+
match &result.vector_index {
1311+
VectorIndexConfiguration::Hnsw(hnsw) => {
1312+
prop_assert_eq!(hnsw, &expected_hnsw);
1313+
}
1314+
_ => prop_assert!(false, "expected hnsw configuration"),
1315+
}
1316+
prop_assert_eq!(result.embedding_function.clone(), embedding);
1317+
}
1318+
}
1319+
1320+
proptest! {
1321+
#[test]
1322+
fn try_from_config_prefers_spann_when_default_is_spann(
1323+
hnsw_config in internal_hnsw_configuration_strategy(),
1324+
embedding in embedding_function_strategy(),
1325+
) {
1326+
let embedding_clone = embedding.clone();
1327+
let collection_config = CollectionConfiguration {
1328+
hnsw: Some(hnsw_config.clone().into()),
1329+
spann: None,
1330+
embedding_function: embedding,
1331+
};
1332+
1333+
let result = InternalCollectionConfiguration::try_from_config(
1334+
collection_config,
1335+
KnnIndex::Spann,
1336+
None,
1337+
)
1338+
.expect("conversion should succeed");
1339+
1340+
let expected_space = hnsw_config.space.clone();
1341+
match &result.vector_index {
1342+
VectorIndexConfiguration::Spann(spann) => {
1343+
prop_assert_eq!(spann.space.clone(), expected_space.clone());
1344+
assert_spann_is_default_with_space(spann, expected_space)?;
1345+
}
1346+
_ => prop_assert!(false, "expected spann configuration"),
1347+
}
1348+
prop_assert_eq!(result.embedding_function.clone(), embedding_clone);
1349+
}
1350+
}
1351+
1352+
proptest! {
1353+
#[test]
1354+
fn try_from_config_prefers_hnsw_when_default_is_hnsw(
1355+
spann_config in internal_spann_configuration_strategy(),
1356+
embedding in embedding_function_strategy(),
1357+
) {
1358+
let embedding_clone = embedding.clone();
1359+
let collection_config = CollectionConfiguration {
1360+
hnsw: None,
1361+
spann: Some(spann_config.clone().into()),
1362+
embedding_function: embedding,
1363+
};
1364+
1365+
let result = InternalCollectionConfiguration::try_from_config(
1366+
collection_config,
1367+
KnnIndex::Hnsw,
1368+
None,
1369+
)
1370+
.expect("conversion should succeed");
1371+
1372+
let expected_space = spann_config.space.clone();
1373+
match &result.vector_index {
1374+
VectorIndexConfiguration::Hnsw(hnsw) => {
1375+
prop_assert_eq!(hnsw.space.clone(), expected_space.clone());
1376+
assert_hnsw_is_default_with_space(hnsw, expected_space)?;
1377+
}
1378+
_ => prop_assert!(false, "expected hnsw configuration"),
1379+
}
1380+
prop_assert_eq!(result.embedding_function.clone(), embedding_clone);
1381+
}
1382+
}
1383+
1384+
proptest! {
1385+
#[test]
1386+
fn try_from_config_defaults_when_configs_absent(
1387+
embedding in embedding_function_strategy(),
1388+
knn in knn_index_strategy(),
1389+
) {
1390+
let collection_config = CollectionConfiguration {
1391+
hnsw: None,
1392+
spann: None,
1393+
embedding_function: embedding.clone(),
1394+
};
1395+
1396+
let result = InternalCollectionConfiguration::try_from_config(
1397+
collection_config,
1398+
knn,
1399+
None,
1400+
)
1401+
.expect("conversion should succeed");
1402+
1403+
match (knn, &result.vector_index) {
1404+
(KnnIndex::Hnsw, VectorIndexConfiguration::Hnsw(hnsw)) => {
1405+
prop_assert_eq!(hnsw, &InternalHnswConfiguration::default());
1406+
}
1407+
(KnnIndex::Spann, VectorIndexConfiguration::Spann(spann)) => {
1408+
prop_assert_eq!(spann, &InternalSpannConfiguration::default());
1409+
}
1410+
_ => prop_assert!(false, "unexpected vector index variant"),
1411+
}
1412+
prop_assert_eq!(result.embedding_function.clone(), embedding);
1413+
}
1414+
}
1415+
1416+
proptest! {
1417+
#[test]
1418+
fn try_from_config_errors_on_multiple_configs(
1419+
hnsw_config in internal_hnsw_configuration_strategy(),
1420+
spann_config in internal_spann_configuration_strategy(),
1421+
embedding in embedding_function_strategy(),
1422+
knn in knn_index_strategy(),
1423+
) {
1424+
let collection_config = CollectionConfiguration {
1425+
hnsw: Some(hnsw_config.into()),
1426+
spann: Some(spann_config.into()),
1427+
embedding_function: embedding,
1428+
};
1429+
1430+
let result = InternalCollectionConfiguration::try_from_config(
1431+
collection_config,
1432+
knn,
1433+
None,
1434+
);
1435+
1436+
prop_assert!(matches!(
1437+
result,
1438+
Err(CollectionConfigurationToInternalConfigurationError::MultipleVectorIndexConfigurations)
1439+
));
1440+
}
1441+
}
1442+
}
10821443
}

0 commit comments

Comments
 (0)