最近学习了es的视频,感觉这个产品对于查询来说非常方便,但是如何应用到我们自己的 产品中来呢。因为我们的产品数据更新太快,其实不太适合用es做主力存储。并且我们的业务还没有到那种巨量级别,产品的服务器容量也有限,所以我打算根据es的倒排索引的原理,自己写一个查询的组件。

我的理解是这样的,有大量的文字需要进行模糊查询,在mysql中,如果使用like的话是非常合适的,目前我就是采用这种方式查询的,因为数据量还未到千万级别,速度也还行,不过马上要突破了,所以要考虑优化的事情了。所以我的思路是这样的:

1 首先将数据库中的大段文字和标题都提取出来。

2 这些文字都对应了主键。

3 使用jcseg分词将一段文字进行分词,然后将分好的词语主键保存到redis中去。

4 为了节省空间,只分重要的业务关键字,其他无关的分词都不需要。

5 因为数据量巨大,在进行数据提取的时候,采用了线程池,优化了采集速度。

 

使用的代码如下:

package com.liandyao.caop.caopdata.service.impl.ESearch;

import cn.hutool.core.util.StrUtil;
import com.liandyao.caop.caopdata.entity.CaiCaop;
import com.liandyao.caop.caopdata.mapper.CaiMapper;
import com.liandyao.caop.utils.ChineseSegment;
import com.liandyao.caop.utils.async.AsyncManager;
import com.liandyao.caop.utils.redis.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 倒排索引的研究
 * @author liandyao
 * @date 2022年6月24日
 */
@Service
public class CaiInvertedIndex {

    /**
     * 原子类型的数字
     */
    public static AtomicInteger atomic = new AtomicInteger();


    /**
     * 每页查询多少条
     */
    public static int SIZE = 1000 ;

    /**
     * KEY
     */
    public static String REDIS_KEY = "INVD_INDX:MYSQL_DATA:CAOPU";

    @Autowired
    CaipMapper caopMapper;

    @Autowired
    RedisUtil redisUtil ;

    /**
     * 同步数据到redis
     */
    @Transactional
    public void sysnCaopDataToRedis(int pages){
        System.out.println(pages);
        //每页显示1000条

        int startRows = (pages - 1) * SIZE ;
        List<CaiCaop> listCaop =caopMapper.selectListByPage(startRows,SIZE);

        System.out.println("查询的条数:"+listCaop.size());
        listCaop.forEach(caop->{
            //加入到redis中
            redisUtil.leftPush(REDIS_KEY,caop);
            //最后一个执行的id,因为多线程的原因可能不是最后一个,这里只是记录一下
            redisUtil.set(REDIS_KEY+"LAST_ID",caop.getId());
        });
    }

    /**
     * 加入分词信息
     */
    public void segCaopData(){
        long caopSize = redisUtil.lGetListSize(REDIS_KEY);
        System.out.println("正在处理,共有:"+caopSize+"条数据");
        int i = 0;
        while(i<caopSize){
            //运行一次增加1
            i++;
            AsyncManager.me().execute(new TimerTask() {
                @Override
                public void run() {
                    CaiCaop caop = (CaiCaop) redisUtil.rightPop(REDIS_KEY);
                    if(caop!=null){
                        String content = caop.getContent()+" "+caop.getAddress();
                        List<String> typeNames = StrUtil.split(caop.getTypeName(),",");

                        //先将种类作为倒序索引加入redis
                        typeNames.forEach(str->{
                            if(StrUtil.isNotBlank(str)){
                                redisUtil.zsetAdd(REDIS_KEY+":"+str,caop.getId(),caop.getUpdateDate().getTime());
                            }

                        });
                        //再进行分词
                        List<String> list = ChineseSegment.segment(content);
                        list.forEach(segWord->{
                            redisUtil.zsetAdd(REDIS_KEY+":"+segWord,caop.getId(),caop.getUpdateDate().getTime());
                        });
                        System.out.println("处理成功:"+caop.getId());
                    }
                }
            });
        }



    }


    public static void main(String[] args) {


    }

}

 

中文分词代码

package com.liandyao.caop.utils;

import cn.hutool.core.util.ArrayUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.lionsoul.jcseg.ISegment;
import org.lionsoul.jcseg.IWord;
import org.lionsoul.jcseg.dic.ADictionary;
import org.lionsoul.jcseg.dic.DictionaryFactory;
import org.lionsoul.jcseg.extractor.impl.TextRankKeyphraseExtractor;
import org.lionsoul.jcseg.extractor.impl.TextRankKeywordsExtractor;
import org.lionsoul.jcseg.segmenter.SegmenterConfig;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;

/**
 * 中文分词
 * @author liandyao
 * @date 2021/12/10 19:16
 */
@Slf4j
public  class  ChineseSegment {
    static  ISegment seg=null;

    /**
     * 初始化
     */
    public static synchronized void init(){
        seg=null;
        SegmenterConfig config = new SegmenterConfig(true);
        String lexicon[] = {"e://lexicon"};
        config.setLexiconPath(lexicon);
        ADictionary dic = DictionaryFactory.createSingletonDictionary(config);

        seg = ISegment.COMPLEX.factory.create(config, dic);

    }

    static{
        init();
    }

