V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
wayn111
V2EX  ›  程序员

可能是全网第一个使用 RediSearch 实战的项目

  •  
  •   wayn111 ·
    wayn111 · 2022-03-29 23:33:24 +08:00 · 1395 次点击
    这是一个创建于 977 天前的主题,其中的信息可能已经有所发展或是发生改变。

    实战项目地址newbeemall,集成 RediSearch ,代码开源已上传,支持的话可以点个 star😁 RediSearch 是基于 Redis 开发的支持二级索引、查询引擎和全文搜索的应用程序。在 2.0 的版本中,简单看下官网测试报告:

    索引构建

    在索引构建测试中,RediSearch 用 221 秒的速度超过了 Elasticsearch 的 349 秒,领先 58%, 测试图片

    查询性能

    数据集建立索引后,我们使用运行在专用负载生成器服务器上的 32 个客户端启动了两个词的搜索查询。如下图所示,RediSearch 的吞吐量达到了 12.5K ops/sec ,而 Elasticsearch 的吞吐量达到了 3.1K ops/sec ,快了 4 倍。此外,RediSearch 的延迟稍好一些,平均为 8 毫秒,而 Elasticsearch 为 10 毫秒。 (ops/sec 每秒操作数)

    测试图片 由此可见,新的 RediSearch 在性能上对比 RediSearch 较有优势,此外对中文项目来说对于中文的支持必不可少,RediSearch 也在官网文档特意列出了支持中文,基于frisoC 语言开发的中文分词项目。 image.png

    一、RediSearch 安装

    Docker 安装最新版

    docker run -p 6379:6379 redislabs/redisearch:latest
    

    通过 redis-cli 连接查看 RediSearch 是否安装成功

    1 、redis-cli -h localhost 
    2 、module list
    82.157.141.70:16789> MODULE LIST 
    
        1) 1) "name"
           2) "search" # 查看是否包含 search 模块
           3) "ver"
           4) (integer) 20210
        2) 1) "name"
           2) "ReJSON" # 查看是否包含 ReJSON 模块
           3) "ver"
           4) (integer) 20007
    
    

    二、客户端集成

    对于 Java 项目直接选用 Jedis4.0 版本就可以,Jedis 在 4.0 版本自动支持 RediSearch ,编写 Jedis 连接 RedisSearch 测试用例,用 RedisSearch 命令创建如下:

    FT.CREATE idx:goods on hash prefix 1 "goods:" language chinese schema goodsName text sortable
    // FT.CREATE 创建索引命令
    // idx:goods 索引名称
    // on hash 索引数据基于 hash 类型源数据构建
    // prefix 1 "goods:" 表示要创建索引的源数据前缀匹配规则
    // language chinese 表示支持中文语言分词
    // schema 表示字段定义,goodsName 元数据属性名 text 字段类型 sortable 自持排序
    
    FT.INFO idx:goods 
    // FT.INFO 查询指定名称索引信息
    
    FT.DROPINDEX idx:goods 
    // FT.DROPINDEX 删除指定名称索引,不会删除源数据
    
    添加索引时,使用 hset 命令添加索引源数据
    删除索引时,使用 del 命令删除索引源数据
    
    1. Jedis 创建 RediSearch 客户端
    @Bean
    public UnifiedJedis unifiedJedis(GenericObjectPoolConfig jedisPoolConfig) {
        UnifiedJedis client;
        if (StringUtils.isNotEmpty(password)) {
            client = new JedisPooled(jedisPoolConfig, host, port, timeout, password, database);
        } else {
            client = new JedisPooled(jedisPoolConfig, host, port, timeout, null, database);
        }
        return client;
    }
    
    
    1. Jedis 创建索引
    @Test
    public void createIndex() {
        System.out.println("begin");
        Schema schema = new Schema()
                .addSortableTextField("goodsName", 1.0)
                .addSortableTextField("goodsIntro", 0.5)
                .addSortableTagField("tag", "|");
        jedisSearch.createIndex(idxName, "goods", schema);
        System.out.println("end");
    }
    
    /**
     * 创建索引
     *
     * @param idxName 索引名称
     * @param prefix  要索引的数据前缀
     * @param schema  索引字段配置
     */
    public void createIndex(String idxName, String prefix, Schema schema) {
        IndexDefinition rule = new IndexDefinition(IndexDefinition.Type.HASH)
                .setPrefixes(prefix)
                .setLanguage(Constants.GOODS_IDX_LANGUAGE); # 设置支持中文分词
        client.ftCreate(idxName,
                IndexOptions.defaultOptions().setDefinition(rule),
                schema);
    }
    
    
    1. Jedis 添加索引源数据
    /**
     * 添加索引数据
     *
     * @param keyPrefix 要索引的数据前缀
     * @param goods     商品信息
     * @return boolean
     */
    public boolean addGoodsIndex(String keyPrefix, Goods goods) {
        Map<String, String> hash = MyBeanUtil.toMap(goods);
        hash.put("_language", Constants.GOODS_IDX_LANGUAGE);
        client.hset(keyPrefix + goods.getGoodsId(), MyBeanUtil.toMap(goods));
        return true;
    }
    
    1. Jedis 中文查询
    public SearchResult search(String goodsIdxName, SearchObjVO searchObjVO,     Page<SearchPageGoodsVO> page) {
        String keyword = searchObjVO.getKeyword(); // 查询关键字
        String queryKey = String.format("@goodsName:(%s)", keyword);
        Query q = new Query(queryKey);
        String sort = searchObjVO.getSidx();
        String order = searchObjVO.getOrder();
        // 查询是否排序
        if (StringUtils.isNotBlank(sort)) {
            q.setSortBy(sort, Constants.SORT_ASC.equals(order));
    
        }
        // 设置中文分词查询
        q.setLanguage(Constants.GOODS_IDX_LANGUAGE);
        // 查询分页
        q.limit((int) page.offset(), (int) page.getSize());
        // 返回查询结果
        return client.ftSearch(goodsIdxName, q);
    }
    

    三、项目实战

    1. 引入 Jedis4.0
    <jedis.version>4.2.0</jedis.version>
    <!-- jedis -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>${jedis.version}</version>
    </dependency>
    
    
    1. newbeemall项目后台商品管理中添加同步按钮 image.png,编写商品全量同步按钮,为了加快同步速度,通过多线程同步
    // 同步商品到 RediSearch
    public boolean syncRs() {
        jedisSearch.dropIndex(Constants.GOODS_IDX_NAME);
        Schema schema = new Schema()
                .addSortableTextField("goodsName", 1.0)
                .addSortableTextField("goodsIntro", 0.5)
                .addSortableNumericField("goodsId")
                .addSortableNumericField("sellingPrice")
                .addSortableNumericField("originalPrice")
                .addSortableTagField("tag", "|");
        jedisSearch.createIndex(Constants.GOODS_IDX_NAME, "goods:", schema);
        List<Goods> list = this.list();
        jedisSearch.deleteGoodsList(Constants.GOODS_IDX_PREFIX);
        return jedisSearch.addGoodsListIndex(Constants.GOODS_IDX_PREFIX, list);
    }
    /**
     * 同步商品索引
     *
     * @param keyPrefix 要索引的数据前缀
     * @return boolean
     */
    public boolean addGoodsListIndex(String keyPrefix, List<Goods> list) {
        int chunk = 200;
        int size = list.size();
        int ceil = (int) Math.ceil(size / (double) chunk);
        // 多线程同步
        List<CompletableFuture<Void>> futures = new ArrayList<>(4);
        for (int i = 0; i < ceil; i++) {
            int toIndex = (i + 1) * chunk;
            if (toIndex > size) {
                toIndex = i * chunk + size % chunk;
            }
            List<Goods> subList = list.subList(i * chunk, toIndex);
            CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> subList).thenAccept(goodsList -> {
                for (Goods goods : goodsList) {
                    Map<String, String> hash = MyBeanUtil.toMap(goods);
                    hash.put("_language", Constants.GOODS_IDX_LANGUAGE);
                    client.hset(keyPrefix + goods.getGoodsId(), MyBeanUtil.toMap(goods));
                }
            });
            futures.add(voidCompletableFuture);
        }
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
        return true;
    }
    
    1. 修改商品页面搜索接口
    @GetMapping("/search")
    public String rsRearch(SearchObjVO searchObjVO, HttpServletRequest request) {
        Page<SearchPageGoodsVO> page = getPage(request, Constants.GOODS_SEARCH_PAGE_LIMIT);
        ...
        // RediSearch 中文搜索
        SearchResult query = jedisSearch.search(Constants.GOODS_IDX_NAME, searchObjVO, page);
        ...
        return "mall/search";
    }
    
    1. 查看搜索结果中包含"小米"、"手机"两个单独分词

    image.png

    四、总结

    通过以上实战项目,使用RediSearch是可以满足基本中文分词需求 image.png 高级用法聚合查询、结果高亮、停用词、扩展 API 、拼写更正、自动补全等可以在官网了解。

    最后贴一下实战项目地址newbeemall,集成 RediSearch ,代码开源已上传

    wayn111
        1
    wayn111  
    OP
       2022-03-30 14:20:10 +08:00
    顶一个
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1043 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 19:33 · PVG 03:33 · LAX 11:33 · JFK 14:33
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.