Prakash Rathod
Prakash Rathod

Reputation: 1

FT.SEARCH not working with cluster based Redis server configuration

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

Upvotes: 0

Views: 432

Answers (1)

sazzad
sazzad

Reputation: 6267

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.

Upvotes: 0

Related Questions