0
  • Redis Version: 7.2.1
  • Installed Modules: RediSearch v.2.4.15, RedisJSON v.99.99.99
  • Number of nodes: xx.x.x.xxx:6379, xx.x.x.xxx:6380, xx.x.x.xxx:6381, xx.x.x.xxx:6382, xx.x.x.xxx:6383, xx.x.x.xxx:6384

pom.xml

<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>4.2.3</version>
</dependency>

Configuration

@Bean
public JedisCluster getRedisCluster() { 
    Set<HostAndPort> jedisClusterNode = new HashSet<>();
    String[] nodes = {"xx.x.x.xxx:6379", "xx.x.x.xxx:6380", "xx.x.x.xxx:6381", "xx.x.x.xxx:6382", "xx.x.x.xxx:6383", "xx.x.x.xxx:6384"};
   for (int i = 0; i < nodes.length; i++) {
    String[] ipAndPort = nodes[i].split(":");
    jedisClusterNode.add(new HostAndPort(ipAndPort[0], Integer.valueOf(ipAndPort[1])));
   }
  JedisCluster jc = new JedisCluster(jedisClusterNode, "default", "admin");
  logger.debug("Redis(FT) connection Successfully.");
  return jc;
}

BookDataSearchIndex.java

@Component
@Order(1)
public class BookDataSearchIndex implements CommandLineRunner {

    private static final Logger logger = LoggerFactory.getLogger(BookDataSearchIndex.class);

    @Autowired
    private UnifiedJedis jedis;

    @Override
    public void run(String... args) throws Exception {
        try {
            Schema schema = new Schema()
                    .addField(new Schema.Field(FieldName.of("$.bookId").as("BOOKID"), Schema.FieldType.TEXT, false,
                            false))
                    .addField(new Schema.Field(FieldName.of("$.title").as("TITLE"), Schema.FieldType.TEXT, false, false))
                    .addField(new Schema.Field(FieldName.of("$.price").as("PRICE"), Schema.FieldType.NUMERIC, true, false));
            IndexDefinition indexDefinition = new IndexDefinition(IndexDefinition.Type.JSON)
                    .setPrefixes("book:");
    
            jedis.ftCreate("bookdata-idx", IndexOptions.defaultOptions().setDefinition(indexDefinition),
                    schema);
        } catch (Exception e) {
            logger.debug("Inside run in BookDataSearchIndex : {}", e.getMessage());
        }
    }
}

BookData.java

public class BookData {
  private String bookId;
  private String title;
  private Long price;
    
  // Setter & Getter
    
}

Page.java

public class Page<T> {

    private List<T> data;
    private Integer totalPage;
    private Integer currentPage;
    private Long total;

    public Page(List<T> data, Integer totalPage, Integer currentPage, Long total) {
        super();
        this.data = data;
        this.totalPage = totalPage;
        this.currentPage = currentPage;
        this.total = total;
    }
    
    // Setter & Getter
}

BookDataHelper.java

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import com.google.gson.Gson;

import redis.clients.jedis.UnifiedJedis;
import redis.clients.jedis.search.Document;
import redis.clients.jedis.search.Query;
import redis.clients.jedis.search.SearchResult;

public class BookDataHelper {

    private static final String ATTHERATE = "@";
    private static final String LESS_THEN = "lt";
    private static final String GREATER_THEN_EQUAL = "gte";
    private static final String GREATER_THEN = "gt";
    private static final String LESS_THEN_EQUAL = "lte";
    private static final String BETWEEN = "between";
    private static final Logger logger = LoggerFactory.getLogger(BookDataHelper.class);

    @Autowired
    private UnifiedJedis jedis;

    public BookData save(BookData data) {
        String memberKey = "book:"+ data.getBookId();
        Gson gson = new Gson();
        jedis.jsonSet(getKey(memberKey), gson.toJson(data));
        jedis.sadd(getKey("bookdata"), getKey(memberKey));
        return data;
    }

    public BookData findByKey(String index, String key, Object value, Class<BookData> dto) {
        Map<String, Object> fields = new HashMap<>();
        fields.put(key, value);
        List<BookData> t = search(index, fields, dto);
        if (!t.isEmpty()) {
            return t.get(0);
        }
        return null;
    }

    public List<BookData> search(String index, Map<String, Object> fields, Class<BookData> dto) {
        String queryCriteria = buildQuery(dto, fields, null);
        return buildResponse(index, queryCriteria, dto);
    }
    