    /**
     * 取地方分词结果
     * @param str
     * @return
     */
    public static synchronized List<String> segmentAddress(String str){

        List<String> list = new ArrayList<>();
        try {

            seg.reset(new StringReader(str));

            //System.out.println("====>"+str+"  ==> "+seg);
            //获取分词结果
            IWord word = null;
            while ( (word = seg.next()) != null ) {
                //1 普通词语
                if(word.getType()==1){
                    //17表示数字
                    if(word.getType()==17 || word.getPartSpeech()==null) {
                        continue;
                    }
                    //处所词
                    if(ArrayUtil.contains(word.getPartSpeech(),"ns")){
                        list.add(word.getValue());
                        log.info("分词结果"+word.getValue());
                    }
                }
            }
        } catch (Exception e) {
            init();
            e.printStackTrace();
        }
        return list;
    }

    /**
     * 分析工种分词结果
     * @param str
     * @return
     */
    public static synchronized List<String> segmentCaitype(String str){
        List<String> list = new ArrayList<>();
        try {
            seg.reset(new StringReader(str));
            //获取分词结果
            IWord word = null;
            while ( (word = seg.next()) != null ) {

                //1 普通词语
                if(word.getType()==1){
                    //17表示数字
                    if(word.getType()==17 || word.getPartSpeech()==null) {
                        continue;
                    }
                    //工种分词
                    if(ArrayUtil.contains(word.getPartSpeech(),"ncn")){
                        list.add(word.getValue());
                        log.info("分词结果"+word.getValue());
                    }
                }

            }
        } catch (IOException e) {
            init();
            e.printStackTrace();
        }
        return list;
    }

    /**
     * 综合分词
     * @param str
     * @return
     */
    public static synchronized List<String> segment(String str){
        List<String> list = new ArrayList<>();
        try {
            seg.reset(new StringReader(str));
            //获取分词结果
            IWord word = null;
            while ( (word = seg.next()) != null ) {
                /**


                 名词n、时间词t、处所词s、方位词f、数词m、量词q、区别词b、代词r、动词v、形容词a、状态词z、副词d、
                 介词p、连词c、助词u、语气词y、叹词e、拟声词o、成语i、习惯用语l、简称j、前接成分h、后接成分k、语素g、非语素字x、标点符号w)外,
                 从语料库应用的角度,增加了专有名词(人名nr、地名ns、机构名称nt、其他专有名词nz)



                 */
                //1 普通词语
                if(word.getType()==1){
                    //17表示数字
                    if(word.getType()==17 || word.getPartSpeech()==null) {
                        continue;
                    }
                    /*

                    //工种分词
                    if(ArrayUtil.contains(word.getPartSpeech(),"ncn")){
                        list.add(word.getValue());
                        log.info("分词结果"+word.getValue());
                    }
                    //处所词
                    if(ArrayUtil.contains(word.getPartSpeech(),"ns")){
                        list.add(word.getValue());
                        log.info("分词结果"+word.getValue());
                    }

                    */

                    if(word.getValue().length()>=2){
                        list.add(word.getValue());
                    }

                }

            }
        } catch (IOException e) {
            init();
            e.printStackTrace();
        }
        return list;
    }

    public static void main(String[] args) {
        //设置要分词的内容
        String str = "今天真是阳光明媚,夕阳西下,白日依山尽!";

            int i = 0 ;

        List<String> keywords = segment(str);

        keywords.forEach(str1 -> {
            System.out.println(str1);
        });


    }

    @Test
    public void test1() throws IOException {
        String str = "四川成都市长期招工信息:招2个砌筑工维修师傅,少数民族勿扰,1个力工,电话微信同步,活在成都";
     str
=""; //2, 构建TextRankKeywordsExtractor关键字提取器 TextRankKeywordsExtractor extractor = new TextRankKeywordsExtractor(seg); //extractor.setMaxIterateNum(100); //设置pagerank算法最大迭代次数,非必须,使用默认即可 //extractor.setWindowSize(5); //设置textRank计算窗口大小,非必须,使用默认即可 // extractor.setKeywordsNum(10); //设置最大返回的关键词个数,默认为10 List<String> keywords = extractor.getKeywords(new StringReader(str)); keywords.forEach(str2 -> System.out.println(str2)); } @Test public void test2() throws IOException { //String str = " 四川成都市长期招工信息:招2个砌筑工维修师傅,少数民族勿扰,1个力工,电话微信同步, "; //2, 构建TextRankKeyphraseExtractor关键短语提取器 TextRankKeyphraseExtractor extractor = new TextRankKeyphraseExtractor(seg); extractor.setMaxIterateNum(100); //设置pagerank算法最大迭代词库,非必须,使用默认即可 extractor.setWindowSize(5); //设置textRank窗口大小,非必须,使用默认即可 extractor.setKeywordsNum(15); //设置最大返回的关键词个数,默认为10 extractor.setMaxWordsNum(4); //设置最大短语词长,默认为5 //3, 从一个输入reader输入流中获取短语 String str = "支持向量机广泛应用于文本挖掘,例如,基于支持向量机的文本自动分类技术研究一文中很详细的介绍支持向量机的算法细节,文本自动分类是文本挖掘技术中的一种!"; List<String> keyphrases = extractor.getKeyphrase(new StringReader(str)); keyphrases.forEach(str2 -> System.out.println(str2)); } }

 

结语

1 本文提供了倒排索引的思路,比较浅显,还可以深入研究

2 使用本组件将关键字放入redis之后,页面上传入的关键字就可以在redis中对应key,这样的速度将非常快,从key中可以找到主键,再用主键到mysql中查询,大大提高了查询速度。

3 需要考虑的问题,如何做到更新就加入关键字到redis中去。是采用实时变更就加入,还是定时一分钟,或者一小时加入,需要结合业务来处理。