专栏首页Spring相关使用Redis实现中英文自动补全功能详解

使用Redis实现中英文自动补全功能详解

1.Redis自动补全功能介绍:

​ Redis可以帮我们实现很多种功能,今天这里着重介绍的是Redis的自动补全功能的实现.我们使用有序集合,并score都为0,这样就按元素值的字典序排序.然后我们可以根据排序号的字符,进行添加前缀和后缀的方式,找到我们想要的区间内容.下面介绍一个简单的Zset的排序内容和思路,以便后续的理解:

名称为redis_concat的Zset集合元素如下:

编号

数值

分值

1

a

0

2

ab

0

3

abcd

0

4

abef

0

5

hjk

0

6

dbfgll

0

7

efhuo

0

8

iop

0

9

lkj

0

10

ghu

0

​ 当所有的数值分值为0的时候,Zset会按照字典升序排列,这里我们如果需要查找上面的a,就应该能找出[ a, ab,abcd,abef]这四个元素,查找上面的ab,就应该能找出[ab,abcd,abef]这三个元素,其他同理.这个时候我们只要想办法在这个搜索条件查找元素的前面后最后都筛选出想要的数据即可:

  • Ascii码里小写字母a的前面是`,z的后面是{
  • 于是我们查找ab匹配的元素,插入 aa{ 和 ab{ 即可( 或者" ab` "和" ab{ " )
  • 找到aa{ 和 ab{ 的下标,通过Zrange()得出相关区间的内容
  • 如果是中文,建议全部将支付转为16进制字符来进行存储,取出时候再转码

2.相关Demo分享

​ 基于此本人建立了一个前后端分离的利用Redis自动补全联系人姓名的项目,前端采用的是Vue,后端采用Java的Spring框架,这个示例功能单一,有好的建议和想法都可以给我留言评论,多加以改进,另外项目GitHub地址在文末,喜欢请关注.下面是项目的简单演示:

redis.gif

项目结构如下:
├─src
│  └─main
│      ├─java
│      │  └─com
│      │      └─home
│      │          ├─config
│      │          ├─constants
│      │          ├─controller
│      │          ├─mapper
│      │          ├─page
│      │          ├─pojo
│      │          └─service
│      │              └─impl
│      ├─resources
│      │  ├─mapper
│      │  └─properties
│      └─webapp
│          └─WEB-INF
│              └─views
│                  └─vue
└─target
    ├─classes
    │  ├─com
    │  │  └─home
    │  │      ├─config
    │  │      ├─constants
    │  │      ├─controller
    │  │      ├─mapper
    │  │      ├─page
    │  │      ├─pojo
    │  │      └─service
    │  │          └─impl
    │  ├─mapper
    │  └─properties
    ├─generated-sources
    │  └─annotations
    ├─qfang-agent-online-mass-client
    │  ├─META-INF
    │  └─WEB-INF
    │      ├─classes
    │      │  ├─com
    │      │  │  └─home
    │      │  │      ├─controller
    │      │  │      ├─mapper
    │      │  │      ├─pojo
    │      │  │      └─service
    │      │  │          └─impl
    │      │  └─mapper
    │      └─lib
    └─redis-web-1.0-SNAPSHOT
        ├─META-INF
        └─WEB-INF
            ├─classes
            │  ├─com
            │  │  └─home
            │  │      ├─config
            │  │      ├─constants
            │  │      ├─controller
            │  │      ├─mapper
            │  │      ├─page
            │  │      ├─pojo
            │  │      └─service
            │  │          └─impl
            │  ├─mapper
            │  └─properties
            ├─lib
            └─views
Vue的构建步骤:
# install dependencies
npm install

# serve with hot reload at localhost:8080
npm run dev

# build for production with minification
npm run build

# build for production and view the bundle analyzer report
npm run build --report

# run unit tests
npm run unit

# run e2e tests
npm run e2e

# run all tests
npm test
Java_Service中相关的方法:
  • 1.分页获取前100条数据,如果Redis中不存该联系人在就放入redis中
  • 2.放入前使用 unicode编码,位于coding方法中,取出相关的数据后记得使用decoding方法解码
  • 3.获得相关数据后删除放入的前缀和后缀,这里都加了UUID,防止有相同的查询带有前后缀的数据被误删(如查找 ab ,数据中本身就含有 ab{ 等)
  • 4.获得前5条或者前10条相关匹配的数据给前台(这里自定义即可,查看注释地方)
相关类详情:
package com.home.service.impl;

import com.home.constants.RedisExpireUtil;
import com.home.mapper.ContactMapper;
import com.home.page.PageObject;
import com.home.page.PageResult;
import com.home.pojo.Contact;
import com.home.service.ContactService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.*;

/**
 * 改造为中英文皆可匹配
 * 这里全部转为16进制存入到redis中
 */
@Service
public class ContactServiceImpl implements ContactService {
    @Autowired
    private ContactMapper contactMapper;

    @Autowired
    private JedisPool jedisPool;

    private static final String REDIS_CONCAT = "redis_concat";

    private static final String VALID_CHARACTERS = "0123456789abcdefg";

    @Override
    public Contact selectByPrimaryKey(int i) {
        return contactMapper.selectByPrimaryKey(i);
    }

    /**
     * 获取相应的条数的数据
     */
    @Override
    public PageResult getDateSplitByNum(PageObject pageObject) {
        // 获取总条数
        int totalCount = contactMapper.getAllCount();
        List<Contact> contactList = contactMapper.selectByPageObject(pageObject);
        return new PageResult(contactList, totalCount, pageObject);
    }

    @Override
    public List<String> getRelatedWord(String name) {
        if (StringUtils.isBlank(name)) {
            return null;
        }

        Jedis jedis = jedisPool.getResource();
        if (jedis.exists(REDIS_CONCAT)) {
            // 拼接字段
            String[] prefixRange = findPrefixRange(coding(name));
            // 放入到redis中
            List<String> strFinds = putIntoRedisAndFind(prefixRange);
            return strFinds;
        } else {
            // 从数据库放入
            transDBToRedis(1, 100);
            // 拼接字段
            String[] prefixRange = findPrefixRange(name);
            // 放入到redis中
            List<String> strFinds = putIntoRedisAndFind(prefixRange);
            return strFinds;
        }
    }

    /**
     * 把数据放入到redis
     *
     * @return
     */
    public void transDBToRedis(int currentPage, int pageSize) {
        Jedis jedis = jedisPool.getResource();
        PageObject po = new PageObject();
        po.setCurrentPage(currentPage);
        po.setPageSize(pageSize);
        // 1.取出前100条数据
        PageResult pageResult = getDateSplitByNum(po);
        List<Contact> contactList = (List<Contact>) pageResult.getData();
        // 2.放入前100条数据到redis
        for (Contact contact : contactList) {
            String contactName = coding(contact.getContactName());
            if (jedis.zrank(REDIS_CONCAT, contactName) == null) {
                jedis.zadd(REDIS_CONCAT, 0D, contactName);
            }
        }
        if (jedis.ttl(REDIS_CONCAT).intValue() == RedisExpireUtil.NEVER_EXPIRE) {
            // 放入redis有效期一小时
            jedis.expire(REDIS_CONCAT, RedisExpireUtil.ONE_HOUR);
        }
    }


    /**
     * 将相关的参数放入redis中
     *
     * @param prefixRange
     */
    private List<String> putIntoRedisAndFind(String[] prefixRange) {
        Jedis jedis = jedisPool.getResource();
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        List<String> list = new ArrayList();
        try {
            jedis.watch(REDIS_CONCAT);
            String start = prefixRange[0] + uuid;
            String end = prefixRange[1] + uuid;


            // 1.放入redis
            jedis.zadd(REDIS_CONCAT, 0, start);
            jedis.zadd(REDIS_CONCAT, 0, end);

            // 2.得到索引的位置
            int begin_index = jedis.zrank(REDIS_CONCAT, start).intValue();
            int end_index = jedis.zrank(REDIS_CONCAT, end).intValue();
            //  3.删除这两个放入的值
            jedis.zrem(REDIS_CONCAT, start);
            jedis.zrem(REDIS_CONCAT, end);
            // 3.因为最多展示5个,所以计算出结束为止
            int erange = Math.min(begin_index + 4, end_index - 2);
            if(begin_index>erange){
                return null;
            }
            // 4.获得其中的值
            Set<String> zrange = jedis.zrange(REDIS_CONCAT, begin_index, erange);
            if (zrange == null) {
                return null;
            }

            list.addAll(zrange);
            ListIterator<String> it = list.listIterator();
            while (it.hasNext()) {
                String next = it.next();
                if (next.indexOf("g") != -1) {
                    it.remove();
                } else {
                    it.set(decoding(next));//把16进制字符串转换回来
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            jedis.unwatch();
        }

        return list;

    }


//    这个方法仅仅适用于匹配英文字符
//    public String[] findPrefixRange(String prefix) {
//        String start = prefix + '`';
//        String end = prefix + '{';
//        System.out.println(prefix + "  " + start + "---" + end);
//        return new String[]{start, end};
//    }


    //unicode编码
    private String coding(String s) {
        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) {
        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();
    }


    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.项目git地址

(喜欢记得点星支持哦,谢谢!)

https://github.com/fengcharly/redis-auto-complete

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 线程安全的Map的小Demo

    Dream城堡
  • oauth2.0实现sso单点登录的方式和相关代码

    百科:SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的...

    Dream城堡
  • 第5章—构建Spring Web应用程序—关于spring中的validate注解后台校验的解析

    在后台开发过程中,对参数的校验成为开发环境不可缺少的一个环节。比如参数不能为null,email那么必须符合email的格式,如果手动进行if判断或者写正则表达...

    Dream城堡
  • java使用poi读取excel文档的一种解决方案

    本人在学习使用java的过程中,需要验证一下excel表格里面的数据是否与数据库中的数据相等。由于数据太多,故想着用java读取excel数据再去数据库验证。上...

    FunTester
  • Java基础笔记12

    dreamkong
  • 阶段01Java基础day12常见对象01

    声明:本文为原创,作者为 对弈,转载时请保留本声明及附带文章链接:http://www.duiyi.xyz/c%e5%ae%9e%e7%8e%b0%e9%9b%...

    对弈
  • String类12个常用的方法

    挨踢小子部落阁
  • Java中常用的API(二)——String

    在第一节中我们介绍了Object类的方法以及操作,那么这一节,我们将介绍一个新的类:String。 String类是我们经常使用的类,应用十分广泛。同时,S...

    roobtyan
  • Java字符串面试问答

    字符串是使用最广泛的Java的类之一。在这里,我列出了一些重要的Java的字符串面试问答。

    淡定的蜗牛
  • Java微信支付快速入门&工具类

    快速入门 1、微信支付官方在线API入口: https://pay.weixin.qq.com/wiki/doc/api/index.html 2、微信支付能力...

    斯武丶风晴

扫码关注云+社区

领取腾讯云代金券