Map Field Mappings
Introduction
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.
Map fields are automatically indexed using Redis JSON path expressions, enabling powerful query capabilities on map values regardless of their keys.
Supported Value Types
Redis OM Spring supports Maps with the following value types:
Basic Types
-
String
- Indexed as TAG fields -
Boolean
- Indexed as NUMERIC fields (stored as 1/0) -
Integer
,Long
,Double
,Float
,BigDecimal
- Indexed as NUMERIC fields -
UUID
,Ulid
- Indexed as TAG fields -
Enum types - Indexed as TAG fields
Basic Usage
Entity Definition with Map Fields
@Data
@Document
public class Product {
@Id
private String id;
@Indexed
private String name;
// Map of string attributes
@Indexed
private Map<String, String> attributes = new HashMap<>();
// Map of numeric specifications
@Indexed
private Map<String, Double> specifications = new HashMap<>();
// Map of boolean features
@Indexed
private Map<String, Boolean> features = new HashMap<>();
// Map of temporal data
@Indexed
private Map<String, LocalDateTime> timestamps = new HashMap<>();
}
Populating Map Fields
Product product = new Product();
product.setName("Smartphone");
// Add string attributes
product.getAttributes().put("brand", "TechCorp");
product.getAttributes().put("model", "X2000");
product.getAttributes().put("color", "Black");
// Add numeric specifications
product.getSpecifications().put("screenSize", 6.5);
product.getSpecifications().put("weight", 175.5);
product.getSpecifications().put("batteryCapacity", 4500.0);
// Add boolean features
product.getFeatures().put("hasNFC", true);
product.getFeatures().put("hasWirelessCharging", true);
product.getFeatures().put("has5G", false);
// Add temporal data
product.getTimestamps().put("manufactured", LocalDateTime.now());
product.getTimestamps().put("lastUpdated", LocalDateTime.now());
productRepository.save(product);
Querying Map Fields
Repository Query Methods
Redis OM Spring provides special query method naming conventions for Map fields using the MapContains
suffix:
public interface ProductRepository extends RedisDocumentRepository<Product, String> {
// Find by string value in map
List<Product> findByAttributesMapContains(String value);
// Find by numeric value in map
List<Product> findBySpecificationsMapContains(Double value);
// Find by boolean value in map
List<Product> findByFeaturesMapContains(Boolean value);
// Numeric comparisons on map values
List<Product> findBySpecificationsMapContainsGreaterThan(Double value);
List<Product> findBySpecificationsMapContainsLessThan(Double value);
// Temporal queries on map values
List<Product> findByTimestampsMapContainsAfter(LocalDateTime date);
List<Product> findByTimestampsMapContainsBefore(LocalDateTime date);
}
Query Examples
// Find products with "TechCorp" as any attribute value
List<Product> techCorpProducts = repository.findByAttributesMapContains("TechCorp");
// Find products with any specification value greater than 1000
List<Product> highSpecProducts = repository.findBySpecificationsMapContainsGreaterThan(1000.0);
// Find products with NFC feature enabled
List<Product> nfcProducts = repository.findByFeaturesMapContains(true);
// Find products updated after a specific date
LocalDateTime lastWeek = LocalDateTime.now().minusWeeks(1);
List<Product> recentlyUpdated = repository.findByTimestampsMapContainsAfter(lastWeek);
Complex Object Values in Maps
Defining Complex Objects as Map Values
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.
// Define the complex object
@Data
public class Position {
@Indexed
private String cusip; // Security identifier
@Indexed
private String description;
@Indexed
private String manager;
@Indexed
private Integer quantity;
@Indexed
private BigDecimal price;
@Indexed
private LocalDate asOfDate;
}
// Use it in a Map field
@Data
@Document
public class Account {
@Id
private String id;
@Indexed
private String accountNumber;
@Indexed
private String accountHolder;
// Map with complex object values
@Indexed(schemaFieldType = SchemaFieldType.NESTED)
private Map<String, Position> positions = new HashMap<>();
@Indexed
private BigDecimal totalValue;
}
Querying Nested Fields in Complex Map Values
Redis OM Spring provides a special query pattern MapContains<NestedField>
for querying nested properties within map values:
public interface AccountRepository extends RedisDocumentRepository<Account, String> {
// Query by nested CUSIP field
List<Account> findByPositionsMapContainsCusip(String cusip);
// Query by nested Manager field
List<Account> findByPositionsMapContainsManager(String manager);
// Numeric comparisons on nested fields
List<Account> findByPositionsMapContainsQuantityGreaterThan(Integer quantity);
List<Account> findByPositionsMapContainsPriceLessThan(BigDecimal price);
// Temporal queries on nested fields
List<Account> findByPositionsMapContainsAsOfDateAfter(LocalDate date);
List<Account> findByPositionsMapContainsAsOfDateBetween(LocalDate start, LocalDate end);
// Combine with regular field queries
List<Account> findByAccountHolderAndPositionsMapContainsManager(
String accountHolder, String manager
);
// Multiple nested field conditions
List<Account> findByPositionsMapContainsCusipAndPositionsMapContainsQuantityGreaterThan(
String cusip, Integer minQuantity
);
}
Usage Example
// Create account with positions
Account account = new Account();
account.setAccountNumber("10190001");
account.setAccountHolder("John Doe");
account.setTotalValue(new BigDecimal("100000.00"));
// Add positions
Position applePosition = new Position();
applePosition.setCusip("AAPL");
applePosition.setDescription("Apple Inc.");
applePosition.setManager("Jane Smith");
applePosition.setQuantity(100);
applePosition.setPrice(new BigDecimal("150.00"));
applePosition.setAsOfDate(LocalDate.now());
account.getPositions().put("AAPL", applePosition);
Position googlePosition = new Position();
googlePosition.setCusip("GOOGL");
googlePosition.setDescription("Alphabet Inc.");
googlePosition.setManager("Bob Johnson");
googlePosition.setQuantity(50);
googlePosition.setPrice(new BigDecimal("2800.00"));
googlePosition.setAsOfDate(LocalDate.now());
account.getPositions().put("GOOGL", googlePosition);
accountRepository.save(account);
// Query examples
// Find all accounts holding Apple stock
List<Account> appleHolders = repository.findByPositionsMapContainsCusip("AAPL");
// Find accounts with positions managed by Jane Smith
List<Account> janesManagedAccounts = repository.findByPositionsMapContainsManager("Jane Smith");
// Find accounts with any position having quantity > 75
List<Account> largePositions = repository.findByPositionsMapContainsQuantityGreaterThan(75);
// Find accounts with positions priced below $200
List<Account> affordablePositions = repository.findByPositionsMapContainsPriceLessThan(
new BigDecimal("200.00")
);
Index Structure
When you use complex objects in Maps, Redis OM Spring creates indexes for each nested field using JSONPath expressions:
// Generated index fields for Map<String, Position>
$.positions.*.cusip -> TAG field (positions_cusip)
$.positions.*.manager -> TAG field (positions_manager)
$.positions.*.quantity -> NUMERIC field (positions_quantity)
$.positions.*.price -> NUMERIC field (positions_price)
$.positions.*.asOfDate -> NUMERIC field (positions_asOfDate)
$.positions.*.description -> TAG field (positions_description)
This structure enables efficient queries across all map values, regardless of their keys.
Handling Uppercase JSON Fields in Complex Map Values
When working with external JSON data that uses uppercase field names, you can use the @Indexed(alias)
annotation on nested object fields within Map values to maintain proper Java naming conventions while preserving the original JSON structure:
// Complex object with uppercase JSON fields
@Data
public class Position {
@Indexed(alias = "CUSIP")
@JsonProperty("CUSIP")
private String cusip;
@Indexed(alias = "QUANTITY")
@JsonProperty("QUANTITY")
private Integer quantity;
@Indexed(alias = "PRICE")
@JsonProperty("PRICE")
private BigDecimal price;
@Indexed
private String manager; // Standard field naming
}
// Entity using the complex object in a Map
@Document
public class Account {
@Id
private String id;
@Indexed(alias = "ACCOUNTID")
@JsonProperty("ACCOUNTID")
private String accountId;
@Indexed(schemaFieldType = SchemaFieldType.NESTED)
private Map<String, Position> positions;
}
// Repository queries work with the alias
public interface AccountRepository extends RedisDocumentRepository<Account, String> {
// Queries the uppercase CUSIP field via alias
List<Account> findByPositionsMapContainsCusip(String cusip);
// Queries the uppercase QUANTITY field via alias
List<Account> findByPositionsMapContainsQuantityGreaterThan(Integer quantity);
// Queries the uppercase PRICE field via alias
List<Account> findByPositionsMapContainsPriceLessThan(BigDecimal price);
}
This approach allows you to: * Maintain clean Java code with standard camelCase naming conventions * Work seamlessly with JSON data that uses uppercase field names * Query nested fields using their aliased names in repository methods * Preserve the original JSON structure for compatibility with external systems
The @Indexed(alias) annotation must match the @JsonProperty value for proper indexing and querying to work.
|
Advanced Examples
Working with Other Complex Value Types
@Data
@Document
public class UserProfile {
@Id
private String id;
@Indexed
private String username;
// UUIDs for external system references
@Indexed
private Map<String, UUID> externalIds = new HashMap<>();
// Enum values for various statuses
@Indexed
private Map<String, Status> statuses = new HashMap<>();
// Geographic locations
@Indexed
private Map<String, Point> locations = new HashMap<>();
// Monetary values with high precision
@Indexed
private Map<String, BigDecimal> balances = new HashMap<>();
public enum Status {
ACTIVE, INACTIVE, PENDING, SUSPENDED
}
}
// Repository interface
public interface UserProfileRepository extends RedisDocumentRepository<UserProfile, String> {
List<UserProfile> findByExternalIdsMapContains(UUID uuid);
List<UserProfile> findByStatusesMapContains(UserProfile.Status status);
List<UserProfile> findByBalancesMapContainsGreaterThan(BigDecimal amount);
}
// Usage example
UserProfile profile = new UserProfile();
profile.setUsername("john_doe");
// Add external IDs
UUID googleId = UUID.randomUUID();
profile.getExternalIds().put("google", googleId);
profile.getExternalIds().put("facebook", UUID.randomUUID());
// Set statuses
profile.getStatuses().put("account", UserProfile.Status.ACTIVE);
profile.getStatuses().put("subscription", UserProfile.Status.PENDING);
// Add locations
profile.getLocations().put("home", new Point(-122.4194, 37.7749)); // San Francisco
profile.getLocations().put("work", new Point(-74.0059, 40.7128)); // New York
// Set balances
profile.getBalances().put("usd", new BigDecimal("1234.56"));
profile.getBalances().put("eur", new BigDecimal("987.65"));
repository.save(profile);
// Query examples
List<UserProfile> googleUsers = repository.findByExternalIdsMapContains(googleId);
List<UserProfile> activeUsers = repository.findByStatusesMapContains(UserProfile.Status.ACTIVE);
List<UserProfile> highBalanceUsers = repository.findByBalancesMapContainsGreaterThan(
new BigDecimal("1000.00")
);
Combining Multiple Map Queries
@Data
@Document
public class Event {
@Id
private String id;
@Indexed
private String name;
@Indexed
private Map<String, String> metadata = new HashMap<>();
@Indexed
private Map<String, Integer> metrics = new HashMap<>();
@Indexed
private Map<String, LocalDateTime> timeline = new HashMap<>();
}
public interface EventRepository extends RedisDocumentRepository<Event, String> {
// Combine multiple map queries
List<Event> findByMetadataMapContainsAndMetricsMapContainsGreaterThan(
String metadataValue, Integer metricThreshold
);
List<Event> findByNameAndTimelineMapContainsAfter(
String name, LocalDateTime after
);
}
Important Considerations
Indexing
-
Map fields must be annotated with
@Indexed
to be searchable -
Each Map field creates a single index for all its values, regardless of keys
-
The index uses JSONPath expressions (e.g.,
$.fieldName.*
) to capture all values
Performance
-
Map value queries search across all values in the map, not specific keys
-
For large maps, consider the performance implications of indexing all values
-
Numeric and temporal comparisons are efficient due to NUMERIC indexing
Type Consistency
-
All values in a Map must be of the same declared type
-
Mixed-type maps are not supported for indexed fields
-
Type conversion follows standard Redis OM Spring serialization rules
Query Patterns
Equality Queries
For exact value matching across all map entries:
// Find entities where any map value equals the parameter
List<Entity> findByMapFieldMapContains(ValueType value);
Range Queries (Numeric/Temporal)
For numeric and temporal value types:
// Greater than
List<Entity> findByMapFieldMapContainsGreaterThan(ValueType value);
// Less than
List<Entity> findByMapFieldMapContainsLessThan(ValueType value);
// Temporal queries
List<Entity> findByMapFieldMapContainsAfter(TemporalType value);
List<Entity> findByMapFieldMapContainsBefore(TemporalType value);
Limitations
-
No key-based queries: You cannot query for specific keys, only values
-
No partial matching: String values in maps use TAG indexing (exact match only)
-
GEO queries: Point values support equality through proximity search with minimal radius
-
Collection values: Maps with collection-type values are not supported
-
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
Best Practices
-
Use meaningful value types: Choose value types that match your query requirements
-
Consider index size: Large maps with many entries will create larger indexes
-
Consistent naming: Use clear, descriptive names for Map fields
-
Initialize maps: Always initialize Map fields to avoid null pointer exceptions
-
Document value semantics: Document what each potential key represents in your maps
Migration Guide
If you’re migrating from a schema with fixed fields to using Maps:
-
Create the Map field with appropriate value type
-
Add
@Indexed
annotation -
Migrate data by populating the Map with key-value pairs
-
Update repository methods to use
MapContains
pattern -
Test queries thoroughly, especially for numeric and temporal types
// Before: Fixed fields
@Document
public class OldProduct {
private String color;
private String size;
private String material;
}
// After: Flexible Map
@Document
public class NewProduct {
@Indexed
private Map<String, String> attributes = new HashMap<>();
}
// Migration code
oldProduct.getColor() -> newProduct.getAttributes().put("color", oldProduct.getColor());
oldProduct.getSize() -> newProduct.getAttributes().put("size", oldProduct.getSize());
oldProduct.getMaterial() -> newProduct.getAttributes().put("material", oldProduct.getMaterial());