springboot+lucene实现公众号关键词回复智能问答
一、场景简介
最近在做公众号关键词回复方面的智能问答相关功能,发现用户输入提问内容和我们运营配置的关键词匹配回复率极低,原因是我们采用的是数据库的Like匹配。
这种模糊匹配首先不是很智能,而且也没有具体的排序功能。为了解决这一问题,我引入了分词器+Lucene来实现智能问答。
二、功能实现
本功能采用springboot项目中引入Lucene相关包,然后实现相关功能。前提大家对springboot要有一定了解。
POM引入Lucene依赖
<!--lucene核心包-->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>7.6.0</version>
</dependency>
<!--对分词索引查询解析-->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>7.6.0</version>
</dependency>
<!-- smartcn中文分词器 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-smartcn</artifactId>
<version>7.6.0</version>
</dependency>
初始化Lucene相关配置Bean
初始化bean类需要知道的几点:
1.实例化 IndexWriter,IndexSearcher 都需要去加载索引文件夹,实例化是是非常消耗资源的,所以我们希望只实例化一次交给spring管理。
2.IndexSearcher 我们一般通过SearcherManager管理,因为IndexSearcher 如果初始化的时候加载了索引文件夹,那么
后面添加、删除、修改的索引都不能通过IndexSearcher 查出来,因为它没有与索引库实时同步,只是第一次有加载。
3.ControlledRealTimeReopenThread创建一个守护线程,如果没有主线程这个也会消失,这个线程作用就是定期更新让SearchManager管理的search能获得最新的索引库,下面是每25S执行一次。
5.要注意引入的lucene版本,不同的版本用法也不同,许多api都有改变。
/**
* @author mazhq
* @Title: LuceneConfig
* @date 2019/9/5 11:29
*/
@Configuration
public class LuceneConfig {
/**
* lucene索引,存放位置
*/
private static final String LUCENE_INDEX_PATH = "lucene/indexDir/";
/**
* 创建一个 Analyzer 实例
*/
@Bean
public Analyzer analyzer() {
return new SmartChineseAnalyzer();
}
/**
* 索引位置
*/
@Bean
public Directory directory() throws IOException {
Path path = Paths.get(LUCENE_INDEX_PATH);
File file = path.toFile();
if (!file.exists()) {
//如果文件夹不存在,则创建
file.mkdirs();
}
return FSDirectory.open(path);
}
/**
* 创建indexWriter
*/
@Bean
public IndexWriter indexWriter(Directory directory, Analyzer analyzer) throws IOException {
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);
// 清空索引
indexWriter.deleteAll();
indexWriter.commit();
return indexWriter;
}
/**
* SearcherManager管理
* ControlledRealTimeReopenThread创建一个守护线程,如果没有主线程这个也会消失,
* 这个线程作用就是定期更新让SearchManager管理的search能获得最新的索引库,下面是每25S执行一次。
*/
@Bean
public SearcherManager searcherManager(Directory directory, IndexWriter indexWriter) throws IOException {
SearcherManager searcherManager = new SearcherManager(indexWriter, false, false, new SearcherFactory());
ControlledRealTimeReopenThread cRTReopenThead = new ControlledRealTimeReopenThread(indexWriter, searcherManager,
5.0, 0.025);
cRTReopenThead.setDaemon(true);
//线程名称
cRTReopenThead.setName("更新IndexReader线程");
// 开启线程
cRTReopenThead.start();
return searcherManager;
}
}
初始化索引库
项目启动后,重建索引库中所有的索引。
@Component
@Order(value = 1)
public class AutoReplyMsgRunner implements ApplicationRunner {
@Autowired
private LuceneManager luceneManager;
@Override
public void run(ApplicationArguments args) throws Exception {
luceneManager.createAutoReplyMsgIndex();
}
}
从数据库中查出所有配置的消息回复内容,并创建这些内容的索引。
索引相关介绍:
我们知道,mysql对每个字段都定义了字段类型,然后根据类型保存相应的值。
那么lucene的存储对象是以document为存储单元,对象中相关的属性值则存放到Field(域)中;
Field类的常用类型
Field类 | 数据类型 | 是否分词 | index是否索引 | Stored是否存储 | 说明 |
StringField | 字符串 | N | Y | Y/N | 构建一个字符串的Field,但不会进行分词,将整串字符串存入索引中,适合存储固定(id,身份证号,订单号等) |
FloatPoint LongPoint DoublePoint |
数值型 | Y | Y | N | 这个Field用来构建一个float数字型Field,进行分词和索引,比如(价格) |
StoredField | 重载方法,,支持多种类型 | N | N | Y | 这个Field用来构建不同类型Field,不分析,不索引,但要Field存储在文档中 |
TextField | 字符串或者流 | Y | Y | Y/N | 一般此对字段需要进行检索查询 |
上面是一些常用的数据类型, 6.0后的版本,数值型建立索引的字段都更改为Point结尾,FloatPoint,LongPoint,DoublePoint等,对于浮点型的docvalue是对应的DocValuesField,整型为NumericDocValuesField,FloatDocValuesField等都为NumericDocValuesField的实现类。
commit()的用法
commit()方法,indexWriter.addDocuments(docs);只是将文档放在内存中,并没有放入索引库,没有commit()的文档,我从索引库中是查询不出来的;
许多博客代码中,都没有进行commit(),但仍然能查出来,因为每次插入,他都把IndexWriter关闭.close(),Lucene关闭前,都会把在内存的文档,提交到索引库中,索引能查出来,在spring中IndexWriter是单例的,不关闭,所以每次对索引都更改时,都需要进行commit()操作;
@Service
public class LuceneManager {
@Autowired
private IndexWriter indexWriter;
@Autowired
private AutoReplyMsgDao autoReplyMsgDao; public void createAutoReplyMsgIndex() throws IOException {
List<AutoReplyMsg> autoReplyMsgList = autoReplyMsgDao.findAllTextConfig();
if(autoReplyMsgList != null){
List<Document> docs = new ArrayList<Document>();
for (AutoReplyMsg autoReplyMsg:autoReplyMsgList) {
Document doc = new Document();
doc.add(new StringField("id", autoReplyMsg.getGuid()+"", Field.Store.YES));
doc.add(new TextField("keywords", autoReplyMsg.getReceiveContent(), Field.Store.YES));
doc.add(new StringField("replyMsgType", autoReplyMsg.getReplyMsgType()+"", Field.Store.YES));
doc.add(new StringField("replyContent", autoReplyMsg.getReplyContent()==null?"":autoReplyMsg.getReplyContent(), Field.Store.YES));
doc.add(new StringField("title", autoReplyMsg.getTitle()==null?"":autoReplyMsg.getTitle(), Field.Store.YES));
doc.add(new StringField("picUrl", autoReplyMsg.getPicUrl()==null?"":autoReplyMsg.getPicUrl(), Field.Store.YES));
doc.add(new StringField("url", autoReplyMsg.getUrl()==null?"":autoReplyMsg.getUrl(), Field.Store.YES));
doc.add(new StringField("mediaId", autoReplyMsg.getMediaId()==null?"":autoReplyMsg.getMediaId(), Field.Store.YES));
docs.add(doc);
}
indexWriter.addDocuments(docs);
indexWriter.commit();
}
}
}
智能查询
searcherManager.maybeRefresh()方法,刷新searcherManager中的searcher,获取到最新的IndexSearcher。
@Service
public class SearchManager {
@Autowired
private Analyzer analyzer;
@Autowired
private SearcherManager searcherManager; public AutoReplyMsg searchAutoReplyMsg(String keyword) throws IOException, ParseException {
searcherManager.maybeRefresh();
IndexSearcher indexSearcher = searcherManager.acquire();
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.add(new QueryParser("keywords", analyzer).parse(keyword), BooleanClause.Occur.MUST);
TopDocs topDocs = indexSearcher.search(builder.build(), 1);
ScoreDoc[] hits = topDocs.scoreDocs;
if(hits != null && hits.length > 0){
Document doc = indexSearcher.doc(hits[0].doc);
AutoReplyMsg autoReplyMsg = new AutoReplyMsg();
autoReplyMsg.setGuid(Long.parseLong(doc.get("id")));
autoReplyMsg.setReceiveContent(keyword);
autoReplyMsg.setReceiveMsgType(1);
autoReplyMsg.setReplyMsgType(Integer.valueOf(doc.get("replyMsgType")));
autoReplyMsg.setReplyContent(doc.get("replyContent"));
autoReplyMsg.setTitle(doc.get("title"));
autoReplyMsg.setPicUrl(doc.get("picUrl"));
autoReplyMsg.setUrl(doc.get("url"));
autoReplyMsg.setMediaId(doc.get("mediaId"));
return autoReplyMsg;
} return null;
}
}
索引维护~删除更新索引
public int delete(AutoReplyMsg autoReplyMsg){
int resp = autoReplyMsgDao.delete(autoReplyMsg.getGuid());
try {
indexWriter.deleteDocuments(new Term("id", autoReplyMsg.getGuid()+""));
indexWriter.commit();
} catch (IOException e) {
e.printStackTrace();
}
return resp;
}
好了,智能问答查询回复功能基本完成了,大大提高公众号智能回复响应效率。
springboot+lucene实现公众号关键词回复智能问答的更多相关文章
- Azure 项目构建 - 用 Azure 认知服务在微信公众号上搭建智能会务系统
通过完整流程详细介绍了如何在Azure平台上快速搭建基于微信公众号的智慧云会务管理系统. 此系列的全部课程 https://school.azure.cn/curriculums/11 立即访问htt ...
- SAP MM01 创建物料主数据 [关注公众号后回复MM01获取更多资料]
操作内容 物料主数据,适用于所有有物料编码物料相关信息的系统维护 业务流程 新项目设计冻结后—M公司 PD用-物料编码申请表D-BOM Material Number Application部门内部 ...
- C#微信公众号开发系列教程六(被动回复与上传下载多媒体文件)
微信公众号开发系列教程一(调试环境部署) 微信公众号开发系列教程一(调试环境部署续:vs远程调试) C#微信公众号开发系列教程二(新手接入指南) C#微信公众号开发系列教程三(消息体签名及加解密) C ...
- weiphp 微信公众号用程序来设置指定内容消息回复业务逻辑操作
微信公众号机器人回复设置 在公众号插件里面的Robot- Model- weixinAddonModel.php里面的 reply设置 reply($dataArr,$keywordArr) 解析方法 ...
- spring-boot-route(二十三)开发微信公众号
在讲微信公众号开发之前,先来大概了解一下微信公众号.微信公众号大体上可以分为服务号和订阅号,订阅号和服务号的区别如下: 服务号可以申请微信支付功能. 服务号只能由企业申请,订阅号可以有企业或个人申请. ...
- 微信公众号开发系统入门教程(公众号注册、开发环境搭建、access_token管理、Demo实现、natapp外网穿透)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/a1786223749/article/ ...
- 使用python django快速搭建微信公众号后台
前言 使用python语言,django web框架,以及wechatpy,快速完成微信公众号后台服务的简易搭建,做记录于此. wechatpy是一个python的微信公众平台sdk,封装了被动消息和 ...
- 如何排版 微信公众号「代码块」之 MarkEditor
前段时间写过一篇文章 如何排版微信公众号「代码块」,讲的是如何使用浏览器插件 Markdown Here 来排版代码块.虽然用 Markdown Here 排版出来的样式还不错,但存在一个问题,就是代 ...
- 转载微信公众号 测试那点事:Jmeter乱码解决
原文地址: http://mp.weixin.qq.com/s/4Li5z_-rT0HPPQx9Iyi5UQ 中文乱码一直都是比较让人棘手的问题,我们在使用Jmeter的过程中,也会遇到中文乱码问题 ...
随机推荐
- Hyper-V “SP2019SER”无法更改状态。操作失败,错误代码为“32788”。
卸载Hyper-V,然后重装,再重启已有的Hyper-V服务器,报错如下: 尝试启动选定的虚拟机时出错.“SP2019SER”无法更改状态. 原因:卸载后导致虚拟网卡出现问题导致的. 解决办法: 右击 ...
- 从0系统学Android--3.5 最常用和最难用的控件---ListView
从0系统学Android-- 3.5 最常用和最难用的控件---ListView 本系列文章目录:更多精品文章分类 本系列持续更新中.... 3.5 最常用和最难用的控件---ListView Lis ...
- 创建以.xxx开头的文件夹的方法
在windows下创建以.xxx开头的文件夹时,点击确认,系统提示“必须键入文件名”. 最方便的方法: (1)新建文件夹 (3)在文件名.xxxxx后再加一个.,也就是把文件名改成这样子:.XXXXX ...
- 数据库死锁的问题,Deadlock found when trying to get lock; try restarting transaction at Query.formatError
场景: 应用刚上线排除大批量请求的问题 线上多次出现的Deadlock found when trying to get lock错误 代码: async batchUpdate(skus, { tr ...
- TypeScript初体验
第一次运行TypeScript 1.创建文件夹并初始化项目 mkdir ts-demo cd ts-demo npm init -y 2.安装typescript与ts-node # 局部安装 npm ...
- nlp英文的数据清洗代码
1.常用的清洗方式 #coding=utf-8 import jieba import unicodedata import sys,re,collections,nltk from nltk.ste ...
- Java之Iterator接口(遍历单列集合的迭代器)
Iterator接口概述 在程序开发中,经常需要遍历集合中的所有元素.针对这种需求,JDK专门提供了一个接口java.util.Iterator . Iterator 接口也是Java集合中的一员,但 ...
- element form 对单个字段做验证
this.$refs[formName].validateField('phone', phoneError => { //验证手机号码是否正确 if (!phoneError) { conso ...
- Java基础(六)
Spring依赖注入的几种方式 1.接口注入 2.setter方法注入 3.构造方法注入 Struts2的拦截器与过滤器的工作原理 1.Structs2拦截器是在访问某个Action或Action的某 ...
- 当面试官要你介绍一下MQ时,该怎么回答?
一.为什么要使用MQ消息中间件? 一个用消息队列的人,不知道为啥用,有点尴尬.没有复习这点,很容易被问蒙,然后就开始胡扯了. 回答:这个问题,咱只答三个最主要的应用场景,不可否认还有其他的,但是只答三 ...