最近在作公众号关键词回复方面的智能问答相关功能,发现用户输入提问内容和咱们运营配置的关键词匹配回复率极低,缘由是咱们采用的是数据库的Like匹配。java
这种模糊匹配首先不是很智能,并且也没有具体的排序功能。为了解决这一问题,我引入了分词器+Lucene来实现智能问答。mysql
本功能采用springboot项目中引入Lucene相关包,而后实现相关功能。前提你们对springboot要有必定了解。spring
<!--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>
初始化bean类须要知道的几点:sql
1.实例化 IndexWriter,IndexSearcher 都须要去加载索引文件夹,实例化是是很是消耗资源的,因此咱们但愿只实例化一次交给spring管理。数据库
2.IndexSearcher 咱们通常经过SearcherManager管理,由于IndexSearcher 若是初始化的时候加载了索引文件夹,那么apache
后面添加、删除、修改的索引都不能经过IndexSearcher 查出来,由于它没有与索引库实时同步,只是第一次有加载。api
3.ControlledRealTimeReopenThread建立一个守护线程,若是没有主线程这个也会消失,这个线程做用就是按期更新让SearchManager管理的search能得到最新的索引库,下面是每25S执行一次。springboot
5.要注意引入的lucene版本,不一样的版本用法也不一样,许多api都有改变。
ide
/** * @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; } }
项目启动后,重建索引库中全部的索引。ui
@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类 | 数据类型 | 是否分词 | 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()方法,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; }
好了,智能问答查询回复功能基本完成了,大大提升公众号智能回复响应效率。