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 一般此對字段需要進行檢索查詢

 

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;
    }

  

 好了,智能問答查詢回覆功能基本完成了,大大提高公衆號智能回覆響應效率。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章