|
17 | 17 | import org.apache.commons.logging.LogFactory; |
18 | 18 | import org.springframework.beans.factory.config.BeanDefinition; |
19 | 19 | import org.springframework.context.ApplicationContext; |
| 20 | +import org.springframework.data.annotation.Id; |
20 | 21 | import org.springframework.data.annotation.Reference; |
21 | 22 | import org.springframework.data.geo.Point; |
22 | 23 | import org.springframework.data.redis.core.RedisHash; |
@@ -481,6 +482,10 @@ private List<SearchField> findIndexFields(java.lang.reflect.Field field, String |
481 | 482 | logger.debug("🪲Found @Reference field " + field.getName() + " in " + field.getDeclaringClass() |
482 | 483 | .getSimpleName()); |
483 | 484 | createIndexedFieldForReferenceIdField(field, isDocument).ifPresent(fields::add); |
| 485 | + |
| 486 | + // Also create index fields for the referenced entity's indexed/searchable fields |
| 487 | + // This enables searching like RefVehicle$.OWNER_NAME.eq("John") |
| 488 | + createIndexedFieldsForReferencedEntity(field, isDocument, prefix).forEach(fields::add); |
484 | 489 | } else if (indexed.schemaFieldType() == SchemaFieldType.AUTODETECT) { |
485 | 490 | // |
486 | 491 | // Any Character class, Boolean or Enum with AUTODETECT -> Tag Search Field |
@@ -1281,6 +1286,208 @@ private Optional<SearchField> createIndexedFieldForReferenceIdField( // |
1281 | 1286 | TagField.of(fieldName).separator('|').sortable())); |
1282 | 1287 | } |
1283 | 1288 |
|
| 1289 | + /** |
| 1290 | + * Creates index fields for the indexed/searchable fields of a referenced entity. |
| 1291 | + * This enables searching on referenced entity properties, e.g., RefVehicle$.OWNER_NAME.eq("John"). |
| 1292 | + * |
| 1293 | + * @param referenceField the @Reference field |
| 1294 | + * @param isDocument whether this is a JSON document (vs Hash) |
| 1295 | + * @param prefix the field prefix |
| 1296 | + * @return list of search fields for the referenced entity's indexed properties |
| 1297 | + */ |
| 1298 | + private List<SearchField> createIndexedFieldsForReferencedEntity(java.lang.reflect.Field referenceField, |
| 1299 | + boolean isDocument, String prefix) { |
| 1300 | + |
| 1301 | + List<SearchField> fields = new ArrayList<>(); |
| 1302 | + Class<?> referencedType = referenceField.getType(); |
| 1303 | + String referenceFieldName = referenceField.getName(); |
| 1304 | + |
| 1305 | + logger.debug( |
| 1306 | + "Processing indexed subfields for @Reference field " + referenceFieldName + " of type " + referencedType |
| 1307 | + .getSimpleName()); |
| 1308 | + |
| 1309 | + // Get all fields from the referenced entity that have indexing annotations |
| 1310 | + List<java.lang.reflect.Field> referencedFields = new ArrayList<>(); |
| 1311 | + referencedFields.addAll(com.redis.om.spring.util.ObjectUtils.getFieldsWithAnnotation(referencedType, |
| 1312 | + Indexed.class)); |
| 1313 | + referencedFields.addAll(com.redis.om.spring.util.ObjectUtils.getFieldsWithAnnotation(referencedType, |
| 1314 | + Searchable.class)); |
| 1315 | + referencedFields.addAll(com.redis.om.spring.util.ObjectUtils.getFieldsWithAnnotation(referencedType, |
| 1316 | + TagIndexed.class)); |
| 1317 | + referencedFields.addAll(com.redis.om.spring.util.ObjectUtils.getFieldsWithAnnotation(referencedType, |
| 1318 | + TextIndexed.class)); |
| 1319 | + referencedFields.addAll(com.redis.om.spring.util.ObjectUtils.getFieldsWithAnnotation(referencedType, |
| 1320 | + NumericIndexed.class)); |
| 1321 | + referencedFields.addAll(com.redis.om.spring.util.ObjectUtils.getFieldsWithAnnotation(referencedType, |
| 1322 | + GeoIndexed.class)); |
| 1323 | + // Remove duplicates (a field might have multiple annotations) |
| 1324 | + referencedFields = referencedFields.stream().distinct().toList(); |
| 1325 | + |
| 1326 | + for (java.lang.reflect.Field subField : referencedFields) { |
| 1327 | + // Skip @Id fields - they're handled separately by createIndexedFieldForReferenceIdField |
| 1328 | + if (subField.isAnnotationPresent(Id.class)) { |
| 1329 | + continue; |
| 1330 | + } |
| 1331 | + // Skip @Reference fields to avoid infinite recursion |
| 1332 | + if (subField.isAnnotationPresent(Reference.class)) { |
| 1333 | + continue; |
| 1334 | + } |
| 1335 | + |
| 1336 | + Class<?> subFieldType = ClassUtils.resolvePrimitiveIfNecessary(subField.getType()); |
| 1337 | + String subFieldName = subField.getName(); |
| 1338 | + |
| 1339 | + // Build the nested field path: referenceField.subField |
| 1340 | + String fieldPath = isDocument ? |
| 1341 | + getFieldPrefix(prefix, true) + referenceFieldName + "." + subFieldName : |
| 1342 | + referenceFieldName + "_" + subFieldName; |
| 1343 | + |
| 1344 | + // Build the alias: referenceField_subField |
| 1345 | + String alias = referenceFieldName + "_" + subFieldName; |
| 1346 | + |
| 1347 | + FieldName fieldName = FieldName.of(fieldPath).as(alias); |
| 1348 | + |
| 1349 | + logger.debug( |
| 1350 | + "Creating index field for " + referenceFieldName + "." + subFieldName + " with path " + fieldPath + " and alias " + alias); |
| 1351 | + |
| 1352 | + // Handle @Searchable fields (full-text search) |
| 1353 | + Searchable searchable = subField.getAnnotation(Searchable.class); |
| 1354 | + if (searchable != null) { |
| 1355 | + TextField textField = TextField.of(fieldName); |
| 1356 | + if (searchable.weight() != 1.0) { |
| 1357 | + textField.weight(searchable.weight()); |
| 1358 | + } |
| 1359 | + if (searchable.sortable()) { |
| 1360 | + textField.sortable(); |
| 1361 | + } |
| 1362 | + if (searchable.nostem()) { |
| 1363 | + textField.noStem(); |
| 1364 | + } |
| 1365 | + if (searchable.noindex()) { |
| 1366 | + textField.noIndex(); |
| 1367 | + } |
| 1368 | + String phonetic = searchable.phonetic(); |
| 1369 | + if (phonetic != null && !phonetic.isEmpty()) { |
| 1370 | + textField.phonetic(phonetic); |
| 1371 | + } |
| 1372 | + if (searchable.indexMissing()) { |
| 1373 | + textField.indexMissing(); |
| 1374 | + } |
| 1375 | + if (searchable.indexEmpty()) { |
| 1376 | + textField.indexEmpty(); |
| 1377 | + } |
| 1378 | + fields.add(SearchField.of(subField, textField)); |
| 1379 | + continue; |
| 1380 | + } |
| 1381 | + |
| 1382 | + // Handle @TextIndexed fields |
| 1383 | + TextIndexed textIndexed = subField.getAnnotation(TextIndexed.class); |
| 1384 | + if (textIndexed != null) { |
| 1385 | + TextField textField = TextField.of(fieldName); |
| 1386 | + if (textIndexed.weight() != 1.0) { |
| 1387 | + textField.weight(textIndexed.weight()); |
| 1388 | + } |
| 1389 | + if (textIndexed.sortable()) { |
| 1390 | + textField.sortable(); |
| 1391 | + } |
| 1392 | + if (textIndexed.nostem()) { |
| 1393 | + textField.noStem(); |
| 1394 | + } |
| 1395 | + if (textIndexed.noindex()) { |
| 1396 | + textField.noIndex(); |
| 1397 | + } |
| 1398 | + String phonetic = textIndexed.phonetic(); |
| 1399 | + if (phonetic != null && !phonetic.isEmpty()) { |
| 1400 | + textField.phonetic(phonetic); |
| 1401 | + } |
| 1402 | + if (textIndexed.indexMissing()) { |
| 1403 | + textField.indexMissing(); |
| 1404 | + } |
| 1405 | + if (textIndexed.indexEmpty()) { |
| 1406 | + textField.indexEmpty(); |
| 1407 | + } |
| 1408 | + fields.add(SearchField.of(subField, textField)); |
| 1409 | + continue; |
| 1410 | + } |
| 1411 | + |
| 1412 | + // Handle @Indexed or @TagIndexed fields |
| 1413 | + Indexed indexed = subField.getAnnotation(Indexed.class); |
| 1414 | + TagIndexed tagIndexed = subField.getAnnotation(TagIndexed.class); |
| 1415 | + NumericIndexed numericIndexed = subField.getAnnotation(NumericIndexed.class); |
| 1416 | + GeoIndexed geoIndexed = subField.getAnnotation(GeoIndexed.class); |
| 1417 | + |
| 1418 | + if (tagIndexed != null || (indexed != null && CharSequence.class.isAssignableFrom(subFieldType))) { |
| 1419 | + // Tag field for strings |
| 1420 | + String separatorStr = tagIndexed != null ? |
| 1421 | + tagIndexed.separator() : |
| 1422 | + (indexed != null ? indexed.separator() : "|"); |
| 1423 | + char separator = separatorStr != null && !separatorStr.isEmpty() ? separatorStr.charAt(0) : '|'; |
| 1424 | + TagField tagField = TagField.of(fieldName).separator(separator); |
| 1425 | + if (indexed != null && indexed.sortable()) { |
| 1426 | + tagField.sortable(); |
| 1427 | + } |
| 1428 | + if (tagIndexed != null && tagIndexed.indexMissing()) { |
| 1429 | + tagField.indexMissing(); |
| 1430 | + } else if (indexed != null && indexed.indexMissing()) { |
| 1431 | + tagField.indexMissing(); |
| 1432 | + } |
| 1433 | + if (tagIndexed != null && tagIndexed.indexEmpty()) { |
| 1434 | + tagField.indexEmpty(); |
| 1435 | + } else if (indexed != null && indexed.indexEmpty()) { |
| 1436 | + tagField.indexEmpty(); |
| 1437 | + } |
| 1438 | + fields.add(SearchField.of(subField, tagField)); |
| 1439 | + } else if (numericIndexed != null || (indexed != null && Number.class.isAssignableFrom(subFieldType))) { |
| 1440 | + // Numeric field |
| 1441 | + NumericField numField = NumericField.of(fieldName); |
| 1442 | + if ((numericIndexed != null && numericIndexed.sortable()) || (indexed != null && indexed.sortable())) { |
| 1443 | + numField.sortable(); |
| 1444 | + } |
| 1445 | + if ((numericIndexed != null && numericIndexed.noindex()) || (indexed != null && indexed.noindex())) { |
| 1446 | + numField.noIndex(); |
| 1447 | + } |
| 1448 | + if (indexed != null && indexed.indexMissing()) { |
| 1449 | + numField.indexMissing(); |
| 1450 | + } |
| 1451 | + // Note: NumericField doesn't support indexEmpty() in current Jedis version |
| 1452 | + fields.add(SearchField.of(subField, numField)); |
| 1453 | + } else if (geoIndexed != null || (indexed != null && Point.class.isAssignableFrom(subFieldType))) { |
| 1454 | + // Geo field |
| 1455 | + GeoField geoField = GeoField.of(fieldName); |
| 1456 | + fields.add(SearchField.of(subField, geoField)); |
| 1457 | + } else if (indexed != null && subFieldType.isEnum()) { |
| 1458 | + // Enum as tag field |
| 1459 | + String separatorStr = indexed.separator(); |
| 1460 | + char separator = separatorStr != null && !separatorStr.isEmpty() ? separatorStr.charAt(0) : '|'; |
| 1461 | + TagField tagField = TagField.of(fieldName).separator(separator); |
| 1462 | + if (indexed.sortable()) { |
| 1463 | + tagField.sortable(); |
| 1464 | + } |
| 1465 | + if (indexed.indexMissing()) { |
| 1466 | + tagField.indexMissing(); |
| 1467 | + } |
| 1468 | + if (indexed.indexEmpty()) { |
| 1469 | + tagField.indexEmpty(); |
| 1470 | + } |
| 1471 | + fields.add(SearchField.of(subField, tagField)); |
| 1472 | + } else if (indexed != null && (subFieldType == Boolean.class || subFieldType == boolean.class)) { |
| 1473 | + // Boolean as tag field |
| 1474 | + TagField tagField = TagField.of(fieldName); |
| 1475 | + if (indexed.sortable()) { |
| 1476 | + tagField.sortable(); |
| 1477 | + } |
| 1478 | + if (indexed.indexMissing()) { |
| 1479 | + tagField.indexMissing(); |
| 1480 | + } |
| 1481 | + if (indexed.indexEmpty()) { |
| 1482 | + tagField.indexEmpty(); |
| 1483 | + } |
| 1484 | + fields.add(SearchField.of(subField, tagField)); |
| 1485 | + } |
| 1486 | + } |
| 1487 | + |
| 1488 | + return fields; |
| 1489 | + } |
| 1490 | + |
1284 | 1491 | private FTCreateParams createIndexDefinition(Class<?> cl, IndexDataType idxType) { |
1285 | 1492 | FTCreateParams params = FTCreateParams.createParams(); |
1286 | 1493 | params.on(idxType); |
|
0 commit comments