一、场景简介

  最近在做公众号关键词回复方面的智能问答相关功能,发现用户输入提问内容和我们运营配置的关键词匹配回复率极低,原因是我们采用的是数据库的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实现公众号关键词回复智能问答的更多相关文章

  1. Azure 项目构建 - 用 Azure 认知服务在微信公众号上搭建智能会务系统

    通过完整流程详细介绍了如何在Azure平台上快速搭建基于微信公众号的智慧云会务管理系统. 此系列的全部课程 https://school.azure.cn/curriculums/11 立即访问htt ...

  2. SAP MM01 创建物料主数据 [关注公众号后回复MM01获取更多资料]

    操作内容 物料主数据,适用于所有有物料编码物料相关信息的系统维护 业务流程 新项目设计冻结后—M公司 PD用-物料编码申请表D-BOM Material Number  Application部门内部 ...

  3. C#微信公众号开发系列教程六(被动回复与上传下载多媒体文件)

    微信公众号开发系列教程一(调试环境部署) 微信公众号开发系列教程一(调试环境部署续:vs远程调试) C#微信公众号开发系列教程二(新手接入指南) C#微信公众号开发系列教程三(消息体签名及加解密) C ...

  4. weiphp 微信公众号用程序来设置指定内容消息回复业务逻辑操作

    微信公众号机器人回复设置 在公众号插件里面的Robot- Model- weixinAddonModel.php里面的 reply设置 reply($dataArr,$keywordArr) 解析方法 ...

  5. spring-boot-route(二十三)开发微信公众号

    在讲微信公众号开发之前,先来大概了解一下微信公众号.微信公众号大体上可以分为服务号和订阅号,订阅号和服务号的区别如下: 服务号可以申请微信支付功能. 服务号只能由企业申请,订阅号可以有企业或个人申请. ...

  6. 微信公众号开发系统入门教程(公众号注册、开发环境搭建、access_token管理、Demo实现、natapp外网穿透)

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/a1786223749/article/ ...

  7. 使用python django快速搭建微信公众号后台

    前言 使用python语言,django web框架,以及wechatpy,快速完成微信公众号后台服务的简易搭建,做记录于此. wechatpy是一个python的微信公众平台sdk,封装了被动消息和 ...

  8. 如何排版 微信公众号「代码块」之 MarkEditor

    前段时间写过一篇文章 如何排版微信公众号「代码块」,讲的是如何使用浏览器插件 Markdown Here 来排版代码块.虽然用 Markdown Here 排版出来的样式还不错,但存在一个问题,就是代 ...

  9. 转载微信公众号 测试那点事:Jmeter乱码解决

    原文地址: http://mp.weixin.qq.com/s/4Li5z_-rT0HPPQx9Iyi5UQ  中文乱码一直都是比较让人棘手的问题,我们在使用Jmeter的过程中,也会遇到中文乱码问题 ...

随机推荐

  1. Geodesic 什么是“测地线的”?

    确定是使用上椭球体(测地线)还是平地上(平面)的最短路径.强烈建议将 Geodesic 方法用于在不适合进行距离测量的坐标系(例如 Web 墨卡托或任何地理坐标系)中存储的数据,以及任何地理区域跨度较 ...

  2. 【Beta阶段】第十周Scrum会议

    [Beta阶段]第十周Scrum会议 本次会议为第十周第一次Scrum Meeting,会议对Alpha阶段的工作进行了反思总结,以及对Beta阶段工作的展望. 会议时间为2019.11.23.会议地 ...

  3. 教你如何在5分钟轻松部署squid正向代理

    正向代理是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返 ...

  4. HTML和css常见问题解答2

    1.将一个块级元素水平和垂直居中有几种方法?分别是什么? 四种方式: (1).要让div等块级元素水平和垂直居中,必需知道该div等块级元素的宽度和高度,然后设置位置为绝对位置,距离页面窗口左边框和上 ...

  5. Python的标准库介绍与常用的第三方库

    Python的标准库介绍与常用的第三方库 Python的标准库: datetime:为日期和时间的处理提供了简单和复杂的方法. zlib:以下模块直接支持通用的数据打包和压缩格式:zlib,gzip, ...

  6. 2019阿里天猫团队Java高级工程师面试题之第二面

    2019阿里天猫团队Java高级工程师面试题之第一面 2019阿里天猫团队Java高级工程师面试题之第三面 1.Tomcat的基本架构是什么? https://blog.csdn.net/xlgen1 ...

  7. python-paramiko对远程服务器终端的操作

    1.with open写文件到本地 2.paramiko SFTPClient将文件推到salt服务端 3.paramiko SSHClient通过salt-cp将文件分发给目标服务器 1. with ...

  8. PostgreSQL数据库一些tricks

    PostgreSQL自带Pgadmin客户端,可用于访问本地和远程PG库,一些tricks如下: 1.联合查询 SELECT * FROM table1 INNER JOIN table2 ON ta ...

  9. windows 使用 curl 命令

    什么是curl命令? curl是利用URL语法在命令行方式下工作的开源文件传输工具.它被广泛应用在Unix.多种Linux发行版中,并且有DOS和Win32.Win64下的移植版本. 如何在windo ...

  10. 周会材料:高并发程序设计<一>

    第一章 几个概念 同步:一次方法调用须等到其返回后才能有后续 异步:一次方法调用后在另一线程执行,调用者可不必等其返回就可进行后续 并发:任务以极短的时间交替进行 并行:任务同时进行 临界区:公共资源 ...