最近學習了es的視頻,感覺這個產品對於查詢來說非常方便,但是如何應用到我們自己的 產品中來呢。因為我們的產品數據更新太快,其實不太適合用es做主力存儲。並且我們的業務還沒有到那種巨量級別,產品的伺服器容量也有限,所以我打算根據es的倒排索引的原理,自己寫一個查詢的組件。 我的理解是這樣的,有大量的文 ...
最近學習了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中去。是採用實時變更就加入,還是定時一分鐘,或者一小時加入,需要結合業務來處理。