Skip to content

Commit d9d53b9

Browse files
committed
test: add integration tests for multi-vector query, UNF/NOINDEX, and multi-field sorting
- Add MultiVectorQueryIntegrationTest with 7 tests for multi-vector query feature (#402) - Add UnfNoindexIntegrationTest with 10 tests for UNF/NOINDEX attributes (#374) - Enhance QuerySortingIntegrationTest with 5 multi-field sorting tests Note: skip_decode and text field weights already have comprehensive unit test coverage
1 parent 6152e77 commit d9d53b9

File tree

3 files changed

+808
-0
lines changed

3 files changed

+808
-0
lines changed
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
package com.redis.vl.query;
2+
3+
import static org.assertj.core.api.Assertions.*;
4+
5+
import com.redis.vl.BaseIntegrationTest;
6+
import com.redis.vl.index.SearchIndex;
7+
import com.redis.vl.schema.*;
8+
import java.util.*;
9+
import org.junit.jupiter.api.*;
10+
11+
/**
12+
* Integration tests for Multi-Vector Query support (#402).
13+
*
14+
* <p>Tests simultaneous search across multiple vector fields with weighted score combination.
15+
*
16+
* <p>Python reference: PR #402 - Multi-vector query support
17+
*/
18+
@Tag("integration")
19+
@DisplayName("Multi-Vector Query Integration Tests")
20+
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
21+
class MultiVectorQueryIntegrationTest extends BaseIntegrationTest {
22+
23+
private static final String INDEX_NAME = "multi_vector_test_idx";
24+
private static SearchIndex searchIndex;
25+
26+
@BeforeAll
27+
static void setupIndex() {
28+
// Clean up any existing index
29+
try {
30+
unifiedJedis.ftDropIndex(INDEX_NAME);
31+
} catch (Exception e) {
32+
// Ignore if index doesn't exist
33+
}
34+
35+
// Create schema with multiple vector fields
36+
IndexSchema schema =
37+
IndexSchema.builder()
38+
.name(INDEX_NAME)
39+
.prefix("product:")
40+
.field(TextField.builder().name("title").build())
41+
.field(TextField.builder().name("description").build())
42+
.field(TagField.builder().name("category").build())
43+
.field(NumericField.builder().name("price").sortable(true).build())
44+
// Text embeddings (3 dimensions)
45+
.field(
46+
VectorField.builder()
47+
.name("text_embedding")
48+
.dimensions(3)
49+
.distanceMetric(VectorField.DistanceMetric.COSINE)
50+
.build())
51+
// Image embeddings (2 dimensions)
52+
.field(
53+
VectorField.builder()
54+
.name("image_embedding")
55+
.dimensions(2)
56+
.distanceMetric(VectorField.DistanceMetric.COSINE)
57+
.build())
58+
.build();
59+
60+
searchIndex = new SearchIndex(schema, unifiedJedis);
61+
searchIndex.create();
62+
63+
// Insert test documents with multiple vector embeddings
64+
Map<String, Object> doc1 = new HashMap<>();
65+
doc1.put("id", "1");
66+
doc1.put("title", "Red Laptop");
67+
doc1.put("description", "Premium laptop");
68+
doc1.put("category", "electronics");
69+
doc1.put("price", 1200);
70+
doc1.put("text_embedding", new float[] {0.1f, 0.2f, 0.3f});
71+
doc1.put("image_embedding", new float[] {0.5f, 0.5f});
72+
73+
Map<String, Object> doc2 = new HashMap<>();
74+
doc2.put("id", "2");
75+
doc2.put("title", "Blue Phone");
76+
doc2.put("description", "Budget smartphone");
77+
doc2.put("category", "electronics");
78+
doc2.put("price", 300);
79+
doc2.put("text_embedding", new float[] {0.4f, 0.5f, 0.6f});
80+
doc2.put("image_embedding", new float[] {0.3f, 0.4f});
81+
82+
Map<String, Object> doc3 = new HashMap<>();
83+
doc3.put("id", "3");
84+
doc3.put("title", "Green Tablet");
85+
doc3.put("description", "Mid-range tablet");
86+
doc3.put("category", "electronics");
87+
doc3.put("price", 500);
88+
doc3.put("text_embedding", new float[] {0.7f, 0.8f, 0.9f});
89+
doc3.put("image_embedding", new float[] {0.1f, 0.2f});
90+
91+
// Load all documents
92+
searchIndex.load(Arrays.asList(doc1, doc2, doc3), "id");
93+
94+
// Wait for indexing
95+
try {
96+
Thread.sleep(100);
97+
} catch (InterruptedException e) {
98+
Thread.currentThread().interrupt();
99+
}
100+
}
101+
102+
@AfterAll
103+
static void cleanupIndex() {
104+
if (searchIndex != null) {
105+
try {
106+
searchIndex.drop();
107+
} catch (Exception e) {
108+
// Ignore
109+
}
110+
}
111+
}
112+
113+
@Test
114+
@Order(1)
115+
@DisplayName("Should create multi-vector query with single vector")
116+
void testSingleVectorQuery() {
117+
Vector textVec =
118+
Vector.builder()
119+
.vector(new float[] {0.1f, 0.2f, 0.3f})
120+
.fieldName("text_embedding")
121+
.dtype("float32")
122+
.weight(1.0)
123+
.build();
124+
125+
MultiVectorQuery query = MultiVectorQuery.builder().vector(textVec).numResults(10).build();
126+
127+
assertThat(query.getVectors()).hasSize(1);
128+
assertThat(query.getNumResults()).isEqualTo(10);
129+
130+
Map<String, Object> params = query.toParams();
131+
assertThat(params).containsKey("vector_0");
132+
assertThat(params.get("vector_0")).isInstanceOf(byte[].class);
133+
}
134+
135+
@Test
136+
@Order(2)
137+
@DisplayName("Should create multi-vector query with multiple vectors")
138+
void testMultipleVectorsQuery() {
139+
Vector textVec =
140+
Vector.builder()
141+
.vector(new float[] {0.1f, 0.2f, 0.3f})
142+
.fieldName("text_embedding")
143+
.weight(0.7)
144+
.build();
145+
146+
Vector imageVec =
147+
Vector.builder()
148+
.vector(new float[] {0.5f, 0.5f})
149+
.fieldName("image_embedding")
150+
.weight(0.3)
151+
.build();
152+
153+
MultiVectorQuery query =
154+
MultiVectorQuery.builder().vectors(textVec, imageVec).numResults(10).build();
155+
156+
assertThat(query.getVectors()).hasSize(2);
157+
158+
// Verify params
159+
Map<String, Object> params = query.toParams();
160+
assertThat(params).containsKeys("vector_0", "vector_1");
161+
162+
// Verify query string format
163+
String queryString = query.toQueryString();
164+
assertThat(queryString)
165+
.contains("@text_embedding:[VECTOR_RANGE 2.0 $vector_0]")
166+
.contains("@image_embedding:[VECTOR_RANGE 2.0 $vector_1]")
167+
.contains(" | ");
168+
169+
// Verify scoring
170+
String formula = query.getScoringFormula();
171+
assertThat(formula).contains("0.70 * score_0").contains("0.30 * score_1");
172+
}
173+
174+
@Test
175+
@Order(3)
176+
@DisplayName("Should combine multi-vector query with filter expression")
177+
void testMultiVectorQueryWithFilter() {
178+
Vector textVec =
179+
Vector.builder().vector(new float[] {0.1f, 0.2f, 0.3f}).fieldName("text_embedding").build();
180+
181+
Filter filter = Filter.tag("category", "electronics");
182+
183+
MultiVectorQuery query =
184+
MultiVectorQuery.builder().vector(textVec).filterExpression(filter).numResults(5).build();
185+
186+
String queryString = query.toQueryString();
187+
assertThat(queryString).contains(" AND ").contains("@category:{electronics}");
188+
}
189+
190+
@Test
191+
@Order(4)
192+
@DisplayName("Should calculate score from multiple vectors with different weights")
193+
void testWeightedScoringCalculation() {
194+
Vector v1 =
195+
Vector.builder()
196+
.vector(new float[] {0.1f, 0.2f, 0.3f})
197+
.fieldName("text_embedding")
198+
.weight(0.6)
199+
.build();
200+
201+
Vector v2 =
202+
Vector.builder()
203+
.vector(new float[] {0.5f, 0.5f})
204+
.fieldName("image_embedding")
205+
.weight(0.4)
206+
.build();
207+
208+
MultiVectorQuery query = MultiVectorQuery.builder().vectors(v1, v2).build();
209+
210+
// Verify individual score calculations
211+
Map<String, String> calculations = query.getScoreCalculations();
212+
assertThat(calculations).hasSize(2);
213+
assertThat(calculations.get("score_0")).isEqualTo("(2 - distance_0)/2");
214+
assertThat(calculations.get("score_1")).isEqualTo("(2 - distance_1)/2");
215+
216+
// Verify combined scoring formula
217+
String formula = query.getScoringFormula();
218+
assertThat(formula).isEqualTo("0.60 * score_0 + 0.40 * score_1");
219+
}
220+
221+
@Test
222+
@Order(5)
223+
@DisplayName("Should support different vector dimensions and dtypes")
224+
void testDifferentDimensionsAndDtypes() {
225+
Vector v1 =
226+
Vector.builder()
227+
.vector(new float[] {0.1f, 0.2f, 0.3f}) // 3 dimensions
228+
.fieldName("text_embedding")
229+
.dtype("float32")
230+
.weight(0.5)
231+
.build();
232+
233+
Vector v2 =
234+
Vector.builder()
235+
.vector(new float[] {0.5f, 0.5f}) // 2 dimensions
236+
.fieldName("image_embedding")
237+
.dtype("float32")
238+
.weight(0.5)
239+
.build();
240+
241+
MultiVectorQuery query = MultiVectorQuery.builder().vectors(v1, v2).build();
242+
243+
assertThat(query.getVectors().get(0).getVector()).hasSize(3);
244+
assertThat(query.getVectors().get(1).getVector()).hasSize(2);
245+
}
246+
247+
@Test
248+
@Order(6)
249+
@DisplayName("Should specify return fields")
250+
void testReturnFields() {
251+
Vector textVec =
252+
Vector.builder().vector(new float[] {0.1f, 0.2f, 0.3f}).fieldName("text_embedding").build();
253+
254+
MultiVectorQuery query =
255+
MultiVectorQuery.builder()
256+
.vector(textVec)
257+
.returnFields("title", "price", "category")
258+
.build();
259+
260+
assertThat(query.getReturnFields()).containsExactly("title", "price", "category");
261+
}
262+
263+
@Test
264+
@Order(7)
265+
@DisplayName("Should use VECTOR_RANGE with threshold 2.0")
266+
void testVectorRangeThreshold() {
267+
Vector textVec =
268+
Vector.builder().vector(new float[] {0.1f, 0.2f, 0.3f}).fieldName("text_embedding").build();
269+
270+
MultiVectorQuery query = MultiVectorQuery.builder().vector(textVec).build();
271+
272+
String queryString = query.toQueryString();
273+
// Distance threshold hardcoded at 2.0 to include all eligible documents
274+
assertThat(queryString).contains("VECTOR_RANGE 2.0");
275+
}
276+
}

0 commit comments

Comments
 (0)