使用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地址在文末,喜欢请关注.下面是项目的简单演示:

项目结构如下:
├─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

原文地址:https://www.cnblogs.com/charlypage/p/11614676.html

时间: 2024-10-10 13:32:59

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

jquery mutilselect 插件添加中英文自动补全

jquery mutilselect默认只能根据设置的option来进行自动提示 $.each(availableTags, function(key, value) { $('#channels') .append($("<option></option>") .attr("value",value) .text(value)); //默认按text中的value来自动提示 }); 阅读下面的需求: 有这么几个选项: 北京 天津 湖北 输入

easyui-combobox 实现简单的自动补全功能

前台: Html页面 <input id="clsydw" name="clsydw" class="easyui-combobox" data-options="" /> JS //自动补全功能 $("#clsydw").combobox({ valueField:'syr', textField:'syr', panelWidth:200, panelHeight:'auto', onCha

c#TextBox输入框自动提示、自动完成、自动补全功能

功能概览 相关属性 TextBox.AutoCompleteCustomSource 属性 获取或设置当 TextBox.AutoCompleteSource 属性设置为 [CustomSource] 时要使用的自定义 T:System.Collections.Specialized.StringCollection. TextBox.AutoCompleteMode 属性 获取或设置一个选项,该选项控制自动完成应用于 TextBox 的方式. 属性值 类型:System.Windows.For

Eclipse自动补全功能和自动生成作者、日期注释等功能设置

以前想实现添加代码作者信息的东西,但不知道怎样实现,今天终于在网上无意中找到解决办法了 Eclipse自动生成作者.日期注释等功能设置 在使用Eclipse 编写Java代码时,自动生成的注释信息都是按照预先设置好的格式生成的. 修改作者.日期注释格式:打开Windows->Preferences->Java->Code Style->Code Templates,点击右边窗口中的Comments,可以看到有很多选项,我们便可对此注释信息模板进行编辑. 如我们希望在一个Java文件

第三百六十八节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)用Django实现搜索的自动补全功能

第三百六十八节,Python分布式爬虫打造搜索引擎Scrapy精讲-用Django实现搜索的自动补全功能 elasticsearch(搜索引擎)提供了自动补全接口 官方说明:https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters-completion.html 创建自动补全字段 自动补全需要用到一个字段名称为suggest类型为Completion类型的一个字段 所以我们需要用

debian下增强bash的自动补全功能

在我们新安装的Debian系统时,发现很多命令都不能自动补全,这是很不方便的,因为每个人的精力都是有限的,不是对每个命令的每一个细节都能完全记住,因此自动补全是一个很实用的功能!对于Debian操作系统,我们可以使用下列方法进行增强命令的自动补全功能! 修改/etc/bash.bashrc文件,将 #if [ -f /etc/bash_completion ]; then # . /etc/bash_completion #fi 前面的#去掉 修改/etc/profile文件,在最后增加一行 s

【Qt编程】自动补全功能

最近写了一个查单词的类似有道词典的软件,里面就有一个自动补全功能(即当你输入一个字母时,就会出现几个候选项).这个自动补全功能十分常见,百度搜索关键词时就会出现.不过它们这些补全功能都是与你输入的进行首字匹配,有时也会不方便.例如,如果我输入一个"好",如果是首字匹配的话会出现下图: 如果是句中匹配的话,则是这种情况: 你可以根据自己的要求进行选择哪一种模式. Qt中自带QCompleter类来实现上面的自动补全功能,读者可以在Qt自带的demo中很容易的学会该类的使用.下面我要讲的是

vim基础学习之自动补全功能

本章我们学习自动补全功能1.自动补全优先从当前的编辑区获得补全列表例如:我们写下如下内容 aaaaa aabbb aaab 当我们再次输入aa,然后我们按下Tab的时候,会弹出一个包含 aaaaa aabbb aaab的列表触发补全模式的条件1.插入模式下 ctrl+p ctrl+n 或者Tab 同时,ctrl+p ctrl+n还能够上下移动选中补全列表项还有其他的补全方法,如下这些方法都是以ctrl + x来启动的,然后跟着你想要的补全样式 1.<c-n>-普通关键字 2.<c-x&g

js 实现类似百度联想输入,自动补全功能

js  实现类似百度联想输入,自动补全功能 方案一: search是搜索框id="search" 1 //点击页面隐藏自动补全提示框 2 document.onclick = function (e) { 3 var e = e ? e : window.event; 4 var tar = e.srcElement || e.target; 5 if (tar.id != search) { 6 if ($("#" + auto).is(":visibl