Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Reference;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.core.RedisHash;
Expand Down Expand Up @@ -481,6 +482,10 @@ private List<SearchField> findIndexFields(java.lang.reflect.Field field, String
logger.debug("🪲Found @Reference field " + field.getName() + " in " + field.getDeclaringClass()
.getSimpleName());
createIndexedFieldForReferenceIdField(field, isDocument).ifPresent(fields::add);

// Also create index fields for the referenced entity's indexed/searchable fields
// This enables searching like RefVehicle$.OWNER_NAME.eq("John")
createIndexedFieldsForReferencedEntity(field, isDocument, prefix).forEach(fields::add);
} else if (indexed.schemaFieldType() == SchemaFieldType.AUTODETECT) {
//
// Any Character class, Boolean or Enum with AUTODETECT -> Tag Search Field
Expand Down Expand Up @@ -1281,6 +1286,208 @@ private Optional<SearchField> createIndexedFieldForReferenceIdField( //
TagField.of(fieldName).separator('|').sortable()));
}

/**
* Creates index fields for the indexed/searchable fields of a referenced entity.
* This enables searching on referenced entity properties, e.g., RefVehicle$.OWNER_NAME.eq("John").
*
* @param referenceField the @Reference field
* @param isDocument whether this is a JSON document (vs Hash)
* @param prefix the field prefix
* @return list of search fields for the referenced entity's indexed properties
*/
private List<SearchField> createIndexedFieldsForReferencedEntity(java.lang.reflect.Field referenceField,
boolean isDocument, String prefix) {

List<SearchField> fields = new ArrayList<>();
Class<?> referencedType = referenceField.getType();
String referenceFieldName = referenceField.getName();

logger.debug(
"Processing indexed subfields for @Reference field " + referenceFieldName + " of type " + referencedType
.getSimpleName());

// Get all fields from the referenced entity that have indexing annotations
List<java.lang.reflect.Field> referencedFields = new ArrayList<>();
referencedFields.addAll(com.redis.om.spring.util.ObjectUtils.getFieldsWithAnnotation(referencedType,
Indexed.class));
referencedFields.addAll(com.redis.om.spring.util.ObjectUtils.getFieldsWithAnnotation(referencedType,
Searchable.class));
referencedFields.addAll(com.redis.om.spring.util.ObjectUtils.getFieldsWithAnnotation(referencedType,
TagIndexed.class));
referencedFields.addAll(com.redis.om.spring.util.ObjectUtils.getFieldsWithAnnotation(referencedType,
TextIndexed.class));
referencedFields.addAll(com.redis.om.spring.util.ObjectUtils.getFieldsWithAnnotation(referencedType,
NumericIndexed.class));
referencedFields.addAll(com.redis.om.spring.util.ObjectUtils.getFieldsWithAnnotation(referencedType,
GeoIndexed.class));
// Remove duplicates (a field might have multiple annotations)
referencedFields = referencedFields.stream().distinct().toList();

for (java.lang.reflect.Field subField : referencedFields) {
// Skip @Id fields - they're handled separately by createIndexedFieldForReferenceIdField
if (subField.isAnnotationPresent(Id.class)) {
continue;
}
// Skip @Reference fields to avoid infinite recursion
if (subField.isAnnotationPresent(Reference.class)) {
continue;
}

Class<?> subFieldType = ClassUtils.resolvePrimitiveIfNecessary(subField.getType());
String subFieldName = subField.getName();

// Build the nested field path: referenceField.subField
String fieldPath = isDocument ?
getFieldPrefix(prefix, true) + referenceFieldName + "." + subFieldName :
referenceFieldName + "_" + subFieldName;

// Build the alias: referenceField_subField
String alias = referenceFieldName + "_" + subFieldName;

FieldName fieldName = FieldName.of(fieldPath).as(alias);

logger.debug(
"Creating index field for " + referenceFieldName + "." + subFieldName + " with path " + fieldPath + " and alias " + alias);

// Handle @Searchable fields (full-text search)
Searchable searchable = subField.getAnnotation(Searchable.class);
if (searchable != null) {
TextField textField = TextField.of(fieldName);
if (searchable.weight() != 1.0) {
textField.weight(searchable.weight());
}
if (searchable.sortable()) {
textField.sortable();
}
if (searchable.nostem()) {
textField.noStem();
}
if (searchable.noindex()) {
textField.noIndex();
}
String phonetic = searchable.phonetic();
if (phonetic != null && !phonetic.isEmpty()) {
textField.phonetic(phonetic);
}
if (searchable.indexMissing()) {
textField.indexMissing();
}
if (searchable.indexEmpty()) {
textField.indexEmpty();
}
fields.add(SearchField.of(subField, textField));
continue;
}

// Handle @TextIndexed fields
TextIndexed textIndexed = subField.getAnnotation(TextIndexed.class);
if (textIndexed != null) {
TextField textField = TextField.of(fieldName);
if (textIndexed.weight() != 1.0) {
textField.weight(textIndexed.weight());
}
if (textIndexed.sortable()) {
textField.sortable();
}
if (textIndexed.nostem()) {
textField.noStem();
}
if (textIndexed.noindex()) {
textField.noIndex();
}
String phonetic = textIndexed.phonetic();
if (phonetic != null && !phonetic.isEmpty()) {
textField.phonetic(phonetic);
}
if (textIndexed.indexMissing()) {
textField.indexMissing();
}
if (textIndexed.indexEmpty()) {
textField.indexEmpty();
}
fields.add(SearchField.of(subField, textField));
continue;
}

// Handle @Indexed or @TagIndexed fields
Indexed indexed = subField.getAnnotation(Indexed.class);
TagIndexed tagIndexed = subField.getAnnotation(TagIndexed.class);
NumericIndexed numericIndexed = subField.getAnnotation(NumericIndexed.class);
GeoIndexed geoIndexed = subField.getAnnotation(GeoIndexed.class);

if (tagIndexed != null || (indexed != null && CharSequence.class.isAssignableFrom(subFieldType))) {
// Tag field for strings
String separatorStr = tagIndexed != null ?
tagIndexed.separator() :
(indexed != null ? indexed.separator() : "|");
char separator = separatorStr != null && !separatorStr.isEmpty() ? separatorStr.charAt(0) : '|';
TagField tagField = TagField.of(fieldName).separator(separator);
if (indexed != null && indexed.sortable()) {
tagField.sortable();
}
if (tagIndexed != null && tagIndexed.indexMissing()) {
tagField.indexMissing();
} else if (indexed != null && indexed.indexMissing()) {
tagField.indexMissing();
}
if (tagIndexed != null && tagIndexed.indexEmpty()) {
tagField.indexEmpty();
} else if (indexed != null && indexed.indexEmpty()) {
tagField.indexEmpty();
}
fields.add(SearchField.of(subField, tagField));
} else if (numericIndexed != null || (indexed != null && Number.class.isAssignableFrom(subFieldType))) {
// Numeric field
NumericField numField = NumericField.of(fieldName);
if ((numericIndexed != null && numericIndexed.sortable()) || (indexed != null && indexed.sortable())) {
numField.sortable();
}
if ((numericIndexed != null && numericIndexed.noindex()) || (indexed != null && indexed.noindex())) {
numField.noIndex();
}
if (indexed != null && indexed.indexMissing()) {
numField.indexMissing();
}
// Note: NumericField doesn't support indexEmpty() in current Jedis version
fields.add(SearchField.of(subField, numField));
} else if (geoIndexed != null || (indexed != null && Point.class.isAssignableFrom(subFieldType))) {
// Geo field
GeoField geoField = GeoField.of(fieldName);
fields.add(SearchField.of(subField, geoField));
} else if (indexed != null && subFieldType.isEnum()) {
// Enum as tag field
String separatorStr = indexed.separator();
char separator = separatorStr != null && !separatorStr.isEmpty() ? separatorStr.charAt(0) : '|';
TagField tagField = TagField.of(fieldName).separator(separator);
if (indexed.sortable()) {
tagField.sortable();
}
if (indexed.indexMissing()) {
tagField.indexMissing();
}
if (indexed.indexEmpty()) {
tagField.indexEmpty();
}
fields.add(SearchField.of(subField, tagField));
} else if (indexed != null && (subFieldType == Boolean.class || subFieldType == boolean.class)) {
// Boolean as tag field
TagField tagField = TagField.of(fieldName);
if (indexed.sortable()) {
tagField.sortable();
}
if (indexed.indexMissing()) {
tagField.indexMissing();
}
if (indexed.indexEmpty()) {
tagField.indexEmpty();
}
fields.add(SearchField.of(subField, tagField));
}
}

return fields;
}

