Skip to content

Commit 291036a

Browse files
committed
feat(#677): generate indexed subfields for @reference fields
When a document has a @reference @indexed field pointing to another entity, the metamodel now generates field accessors for the referenced entity's indexed/searchable fields, enabling queries like: entityStream.of(RefVehicle.class) .filter(RefVehicle$.OWNER_NAME.eq("John")) .collect(...) Changes: - MetamodelGenerator: Add processReferencedEntityIndexableFields() to generate metamodel fields for all indexed annotations (@indexed, @searchable, @TagIndexed, @TextIndexed, @NumericIndexed, @GeoIndexed, @VectorIndexed) - RediSearchIndexer: Add createIndexedFieldsForReferencedEntity() to create index fields with full attribute support (sortable, noindex, indexMissing, indexEmpty, phonetic, weight, nostem, separator) - Support for VectorIndexed fields with FLAT/HNSW algorithms - Support for Date/time types (LocalDateTime, LocalDate, Date, Instant, OffsetDateTime) as numeric fields - Support for UUID and Ulid types as tag fields Note: @reference fields store only the entity ID, not the full object. Searching by referenced entity properties requires denormalized data. The metamodel fields are generated for API consistency.
1 parent 548b1aa commit 291036a

File tree

4 files changed

+136
-4
lines changed

4 files changed

+136
-4
lines changed

redis-om-spring/src/main/java/com/redis/om/spring/indexing/RediSearchIndexer.java

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,6 +1320,8 @@ private List<SearchField> createIndexedFieldsForReferencedEntity(java.lang.refle
13201320
NumericIndexed.class));
13211321
referencedFields.addAll(com.redis.om.spring.util.ObjectUtils.getFieldsWithAnnotation(referencedType,
13221322
GeoIndexed.class));
1323+
referencedFields.addAll(com.redis.om.spring.util.ObjectUtils.getFieldsWithAnnotation(referencedType,
1324+
VectorIndexed.class));
13231325
// Remove duplicates (a field might have multiple annotations)
13241326
referencedFields = referencedFields.stream().distinct().toList();
13251327

