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..edc522e2 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 @@ -8,6 +8,8 @@ import java.util.List; import java.util.Set; +import com.redis.om.spring.ops.CommandListener; +import com.redis.om.spring.ops.NoOpCommandListener; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -202,6 +204,7 @@ RedisModulesClient redisModulesClient( // * @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,10 +218,28 @@ 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(); + } + + /** * Creates the JSON operations bean for RedisJSON commands. *

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..ecc96fcc --- /dev/null +++ b/redis-om-spring/src/main/java/com/redis/om/spring/ops/CommandListener.java @@ -0,0 +1,90 @@ +package com.redis.om.spring.ops; + +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; +import java.util.List; +import java.util.Map; +import java.util.Set; + +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){} + + 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..170e6125 --- /dev/null +++ b/redis-om-spring/src/main/java/com/redis/om/spring/ops/NoOpCommandListener.java @@ -0,0 +1,7 @@ +package com.redis.om.spring.ops; + +import org.springframework.stereotype.Component; + +@Component +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..19c564ba 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 @@ -26,7 +26,8 @@ * @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 ba9d706a..1c77e90f 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 @@ -3,7 +3,7 @@ import java.util.List; import java.util.Map; import java.util.Set; - +import com.redis.om.spring.ops.CommandListener; import org.springframework.data.redis.core.StringRedisTemplate; import com.google.gson.Gson; @@ -39,6 +39,7 @@ 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. @@ -46,77 +47,117 @@ public class SearchOperationsImpl implements SearchOperations { * @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); - } + @Override + public String createIndex(Schema schema, IndexOptions options) { + commandListener.createIndexStarted(index.toString(), null, null, schema, options); + final String s = search.ftCreate(index.toString(), options, schema); + commandListener.createIndexFinished(index.toString(), null, null, schema, options, s); + return s; + } - @Override - public String createIndex(FTCreateParams params, List fields) { - return search.ftCreate(index.toString(), params, fields); - } + @Override + public String createIndex(FTCreateParams params, List fields) { + commandListener.createIndexStarted(index.toString(), params, fields, null, null); + final String s = search.ftCreate(index.toString(), params, fields); + commandListener.createIndexFinished(index.toString(), params, fields, null, null, s); + return s; + } - @Override - public SearchResult search(Query q) { - return search.ftSearch(SafeEncoder.encode(index.toString()), q); - } + @Override + @Deprecated + public SearchResult search(Query q) { + commandListener.searchStarted(index.toString(), q, null); + final SearchResult searchResult = search.ftSearch(SafeEncoder.encode(index.toString()), q); + commandListener.searchFinished(index.toString(), q, null, searchResult); + return searchResult; + } - @Override - public SearchResult search(Query q, FTSearchParams params) { - return search.ftSearch(index.toString(), q.toString(), params); - } + @Override + public SearchResult search(Query q, FTSearchParams params) { + commandListener.searchStarted(index.toString(), q, null); + final SearchResult searchResult = search.ftSearch(index.toString(), q.toString(), params); + commandListener.searchFinished(index.toString(), q, null, searchResult); + return searchResult; + } - @Override - public AggregationResult aggregate(AggregationBuilder q) { - return search.ftAggregate(index.toString(), q); - } + @Override + public AggregationResult aggregate(AggregationBuilder q) { + commandListener.aggregateStarted(index.toString(), q); + final AggregationResult aggregationResult = search.ftAggregate(index.toString(), q); + commandListener.aggregateFinished(index.toString(), q); + return aggregationResult; + } - @Override - public String cursorDelete(long cursorId) { - return search.ftCursorDel(index.toString(), cursorId); - } + @Override + public String cursorDelete(long cursorId) { + commandListener.cursorDeleteStarted(index.toString(), cursorId); + final String result = search.ftCursorDel(index.toString(), cursorId); + 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); + final AggregationResult aggregationResult = search.ftCursorRead(index.toString(), cursorId, count); + commandListener.cursorReadFinished(index.toString(), cursorId, count, aggregationResult); + return aggregationResult; } @Override public String explain(Query q) { - return search.ftExplain(index.toString(), q); + commandListener.explainStarted(index.toString(), q); + final String s = search.ftExplain(index.toString(), q); + commandListener.explainFinished(index.toString(), q, s); + return s; } @Override public Map getInfo() { - return search.ftInfo(index.toString()); + commandListener.infoStarted(index.toString()); + final Map result = search.ftInfo(index.toString()); + commandListener.infoFinished(index.toString(), result); + return result; } @Override public String dropIndex() { - return search.ftDropIndex(index.toString()); + commandListener.dropIndexStarted(index.toString()); + final String result = search.ftDropIndex(index.toString()); + commandListener.dropIndexFinished(index.toString(), result); + return result; } @Override public String dropIndexAndDocuments() { - return search.ftDropIndexDD(index.toString()); + commandListener.dropIndexAndDocumentsStarted(index.toString()); + final String result = search.ftDropIndexDD(index.toString()); + 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); + final long result = search.ftSugAdd(key, suggestion, score); + commandListener.addSuggestionFinished(index.toString(), key, suggestion, score, result); + return result; } @Override @@ -126,101 +167,142 @@ 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); - String json = payload != null ? payload.toString() : "{}"; - Map payloadMap = gson.fromJson(json, new TypeToken>() { - }.getType()); - return new Suggestion(suggestion.getElement(), suggestion.getScore(), payloadMap); - } else { - return new Suggestion(suggestion.getElement(), suggestion.getScore()); - } - }).toList(); + List list = 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); + String json = payload != null ? payload.toString() : "{}"; + Map payloadMap = gson.fromJson(json, new TypeToken>() { + }.getType()); + return new Suggestion(suggestion.getElement(), suggestion.getScore(), payloadMap); + } else { + return new Suggestion(suggestion.getElement(), suggestion.getScore()); + } + }).toList(); + commandListener.getSuggestionFinished(index.toString(), key, prefix, options, list); + return list; } else { List suggestions = search.ftSugGet(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); - String json = payload != null ? payload.toString() : "{}"; - Map payloadMap = gson.fromJson(json, new TypeToken>() { - }.getType()); - return new Suggestion(suggestion, payloadMap); - } else { - return new Suggestion(suggestion); - } - }).toList(); + List list = 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); + String json = payload != null ? payload.toString() : "{}"; + Map payloadMap = gson.fromJson(json, new TypeToken>() { + }.getType()); + return new Suggestion(suggestion, payloadMap); + } else { + return new Suggestion(suggestion); + } + }).toList(); + commandListener.getSuggestionFinished(index.toString(), key, prefix, options, list); + return list; } } @Override public Boolean deleteSuggestion(String key, String entry) { - return search.ftSugDel(key, entry); + commandListener.deleteSuggestionStarted(index.toString(), key, entry); + final boolean result = search.ftSugDel(key, entry); + commandListener.deleteSuggestionFinished(index.toString(), key, entry, result); + return result; } @Override public Long getSuggestionLength(String key) { - return search.ftSugLen(key); + commandListener.getSuggestionLengthStarted(index.toString(), key); + final long result = search.ftSugLen(key); + commandListener.getSuggestionLengthFinished(index.toString(), key, result); + return result; } @Override public String alterIndex(SchemaField... fields) { - return search.ftAlter(index.toString(), fields); + commandListener.alterIndexStarted(index.toString(), fields); + final String result = search.ftAlter(index.toString(), fields); + commandListener.alterIndexFinished(index.toString(), fields, result); + return result; } @Override public String setConfig(String option, String value) { - return search.ftConfigSet(option, value); + commandListener.setConfigStarted(index.toString(), option, value); + final String result = search.ftConfigSet(option, value); + commandListener.setConfigFinished(index.toString(), option, value, result); + return result; } @Override public Map getConfig(String option) { - return search.ftConfigGet(option); + commandListener.getConfigStarted(index.toString(), option); + final Map result = search.ftConfigGet(option); + commandListener.getConfigFinished(index.toString(), option, result); + return result; } @Override public Map getIndexConfig(String option) { - return search.ftConfigGet(index.toString(), option); + commandListener.getIndexConfigStarted(index.toString(), option); + final Map result = search.ftConfigGet(index.toString(), option); + commandListener.getIndexConfigFinished(index.toString(), option, result); + return result; } @Override public String addAlias(String name) { - return search.ftAliasAdd(name, index.toString()); + commandListener.addAliasStarted(index.toString(), name); + final String result = search.ftAliasAdd(name, index.toString()); + commandListener.addAliasFinished(index.toString(), name, result); + return result; } @Override public String updateAlias(String name) { - return search.ftAliasUpdate(name, index.toString()); + commandListener.updateAliasStarted(index.toString(), name); + final String result = search.ftAliasUpdate(name, index.toString()); + commandListener.updateAliasFinished(index.toString(), name, result); + return result; } @Override public String deleteAlias(String name) { - return search.ftAliasDel(name); + commandListener.deleteAliasStarted(index.toString(), name); + final String result = search.ftAliasDel(name); + commandListener.deleteAliasFinished(index.toString(), name, result); + return result; } @Override public String updateSynonym(String synonymGroupId, String... terms) { - return search.ftSynUpdate(index.toString(), synonymGroupId, terms); + commandListener.updateSynonymStarted(index.toString(), synonymGroupId, terms); + final String result = search.ftSynUpdate(index.toString(), synonymGroupId, terms); + commandListener.updateSynonymFinished(index.toString(), synonymGroupId, terms, result); + return result; } @Override public Map> dumpSynonym() { - return search.ftSynDump(index.toString()); + commandListener.dumpSynonymStarted(index.toString()); + final Map> result = search.ftSynDump(index.toString()); + commandListener.dumpSynonymFinished(index.toString(), result); + return result; } @Override public Set tagVals(String field) { - return search.ftTagVals(index.toString(), field); + commandListener.tagValsStarted(index.toString(), field); + final Set result = search.ftTagVals(index.toString(), field); + commandListener.tagValsFinished(index.toString(), field, result); + return result; } }