    public Page<BookData> search(String index, String queryCriteria, Integer offset, Integer limit, Class<BookData> dto) {
        Query query = null;
        if (queryCriteria.isEmpty()) {
            query = new Query();
        } else {
            query = new Query(queryCriteria);
        }

        query.limit(offset, limit);
        SearchResult searchResult = jedis.ftSearch(index, query);
        Long total = searchResult.getTotalResults();
        int totalPage = (int) Math.ceil((double) total / limit);

        List<BookData> orderDataList = searchResult.getDocuments().stream()
                .map(document -> convertDocumentToModel(document, dto)).collect(Collectors.toList());
        return new Page<>(orderDataList, totalPage, offset, total);
    }
    
    ////////////////////////////////////////////////////////////////////////////////////////////////////////
    ////////                                 PRIVATE METHODS                                        ////////
    ////////////////////////////////////////////////////////////////////////////////////////////////////////
    
    private List<BookData> buildResponse(String index, String queryCriteria, Class<BookData> dto) {
        int offset = 0;
        int limit = 10;
        boolean done = false;
        List<BookData> result = new ArrayList<>();
        while (!done) {
            Page<BookData> pageResult = search(index, queryCriteria,offset, limit, dto);
            result.addAll(pageResult.getData());

            // Check if there are more pages of results
            if (pageResult.getData().isEmpty() || pageResult.getTotal() < limit) {
                done = true;
            } else {
                offset += limit;
            }
        }
        return result;
    }

    private static synchronized String buildQuery(Class<?> dto, Map<String, Object> fields, Map<String, String> operators) {
        StringBuilder queryBuilder = new StringBuilder();
        List<String> entityNumberTypeFields = getNumberFields(dto);
        for (Entry<String, Object> entry : fields.entrySet()) {
            String fieldName = entry.getKey().trim().toUpperCase();
            Object fieldValue = toValue(entry.getValue());
            if (null != operators && operators.containsKey(entry.getKey().trim())) {
                String operator = operators.get(entry.getKey().trim()).trim();
                buildOperatorsQuery(queryBuilder, operator, fieldName, fieldValue, entry.getValue());
            } else {
                if(entityNumberTypeFields.contains(fieldName)) {
                    queryBuilder.append(ATTHERATE).append(fieldName).append(":[").append(fieldValue).append(",")
                    .append(fieldValue).append("]").append(" ");
                } else {
                    queryBuilder.append(ATTHERATE).append(fieldName).append(":").append(fieldValue).append(" ");
                }
            }
        }
        return queryBuilder.toString();
    }
    
    private static synchronized void buildOperatorsQuery(StringBuilder buildQuery, String operator, String fieldName, Object fieldValue, Object originFieldValue) {
        if (GREATER_THEN.equalsIgnoreCase(operator)) {

            buildQuery.append(ATTHERATE).append(fieldName).append(":[")
                    .append(getIncrementalVal(originFieldValue)).append(" > ").append(Integer.MAX_VALUE)
                    .append("]").append(" ");

        } else if (LESS_THEN.equalsIgnoreCase(operator)) {

            buildQuery.append(ATTHERATE).append(fieldName).append(":[").append(Integer.MIN_VALUE)
                    .append(" < ").append(getDecrementalVal(originFieldValue)).append("]").append(" ");

        } else if (GREATER_THEN_EQUAL.equalsIgnoreCase(operator)) {

            buildQuery.append(ATTHERATE).append(fieldName).append(":[").append(fieldValue).append(" > ")
                    .append(Integer.MAX_VALUE).append("]").append(" ");

        } else if (LESS_THEN_EQUAL.equalsIgnoreCase(operator)) {

            buildQuery.append(ATTHERATE).append(fieldName).append(":[").append(Integer.MIN_VALUE)
                    .append(" < ").append(toValue(fieldValue)).append("]").append(" ");

        } else if (BETWEEN.equalsIgnoreCase(operator) && originFieldValue instanceof List) {

            List<?> range = (List<?>) originFieldValue;
            if (range.size() == 2) {
                buildQuery.append(ATTHERATE).append(fieldName).append(":[").append(toValue(range.get(0))).append(",")
                        .append(toValue(range.get(1))).append("]").append(" ");
            } else {
                throw new IllegalArgumentException("Invalid range for 'between' operator");
            }

        } else {
            buildQuery.append(ATTHERATE).append(fieldName).append(":").append(fieldValue).append(" ");
        }
    }
    
