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的过程中,也会遇到中文乱码问题 ...
随机推荐
- zabbix批量清理模板,添加新模板
import requests import json import sys def get_token(): data = { "jsonrpc": "2.0" ...
- 【cf961G】G. Partitions(组合意义+第二类斯特林数)
传送门 题意: 给出\(n\)个元素,每个元素有价值\(w_i\).现在要对这\(n\)个元素进行划分,共划分为\(k\)组.每一组的价值为\(|S|\sum_{i=0}^{|S|}w_i\). 最后 ...
- js对文中某一处关键字自动检索和全文检索
部分检索: 代码: <%@ page language="java" contentType="text/html; charset=utf-8" pag ...
- Redux Class(immutable Record)引入的必要性 && Navigation引入方式
我的意见 和大家讨论一下几个问题 1. 项目里面没有用class规定的请求数据结构,调试数据的时候无法从前端获取请求的数据格式,要看后端接口,增加了调试的难度.我们以前会用immutable Reco ...
- js中触发表单提交
html代码: <form action="/test/action" method="get" id="myForm"> &l ...
- redis缓存穿透,缓存击穿,缓存雪崩
概念解释 redis 缓存穿透 key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源.比如用一个不存在的用户id获取用户信息,不论缓存还是数据库 ...
- IT兄弟连 HTML5教程 CSS3揭秘 CSS3属性5
9 透明属性 元素透明也是我们常用的样式,在CSS2中使用滤镜属性opacity实现透明效果.现在有了CSS3的rgba属性,就不用这么麻烦了,当然这也得要浏览器支持才行.通常我们定义颜色都是用十六 ...
- Centos 7 下安装 Jenkins
Jenkins介绍 Jenkins是一个开源的支持自动化构建.部署等任务的平台.基本上可以说是持续集成(CI).持续发布(CD)不可或缺的工具. 安装Java环境 CentOS 7 安装 JAVA环境 ...
- Egg 企业级应用开发框架的搭建
在之前的文章中我们介绍了一下基于 nodejs 开发的 koa2 框架,在之前还有 espress 框架,接下来我们再学习一个 Egg.js. Egg.js 中文官网:https://eggjs.or ...
- vue 客户端渲染和服务端渲染
参考链接 https://www.cnblogs.com/tiedaweishao/p/6644267.html