@@ -1415,8 +1417,9 @@ private List<SearchField> createIndexedFieldsForReferencedEntity(java.lang.refle
14151417
NumericIndexed numericIndexed = subField.getAnnotation(NumericIndexed.class);
14161418
GeoIndexed geoIndexed = subField.getAnnotation(GeoIndexed.class);
14171419

1418-
if (tagIndexed != null || (indexed != null && CharSequence.class.isAssignableFrom(subFieldType))) {
1419-
// Tag field for strings
1420+
if (tagIndexed != null || (indexed != null && (CharSequence.class.isAssignableFrom(
1421+
subFieldType) || subFieldType == java.util.UUID.class || subFieldType == com.github.f4b6a3.ulid.Ulid.class))) {
1422+
// Tag field for strings, UUID, and Ulid
14201423
String separatorStr = tagIndexed != null ?
14211424
tagIndexed.separator() :
14221425
(indexed != null ? indexed.separator() : "|");
@@ -1436,7 +1439,8 @@ private List<SearchField> createIndexedFieldsForReferencedEntity(java.lang.refle
14361439
tagField.indexEmpty();
14371440
}
14381441
fields.add(SearchField.of(subField, tagField));
1439-
} else if (numericIndexed != null || (indexed != null && Number.class.isAssignableFrom(subFieldType))) {
1442+
} else if (numericIndexed != null || (indexed != null && (Number.class.isAssignableFrom(
1443+
subFieldType) || subFieldType == java.time.LocalDateTime.class || subFieldType == java.time.LocalDate.class || subFieldType == java.util.Date.class || subFieldType == java.time.Instant.class || subFieldType == java.time.OffsetDateTime.class))) {
14401444
// Numeric field
14411445
NumericField numField = NumericField.of(fieldName);
14421446
if ((numericIndexed != null && numericIndexed.sortable()) || (indexed != null && indexed.sortable())) {
@@ -1483,6 +1487,48 @@ private List<SearchField> createIndexedFieldsForReferencedEntity(java.lang.refle
14831487
}
14841488
fields.add(SearchField.of(subField, tagField));
14851489
}
1490+
1491+
// Handle @VectorIndexed fields
1492+
VectorIndexed vectorIndexed = subField.getAnnotation(VectorIndexed.class);
1493+
if (vectorIndexed != null) {
1494+
VectorField.VectorAlgorithm algorithm = vectorIndexed.algorithm();
1495+
VectorType vectorType = vectorIndexed.type();
1496+
int dimension = vectorIndexed.dimension();
1497+
DistanceMetric distanceMetric = vectorIndexed.distanceMetric();
1498+
int initialCap = vectorIndexed.initialCapacity();
1499+
1500+
Map<String, Object> vectorAttrs = new HashMap<>();
1501+
vectorAttrs.put("TYPE", vectorType.toString());
1502+
vectorAttrs.put("DIM", dimension);
1503+
vectorAttrs.put("DISTANCE_METRIC", distanceMetric.toString());
1504+
if (initialCap > 0) {
1505+
vectorAttrs.put("INITIAL_CAP", initialCap);
1506+
}
1507+
1508+
if (algorithm == VectorField.VectorAlgorithm.HNSW) {
1509+
int m = vectorIndexed.m();
1510+
int efConstruction = vectorIndexed.efConstruction();
1511+
int efRuntime = vectorIndexed.efRuntime();
1512+
double epsilon = vectorIndexed.epsilon();
1513+
if (m > 0)
1514+
vectorAttrs.put("M", m);
1515+
if (efConstruction > 0)
1516+
vectorAttrs.put("EF_CONSTRUCTION", efConstruction);
1517+
if (efRuntime > 0)
1518+
vectorAttrs.put("EF_RUNTIME", efRuntime);
1519+
if (epsilon > 0)
1520+
vectorAttrs.put("EPSILON", epsilon);
1521+
} else if (algorithm == VectorField.VectorAlgorithm.FLAT) {
1522+
int blockSize = vectorIndexed.blockSize();
1523+
if (blockSize > 0)
1524+
vectorAttrs.put("BLOCK_SIZE", blockSize);
1525+
}
1526+
1527+
VectorField vectorField = VectorField.builder().fieldName(fieldName).algorithm(algorithm).attributes(
1528+
vectorAttrs).build();
1529+
1530+
fields.add(SearchField.of(subField, vectorField));
1531+
}
14861532
}
14871533

14881534
return fields;

redis-om-spring/src/main/java/com/redis/om/spring/metamodel/MetamodelGenerator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -853,7 +853,7 @@ private List<Triple<ObjectGraphFieldSpec, FieldSpec, CodeBlock>> processReferenc
853853
boolean fieldIsIndexed = (field.getAnnotation(Indexed.class) != null) || (field.getAnnotation(
854854
Searchable.class) != null) || (field.getAnnotation(NumericIndexed.class) != null) || (field.getAnnotation(
855855
TagIndexed.class) != null) || (field.getAnnotation(TextIndexed.class) != null) || (field.getAnnotation(
856-
GeoIndexed.class) != null);
856+
GeoIndexed.class) != null) || (field.getAnnotation(VectorIndexed.class) != null);
857857

858858
// Skip @Id fields and @Reference fields (to avoid infinite recursion)
859859
boolean isIdField = field.getAnnotation(Id.class) != null;