    private static String getKey(String key) {
        return key.replace("-", "").replace("_", "");
    }
    
    public static <T> T convertDocumentToModel(Document document, Class<T> model) {
        Gson gson = new Gson();
        String jsonDoc = document.getProperties().iterator().next().getValue().toString();
        return gson.fromJson(jsonDoc, model);
    }
    
    public static List<String> getNumberFields(Class<?> obj) {
        List<String> fieldList = new ArrayList<>();
        try {
            Class<?> clazz = obj.newInstance().getClass();
            Field[] fields = clazz.getDeclaredFields();

            for (Field field : fields) {
                if (isNumberType(field.getType())) {
                    field.setAccessible(true); // Make the field accessible
                    fieldList.add(field.getName().toUpperCase());
                }
            }
        } catch (InstantiationException | IllegalAccessException e) {
            logger.error("Error while find number fields. {}", e);
        }
        return fieldList;
    }
    
    private static boolean isNumberType(Class<?> fieldType) {
        return fieldType == int.class || fieldType == Integer.class || fieldType == long.class
                || fieldType == Long.class || fieldType == short.class || fieldType == Short.class
                || fieldType == byte.class || fieldType == Byte.class;
    }
    
    public static Object toValue(Object value) {
        try {
            if(value instanceof Date) {
                Date date = (Date) value;
                return date.getTime();
            } else if(value instanceof String) {
                return value.toString().trim().replace("-", "*").replace("_", "*");
            }
        } catch (Exception e) {
            logger.error("Error in toValue while parsing value. {}", e);
        }
        return value;
    }
    
    public static Object getIncrementalVal(Object value) {
        try {
            if(value instanceof Integer || value instanceof Long || value instanceof String) {
                long val = Long.parseLong((String) value.toString());
                return val + 1;
            } else if(value instanceof Double || value instanceof Float) {
                double val = Double.parseDouble((String) value.toString());
                return val + 0.1;
            } else if(value instanceof Date) {
                Date date = (Date) value;
                Calendar calendar = Calendar.getInstance();
                calendar.setTime(date);
                // Add one day
                calendar.add(Calendar.DAY_OF_YEAR, 1);
                return calendar.getTime().getTime();
            }
        } catch (Exception e) {
            logger.error("Error in getIncrementalVal while parsing value. {}", e);
        }
        return value;
    }
    
    public static Object getDecrementalVal(Object value) {
        try {
            if(value instanceof Integer || value instanceof Long || value instanceof String) {
                long val = Long.parseLong((String) value.toString());
                return val - 1;
            } else if(value instanceof Double || value instanceof Float) {
                double val = Double.parseDouble((String) value.toString());
                return val - 0.1;
            } else if(value instanceof Date) {
                Date date = (Date) value;
                Calendar calendar = Calendar.getInstance();
                calendar.setTime(date);
                // Subtract one day
                calendar.add(Calendar.DAY_OF_YEAR, -1);
                return calendar.getTime().getTime();
            }
        } catch (Exception e) {
            logger.error("Error in getDecrementalVal while parsing value. {}", e);
        }
        return value;
    }
}

Save below data by calling BookDataHelper.save method.

bookId = "HFDP-1"
title = "Head First Design Patterns"
price = 200

Retrieve data by calling BookDataHelper.findByKey method.

BookDataHelper.findByKey("bookdata-idx", "bookId", "HFDP-1", BookData.class);

Unfortunately getting no data result, Also tried with CLI by below command.

FT.SEARCH bookdata-idx @BOOKID:HFDP*1
2
  • Provide a minimal example. There are too much code in your question and not all of those are necessary to get the help you're looking for. Commented Nov 13, 2023 at 12:12
  • "Unfortunately getting no data result, Also tried with CLI by below command." - Redis cluster has multiple nodes. Which node(s) did you try with CLI? All nodes or a specific node or a random node? Commented Nov 13, 2023 at 12:14

1 Answer 1

0

You have mentioned not getting any result but have not mentioned getting any error. I am also assuming you are using regular RediSearch (installed properly in all of the nodes).

Based on these, I think you have to execute ftCreate in all the nodes and fetch by ftSearch from all the nodes.

Since Jedis 4.4.0+ (use the latest version possible):

  • ftCreate is executed to all the nodes by default.
  • Fetching from all the nodes can easily by using ftSearchIteration method instead of ftSearch.
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.