Skip to content

Commit c577574

Browse files
authored
Merge branch 'main' into fix-gradle
2 parents 62fdce5 + b61e631 commit c577574

File tree

16 files changed

+1273
-16
lines changed

16 files changed

+1273
-16
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ build/
4545
.gradle/
4646
compile_debug.log
4747
docs/.cache/*
48+
/test_output.log

docs/content/modules/ROOT/pages/json-map-fields.adoc

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ Redis OM Spring supports Maps with the following value types:
2727
=== Spatial Types
2828
* `Point` - Indexed as GEO fields for spatial queries
2929

30+
=== Complex Object Types (New in 1.0.0)
31+
* Any custom class with `@Indexed` fields - Enables querying nested properties within map values
32+
3033
== Basic Usage
3134

3235
=== Entity Definition with Map Fields
@@ -136,9 +139,159 @@ LocalDateTime lastWeek = LocalDateTime.now().minusWeeks(1);
136139
List<Product> recentlyUpdated = repository.findByTimestampsMapContainsAfter(lastWeek);
137140
----
138141

142+
== Complex Object Values in Maps
143+
144+
=== Defining Complex Objects as Map Values
145+
146+
Redis OM Spring now supports Maps with complex object values, enabling you to query nested fields within those objects. This is particularly useful for scenarios like financial portfolios, inventory systems, or any domain requiring dynamic collections of structured data.
147+
148+
[source,java]
149+
----
150+
// Define the complex object
151+
@Data
152+
public class Position {
153+
@Indexed
154+
private String cusip; // Security identifier
155+
156+
@Indexed
157+
private String description;
158+
159+
@Indexed
160+
private String manager;
161+
162+
@Indexed
163+
private Integer quantity;
164+
165+
@Indexed
166+
private BigDecimal price;
167+
168+
@Indexed
169+
private LocalDate asOfDate;
170+
}
171+
172+
// Use it in a Map field
173+
@Data
174+
@Document
175+
public class Account {
176+
@Id
177+
private String id;
178+
179+
@Indexed
180+
private String accountNumber;
181+
182+
@Indexed
183+
private String accountHolder;
184+
185+
// Map with complex object values
186+
@Indexed(schemaFieldType = SchemaFieldType.NESTED)
187+
private Map<String, Position> positions = new HashMap<>();
188+
189+
@Indexed
190+
private BigDecimal totalValue;
191+
}
192+
----
193+
194+
=== Querying Nested Fields in Complex Map Values
195+
196+
Redis OM Spring provides a special query pattern `MapContains<NestedField>` for querying nested properties within map values:
197+
198+
[source,java]
199+
----
200+
public interface AccountRepository extends RedisDocumentRepository<Account, String> {
201+
202+
// Query by nested CUSIP field
203+
List<Account> findByPositionsMapContainsCusip(String cusip);
204+
205+
// Query by nested Manager field
206+
List<Account> findByPositionsMapContainsManager(String manager);
207+
208+
// Numeric comparisons on nested fields
209+
List<Account> findByPositionsMapContainsQuantityGreaterThan(Integer quantity);
210+
List<Account> findByPositionsMapContainsPriceLessThan(BigDecimal price);
211+
212+
// Temporal queries on nested fields
213+
List<Account> findByPositionsMapContainsAsOfDateAfter(LocalDate date);
214+
List<Account> findByPositionsMapContainsAsOfDateBetween(LocalDate start, LocalDate end);
215+
216+
// Combine with regular field queries
217+
List<Account> findByAccountHolderAndPositionsMapContainsManager(
218+
String accountHolder, String manager
219+
);
220+
221+
// Multiple nested field conditions
222+
List<Account> findByPositionsMapContainsCusipAndPositionsMapContainsQuantityGreaterThan(
223+
String cusip, Integer minQuantity
224+
);
225+
}
226+
----
227+
228+
=== Usage Example
229+
230+
[source,java]
231+
----
232+
// Create account with positions
233+
Account account = new Account();
234+
account.setAccountNumber("10190001");
235+
account.setAccountHolder("John Doe");
236+
account.setTotalValue(new BigDecimal("100000.00"));
237+
238+
// Add positions
239+
Position applePosition = new Position();
240+
applePosition.setCusip("AAPL");
241+
applePosition.setDescription("Apple Inc.");
242+
applePosition.setManager("Jane Smith");
243+
applePosition.setQuantity(100);
244+
applePosition.setPrice(new BigDecimal("150.00"));
245+
applePosition.setAsOfDate(LocalDate.now());
246+
account.getPositions().put("AAPL", applePosition);
247+
248+
Position googlePosition = new Position();
249+
googlePosition.setCusip("GOOGL");
250+
googlePosition.setDescription("Alphabet Inc.");
251+
googlePosition.setManager("Bob Johnson");
252+
googlePosition.setQuantity(50);
253+
googlePosition.setPrice(new BigDecimal("2800.00"));
254+
googlePosition.setAsOfDate(LocalDate.now());
255+
account.getPositions().put("GOOGL", googlePosition);
256+
257+
accountRepository.save(account);
258+
259+
// Query examples
260+
// Find all accounts holding Apple stock
261+
List<Account> appleHolders = repository.findByPositionsMapContainsCusip("AAPL");
262+
263+
// Find accounts with positions managed by Jane Smith
264+
List<Account> janesManagedAccounts = repository.findByPositionsMapContainsManager("Jane Smith");
265+
266+
// Find accounts with any position having quantity > 75
267+
List<Account> largePositions = repository.findByPositionsMapContainsQuantityGreaterThan(75);
268+
269+
// Find accounts with positions priced below $200
270+
List<Account> affordablePositions = repository.findByPositionsMapContainsPriceLessThan(
271+
new BigDecimal("200.00")
272+
);
273+
----
274+
275+
=== Index Structure
276+
277+
When you use complex objects in Maps, Redis OM Spring creates indexes for each nested field using JSONPath expressions:
278+
279+
[source,text]
280+
----
281+
// Generated index fields for Map<String, Position>
282+
$.positions.*.cusip -> TAG field (positions_cusip)
283+
$.positions.*.manager -> TAG field (positions_manager)
284+
$.positions.*.quantity -> NUMERIC field (positions_quantity)
285+
$.positions.*.price -> NUMERIC field (positions_price)
286+
$.positions.*.asOfDate -> NUMERIC field (positions_asOfDate)
287+
$.positions.*.description -> TAG field (positions_description)
288+
----
289+
290+
This structure enables efficient queries across all map values, regardless of their keys.
291+
139292
== Advanced Examples
140293

141-
=== Working with Complex Value Types
294+
=== Working with Other Complex Value Types
142295

143296
[source,java]
144297
----
@@ -326,6 +479,7 @@ List<Entity> findByRegularFieldAndMapFieldMapContains(
326479
* **No partial matching**: String values in maps use TAG indexing (exact match only)
327480
* **GEO queries**: Point values support equality through proximity search with minimal radius
328481
* **Collection values**: Maps with collection-type values are not supported
482+
* **Complex object nesting depth**: While you can query nested fields in complex Map values, deeply nested objects (object within object within map) may have limited query support
329483

330484
== Best Practices
331485

docs/content/modules/ROOT/pages/json_mappings.adoc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ public class Company {
207207

208208
=== Map Field Support
209209

210-
Redis OM Spring provides comprehensive support for `Map<String, T>` fields, enabling dynamic key-value pairs with full indexing and query capabilities:
210+
Redis OM Spring provides comprehensive support for `Map<String, T>` fields, enabling dynamic key-value pairs with full indexing and query capabilities. Starting with version 1.0.0, this includes support for complex object values with queryable nested fields:
211211

212212
[source,java]
213213
----
@@ -227,6 +227,9 @@ public class Product {
227227
228228
@Indexed
229229
private Map<String, LocalDateTime> events; // Temporal data
230+
231+
@Indexed(schemaFieldType = SchemaFieldType.NESTED)
232+
private Map<String, ComplexObject> items; // Complex objects (v1.0.0+)
230233
}
231234
----
232235

@@ -239,6 +242,9 @@ public interface ProductRepository extends RedisDocumentRepository<Product, Stri
239242
List<Product> findByAttributesMapContains(String value);
240243
List<Product> findBySpecificationsMapContainsGreaterThan(Double value);
241244
List<Product> findByFeaturesMapContains(Boolean hasFeature);
245+
246+
// Query nested fields in complex map values (v1.0.0+)
247+
List<Product> findByItemsMapContainsPropertyName(String propertyValue);
242248
}
243249
----
244250

docs/content/modules/ROOT/pages/overview.adoc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ These features are designed for power users, customization, and advanced integra
477477
* JSON operations via `JSONOperations` for low-level JSON document manipulation
478478
* Search operations via `SearchOperations` for direct RediSearch functionality
479479
* Probabilistic data structure operations: `BloomOperations`, `CuckooFilterOperations`, `CountMinSketchOperations`, `TopKOperations`, `TDigestOperations`
480+
* **Transaction Support**: JSON operations automatically participate in Redis transactions when executed within a transaction context
480481

481482
[source,java]
482483
----
@@ -488,6 +489,30 @@ JSONOperations<String> jsonOps = modulesOperations.opsForJSON();
488489
jsonOps.set("obj", myObject);
489490
MyObject retrieved = jsonOps.get("obj", MyObject.class);
490491
492+
// JSON operations with transactions (automatically participates)
493+
@Autowired
494+
private StringRedisTemplate stringRedisTemplate;
495+
496+
List<Object> results = stringRedisTemplate.execute(new SessionCallback<List<Object>>() {
497+
@Override
498+
public List<Object> execute(RedisOperations operations) throws DataAccessException {
499+
operations.watch("obj"); // Watch for concurrent modifications
500+
501+
// Read current value
502+
MyObject current = jsonOps.get("obj", MyObject.class);
503+
504+
// Start transaction
505+
operations.multi();
506+
507+
// JSON operations are automatically queued in the transaction
508+
jsonOps.set("obj", updatedObject);
509+
jsonOps.set("backup", current);
510+
511+
// Execute transaction (returns empty list if watched key was modified)
512+
return operations.exec();
513+
}
514+
});
515+
491516
// Direct search operations
492517
SearchOperations<String> searchOps = modulesOperations.opsForSearch("myIndex");
493518
SearchResult result = searchOps.search(new Query("@name:redis"));

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

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -601,9 +601,63 @@ else if (Map.class.isAssignableFrom(fieldType) && isDocument) {
601601
GeoField geoField = GeoField.of(FieldName.of(mapJsonPath).as(mapFieldAlias));
602602
fields.add(SearchField.of(field, geoField));
603603
logger.info(String.format("Added GEO field for Map: %s as %s", field.getName(), mapFieldAlias));
604+
} else {
605+
// Handle complex object values in Map by recursively indexing their @Indexed fields
606+
logger.info(String.format("Processing complex object Map field: %s with value type %s", field.getName(),
607+
valueType.getName()));
608+
609+
// Recursively process @Indexed fields within the Map value type
610+
for (java.lang.reflect.Field subfield : getDeclaredFieldsTransitively(valueType)) {
611+
if (subfield.isAnnotationPresent(Indexed.class)) {
612+
Indexed subfieldIndexed = subfield.getAnnotation(Indexed.class);
613+
String nestedJsonPath = (prefix == null || prefix.isBlank()) ?
614+
"$." + field.getName() + ".*." + subfield.getName() :
615+
"$." + prefix + "." + field.getName() + ".*." + subfield.getName();
616+
String nestedFieldAlias = field.getName() + "_" + subfield.getName();
617+
618+
logger.info(String.format("Processing nested field %s in Map value type, path: %s, alias: %s",
619+
subfield.getName(), nestedJsonPath, nestedFieldAlias));
620+
621+
Class<?> subfieldType = subfield.getType();
622+
623+
// Create appropriate index field based on subfield type
624+
if (CharSequence.class.isAssignableFrom(
625+
subfieldType) || subfieldType == UUID.class || subfieldType == Ulid.class || subfieldType
626+
.isEnum()) {
627+
// Index as TAG field
628+
TagField tagField = TagField.of(FieldName.of(nestedJsonPath).as(nestedFieldAlias));
629+
if (subfieldIndexed.sortable())
630+
tagField.sortable();
631+
if (subfieldIndexed.indexMissing())
632+
tagField.indexMissing();
633+
if (subfieldIndexed.indexEmpty())
634+
tagField.indexEmpty();
635+
if (!subfieldIndexed.separator().isEmpty()) {
636+
tagField.separator(subfieldIndexed.separator().charAt(0));
637+
}
638+
fields.add(SearchField.of(subfield, tagField));
639+
logger.info(String.format("Added nested TAG field for Map value: %s", nestedFieldAlias));
640+
} else if (Number.class.isAssignableFrom(
641+
subfieldType) || subfieldType == Boolean.class || subfieldType == LocalDateTime.class || subfieldType == LocalDate.class || subfieldType == Date.class || subfieldType == Instant.class || subfieldType == OffsetDateTime.class) {
642+
// Index as NUMERIC field
643+
NumericField numField = NumericField.of(FieldName.of(nestedJsonPath).as(nestedFieldAlias));
644+
if (subfieldIndexed.sortable())
645+
numField.sortable();
646+
if (subfieldIndexed.noindex())
647+
numField.noIndex();
648+
if (subfieldIndexed.indexMissing())
649+
numField.indexMissing();
650+
fields.add(SearchField.of(subfield, numField));
651+
logger.info(String.format("Added nested NUMERIC field for Map value: %s", nestedFieldAlias));
652+
} else if (subfieldType == Point.class) {
653+
// Index as GEO field
654+
GeoField geoField = GeoField.of(FieldName.of(nestedJsonPath).as(nestedFieldAlias));
655+
fields.add(SearchField.of(subfield, geoField));
656+
logger.info(String.format("Added nested GEO field for Map value: %s", nestedFieldAlias));
657+
}
658+
}
659+
}
604660
}
605-
// For complex object values, we could recursively index their fields
606-
// but that would require more complex implementation
607661
}
608662
}
609663
//

0 commit comments

Comments
 (0)