一、場景簡介
最近在做公眾號關鍵詞回復方面的智能問答相關功能,發現用戶輸入提問內容和我們運營配置的關鍵詞匹配回復率極低,原因是我們采用的是數據庫的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; }
好了,智能問答查詢回復功能基本完成了,大大提高公眾號智能回復響應效率。