最近学习了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中去。是采用实时变更就加入,还是定时一分钟,或者一小时加入,需要结合业务来处理。
我正在尝试设置一个puppet节点,但rubygems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由rubygems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby
我正在用Ruby编写一个简单的程序来检查域列表是否被占用。基本上它循环遍历列表,并使用以下函数进行检查。require'rubygems'require'whois'defcheck_domain(domain)c=Whois::Client.newc.query("google.com").available?end程序不断出错(即使我在google.com中进行硬编码),并打印以下消息。鉴于该程序非常简单,我已经没有什么想法了-有什么建议吗?/Library/Ruby/Gems/1.8/gems/whois-2.0.2/lib/whois/server/adapters/base.
我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer
我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢
我已经从我的命令行中获得了一切,所以我可以运行rubymyfile并且它可以正常工作。但是当我尝试从sublime中运行它时,我得到了undefinedmethod`require_relative'formain:Object有人知道我的sublime设置中缺少什么吗?我正在使用OSX并安装了rvm。 最佳答案 或者,您可以只使用“require”,它应该可以正常工作。我认为“require_relative”仅适用于ruby1.9+ 关于ruby-主要:Objectwhenrun
我知道我可以指定某些字段来使用pluck查询数据库。ids=Item.where('due_at但是我想知道,是否有一种方法可以指定我想避免从数据库查询的某些字段。某种反拔?posts=Post.where(published:true).do_not_lookup(:enormous_field) 最佳答案 Model#attribute_names应该返回列/属性数组。您可以排除其中一些并传递给pluck或select方法。像这样:posts=Post.where(published:true).select(Post.attr
我有一些代码在几个不同的位置之一运行:作为具有调试输出的命令行工具,作为不接受任何输出的更大程序的一部分,以及在Rails环境中。有时我需要根据代码的位置对代码进行细微的更改,我意识到以下样式似乎可行:print"Testingnestedfunctionsdefined\n"CLI=trueifCLIdeftest_printprint"CommandLineVersion\n"endelsedeftest_printprint"ReleaseVersion\n"endendtest_print()这导致:TestingnestedfunctionsdefinedCommandLin
我有一个只接受一个参数的方法:defmy_method(number)end如果使用number调用方法,我该如何引发错误??通常,我如何定义方法参数的条件?比如我想在调用的时候报错:my_method(1) 最佳答案 您可以添加guard在函数的开头,如果参数无效则引发异常。例如:defmy_method(number)failArgumentError,"Inputshouldbegreaterthanorequalto2"ifnumbereputse.messageend#=>Inputshouldbegreaterthano
我使用Ember作为我的前端和GrapeAPI来为我的API提供服务。前端发送类似:{"service"=>{"name"=>"Name","duration"=>"30","user"=>nil,"organization"=>"org","category"=>nil,"description"=>"description","disabled"=>true,"color"=>nil,"availabilities"=>[{"day"=>"Saturday","enabled"=>false,"timeSlots"=>[{"startAt"=>"09:00AM","endAt"=>
我想获取模块中定义的所有常量的值:moduleLettersA='apple'.freezeB='boy'.freezeendconstants给了我常量的名字:Letters.constants(false)#=>[:A,:B]如何获取它们的值的数组,即["apple","boy"]? 最佳答案 为了做到这一点,请使用mapLetters.constants(false).map&Letters.method(:const_get)这将返回["a","b"]第二种方式:Letters.constants(false).map{|c