一、场景简介

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

    hiahiahia我又来作恶了,莫名其妙的第八次实验还要更新! 文章目录 一.Git 1.1 概述 1.1.1 Git 1.1.2 分布式版本控制系统 1.1.3 指令集 1.2 版本控制系统 1.2 ...

  2. maven配置多个镜像

    问题场景 1.国内访问maven默认远程中央镜像特别慢 2.用阿里的镜像替代远程中央镜像 3.大部分jar包都可以在阿里镜像中找到,部分jar包在阿里镜像中没有,需要单独配置镜像 解决方案 setti ...

  3. 冒泡排序(C语言)

    # include<stdio.h> int main(void) { int arr[10]={5,4,7,9,2,3,1,6,10,8}; //定义一个位排序的数组 int i; // ...

  4. 强大的django-debug-toolbar,django项目性能分析工具

    强大的django-debug-toolbar,django项目性能分析工具 给大家介绍一个用于django中debug模式下查看网站性能等其他信息的插件django-debug-toolbar 首先 ...

  5. linux用户组相关,密码相关,文件操作,和启动级别

    一.开机重启和用户切换 注意,当关机或重启前,都应当先执行一下sync指令,把内存的数据写入磁盘,防止数据丢失. shutdown命令 shutdown -h now :立即关机 shutdown - ...

  6. 【Git教程】如何清除git仓库的所有提交记录,成为一个新的干净仓库

    一.引言 马三也算Github的忠实用户了,经常会把一些练手的项目传到Github上面进行备份.其中有一个名为ColaFramework的Unity框架项目,马三开发了一年多了,期间提交代码的时候在L ...

  7. Redis入门(二)-Redis能够做什么

    引言 在上篇文章中,我们讲述了Redis的基本知识让读者对Redis有了基本的了解.那么这一节我们就来看一下Redis究竟能做什么. 上一节我们提到了Redis可用作数据库,高速缓存和消息队列代理.这 ...

  8. Linux CentOS上安装 MySQL 8.0.16

    前言: 因为我需要在我新安装的Linux CentOS系统服务器中安装和配置MySQL服务器,然而对于我们这种Linux使用小白而言在Linux系统中下载,解压,配置MySQL等一系列的操作还是有些耗 ...

  9. 随机的标识符GUID

    Guid guid = Guid.NewGuid();Console.WriteLine(guid.ToString());

  10. 基于JWT的Token登录认证

    1.JWT简介   JSON Web Token(缩写 JWT),是目前最流行的跨域认证解决方案. 2.JWT的原理        JWT的原理是,服务器认证以后,生成一个JSON格式的对象,发回给客 ...