Skip to content

Commit bc9dccd

Browse files
committed
feat: add numeric array membership methods to NumericField (#400)
Add containsDouble(), containsLong(), and containsInt() methods to NumericField class to enable array membership queries similar to TagField.in() functionality. These methods allow users to query for entities where numeric array fields contain any of the specified values using EntityStream. Resolves #400
1 parent 6db190d commit bc9dccd

File tree

4 files changed

+307
-0
lines changed

4 files changed

+307
-0
lines changed

redis-om-spring/src/main/java/com/redis/om/spring/metamodel/indexed/NumericField.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,40 @@ public Consumer<E> decrBy(Long value) {
163163
return new NumIncrByAction<>(searchFieldAccessor, -value);
164164
}
165165

166+
/**
167+
* Creates an array membership predicate for this numeric field to check if the array contains any of the specified Double values.
168+
* This method is similar to TagField.in() but for numeric arrays.
169+
*
170+
* @param values the Double values to check for membership in the array
171+
* @return an InPredicate that matches entities where this numeric array field contains any of the specified values
172+
*/
173+
@SuppressWarnings("unchecked")
174+
public InPredicate<E, ?> containsDouble(Double... values) {
175+
return new InPredicate<>(searchFieldAccessor, Arrays.asList(values));
176+
}
177+
178+
/**
179+
* Creates an array membership predicate for this numeric field to check if the array contains any of the specified Long values.
180+
* This method is similar to TagField.in() but for numeric arrays.
181+
*
182+
* @param values the Long values to check for membership in the array
183+
* @return an InPredicate that matches entities where this numeric array field contains any of the specified values
184+
*/
185+
@SuppressWarnings("unchecked")
186+
public InPredicate<E, ?> containsLong(Long... values) {
187+
return new InPredicate<>(searchFieldAccessor, Arrays.asList(values));
188+
}
189+
190+
/**
191+
* Creates an array membership predicate for this numeric field to check if the array contains any of the specified Integer values.
192+
* This method is similar to TagField.in() but for numeric arrays.
193+
*
194+
* @param values the Integer values to check for membership in the array
195+
* @return an InPredicate that matches entities where this numeric array field contains any of the specified values
196+
*/
197+
@SuppressWarnings("unchecked")
198+
public InPredicate<E, ?> containsInt(Integer... values) {
199+
return new InPredicate<>(searchFieldAccessor, Arrays.asList(values));
200+
}
201+
166202
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.redis.om.spring.fixtures.document.model;
2+
3+
import java.util.List;
4+
5+
import org.springframework.data.annotation.Id;
6+
7+
import com.redis.om.spring.annotations.Document;
8+
import com.redis.om.spring.annotations.NumericIndexed;
9+
import com.redis.om.spring.annotations.TagIndexed;
10+
11+
import lombok.AccessLevel;
12+
import lombok.AllArgsConstructor;
13+
import lombok.Data;
14+
import lombok.NoArgsConstructor;
15+
16+
/**
17+
* Test model for demonstrating GitHub issue #400:
18+
* NumericField lacks methods to check if a numeric array contains specific numbers
19+
*/
20+
@Data
21+
@NoArgsConstructor
22+
@AllArgsConstructor(access = AccessLevel.PROTECTED)
23+
@Document
24+
public class NumericArrayTestData {
25+
@Id
26+
private String id;
27+
28+
@TagIndexed
29+
private String name;
30+
31+
@NumericIndexed
32+
private List<Double> measurements;
33+
34+
@NumericIndexed
35+
private List<Long> counts;
36+
37+
@NumericIndexed
38+
private List<Integer> ratings;
39+
40+
@TagIndexed
41+
private List<String> tags; // For comparison - this works with TagField.in()
42+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package com.redis.om.spring.fixtures.document.repository;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.junit.jupiter.api.Assertions.*;
5+
6+
import java.util.Arrays;
7+
import java.util.List;
8+
import java.util.stream.Collectors;
9+
10+
import org.junit.jupiter.api.AfterEach;
11+
import org.junit.jupiter.api.BeforeEach;
12+
import org.junit.jupiter.api.Test;
13+
import org.springframework.beans.factory.annotation.Autowired;
14+
15+
import com.redis.om.spring.AbstractBaseDocumentTest;
16+
import com.redis.om.spring.fixtures.document.model.NumericArrayTestData;
17+
import com.redis.om.spring.fixtures.document.model.NumericArrayTestData$;
18+
import com.redis.om.spring.search.stream.EntityStream;
19+
20+
/**
21+
* Test class to demonstrate the issue described in GitHub issue #400:
22+
* https://github.com/redis/redis-om-spring/issues/400
23+
*
24+
* The issue is that NumericField lacks methods to check if a numeric array
25+
* contains specific numbers when using EntityStream, similar to how TagField.in() works.
26+
*
27+
* This test proves that the functionality is missing and shows what should work
28+
* once the feature is implemented.
29+
*/
30+
class NumericArraySearchTest extends AbstractBaseDocumentTest {
31+
32+
@Autowired
33+
NumericArrayTestDataRepository repository;
34+
35+
@Autowired
36+
EntityStream entityStream;
37+
38+
private NumericArrayTestData testData1;
39+
private NumericArrayTestData testData2;
40+
private NumericArrayTestData testData3;
41+
42+
@BeforeEach
43+
void loadTestData() {
44+
testData1 = new NumericArrayTestData();
45+
testData1.setName("data1");
46+
testData1.setMeasurements(Arrays.asList(1.5, 2.5, 3.5));
47+
testData1.setCounts(Arrays.asList(10L, 20L, 30L));
48+
testData1.setRatings(Arrays.asList(1, 2, 3));
49+
testData1.setTags(Arrays.asList("tag1", "tag2"));
50+
51+
testData2 = new NumericArrayTestData();
52+
testData2.setName("data2");
53+
testData2.setMeasurements(Arrays.asList(2.5, 4.5, 6.5));
54+
testData2.setCounts(Arrays.asList(20L, 40L, 60L));
55+
testData2.setRatings(Arrays.asList(2, 4, 6));
56+
testData2.setTags(Arrays.asList("tag2", "tag3"));
57+
58+
testData3 = new NumericArrayTestData();
59+
testData3.setName("data3");
60+
testData3.setMeasurements(Arrays.asList(3.5, 7.5, 9.5));
61+
testData3.setCounts(Arrays.asList(30L, 70L, 90L));
62+
testData3.setRatings(Arrays.asList(3, 7, 9));
63+
testData3.setTags(Arrays.asList("tag1", "tag4"));
64+
65+
repository.saveAll(Arrays.asList(testData1, testData2, testData3));
66+
}
67+
68+
@AfterEach
69+
void cleanUp() {
70+
repository.deleteAll();
71+
}
72+
73+
/**
74+
* This test shows that TagField.in() works perfectly for string arrays with EntityStream.
75+
* This is the pattern that NumericField should follow for numeric arrays.
76+
*/
77+
@Test
78+
void testTagFieldInWorksWithEntityStream() {
79+
// This demonstrates TagField.in() working with EntityStream:
80+
List<NumericArrayTestData> results = entityStream
81+
.of(NumericArrayTestData.class)
82+
.filter(NumericArrayTestData$.TAGS.in("tag1", "tag3"))
83+
.collect(Collectors.toList());
84+
85+
// Verify the results - should find testData1 and testData3 (both have "tag1") and testData2 (has "tag3")
86+
assertThat(results).hasSize(3);
87+
assertThat(results).contains(testData1, testData2, testData3);
88+
}
89+
90+
/**
91+
* This test demonstrates the MISSING functionality for numeric arrays.
92+
* This is the core issue from GitHub #400.
93+
*/
94+
@Test
95+
void testNumericFieldLacksContainsFunctionalityWithEntityStream() {
96+
// Test basic data setup first
97+
assertThat(testData1.getMeasurements()).contains(2.5);
98+
assertThat(testData2.getMeasurements()).contains(2.5);
99+
assertThat(testData3.getMeasurements()).contains(7.5);
100+
101+
// Should be able to find entities where measurements contains any of these values
102+
List<NumericArrayTestData> measurementResults = entityStream
103+
.of(NumericArrayTestData.class)
104+
.filter(NumericArrayTestData$.MEASUREMENTS.containsDouble(2.5, 7.5))
105+
.collect(Collectors.toList());
106+
assertThat(measurementResults).hasSize(3); // All entities have at least one of these values
107+
108+
// Should be able to find entities where counts contains any of these values
109+
List<NumericArrayTestData> countResults = entityStream
110+
.of(NumericArrayTestData.class)
111+
.filter(NumericArrayTestData$.COUNTS.containsLong(20L, 70L))
112+
.collect(Collectors.toList());
113+
assertThat(countResults).hasSize(3); // All entities have at least one of these values
114+
115+
// Should be able to find entities where ratings contains any of these values
116+
List<NumericArrayTestData> ratingResults = entityStream
117+
.of(NumericArrayTestData.class)
118+
.filter(NumericArrayTestData$.RATINGS.containsInt(2, 7))
119+
.collect(Collectors.toList());
120+
assertThat(ratingResults).hasSize(3); // testData1 has 2, testData2 has 2, testData3 has 7
121+
}
122+
123+
/**
124+
* This test shows what currently happens when you try to use the existing in() method
125+
* on NumericField with EntityStream - it doesn't work as expected for arrays.
126+
*/
127+
@Test
128+
void testCurrentNumericFieldInMethodLimitation() {
129+
// The current in() method on NumericField works for scalar matching, not array membership
130+
// This is different from TagField.in() which works for array membership
131+
132+
// The current NumericField.in() method expects List<T> not individual values
133+
// This shows the fundamental difference from TagField.in() which accepts varargs
134+
135+
// This would fail to compile: TestData$.MEASUREMENTS.in(2.5)
136+
// because NumericField.in() expects List<Double>, not double
137+
138+
// NumericField.in() signature: in(List<Double> values)
139+
// TagField.in() signature: in(String... values) or in(Object... values)
140+
141+
// The current NumericField.in() method would require the metamodel:
142+
// List<NumericArrayTestData> results = entityStream
143+
// .of(NumericArrayTestData.class)
144+
// .filter(NumericArrayTestData$.MEASUREMENTS.in(Arrays.asList(2.5))) // Must pass as List
145+
// .collect(Collectors.toList());
146+
147+
// Demonstrate the data exists but we can't query it effectively
148+
assertThat(testData1.getMeasurements()).contains(2.5);
149+
assertThat(testData2.getMeasurements()).contains(2.5);
150+
151+
// The issue: no easy way to find entities where numeric array contains any of multiple values
152+
153+
// But even if this worked, it doesn't give us "array contains any of these values" semantics
154+
// It's checking if the field value equals the provided list, not membership
155+
assertTrue(true, "Current NumericField.in() doesn't support array membership like TagField.in() does");
156+
157+
// The key issue: NumericField.in() ≠ TagField.in() for array membership behavior
158+
assertTrue(true, "NumericField.in() doesn't support array membership like TagField.in() does");
159+
}
160+
161+
/**
162+
* This test demonstrates the exact scenario from GitHub issue #400.
163+
* User wants to query: "Find all entities where numeric array contains any of these values"
164+
*/
165+
@Test
166+
void testGitHubIssue400Scenario() {
167+
// Issue #400 specifically asks for functionality like:
168+
// field.containsLong(Long... values) similar to TagField.in()
169+
170+
// Verify test data setup
171+
assertThat(testData1.getCounts()).contains(20L);
172+
assertThat(testData2.getCounts()).contains(20L);
173+
assertThat(testData3.getCounts()).contains(70L);
174+
175+
// This should now work with the new containsLong method:
176+
List<NumericArrayTestData> results = entityStream
177+
.of(NumericArrayTestData.class)
178+
.filter(NumericArrayTestData$.COUNTS.containsLong(20L, 70L))
179+
.collect(Collectors.toList());
180+
181+
// Should find testData1 (has 20L), testData2 (has 20L), and testData3 (has 70L)
182+
assertThat(results).hasSize(3);
183+
assertThat(results).contains(testData1, testData2, testData3);
184+
}
185+
186+
/**
187+
* This test proves the issue exists by showing the compilation failure.
188+
* When the feature is implemented, this test should be updated to verify the functionality works.
189+
*/
190+
@Test
191+
void testCompilationFailureProvesIssueExists() {
192+
// Verify test data has the expected values
193+
assertThat(testData1.getMeasurements()).containsAnyOf(1.5, 4.5, 7.5);
194+
assertThat(testData1.getCounts()).containsAnyOf(10L, 40L, 70L);
195+
assertThat(testData1.getRatings()).containsAnyOf(1, 4, 7);
196+
197+
// Now these methods should work and compile successfully:
198+
199+
List<NumericArrayTestData> measurementResults = entityStream
200+
.of(NumericArrayTestData.class)
201+
.filter(NumericArrayTestData$.MEASUREMENTS.containsDouble(1.5, 4.5, 7.5))
202+
.collect(Collectors.toList());
203+
204+
List<NumericArrayTestData> countResults = entityStream
205+
.of(NumericArrayTestData.class)
206+
.filter(NumericArrayTestData$.COUNTS.containsLong(10L, 40L, 70L))
207+
.collect(Collectors.toList());
208+
209+
List<NumericArrayTestData> ratingResults = entityStream
210+
.of(NumericArrayTestData.class)
211+
.filter(NumericArrayTestData$.RATINGS.containsInt(1, 4, 7))
212+
.collect(Collectors.toList());
213+
214+
// Verify the methods work as expected
215+
assertThat(measurementResults).hasSize(3); // All entities have at least one of these measurements
216+
assertThat(countResults).hasSize(3); // All entities have at least one of these counts
217+
assertThat(ratingResults).hasSize(3); // All entities have at least one of these ratings
218+
}
219+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.redis.om.spring.fixtures.document.repository;
2+
3+
import com.redis.om.spring.fixtures.document.model.NumericArrayTestData;
4+
import com.redis.om.spring.repository.RedisDocumentRepository;
5+
6+
/**
7+
* Repository for NumericArrayTestData to demonstrate GitHub issue #400
8+
*/
9+
public interface NumericArrayTestDataRepository extends RedisDocumentRepository<NumericArrayTestData, String> {
10+
}

0 commit comments

Comments
 (0)