基於Redis實現聯想查找自動補全

基於Redis實現聯想查找自動補全

  • 本文的自動補全只指最前匹配

  • 常用的方案有哪些?

    1. 利用數據庫的模塊匹配來做,利如mysql的like %這種方式來完成,雖然最前匹配能保證用到索引,但是效率不高。
    2. 利用搜索引擎,比如elasticsearch,sphinx 一般都用此方案
    3. 通過redis的有序集合來實現(本文)

一 補全原理

  1. 拆分詞,加入到有序集合,注意添加到redis時score都設置爲0,這些字符就會按照自然排序排好。

    zadd demo 0 內容
    
  2. 利用zrank命令,定位關鍵字的位置索引,然後通過索引來獲取所有以關鍵字開頭的集合

    zrank demo 關鍵字
    
  3. 通過zrange獲取數據

    zrange demo 1 -1
    

二基於java實現

  1. 因爲redis中不能存中文,所以需要一個轉換方案,從新編解碼,爲了方便解碼我們把字符用“-”隔開,

    //unicode編碼
        private String coding(String s) {
            if (s==null){
                return "";
            }
            char[] chars = s.toCharArray();
            StringBuffer buffer = new StringBuffer();
            for (char aChar : chars) {
                String s1 = Integer.toString(aChar, 16);
                buffer.append("-" + s1);
            }
            String encoding = buffer.toString();
            return encoding;
        }
    
        //unicode解碼
        private String decoding(String s) {
            if (s==null){
                return "";
            }
            String[] split = s.split("-");
            StringBuffer buffer = new StringBuffer();
    
            for (String s1 : split) {
                if (!s1.trim().equals("")) {
                    char i = (char) Integer.parseInt(s1, 16);
                    buffer.append(i);
                }
            }
            return buffer.toString();
        }
    
  2. 確定轉換過的順序,因爲16進制和連接符我們確定好順序字符標準串,然後拿前綴字符

      private String[] findPrefixRange(String prefix) {
             //查找出前綴字符串最後一個字符在列表中的位置
            int posn = VALID_CHARACTERS.indexOf(prefix.charAt(prefix.length() - 1));
            //找出前驅字符
            char suffix = VALID_CHARACTERS.charAt(posn > 0 ? posn - 1 : 0);
            //生成前綴字符串的前驅字符串
            String start = prefix.substring(0, prefix.length() - 1) + suffix + 'g';
            //生成前綴字符串的後繼字符串
            String end = prefix + 'g';
            return new String[]{start, end};
        }
    
    
  3. 實現redis查找命令

    @Autowired
    private IGlobalCache globalCache;
    
    @Autowired
    RedisTemplate redisTemplate;
    
    private static final String REDIS_SALE_CUSTOMER = "redis_customer";
    
    private static final String VALID_CHARACTERS = "-0123456789abcdefg";
    
    public List<String> getRelateCustomerWord(String name) {
            if (StringUtils.isBlank(name)) {
                return null;
            }
            if (globalCache.hasKey(REDIS_SALE_CUSTOMER)) {
                // 拼接字段
                String[] prefixRange = findPrefixRange(coding(name));
                // 放入到redis中
                List<String> strFinds = autoFind(prefixRange);
                return strFinds;
            } 
        }
    
    
    private List<String> autoFind(String[] prefixRange) {
            List<String> list = new ArrayList<>();
            try {
                String uuid = UUID.randomUUID().toString().replaceAll("-", "");
    //	防止多個羣成員可以同時操作有序集合,將相同的前驅字符串和後繼字符串插入有序集合
                String start = prefixRange[0] + uuid;
                String end = prefixRange[1] + uuid;
                // 1.放入redis
                redisTemplate.opsForZSet().add(REDIS_SALE_CUSTOMER, start, 0);
                redisTemplate.opsForZSet().add(REDIS_SALE_CUSTOMER, end, 0);
                // 2.得到索引的位置
                int begin_index = redisTemplate.opsForZSet().rank(REDIS_SALE_CUSTOMER, start).intValue();
                int end_index = redisTemplate.opsForZSet().rank(REDIS_SALE_CUSTOMER, end).intValue();
                // 3.因爲最多展示10個,所以計算出結束爲止
                int erange = Math.min(begin_index + 9, end_index - 2);
    
                //  3.刪除這兩個放入的值
                redisTemplate.opsForZSet().remove(REDIS_SALE_CUSTOMER, start);
                redisTemplate.opsForZSet().remove(REDIS_SALE_CUSTOMER, end);
                // 4.獲得其中的值
                Set<String> zrange = redisTemplate.opsForZSet().range(REDIS_SALE_CUSTOMER, begin_index, erange);
    
                list.addAll(zrange);
                ListIterator<String> it = list.listIterator();
                while (it.hasNext()) {
                    String next = it.next();
                    if (next.indexOf("g") != -1) {
                        it.remove();
                    } else {
                        //把16進制字符串轉換回來
                        it.set(decoding(next));
                    }
                }
    
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            return list;
    
        }
    
    
  4. 實現redis添加命令

     public boolean putCustomerDataToRedis(String keyword) {
            Boolean add = redisTemplate.opsForZSet().add(REDIS_SALE_CUSTOMER, coding(keyword), 0);
            return add;
        }
    

tips

  • redis事務不支持集羣,所以上附代碼沒有添加事務,可以考慮基於java的鎖來實現對key的操作
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章