Skip to content

Commit 3d8b53f

Browse files
committed
feat: add comprehensive Map field support for JSON documents (#348)
Implement full Map<String, T> field support for Redis JSON documents with: - Support for all value types: String, Boolean, numeric types, temporal types, UUID, Ulid, enums, and Point - Automatic indexing using JSONPath expressions ($.fieldName.*) - Repository query methods using MapContains pattern - EntityStream support for Map field queries - Metamodel generation for Map VALUES fields - Custom Gson serializers for Boolean values (1/0 format) - Comprehensive test coverage for all Map value types - Complete documentation with examples and best practices Note: Boolean values in Map fields are indexed as NUMERIC fields for consistency with Redis JSON serialization
1 parent cdd74c9 commit 3d8b53f

File tree

22 files changed

+1787
-10
lines changed

22 files changed

+1787
-10
lines changed

docs/content/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* xref:json_mappings.adoc[Redis JSON Basics]
2121
* xref:document-annotation.adoc[Document Annotation]
2222
* xref:json-repositories.adoc[Document Repositories]
23+
* xref:json-map-fields.adoc[Map Field Mappings]
2324
2425
.Indexing and Search
2526
* xref:search.adoc[Redis Query Engine Integration]
Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
= Map Field Mappings
2+
:page-toclevels: 3
3+
:experimental:
4+
:source-highlighter: highlight.js
5+
6+
== Introduction
7+
8+
Redis OM Spring provides comprehensive support for `Map<String, T>` fields in JSON documents, allowing you to store dynamic key-value pairs where keys are strings and values can be of various types. This feature is particularly useful for storing flexible, schema-less data within your entities.
9+
10+
Map fields are automatically indexed using Redis JSON path expressions, enabling powerful query capabilities on map values regardless of their keys.
11+
12+
== Supported Value Types
13+
14+
Redis OM Spring supports Maps with the following value types:
15+
16+
=== Basic Types
17+
* `String` - Indexed as TAG fields
18+
* `Boolean` - Indexed as NUMERIC fields (stored as 1/0)
19+
* `Integer`, `Long`, `Double`, `Float`, `BigDecimal` - Indexed as NUMERIC fields
20+
* `UUID`, `Ulid` - Indexed as TAG fields
21+
* Enum types - Indexed as TAG fields
22+
23+
=== Temporal Types
24+
* `LocalDateTime`, `LocalDate` - Indexed as NUMERIC fields
25+
* `Date`, `Instant`, `OffsetDateTime` - Indexed as NUMERIC fields (epoch milliseconds)
26+
27+
=== Spatial Types
28+
* `Point` - Indexed as GEO fields for spatial queries
29+
30+
== Basic Usage
31+
32+
=== Entity Definition with Map Fields
33+
34+
[source,java]
35+
----
36+
@Data
37+
@Document
38+
public class Product {
39+
@Id
40+
private String id;
41+
42+
@Indexed
43+
private String name;
44+
45+
// Map of string attributes
46+
@Indexed
47+
private Map<String, String> attributes = new HashMap<>();
48+
49+
// Map of numeric specifications
50+
@Indexed
51+
private Map<String, Double> specifications = new HashMap<>();
52+
53+
// Map of boolean features
54+
@Indexed
55+
private Map<String, Boolean> features = new HashMap<>();
56+
57+
// Map of temporal data
58+
@Indexed
59+
private Map<String, LocalDateTime> timestamps = new HashMap<>();
60+
}
61+
----
62+
63+
=== Populating Map Fields
64+
65+
[source,java]
66+
----
67+
Product product = new Product();
68+
product.setName("Smartphone");
69+
70+
// Add string attributes
71+
product.getAttributes().put("brand", "TechCorp");
72+
product.getAttributes().put("model", "X2000");
73+
product.getAttributes().put("color", "Black");
74+
75+
// Add numeric specifications
76+
product.getSpecifications().put("screenSize", 6.5);
77+
product.getSpecifications().put("weight", 175.5);
78+
product.getSpecifications().put("batteryCapacity", 4500.0);
79+
80+
// Add boolean features
81+
product.getFeatures().put("hasNFC", true);
82+
product.getFeatures().put("hasWirelessCharging", true);
83+
product.getFeatures().put("has5G", false);
84+
85+
// Add temporal data
86+
product.getTimestamps().put("manufactured", LocalDateTime.now());
87+
product.getTimestamps().put("lastUpdated", LocalDateTime.now());
88+
89+
productRepository.save(product);
90+
----
91+
92+
== Querying Map Fields
93+
94+
=== Repository Query Methods
95+
96+
Redis OM Spring provides special query method naming conventions for Map fields using the `MapContains` suffix:
97+
98+
[source,java]
99+
----
100+
public interface ProductRepository extends RedisDocumentRepository<Product, String> {
101+
102+
// Find by string value in map
103+
List<Product> findByAttributesMapContains(String value);
104+
105+
// Find by numeric value in map
106+
List<Product> findBySpecificationsMapContains(Double value);
107+
108+
// Find by boolean value in map
109+
List<Product> findByFeaturesMapContains(Boolean value);
110+
111+
// Numeric comparisons on map values
112+
List<Product> findBySpecificationsMapContainsGreaterThan(Double value);
113+
List<Product> findBySpecificationsMapContainsLessThan(Double value);
114+
115+
// Temporal queries on map values
116+
List<Product> findByTimestampsMapContainsAfter(LocalDateTime date);
117+
List<Product> findByTimestampsMapContainsBefore(LocalDateTime date);
118+
}
119+
----
120+
121+
=== Query Examples
122+
123+
[source,java]
124+
----
125+
// Find products with "TechCorp" as any attribute value
126+
List<Product> techCorpProducts = repository.findByAttributesMapContains("TechCorp");
127+
128+
// Find products with any specification value greater than 1000
129+
List<Product> highSpecProducts = repository.findBySpecificationsMapContainsGreaterThan(1000.0);
130+
131+
// Find products with NFC feature enabled
132+
List<Product> nfcProducts = repository.findByFeaturesMapContains(true);
133+
134+
// Find products updated after a specific date
135+
LocalDateTime lastWeek = LocalDateTime.now().minusWeeks(1);
136+
List<Product> recentlyUpdated = repository.findByTimestampsMapContainsAfter(lastWeek);
137+
----
138+
139+
== Advanced Examples
140+
141+
=== Working with Complex Value Types
142+
143+
[source,java]
144+
----
145+
@Data
146+
@Document
147+
public class UserProfile {
148+
@Id
149+
private String id;
150+
151+
@Indexed
152+
private String username;
153+
154+
// UUIDs for external system references
155+
@Indexed
156+
private Map<String, UUID> externalIds = new HashMap<>();
157+
158+
// Enum values for various statuses
159+
@Indexed
160+
private Map<String, Status> statuses = new HashMap<>();
161+
162+
// Geographic locations
163+
@Indexed
164+
private Map<String, Point> locations = new HashMap<>();
165+
166+
// Monetary values with high precision
167+
@Indexed
168+
private Map<String, BigDecimal> balances = new HashMap<>();
169+
170+
public enum Status {
171+
ACTIVE, INACTIVE, PENDING, SUSPENDED
172+
}
173+
}
174+
----
175+
176+
[source,java]
177+
----
178+
// Repository interface
179+
public interface UserProfileRepository extends RedisDocumentRepository<UserProfile, String> {
180+
List<UserProfile> findByExternalIdsMapContains(UUID uuid);
181+
List<UserProfile> findByStatusesMapContains(UserProfile.Status status);
182+
List<UserProfile> findByBalancesMapContainsGreaterThan(BigDecimal amount);
183+
}
184+
185+
// Usage example
186+
UserProfile profile = new UserProfile();
187+
profile.setUsername("john_doe");
188+
189+
// Add external IDs
190+
UUID googleId = UUID.randomUUID();
191+
profile.getExternalIds().put("google", googleId);
192+
profile.getExternalIds().put("facebook", UUID.randomUUID());
193+
194+
// Set statuses
195+
profile.getStatuses().put("account", UserProfile.Status.ACTIVE);
196+
profile.getStatuses().put("subscription", UserProfile.Status.PENDING);
197+
198+
// Add locations
199+
profile.getLocations().put("home", new Point(-122.4194, 37.7749)); // San Francisco
200+
profile.getLocations().put("work", new Point(-74.0059, 40.7128)); // New York
201+
202+
// Set balances
203+
profile.getBalances().put("usd", new BigDecimal("1234.56"));
204+
profile.getBalances().put("eur", new BigDecimal("987.65"));
205+
206+
repository.save(profile);
207+
208+
// Query examples
209+
List<UserProfile> googleUsers = repository.findByExternalIdsMapContains(googleId);
210+
List<UserProfile> activeUsers = repository.findByStatusesMapContains(UserProfile.Status.ACTIVE);
211+
List<UserProfile> highBalanceUsers = repository.findByBalancesMapContainsGreaterThan(
212+
new BigDecimal("1000.00")
213+
);
214+
----
215+
216+
=== Combining Multiple Map Queries
217+
218+
[source,java]
219+
----
220+
@Data
221+
@Document
222+
public class Event {
223+
@Id
224+
private String id;
225+
226+
@Indexed
227+
private String name;
228+
229+
@Indexed
230+
private Map<String, String> metadata = new HashMap<>();
231+
232+
@Indexed
233+
private Map<String, Integer> metrics = new HashMap<>();
234+
235+
@Indexed
236+
private Map<String, LocalDateTime> timeline = new HashMap<>();
237+
}
238+
239+
public interface EventRepository extends RedisDocumentRepository<Event, String> {
240+
// Combine multiple map queries
241+
List<Event> findByMetadataMapContainsAndMetricsMapContainsGreaterThan(
242+
String metadataValue, Integer metricThreshold
243+
);
244+
245+
List<Event> findByNameAndTimelineMapContainsAfter(
246+
String name, LocalDateTime after
247+
);
248+
}
249+
----
250+
251+
== Important Considerations
252+
253+
=== Indexing
254+
255+
* Map fields must be annotated with `@Indexed` to be searchable
256+
* Each Map field creates a single index for all its values, regardless of keys
257+
* The index uses JSONPath expressions (e.g., `$.fieldName.*`) to capture all values
258+
259+
=== Performance
260+
261+
* Map value queries search across all values in the map, not specific keys
262+
* For large maps, consider the performance implications of indexing all values
263+
* Numeric and temporal comparisons are efficient due to NUMERIC indexing
264+
265+
=== Type Consistency
266+
267+
* All values in a Map must be of the same declared type
268+
* Mixed-type maps are not supported for indexed fields
269+
* Type conversion follows standard Redis OM Spring serialization rules
270+
271+
=== Temporal Precision
272+
273+
* Date/time values may experience precision loss during serialization
274+
* Millisecond precision is preserved for most temporal types
275+
* Consider using tolerance when comparing temporal values in tests
276+
277+
=== Boolean Values
278+
279+
* Boolean values in Maps are indexed as NUMERIC fields (1 for true, 0 for false)
280+
* This differs from regular Boolean entity fields, which are indexed as TAG fields
281+
* Queries work transparently with both `true`/`false` parameters
282+
283+
== Query Patterns
284+
285+
=== Equality Queries
286+
287+
For exact value matching across all map entries:
288+
289+
[source,java]
290+
----
291+
// Find entities where any map value equals the parameter
292+
List<Entity> findByMapFieldMapContains(ValueType value);
293+
----
294+
295+
=== Range Queries (Numeric/Temporal)
296+
297+
For numeric and temporal value types:
298+
299+
[source,java]
300+
----
301+
// Greater than
302+
List<Entity> findByMapFieldMapContainsGreaterThan(ValueType value);
303+
304+
// Less than
305+
List<Entity> findByMapFieldMapContainsLessThan(ValueType value);
306+
307+
// Temporal queries
308+
List<Entity> findByMapFieldMapContainsAfter(TemporalType value);
309+
List<Entity> findByMapFieldMapContainsBefore(TemporalType value);
310+
----
311+
312+
=== Combining with Other Fields
313+
314+
Map queries can be combined with regular field queries:
315+
316+
[source,java]
317+
----
318+
List<Entity> findByRegularFieldAndMapFieldMapContains(
319+
String regularValue, MapValueType mapValue
320+
);
321+
----
322+
323+
== Limitations
324+
325+
* **No key-based queries**: You cannot query for specific keys, only values
326+
* **No partial matching**: String values in maps use TAG indexing (exact match only)
327+
* **GEO queries**: Point values support equality through proximity search with minimal radius
328+
* **Collection values**: Maps with collection-type values are not supported
329+
330+
== Best Practices
331+
332+
1. **Use meaningful value types**: Choose value types that match your query requirements
333+
2. **Consider index size**: Large maps with many entries will create larger indexes
334+
3. **Consistent naming**: Use clear, descriptive names for Map fields
335+
4. **Initialize maps**: Always initialize Map fields to avoid null pointer exceptions
336+
5. **Document value semantics**: Document what each potential key represents in your maps
337+
338+
== Migration Guide
339+
340+
If you're migrating from a schema with fixed fields to using Maps:
341+
342+
1. Create the Map field with appropriate value type
343+
2. Add `@Indexed` annotation
344+
3. Migrate data by populating the Map with key-value pairs
345+
4. Update repository methods to use `MapContains` pattern
346+
5. Test queries thoroughly, especially for numeric and temporal types
347+
348+
[source,java]
349+
----
350+
// Before: Fixed fields
351+
@Document
352+
public class OldProduct {
353+
private String color;
354+
private String size;
355+
private String material;
356+
}
357+
358+
// After: Flexible Map
359+
@Document
360+
public class NewProduct {
361+
@Indexed
362+
private Map<String, String> attributes = new HashMap<>();
363+
}
364+
365+
// Migration code
366+
oldProduct.getColor() -> newProduct.getAttributes().put("color", oldProduct.getColor());
367+
oldProduct.getSize() -> newProduct.getAttributes().put("size", oldProduct.getSize());
368+
oldProduct.getMaterial() -> newProduct.getAttributes().put("material", oldProduct.getMaterial());
369+
----
370+
371+
== Conclusion
372+
373+
Map field support in Redis OM Spring provides a powerful way to handle dynamic, schema-less data within your Redis JSON documents. With comprehensive type support and intuitive query methods, you can build flexible data models while maintaining full search capabilities.

0 commit comments

Comments
 (0)