private FTCreateParams createIndexDefinition(Class<?> cl, IndexDataType idxType) {
FTCreateParams params = FTCreateParams.createParams();
params.on(idxType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ private List<Triple<ObjectGraphFieldSpec, FieldSpec, CodeBlock>> processFieldMet
//
targetInterceptor = ReferenceField.class;
searchSchemaAlias = indexed.alias();

// Also process indexed/searchable fields from the referenced entity
// This generates fields like OWNER_NAME, OWNER_EMAIL for a @Reference @Indexed Owner owner field
fieldMetamodelSpec.addAll(processReferencedEntityIndexableFields(entity, chain));
} else if (searchable != null || textIndexed != null) {
//
// @Searchable/@TextIndexed: Field is a full-text field
Expand Down Expand Up @@ -814,6 +818,60 @@ private List<Triple<ObjectGraphFieldSpec, FieldSpec, CodeBlock>> processNestedIn
return fieldMetamodels;
}

/**
* Process indexed and searchable fields from a referenced entity.
* This generates metamodel fields like OWNER_NAME, OWNER_EMAIL when a field
* is annotated with both @Reference and @Indexed.
*
* @param entity the parent entity type
* @param chain the chain of elements from the root entity to the reference field
* @return list of field metamodel specifications for the referenced entity's indexed fields
*/
private List<Triple<ObjectGraphFieldSpec, FieldSpec, CodeBlock>> processReferencedEntityIndexableFields(
TypeName entity, List<Element> chain) {
Element referenceField = chain.get(chain.size() - 1);
TypeMirror typeMirror = referenceField.asType();

// Get the referenced entity type element
Element referencedEntity;
if (typeMirror instanceof DeclaredType declaredType) {
referencedEntity = declaredType.asElement();
} else {
return Collections.emptyList();
}

List<Triple<ObjectGraphFieldSpec, FieldSpec, CodeBlock>> fieldMetamodels = new ArrayList<>();

messager.printMessage(Diagnostic.Kind.NOTE, "Processing @Reference field " + referenceField
.getSimpleName() + " of type " + referencedEntity);

// Get all instance fields from the referenced entity
Map<? extends Element, String> enclosedFields = getInstanceFields(referencedEntity);

enclosedFields.forEach((field, getter) -> {
// Check if the field has any indexing annotation
boolean fieldIsIndexed = (field.getAnnotation(Indexed.class) != null) || (field.getAnnotation(
Searchable.class) != null) || (field.getAnnotation(NumericIndexed.class) != null) || (field.getAnnotation(
TagIndexed.class) != null) || (field.getAnnotation(TextIndexed.class) != null) || (field.getAnnotation(
GeoIndexed.class) != null);

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

if (fieldIsIndexed && !isIdField && !isReferenceField) {
// Create a new chain that includes the reference field and the subfield
List<Element> newChain = new ArrayList<>(chain);
newChain.add(field);

// Process the subfield
fieldMetamodels.addAll(processFieldMetamodel(entity, entity.toString(), newChain));
}
});

return fieldMetamodels;
}

private Map<? extends Element, String> getInstanceFields(Element element) {
if (objectTypeElement.equals(element)) {
return Collections.emptyMap();
Expand Down
Loading