|
33 | 33 | import org.junit.jupiter.api.Disabled; |
34 | 34 | import org.junit.jupiter.api.Tag; |
35 | 35 | import org.junit.jupiter.api.Test; |
| 36 | +import org.junit.jupiter.params.ParameterizedTest; |
| 37 | +import org.junit.jupiter.params.provider.MethodSource; |
36 | 38 |
|
37 | 39 | import java.nio.ByteBuffer; |
38 | 40 | import java.nio.ByteOrder; |
|
45 | 47 | import java.util.List; |
46 | 48 | import java.util.Map; |
47 | 49 | import java.util.Set; |
| 50 | +import java.util.stream.Stream; |
48 | 51 |
|
49 | 52 | import static io.lettuce.TestTags.INTEGRATION_TEST; |
50 | 53 | import static org.assertj.core.api.Assertions.assertThat; |
@@ -1186,4 +1189,94 @@ public ByteBuffer encodeValue(Object value) { |
1186 | 1189 | } |
1187 | 1190 | } |
1188 | 1191 |
|
| 1192 | + static Stream<VectorFieldArgs.VectorType> quantizedVectorTypes() { |
| 1193 | + return Stream.of(VectorFieldArgs.VectorType.INT8, VectorFieldArgs.VectorType.UINT8); |
| 1194 | + } |
| 1195 | + |
| 1196 | + @ParameterizedTest |
| 1197 | + @MethodSource("quantizedVectorTypes") |
| 1198 | + void testQuantizedVectorTypes(VectorFieldArgs.VectorType vectorType) { |
| 1199 | + assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("8.0")); |
| 1200 | + |
| 1201 | + String typeName = vectorType.name(); |
| 1202 | + String indexName = typeName.toLowerCase() + "-idx"; |
| 1203 | + String prefix = typeName.toLowerCase() + ":"; |
| 1204 | + String fieldName = "embedding_" + typeName.toLowerCase(); |
| 1205 | + |
| 1206 | + // Create vector field with appropriate algorithm based on type |
| 1207 | + FieldArgs<String> vectorField; |
| 1208 | + if (vectorType == VectorFieldArgs.VectorType.INT8) { |
| 1209 | + // INT8 with FLAT algorithm and L2 distance |
| 1210 | + vectorField = VectorFieldArgs.<String> builder().name(fieldName).flat().type(vectorType).dimensions(4) |
| 1211 | + .distanceMetric(VectorFieldArgs.DistanceMetric.L2).build(); |
| 1212 | + } else { |
| 1213 | + // UINT8 with HNSW algorithm and COSINE distance |
| 1214 | + vectorField = VectorFieldArgs.<String> builder().name(fieldName).hnsw().type(vectorType).dimensions(4) |
| 1215 | + .distanceMetric(VectorFieldArgs.DistanceMetric.COSINE).attribute("M", 16).attribute("EF_CONSTRUCTION", 200) |
| 1216 | + .build(); |
| 1217 | + } |
| 1218 | + |
| 1219 | + FieldArgs<String> nameField = TextFieldArgs.<String> builder().name("name").build(); |
| 1220 | + |
| 1221 | + CreateArgs<String, String> createArgs = CreateArgs.<String, String> builder().withPrefix(prefix) |
| 1222 | + .on(CreateArgs.TargetType.HASH).build(); |
| 1223 | + |
| 1224 | + redis.ftCreate(indexName, createArgs, Arrays.asList(vectorField, nameField)); |
| 1225 | + |
| 1226 | + // Add vectors based on type |
| 1227 | + byte[] vector1, vector2, vector3, queryVector; |
| 1228 | + if (vectorType == VectorFieldArgs.VectorType.INT8) { |
| 1229 | + // INT8 vectors (signed 8-bit: -128 to 127) |
| 1230 | + vector1 = new byte[] { 10, 20, 30, 40 }; |
| 1231 | + vector2 = new byte[] { -50, 60, -70, 80 }; |
| 1232 | + vector3 = new byte[] { 15, 25, 35, 45 }; |
| 1233 | + queryVector = new byte[] { 12, 22, 32, 42 }; |
| 1234 | + } else { |
| 1235 | + // UINT8 vectors (unsigned 8-bit: 0 to 255, stored as signed bytes in Java) |
| 1236 | + vector1 = new byte[] { (byte) 100, (byte) 150, (byte) 200, (byte) 250 }; |
| 1237 | + vector2 = new byte[] { (byte) 50, (byte) 100, (byte) 150, (byte) 200 }; |
| 1238 | + vector3 = new byte[] { (byte) 110, (byte) 160, (byte) 210, (byte) 240 }; |
| 1239 | + queryVector = new byte[] { (byte) 105, (byte) 155, (byte) 205, (byte) 245 }; |
| 1240 | + } |
| 1241 | + |
| 1242 | + // Store documents |
| 1243 | + Map<String, Object> doc1 = new HashMap<>(); |
| 1244 | + doc1.put("name", typeName + " Vector 1"); |
| 1245 | + doc1.put(fieldName, vector1); |
| 1246 | + storeHashDocument(prefix + "1", doc1); |
| 1247 | + |
| 1248 | + Map<String, Object> doc2 = new HashMap<>(); |
| 1249 | + doc2.put("name", typeName + " Vector 2"); |
| 1250 | + doc2.put(fieldName, vector2); |
| 1251 | + storeHashDocument(prefix + "2", doc2); |
| 1252 | + |
| 1253 | + Map<String, Object> doc3 = new HashMap<>(); |
| 1254 | + doc3.put("name", typeName + " Vector 3"); |
| 1255 | + doc3.put(fieldName, vector3); |
| 1256 | + storeHashDocument(prefix + "3", doc3); |
| 1257 | + |
| 1258 | + // Test KNN search |
| 1259 | + ByteBuffer queryBuffer = ByteBuffer.wrap(queryVector); |
| 1260 | + ByteBuffer blobKey = ByteBuffer.wrap("BLOB".getBytes(StandardCharsets.UTF_8)); |
| 1261 | + SearchArgs<ByteBuffer, ByteBuffer> searchArgs = SearchArgs.<ByteBuffer, ByteBuffer> builder() |
| 1262 | + .param(blobKey, queryBuffer).limit(0, 2).build(); |
| 1263 | + |
| 1264 | + ByteBuffer queryString = ByteBuffer |
| 1265 | + .wrap(("*=>[KNN 2 @" + fieldName + " $BLOB AS distance]").getBytes(StandardCharsets.UTF_8)); |
| 1266 | + SearchReply<ByteBuffer, ByteBuffer> results = redisBinary.ftSearch(indexName, queryString, searchArgs); |
| 1267 | + |
| 1268 | + assertThat(results.getCount()).isEqualTo(2); |
| 1269 | + assertThat(results.getResults()).hasSize(2); |
| 1270 | + |
| 1271 | + // Verify that the search worked with the quantized vectors |
| 1272 | + ByteBuffer nameKey = ByteBuffer.wrap("name".getBytes(StandardCharsets.UTF_8)); |
| 1273 | + for (SearchReply.SearchResult<ByteBuffer, ByteBuffer> result : results.getResults()) { |
| 1274 | + String name = new String(result.getFields().get(nameKey).array(), StandardCharsets.UTF_8); |
| 1275 | + assertThat(name).contains(typeName + " Vector"); |
| 1276 | + } |
| 1277 | + |
| 1278 | + // Cleanup |
| 1279 | + redis.ftDropindex(indexName); |
| 1280 | + } |
| 1281 | + |
1189 | 1282 | } |
0 commit comments