tests/src/test/java/com/redis/om/spring/annotations/document/ReferenceIndexedSubfieldsTest.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,4 +244,75 @@ void testAllOwnerIndexedFieldsAreGenerated() throws Exception {
244244
assertThat(value).as("Field %s should have a non-null value", fieldName).isNotNull();
245245
}
246246
}
247+
248+
// ==================================================================================
249+
// Tests for Date/time and UUID types (PR review feedback)
250+
// ==================================================================================
251+
252+
/**
253+
* Test that the metamodel generates OWNER_BIRTH_DATE field for @Indexed LocalDate fields.
254+
*/
255+
@Test
256+
void testMetamodelGeneratesOwnerBirthDateField() throws Exception {
257+
Class<?> metamodelClass = Class.forName("com.redis.om.spring.fixtures.document.model.RefVehicle$");
258+
259+
// Check that the static field OWNER_BIRTH_DATE exists (from @Indexed LocalDate)
260+
Field ownerBirthDateField = metamodelClass.getDeclaredField("OWNER_BIRTH_DATE");
261+
assertThat(ownerBirthDateField).isNotNull();
262+
263+
Object ownerBirthDateValue = ownerBirthDateField.get(null);
264+
assertThat(ownerBirthDateValue).isNotNull();
265+
}
266+
267+
/**
268+
* Test that the metamodel generates OWNER_CREATED_AT field for @Indexed LocalDateTime fields.
269+
*/
270+
@Test
271+
void testMetamodelGeneratesOwnerCreatedAtField() throws Exception {
272+
Class<?> metamodelClass = Class.forName("com.redis.om.spring.fixtures.document.model.RefVehicle$");
273+
274+
// Check that the static field OWNER_CREATED_AT exists (from @Indexed LocalDateTime)
275+
Field ownerCreatedAtField = metamodelClass.getDeclaredField("OWNER_CREATED_AT");
276+
assertThat(ownerCreatedAtField).isNotNull();
277+
278+
Object ownerCreatedAtValue = ownerCreatedAtField.get(null);
279+
assertThat(ownerCreatedAtValue).isNotNull();
280+
}
281+
282+
/**
283+
* Test that the metamodel generates OWNER_EXTERNAL_ID field for @Indexed UUID fields.
284+
*/
285+
@Test
286+
void testMetamodelGeneratesOwnerExternalIdField() throws Exception {
287+
Class<?> metamodelClass = Class.forName("com.redis.om.spring.fixtures.document.model.RefVehicle$");
288+
289+
// Check that the static field OWNER_EXTERNAL_ID exists (from @Indexed UUID)
290+
Field ownerExternalIdField = metamodelClass.getDeclaredField("OWNER_EXTERNAL_ID");
291+
assertThat(ownerExternalIdField).isNotNull();
292+
293+
Object ownerExternalIdValue = ownerExternalIdField.get(null);
294+
assertThat(ownerExternalIdValue).isNotNull();
295+
}
296+
297+
/**
298+
* Test that all Date/time and UUID fields from Owner are generated in RefVehicle$.
299+
*/
300+
@Test
301+
void testAllDateTimeAndUuidFieldsAreGenerated() throws Exception {
302+
Class<?> metamodelClass = Class.forName("com.redis.om.spring.fixtures.document.model.RefVehicle$");
303+
304+
// Verify Date/time and UUID fields exist
305+
String[] expectedFields = {
306+
"OWNER_BIRTH_DATE", // @Indexed LocalDate
307+
"OWNER_CREATED_AT", // @Indexed LocalDateTime
308+
"OWNER_EXTERNAL_ID" // @Indexed UUID
309+
};
310+
311+
for (String fieldName : expectedFields) {
312+
Field field = metamodelClass.getDeclaredField(fieldName);
313+
assertThat(field).as("Field %s should exist", fieldName).isNotNull();
314+
Object value = field.get(null);
315+
assertThat(value).as("Field %s should have a non-null value", fieldName).isNotNull();
316+
}
317+
}
247318
}

tests/src/test/java/com/redis/om/spring/fixtures/document/model/Owner.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package com.redis.om.spring.fixtures.document.model;
22

3+
import java.time.LocalDate;
4+
import java.time.LocalDateTime;
5+
import java.util.UUID;
6+
37
import org.springframework.data.annotation.Id;
48

59
import com.redis.om.spring.annotations.Document;
@@ -21,6 +25,8 @@
2125
* - @TagIndexed with indexMissing/indexEmpty
2226
* - @NumericIndexed with sortable
2327
* - @Indexed on Boolean
28+
* - @Indexed on Date/time types (LocalDate, LocalDateTime)
29+
* - @Indexed on UUID
2430
*/
2531
@Data
2632
@RequiredArgsConstructor(staticName = "of")
@@ -47,4 +53,13 @@ public class Owner {
4753

4854
@Indexed(sortable = true, indexMissing = true, indexEmpty = true)
4955
private Boolean active;
56+
57+
@Indexed(sortable = true)
58+
private LocalDate birthDate;
59+
60+
@Indexed(sortable = true)
61+
private LocalDateTime createdAt;
62+
63+
@Indexed
64+
private UUID externalId;
5065
}

0 commit comments

Comments
 (0)