diff --git a/demos/roms-multi-acl-account/src/main/java/com/redis/romsmultiaclaccount/config/RedisConnectionFactoryConfig.java b/demos/roms-multi-acl-account/src/main/java/com/redis/romsmultiaclaccount/config/RedisConnectionFactoryConfig.java
index d904b0a3..8e416c34 100644
--- a/demos/roms-multi-acl-account/src/main/java/com/redis/romsmultiaclaccount/config/RedisConnectionFactoryConfig.java
+++ b/demos/roms-multi-acl-account/src/main/java/com/redis/romsmultiaclaccount/config/RedisConnectionFactoryConfig.java
@@ -27,6 +27,7 @@
import com.redis.om.spring.RedisOMProperties;
import com.redis.om.spring.client.RedisModulesClient;
import com.redis.om.spring.indexing.RediSearchIndexer;
+import com.redis.om.spring.ops.CommandListener;
import com.redis.om.spring.ops.RedisModulesOperations;
import com.redis.om.spring.vectorize.Embedder;
@@ -120,8 +121,8 @@ public RedisModulesOperations> writeRedisModulesOperations(@Qualifier(
"writeJedisConnectionFactory"
) JedisConnectionFactory factory, @Qualifier(
"omGsonBuilder"
- ) GsonBuilder builder) {
- return new RedisModulesOperations<>(client, new StringRedisTemplate(factory), builder);
+ ) GsonBuilder builder, final CommandListener commandListener) {
+ return new RedisModulesOperations<>(client, new StringRedisTemplate(factory), builder, commandListener);
}
@Bean(
@@ -133,8 +134,8 @@ public RedisModulesOperations> readRedisModulesOperations(@Qualifier(
"readJedisConnectionFactory"
) JedisConnectionFactory factory, @Qualifier(
"omGsonBuilder"
- ) GsonBuilder builder) {
- return new RedisModulesOperations<>(client, new StringRedisTemplate(factory), builder);
+ ) GsonBuilder builder, final CommandListener commandListener) {
+ return new RedisModulesOperations<>(client, new StringRedisTemplate(factory), builder, commandListener);
}
@Bean(
diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/RedisModulesConfiguration.java b/redis-om-spring/src/main/java/com/redis/om/spring/RedisModulesConfiguration.java
index f866b479..16b63cf3 100644
--- a/redis-om-spring/src/main/java/com/redis/om/spring/RedisModulesConfiguration.java
+++ b/redis-om-spring/src/main/java/com/redis/om/spring/RedisModulesConfiguration.java
@@ -42,6 +42,8 @@
import com.redis.om.spring.client.RedisModulesClient;
import com.redis.om.spring.indexing.RediSearchIndexer;
import com.redis.om.spring.mapping.RedisEnhancedMappingContext;
+import com.redis.om.spring.ops.CommandListener;
+import com.redis.om.spring.ops.NoOpCommandListener;
import com.redis.om.spring.ops.RedisModulesOperations;
import com.redis.om.spring.ops.json.JSONOperations;
import com.redis.om.spring.ops.pds.BloomOperations;
@@ -199,9 +201,10 @@ RedisModulesClient redisModulesClient( //
* including JSON, Search, Bloom filters, and other probabilistic data structures.
* It serves as the central operations hub for Redis OM Spring functionality.
*
- * @param rmc the Redis modules client for low-level access
- * @param template the string Redis template for basic operations
- * @param gsonBuilder the Gson builder for JSON serialization
+ * @param rmc the Redis modules client for low-level access
+ * @param template the string Redis template for basic operations
+ * @param gsonBuilder the Gson builder for JSON serialization
+ * @param commandListener a command listener for monitoring Redis commands
* @return the Redis modules operations instance
*/
@Bean(
@@ -215,8 +218,25 @@ RedisModulesOperations> redisModulesOperations( //
StringRedisTemplate template, //
@Qualifier(
"omGsonBuilder"
- ) GsonBuilder gsonBuilder) {
- return new RedisModulesOperations<>(rmc, template, gsonBuilder);
+ ) GsonBuilder gsonBuilder, final CommandListener commandListener) {
+ return new RedisModulesOperations<>(rmc, template, gsonBuilder, commandListener);
+ }
+
+ /**
+ * Provides a default implementation of the CommandListener bean.
+ *
+ * This method creates a no-operation (NoOp) implementation of the CommandListener interface.
+ * It is used as a fallback when no other CommandListener bean is defined in the application context.
+ *
+ * The {@code @Fallback} annotation ensures that this bean is only used when no other
+ * CommandListener bean is available, allowing developers to override it with a custom implementation if needed.
+ *
+ * @return a NoOpCommandListener instance, which performs no operations.
+ */
+ @Bean
+ @Fallback
+ public CommandListener commandListener() {
+ return new NoOpCommandListener();
}
/**
diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/ops/CommandListener.java b/redis-om-spring/src/main/java/com/redis/om/spring/ops/CommandListener.java
new file mode 100644
index 00000000..60d41aed
--- /dev/null
+++ b/redis-om-spring/src/main/java/com/redis/om/spring/ops/CommandListener.java
@@ -0,0 +1,165 @@
+package com.redis.om.spring.ops;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.redis.om.spring.autocomplete.Suggestion;
+import com.redis.om.spring.repository.query.autocomplete.AutoCompleteOptions;
+
+import redis.clients.jedis.search.FTCreateParams;
+import redis.clients.jedis.search.FTSearchParams;
+import redis.clients.jedis.search.IndexOptions;
+import redis.clients.jedis.search.Query;
+import redis.clients.jedis.search.Schema;
+import redis.clients.jedis.search.SearchProtocol;
+import redis.clients.jedis.search.SearchResult;
+import redis.clients.jedis.search.aggr.AggregationBuilder;
+import redis.clients.jedis.search.aggr.AggregationResult;
+import redis.clients.jedis.search.schemafields.SchemaField;
+
+public interface CommandListener {
+ default void searchStarted(String indexName, Query q, FTSearchParams params) {
+ }
+
+ default void searchFinished(String indexName, Query q, FTSearchParams params, SearchResult searchResult) {
+ }
+
+ default void createIndexStarted(String indexName, FTCreateParams params, List fields, Schema schema,
+ IndexOptions options) {
+ }
+
+ default void createIndexFinished(String indexName, FTCreateParams params, List fields, Schema schema,
+ IndexOptions options, String result) {
+ }
+
+ default void aggregateStarted(String indexName, AggregationBuilder q) {
+ }
+
+ default void aggregateFinished(String indexName, AggregationBuilder q, AggregationResult result) {
+ }
+
+ default void cursorDeleteStarted(String string, long cursorId) {
+ }
+
+ default void cursorDeleteFinished(String string, long cursorId, String result) {
+ }
+
+ default void cursorReadStarted(String string, long cursorId, int count) {
+ }
+
+ default void cursorReadFinished(String string, long cursorId, int count, AggregationResult aggregationResult) {
+ }
+
+ default void explainStarted(String string, Query q) {
+ }
+
+ default void explainFinished(String string, Query q, String s) {
+ }
+
+ default void infoStarted(String string) {
+ }
+
+ default void infoFinished(String string, Map stringObjectMap) {
+ }
+
+ default void dropIndexStarted(String string) {
+ }
+
+ default void dropIndexFinished(String string, String result) {
+ }
+
+ default void dropIndexAndDocumentsStarted(String string) {
+ }
+
+ default void dropIndexAndDocumentsFinished(String string, String result) {
+ }
+
+ default void addSuggestionStarted(String string, String key, String suggestion, double score) {
+ }
+
+ default void addSuggestionFinished(String string, String key, String suggestion, double score, long result) {
+ }
+
+ default void getSuggestionStarted(String string, String key, String prefix, AutoCompleteOptions options) {
+ }
+
+ default void getSuggestionFinished(String string, String key, String prefix, AutoCompleteOptions options,
+ List list) {
+ }
+
+ default void deleteSuggestionStarted(String string, String key, String entry) {
+ }
+
+ default void deleteSuggestionFinished(String string, String key, String entry, boolean result) {
+ }
+
+ default void getSuggestionLengthStarted(String string, String key) {
+ }
+
+ default void getSuggestionLengthFinished(String string, String key, long result) {
+ }
+
+ default void alterIndexStarted(String string, SchemaField[] fields) {
+ }
+
+ default void alterIndexFinished(String string, SchemaField[] fields, String result) {
+ }
+
+ default void setConfigStarted(String string, String option, String value) {
+ }
+
+ default void setConfigFinished(String string, String option, String value, String result) {
+ }
+
+ default void getConfigStarted(String string, String option) {
+ }
+
+ default void getConfigFinished(String string, String option, Map result) {
+ }
+
+ default void getIndexConfigStarted(String string, String option) {
+ }
+
+ default void getIndexConfigFinished(String string, String option, Map result) {
+ }
+
+ default void addAliasStarted(String string, String name) {
+ }
+
+ default void addAliasFinished(String string, String name, String result) {
+ }
+
+ default void updateAliasStarted(String string, String name) {
+ }
+
+ default void updateAliasFinished(String string, String name, String result) {
+ }
+
+ default void deleteAliasStarted(String string, String name) {
+ }
+
+ default void deleteAliasFinished(String string, String name, String result) {
+ }
+
+ default void updateSynonymStarted(String string, String synonymGroupId, String[] terms) {
+ }
+
+ default void updateSynonymFinished(String string, String synonymGroupId, String[] terms, String result) {
+ }
+
+ default void dumpSynonymStarted(String string) {
+ }
+
+ default void dumpSynonymFinished(String string, Map> result) {
+ }
+
+ default void tagValsStarted(String string, String field) {
+ }
+
+ default void tagValsFinished(String string, String field, Set result) {
+ }
+
+ default void commandFailed(SearchProtocol.SearchCommand command, String indexName, Throwable t) {
+ }
+}
diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/ops/NoOpCommandListener.java b/redis-om-spring/src/main/java/com/redis/om/spring/ops/NoOpCommandListener.java
new file mode 100644
index 00000000..bfead3ea
--- /dev/null
+++ b/redis-om-spring/src/main/java/com/redis/om/spring/ops/NoOpCommandListener.java
@@ -0,0 +1,4 @@
+package com.redis.om.spring.ops;
+
+public class NoOpCommandListener implements CommandListener {
+}
diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/ops/RedisModulesOperations.java b/redis-om-spring/src/main/java/com/redis/om/spring/ops/RedisModulesOperations.java
index 3539058a..2b1b019d 100644
--- a/redis-om-spring/src/main/java/com/redis/om/spring/ops/RedisModulesOperations.java
+++ b/redis-om-spring/src/main/java/com/redis/om/spring/ops/RedisModulesOperations.java
@@ -22,11 +22,12 @@
* and provides typed access to module-specific operations through the {@code opsFor*} methods.
*
*
- * @param the type of keys used in Redis operations
- * @param client the Redis modules client for executing commands
- * @param template the Spring Data Redis template for additional Redis operations
- * @param gsonBuilder the Gson builder for JSON serialization/deserialization configuration
- *
+ * @param the type of keys used in Redis operations
+ * @param client the Redis modules client for executing commands
+ * @param template the Spring Data Redis template for additional Redis operations
+ * @param gsonBuilder the Gson builder for JSON serialization/deserialization configuration
+ * @param commandListener A command listener for monitoring Redis commands
+ *
* @author Redis OM Spring Team
* @see JSONOperations
* @see SearchOperations
@@ -37,7 +38,7 @@
* @see TDigestOperations
*/
public record RedisModulesOperations(RedisModulesClient client, StringRedisTemplate template,
- GsonBuilder gsonBuilder) {
+ GsonBuilder gsonBuilder, CommandListener commandListener) {
/**
* Creates and returns operations for interacting with RedisJSON module.
@@ -65,7 +66,7 @@ public JSONOperations opsForJSON() {
* @return a {@link SearchOperations} instance for search and indexing operations
*/
public SearchOperations opsForSearch(K index) {
- return new SearchOperationsImpl<>(index, client, template);
+ return new SearchOperationsImpl<>(index, client, template, commandListener);
}
/**
diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/ops/search/SearchOperationsImpl.java b/redis-om-spring/src/main/java/com/redis/om/spring/ops/search/SearchOperationsImpl.java
index 19fdc18a..afd766bf 100644
--- a/redis-om-spring/src/main/java/com/redis/om/spring/ops/search/SearchOperationsImpl.java
+++ b/redis-om-spring/src/main/java/com/redis/om/spring/ops/search/SearchOperationsImpl.java
@@ -10,6 +10,7 @@
import com.google.gson.reflect.TypeToken;
import com.redis.om.spring.autocomplete.Suggestion;
import com.redis.om.spring.client.RedisModulesClient;
+import com.redis.om.spring.ops.CommandListener;
import com.redis.om.spring.repository.query.autocomplete.AutoCompleteOptions;
import redis.clients.jedis.resps.Tuple;
@@ -39,84 +40,191 @@ public class SearchOperationsImpl implements SearchOperations {
private final RedisModulesClient modulesClient;
private final K index;
private final StringRedisTemplate template;
+ private final CommandListener commandListener;
/**
* Creates a new search operations implementation.
*
- * @param index the search index identifier
- * @param modulesClient the Redis modules client for search operations
- * @param template the string Redis template for additional operations
+ * @param index the search index identifier
+ * @param modulesClient the Redis modules client for search operations
+ * @param template the string Redis template for additional operations
+ * @param commandListener A command listener for monitoring Redis commands
*/
- public SearchOperationsImpl(K index, RedisModulesClient modulesClient, StringRedisTemplate template) {
+ public SearchOperationsImpl(K index, RedisModulesClient modulesClient, StringRedisTemplate template,
+ final CommandListener commandListener) {
this.index = index;
this.modulesClient = modulesClient;
this.search = modulesClient.clientForSearch();
this.template = template;
+ this.commandListener = commandListener;
}
@Override
public String createIndex(Schema schema, IndexOptions options) {
- return search.ftCreate(index.toString(), options, schema);
+ commandListener.createIndexStarted(index.toString(), null, null, schema, options);
+ String result = null;
+ try {
+ result = search.ftCreate(index.toString(), options, schema);
+ } catch (Exception e) {
+ throw e;
+ } finally {
+ commandListener.createIndexFinished(index.toString(), null, null, schema, options, result);
+ }
+ return result;
}
@Override
public String createIndex(FTCreateParams params, List fields) {
- return search.ftCreate(index.toString(), params, fields);
+ commandListener.createIndexStarted(index.toString(), params, fields, null, null);
+ String result = null;
+ try {
+ result = search.ftCreate(index.toString(), params, fields);
+ } catch (Exception e) {
+ throw e;
+ } finally {
+ commandListener.createIndexFinished(index.toString(), params, fields, null, null, result);
+ }
+ return result;
}
@Override
+ @Deprecated
public SearchResult search(Query q) {
- return search.ftSearch(SafeEncoder.encode(index.toString()), q);
+ commandListener.searchStarted(index.toString(), q, null);
+ SearchResult result = null;
+ try {
+ result = search.ftSearch(SafeEncoder.encode(index.toString()), q);
+ } catch (Exception e) {
+ throw e;
+ } finally {
+ commandListener.searchFinished(index.toString(), q, null, result);
+ }
+ return result;
}
@Override
public SearchResult search(Query q, FTSearchParams params) {
- return search.ftSearch(index.toString(), q.toString(), params);
+ commandListener.searchStarted(index.toString(), q, null);
+ final SearchResult result = search.ftSearch(index.toString(), q.toString(), params);
+ commandListener.searchFinished(index.toString(), q, null, result);
+ return result;
}
@Override
public AggregationResult aggregate(AggregationBuilder q) {
- return search.ftAggregate(index.toString(), q);
+ commandListener.aggregateStarted(index.toString(), q);
+ AggregationResult result = null;
+ try {
+ result = search.ftAggregate(index.toString(), q);
+ } catch (Exception e) {
+ throw e;
+ } finally {
+ commandListener.aggregateFinished(index.toString(), q, result);
+ }
+ return result;
}
@Override
public String cursorDelete(long cursorId) {
- return search.ftCursorDel(index.toString(), cursorId);
+ commandListener.cursorDeleteStarted(index.toString(), cursorId);
+ String result = null;
+ try {
+ result = search.ftCursorDel(index.toString(), cursorId);
+ } catch (Exception e) {
+ throw e;
+ } finally {
+ commandListener.cursorDeleteFinished(index.toString(), cursorId, result);
+ }
+ return result;
}
@Override
public AggregationResult cursorRead(long cursorId, int count) {
- return search.ftCursorRead(index.toString(), cursorId, count);
+ commandListener.cursorReadStarted(index.toString(), cursorId, count);
+ AggregationResult result = null;
+ try {
+ result = search.ftCursorRead(index.toString(), cursorId, count);
+ } catch (Exception e) {
+ throw e;
+ } finally {
+ commandListener.cursorReadFinished(index.toString(), cursorId, count, result);
+ }
+ return result;
}
@Override
public String explain(Query q) {
- return search.ftExplain(index.toString(), q);
+ commandListener.explainStarted(index.toString(), q);
+ String result = null;
+ try {
+ result = search.ftExplain(index.toString(), q);
+ } catch (Exception e) {
+ throw e;
+ } finally {
+ commandListener.explainFinished(index.toString(), q, result);
+ }
+ return result;
}
@Override
public Map getInfo() {
- return search.ftInfo(index.toString());
+ commandListener.infoStarted(index.toString());
+ Map result = Map.of();
+ try {
+ result = search.ftInfo(index.toString());
+ } catch (Exception e) {
+ throw e;
+ } finally {
+ commandListener.infoFinished(index.toString(), result);
+ }
+ return result;
}
@Override
public String dropIndex() {
- return search.ftDropIndex(index.toString());
+ commandListener.dropIndexStarted(index.toString());
+ String result = null;
+ try {
+ result = search.ftDropIndex(index.toString());
+ } catch (Exception e) {
+ throw e;
+ } finally {
+ commandListener.dropIndexFinished(index.toString(), result);
+ }
+ return result;
}
@Override
public String dropIndexAndDocuments() {
- return search.ftDropIndexDD(index.toString());
+ commandListener.dropIndexAndDocumentsStarted(index.toString());
+ String result = null;
+ try {
+ result = search.ftDropIndexDD(index.toString());
+ } catch (Exception e) {
+ throw e;
+ } finally {
+ commandListener.dropIndexAndDocumentsFinished(index.toString(), result);
+ }
+ return result;
}
@Override
public Long addSuggestion(String key, String suggestion) {
- return search.ftSugAdd(key, suggestion, 1.0);
+ return addSuggestion(key, suggestion, 1.0);
}
@Override
public Long addSuggestion(String key, String suggestion, double score) {
- return search.ftSugAdd(key, suggestion, score);
+ commandListener.addSuggestionStarted(index.toString(), key, suggestion, score);
+ long result = 0;
+ try {
+ result = search.ftSugAdd(key, suggestion, score);
+ } catch (Exception e) {
+ throw e;
+ } finally {
+ commandListener.addSuggestionFinished(index.toString(), key, suggestion, score, result);
+ }
+ return result;
}
@Override
@@ -126,101 +234,226 @@ public List getSuggestion(String key, String prefix) {
@Override
public List getSuggestion(String key, String prefix, AutoCompleteOptions options) {
+ commandListener.getSuggestionStarted(index.toString(), key, prefix, options);
Gson gson = modulesClient.gsonBuilder().create();
if (options.isWithScore()) {
List suggestions = search.ftSugGetWithScores(key, prefix, options.isFuzzy(), options.getLimit());
- return suggestions.stream().map(suggestion -> {
- if (options.isWithPayload()) {
- String[] keyParts = key.split(":");
- String payLoadKey = String.format("sugg:payload:%s:%s", keyParts[keyParts.length - 2],
- keyParts[keyParts.length - 1]);
- Object payload = template.opsForHash().get(payLoadKey, suggestion.getElement());
- String json = payload != null ? payload.toString() : "{}";
- Map payloadMap = gson.fromJson(json, new TypeToken