-
Notifications
You must be signed in to change notification settings - Fork 101
Dynamic Indexing Feature Design
This document presents implementation options for dynamic indexing in Redis OM Spring, leveraging existing Spring Data Redis infrastructure and Redis OM Spring's current capabilities. All solutions guarantee 100% backward compatibility - existing applications continue working without any changes.
- Dynamic Index Names (#634, #638): Runtime index name generation for tenant isolation and ACL control
- Custom Key Prefixes (#634): Tenant-specific prefixes for data isolation
- ACL Integration (#634): Index naming patterns that work with Redis ACLs for fine-grained access control
- Index Versioning (#26): Support for versioned indexes during schema evolution
- Index Aliasing (#590, #26): Enable blue-green deployments and zero-downtime migrations
- Migration Tools (#26): Maven/Gradle tasks for index management and migration
- Index Maintenance (#26): Tools to export, review, and manage index definitions
- Ephemeral Indexes (#376): Temporary indexes with TTL for transient data
- Index Decoupling (#638): Separate index definition from document definition
- Config Reuse (#332): Use single entity class with multiple index configurations
- CQRS Support (#590): Different indexes for read and write operations
Related issues:
- #634 - Custom index names for ACL control
- #638 - Decouple index and document definitions
- #590 - Blue-green deployment support
- #376 - Ephemeral indexes
- #332 - Multiple configs with single class
- #26 - Index maintenance and migrations
Spring Data Redis provides an index infrastructure in org.springframework.data.redis.core.index
:
// Core interfaces we can leverage
public interface IndexDefinition {
String getKeyspace();
Collection<Condition<?>> getConditions();
IndexValueTransformer valueTransformer();
String getIndexName();
}
public interface IndexDefinitionProvider {
boolean hasIndexFor(Serializable keyspace);
Set<IndexDefinition> getIndexDefinitionsFor(Serializable keyspace);
}
public interface ConfigurableIndexDefinitionProvider
extends IndexDefinitionProvider, IndexDefinitionRegistry {
// Allows dynamic registration of index definitions
}
Redis OM Spring has its own parallel indexing system:
- RediSearchIndexer: Manages RediSearch indexes independently
- IndexingOptions: Controls index creation behavior
-
IndexCreationMode:
SKIP_IF_EXIST
,DROP_AND_RECREATE
,SKIP_ALWAYS
-
Minimal Integration: Uses empty
IndexConfiguration()
in repository setup
Create a bridge between Redis OM's RediSearchIndexer and Spring Data Redis's ConfigurableIndexDefinitionProvider:
// NEW: Bridge class implementing Spring Data Redis interface
@Component
public class RediSearchIndexDefinitionProvider
implements ConfigurableIndexDefinitionProvider {
private final RediSearchIndexer indexer;
private final Map<String, Set<RediSearchIndexDefinition>> definitions;
@Override
public Set<IndexDefinition> getIndexDefinitionsFor(Serializable keyspace) {
// Bridge to RediSearchIndexer's index definitions
Class<?> entityClass = indexer.getEntityClassForKeyspace(keyspace.toString());
if (entityClass == null) return Collections.emptySet();
// Convert RediSearch schema to IndexDefinition
return convertToIndexDefinitions(entityClass);
}
@Override
public void addIndexDefinition(IndexDefinition definition) {
// Allow runtime registration of new index definitions
if (definition instanceof RediSearchIndexDefinition) {
registerDynamicIndex((RediSearchIndexDefinition) definition);
}
}
}
// NEW: Custom IndexDefinition for RediSearch
public class RediSearchIndexDefinition implements IndexDefinition {
private final String indexName;
private final String keyspace;
private final List<SchemaField> schemaFields;
private final IndexNamingStrategy namingStrategy;
@Override
public String getIndexName() {
// Can be dynamic based on context
RedisIndexContext context = RedisIndexContextHolder.getContext();
return namingStrategy != null && context != null
? namingStrategy.getIndexName(keyspace, context)
: indexName;
}
}
- ✅ Integrates with Spring Data Redis infrastructure
- ✅ Allows runtime index registration
- ✅ Maintains RediSearch-specific features
- ✅ Zero breaking changes
- ❌ Complexity of bridging two systems
- ❌ Potential performance overhead
Extend the existing IndexingOptions
annotation to support dynamic resolution:
// ENHANCE: Existing annotation with new capabilities
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface IndexingOptions {
String indexName() default "";
// NEW: Support for SpEL expressions
String dynamicIndexName() default "";
// NEW: Support for custom naming strategy
Class<? extends IndexNamingStrategy> namingStrategy() default DefaultNamingStrategy.class;
// Existing
IndexCreationMode creationMode() default IndexCreationMode.SKIP_IF_EXIST;
// NEW: Support for runtime index creation
boolean allowRuntimeCreation() default false;
}
// NEW: Interface for dynamic naming
public interface IndexNamingStrategy {
String resolveIndexName(Class<?> entityClass, String baseIndexName);
String resolveKeyPrefix(Class<?> entityClass, String basePrefix);
}
// ENHANCE: RediSearchIndexer to support dynamic resolution
public class RediSearchIndexer {
private final Map<Class<?>, IndexNamingStrategy> namingStrategies = new ConcurrentHashMap<>();
public void createIndexFor(Class<?> cl) {
IndexingOptions options = cl.getAnnotation(IndexingOptions.class);
// Dynamic index name resolution
String indexName;
if (options != null && !options.dynamicIndexName().isEmpty()) {
// Evaluate SpEL expression
indexName = evaluateExpression(options.dynamicIndexName(), cl);
} else if (options != null && options.namingStrategy() != DefaultNamingStrategy.class) {
// Use custom naming strategy
IndexNamingStrategy strategy = getOrCreateStrategy(options.namingStrategy());
indexName = strategy.resolveIndexName(cl, options.indexName());
} else {
// Current behavior
indexName = getCurrentIndexName(cl, options);
}
// Rest remains the same...
}
}
- ✅ Minimal changes to existing code
- ✅ Leverages existing annotation infrastructure
- ✅ Clear migration path
- ✅ SpEL support for simple cases
- ❌ Limited to compile-time configuration
- ❌ Cannot change strategy at runtime
Enhance RediSearchIndexer to be context-aware while maintaining backward compatibility:
// NEW: Context for index resolution
public class RedisIndexContext {
private final Map<String, Object> attributes;
private final String tenantId;
private final String environment;
// ThreadLocal management
private static final ThreadLocal<RedisIndexContext> CONTEXT = new ThreadLocal<>();
public static void setContext(RedisIndexContext context) {
CONTEXT.set(context);
}
public static RedisIndexContext getContext() {
return CONTEXT.get();
}
}
// ENHANCE: RediSearchIndexer with context support
@Component
public class RediSearchIndexer {
// NEW: Index resolver for dynamic resolution
private IndexResolver indexResolver = new DefaultIndexResolver();
public String getIndexName(Class<?> entityClass) {
// Check for context first
RedisIndexContext context = RedisIndexContext.getContext();
if (context != null) {
// Dynamic resolution based on context
return indexResolver.resolveIndexName(entityClass, context);
}
// Fall back to current behavior
return entityClassToIndexName.get(entityClass);
}
// NEW: Support for runtime index creation
public void createIndexForContext(Class<?> entityClass, RedisIndexContext context) {
String contextualIndexName = indexResolver.resolveIndexName(entityClass, context);
// Check if index already exists for this context
if (!indexExistsForContext(entityClass, context)) {
// Create index with context-specific name and prefix
createContextualIndex(entityClass, context, contextualIndexName);
}
}
}
// NEW: Interface for index resolution
public interface IndexResolver {
String resolveIndexName(Class<?> entityClass, RedisIndexContext context);
String resolveKeyPrefix(Class<?> entityClass, RedisIndexContext context);
}
- ✅ True runtime dynamism
- ✅ Clean separation of concerns
- ✅ Thread-safe context handling
- ✅ Extensible resolver pattern
- ❌ Requires context management
- ❌ More complex than annotation-based
Support versioned indexes for zero-downtime schema migrations:
@Document("users")
@IndexingOptions(
indexName = "users_idx",
versioningStrategy = IndexVersioningStrategy.TIMESTAMP,
aliasName = "users_current" // Alias always points to active version
)
public class User {
// When schema changes, new index created as users_idx_20240115_1420
// Alias 'users_current' switches after data migration
}
@Component
public class IndexMigrationService {
public void migrateIndex(Class<?> entityClass, MigrationStrategy strategy) {
// 1. Create new versioned index
String newIndexName = createVersionedIndex(entityClass);
// 2. Migrate data (configurable strategy)
switch (strategy) {
case DUAL_WRITE:
enableDualWrite(entityClass, newIndexName);
reindexInBackground(entityClass, newIndexName);
break;
case BLUE_GREEN:
reindexToNewIndex(entityClass, newIndexName);
switchAlias(entityClass, newIndexName);
break;
}
// 3. Verify and cleanup
verifyIndexIntegrity(newIndexName);
scheduleOldIndexCleanup(entityClass);
}
}
<plugin>
<groupId>com.redis.om</groupId>
<artifactId>redis-om-spring-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>export-indexes</goal> <!-- Export index definitions -->
<goal>validate-indexes</goal> <!-- Validate against Redis -->
<goal>migrate-indexes</goal> <!-- Run migrations -->
</goals>
</execution>
</executions>
</plugin>
task exportIndexes(type: RedisIndexExportTask) {
outputDir = file("$buildDir/redis-indexes")
format = 'json' // or 'redis-cli'
}
task migrateIndexes(type: RedisIndexMigrationTask) {
migrationDir = file("src/main/resources/redis/migrations")
strategy = 'blue-green'
}
Support index naming patterns that work with Redis ACLs:
public class AclAwareIndexNamingStrategy implements IndexNamingStrategy {
// Generate: <env>:<service>:<tenant>:<entity>:<version>
// Example: prod:user-service:tenant123:users:v1
@Override
public String resolveIndexName(Class<?> entityClass, RedisIndexContext context) {
return String.format("%s:%s:%s:%s:v%d",
context.getEnvironment(), // prod, staging, dev
context.getServiceName(), // microservice name
context.getTenantId(), // tenant identifier
entityClass.getSimpleName().toLowerCase(),
context.getSchemaVersion()
);
}
}
// Redis ACL configuration
// ACL SETUSER tenant123 ~prod:user-service:tenant123:* &prod:user-service:tenant123:* +@read +@write
@Configuration
public class TenantIndexConfiguration {
@Bean
public IndexNamingStrategy tenantAwareStrategy() {
return new TenantAwareIndexNamingStrategy();
}
@Bean
public TenantContextResolver tenantResolver() {
return () -> {
// Extract from JWT/OAuth2 token
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return ((OAuth2Authentication) auth).getTenantId();
};
}
}
Combine the best aspects of each option:
@Document("users")
@IndexingOptions(
indexName = "users_idx",
dynamicIndexName = "#{T(com.example.TenantContext).currentTenant + '_users_idx'}",
creationMode = IndexCreationMode.SKIP_IF_EXIST
)
public class User {
// ...
}
// For programmatic control
@Service
public class TenantAwareService {
@Autowired
private RediSearchIndexer indexer;
public void setupTenantIndex(String tenantId) {
RedisIndexContext context = RedisIndexContext.builder()
.tenantId(tenantId)
.build();
// Create tenant-specific index
indexer.createIndexForContext(User.class, context);
}
}
Implement ConfigurableIndexDefinitionProvider to fully integrate with Spring Data Redis ecosystem.
// Existing code works exactly as today
@Document("products")
public class Product {
@Id private String id;
@Indexed private String name;
}
// No changes needed to repositories
@Repository
public interface ProductRepository extends RedisDocumentRepository<Product, String> {
List<Product> findByName(String name);
}
// Only when explicitly configured
@Document("orders")
@IndexingOptions(
dynamicIndexName = "#{@tenantService.currentTenant + '_orders_idx'}"
)
public class Order {
// Dynamic index only when SpEL expression is present
}
- Enhance IndexingOptions with SpEL support
- Add IndexNamingStrategy interface
- Update RediSearchIndexer to evaluate dynamic expressions
- Implement RedisIndexContext
- Add context-aware methods to RediSearchIndexer
- Create IndexResolver interface and implementations
- Support for runtime index creation
- Index aliasing support
- Migration tools
- Implement ConfigurableIndexDefinitionProvider
- Bridge RediSearch indexes with Spring Data Redis
- Full integration testing
@Test
public void existingCodeWorksUnchanged() {
// All existing tests must pass without modification
}
@Test
public void dynamicIndexNameResolution() {
// Test SpEL evaluation in IndexingOptions
}
@Test
public void contextAwareIndexing() {
// Test context-based index resolution
}
@Test
public void multiTenantScenario() {
// Test isolation between tenants
}
Feature | Redis OM Spring | MongoDB | Elasticsearch | JPA |
---|---|---|---|---|
Dynamic Collection/Index Names | Via SpEL + Context | Template methods | SpEL in @Document | N/A |
Multi-tenancy | Context-based | Database switching | Index per tenant | Schema/Discriminator |
Runtime Creation | Supported | Supported | Supported | Limited |
@Document("users")
@IndexingOptions(
namingStrategy = TenantAwareIndexNamingStrategy.class,
creationMode = IndexCreationMode.SKIP_IF_EXIST
)
public class User {
@Id private String id;
@Indexed private String tenantId;
@Indexed private String email;
@Searchable private String name;
}
@RestController
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping("/users/{id}")
public User getUser(@PathVariable String id,
@AuthenticationPrincipal OAuth2User principal) {
// Context automatically set from OAuth2 token
String tenantId = principal.getAttribute("tenant_id");
try (var context = RedisIndexContext.withTenant(tenantId)) {
// Repository uses tenant-specific index: tenant123_users_idx
return userRepository.findById(id).orElseThrow();
}
}
}
@Service
public class DeploymentService {
@Autowired
private IndexMigrationService migrationService;
public void performBlueGreenDeployment() {
// 1. Create new "green" index with updated schema
String greenIndex = "products_idx_green_v2";
createIndexWithNewSchema(Product.class, greenIndex);
// 2. Dual-write to both indexes
enableDualWrite("products_idx_blue_v1", greenIndex);
// 3. Backfill green index
reindexInBackground(Product.class, greenIndex);
// 4. Switch alias atomically
switchAlias("products_current", greenIndex);
// 5. Cleanup old index after verification
scheduleCleanup("products_idx_blue_v1", Duration.ofHours(24));
}
}
@Component
public class BatchProcessor {
@Autowired
private RediSearchIndexer indexer;
public void processBatch(List<Order> orders) {
// Create temporary index for batch
String tempIndex = String.format("batch_%s_idx", UUID.randomUUID());
RedisIndexContext context = RedisIndexContext.builder()
.attribute("indexName", tempIndex)
.attribute("ttl", Duration.ofHours(2))
.build();
// Create ephemeral index
indexer.createIndexForContext(Order.class, context);
try {
// Process batch with dedicated index
processBatchWithIndex(orders, tempIndex);
} finally {
// Auto-cleanup after TTL or manual cleanup
indexer.dropIndex(tempIndex);
}
}
}
@Document("events")
@IndexingOptions(
indexName = "events_write_idx",
readIndexName = "events_read_idx", // Optimized for queries
cqrsMode = true
)
public class Event {
@Id private String id;
@Indexed private Instant timestamp;
@Indexed private String type;
@Searchable private String description;
}
@Service
public class EventService {
@Autowired
private EventRepository repository;
public void writeEvent(Event event) {
// Writes go to write-optimized index
repository.save(event);
// Async projection to read index
projectToReadIndex(event);
}
public List<Event> searchEvents(String query) {
// Queries use read-optimized index with materialized views
return repository.searchFromReadIndex(query);
}
}
This design addresses all community requirements by:
- Multi-tenancy & Security: Dynamic index naming with ACL support (#634, #638)
- DevOps & Lifecycle: Versioning, aliasing, and migration tools (#26, #590)
- Flexibility: Ephemeral indexes and config reuse (#376, #332)
- Spring Integration: Leveraging existing Spring Data Redis infrastructure
The solution provides:
- 100% backward compatibility - existing applications work unchanged
- Gradual adoption - features can be enabled incrementally
- Production-ready patterns - blue-green deployments, CQRS, multi-tenancy
- DevOps friendly - Maven/Gradle plugins for index management
All changes build upon Redis OM Spring's existing infrastructure (IndexingOptions, IndexCreationMode, RediSearchIndexer) while adding the dynamic capabilities the community needs.