lucene 思惟導圖,讓搜索引擎再也不難懂

image

   (公衆號回覆「lucene」獲取源導圖)java

今天,咱們來說講lucene,同窗們搬好板凳坐好啦。git

(lucene幹嗎的呀?)github

首先咱們來看張思惟導圖:數據庫

image

以上是咱們java經常使用的全文搜索引擎框架,不少項目的搜索功能都是基於以上4個框架完成的。apache

因此lucene究竟是幹啥的?服務器

Lucene是一套用於全文檢索和搜索的開放源代碼程序庫,一個可以輕鬆集添加搜索功能到一個應用程序中的簡單卻強大的核心代碼庫和API。網絡

Lucene,目前最受歡迎的Java全文搜索框架。緣由很簡單,hibernate search、solr、elasticsearch都是基於lucene拓展出來的搜索引擎。架構

Hibernate Search是在apache Lucene的基礎上創建的主要用於Hibernate的持久化模型的全文檢索工具。框架

Elasticsearch也使用Java開發並使用Lucene做爲其核心來實現全部索引和搜索的功能,可是它的目的是經過簡單的RESTful API來隱藏Lucene的複雜性,從而讓全文搜索變得簡單。elasticsearch

Solr它是一種開放源碼的、基於 Lucene Java 的搜索服務器,易於加入到 Web 應用程序中。提供了層面搜索(就是統計)、命中醒目顯示而且支持多種輸出格式(包括XML/XSLT 和JSON等格式)。

因此lucene牛不牛逼!!

接下來,咱們分爲如下幾個部分去理解、打開lucene的真面目。

  • 相關概念

  • 構建索引與查詢索引過程

  • 倒排索引

  • 可視化工具

  • 項目應用指南

相關概念

lucene官方網站:http://lucene.apache.org/

既然是全文搜索工具,確定有必定的排序結構和規則。當咱們輸入關鍵字的時候,lucene能安裝內部的層次結構快速檢索出我須要的內容。這裏面會涉及到幾個層次和概念。

image

索引庫(Index)

一個目錄一個索引庫,同一文件夾中的全部的文件構成一個Lucene索引庫。相似數據庫的表的概念。

image

(lucene的索引實例)

段(Segment)

Lucene索引可能由多個子索引組成,這些子索引成爲段。每一段都是完整獨立的索引,能被搜索。

文檔(Document)

一個索引能夠包含多個段,段與段之間是獨立的,添加新文檔能夠生成新的段,不一樣的段能夠合併。段是索引數據存儲的單元。相似數據庫內的行或者文檔數據庫內的文檔的概念。

域(Field)

一篇文檔包含不一樣類型的信息,能夠分開索引,好比標題,時間,正文,做者等。相似於數據庫表中的字段*。*

詞(Term)

詞是索引的最小單位,是通過詞法分析和語言處理後的字符串。一個Field由一個或多個Term組成。好比標題內容是「hello lucene」,通過分詞以後就是「hello」,「lucene」,這兩個單詞就是Term的內容信息,當關鍵字搜索「hello」或者「lucene」的時候這個標題就會被搜索出來。

**分詞器(**Analyzer)

一段有意義的文字須要經過Analyzer來分割成一個個詞語後才能按關鍵詞搜索。StandartdAnalyzer是Lucene中經常使用的分析器,中文分詞有CJKAnalyzer、SmartChinieseAnalyzer等。

image

(lucene 索引存儲結構概念圖)

上圖大概能夠這樣理解,索引內部由多個段組成,當新文檔添加進來時候會生成新的段,不一樣的段之間能夠合併(Segment-0、Segment-一、Segment-2合併成Segment-4),段內含有文檔號與文檔的索引信息。而每一個文檔內有多個域能夠進行索引,每一個域能夠指定不一樣類型(StringField,TextField)。

因此,從圖中能夠看出,lucene的層次結構依次以下:索引(Index) –> 段(segment) –> 文檔(Document) –> 域(Field) –> 詞(Term)

在上面咱們瞭解了lucene的一些基本概念,接下來咱們進入原理分析的環節。

(爲何lucene搜索引擎查詢這麼快?)

倒排索引

咱們都知道要想提升檢索速度要創建索引,重點就在這裏,lucene使用了倒排索引(也叫反向索引)的結構。

倒排索引(反向索引)天然就有正排索引(正向索引)。

  • 正排索引是指從文檔檢索出單詞,正常查詢的話咱們都是從文檔裏面去檢索有沒這個關鍵字單詞。

  • 倒排索引是指從單詞檢索出文檔,與從正排索引是倒過來的概念,須要預先爲文檔準備關鍵字,而後查詢時候直接匹配關鍵字獲得對應的文檔。

有一句這樣的總結:因爲不是由記錄來肯定屬性值,而是由屬性值來肯定記錄的位置,於是稱爲倒排索引(inverted index)。

image

(具體怎麼實現的呀?)

我們來舉個例子來研究一下(例子來源於網絡):

假如如今有兩個文檔,內容分別是:

  • 文檔1:home sales rise in July.

  • 文檔2:increase in home sales in July.     

image

分析上圖可知,首先文檔通過分詞器(Analyzer)分詞以後,咱們能夠獲得詞(term),詞和文檔ID是對應起來的,接下來這些詞集進行一次排序,而後合併相同的詞並統計出現頻率,以及記錄出現的文檔ID。

因此:

實現時,lucene將上面三列分別做爲詞典文件(Term Dictionary)、*頻率文件(frequencies)、位置文件 (positions)*保存。其中詞典文件不只保存有每一個關鍵詞,還保留了指向頻率文件和位置文件的指針,經過指針能夠找到該關鍵字的頻率信息和位置信息。 

索引時,假設要查詢單詞 「sales」,lucene先對詞典二元查找、找到該詞,經過指向頻率文件的指針讀出全部文章號,而後返回結果。詞典一般很是小,於是,整個過程的時間是毫秒級的。  

(原來如此!)

lucne可視化工具Luke

image

構建索引與查詢索引過程

以上咱們知道了lucene構建索引的原理,接下來咱們在代碼層面去使用lucene。

咱們先來看一張圖:

image

檢索文件以前先要創建索引,因此上圖得從「待檢索文件」節點開始看。

構建索引過程:

一、爲每個待檢索的文件構建Document類對象,將文件中各部份內容做爲Field類對象。

二、使用Analyzer類實現對文檔中的天然語言文本進行分詞處理,並使用IndexWriter類構建索引。

三、使用FSDirectory類設定索引存儲的方式和位置,實現索引的存儲。

檢索索引過程:

四、使用IndexReader類讀取索引。

五、使用Term類表示用戶所查找的關鍵字以及關鍵字所在的字段,使用QueryParser類表示用戶的查詢條件。

六、使用IndexSearcher類檢索索引,返回符合查詢條件的Document類對象。

其中虛線指向的是這個類所在的包名(packege)。如Analyzer在org.apache.lucene.analysis包下。

image

構建索引代碼:

//建立索引
public class CreateTest {

    public static void main(String[] args) throws Exception {
        Path indexPath = FileSystems.getDefault().getPath("d:\\index\\");

//        FSDirectory有三個主要的子類,open方法會根據系統環境自動挑選最合適的子類建立
//        MMapDirectory:Linux, MacOSX, Solaris
//        NIOFSDirectory:other non-Windows JREs
//        SimpleFSDirectory:other JREs on Windows
        Directory dir = FSDirectory.open(indexPath);

        // 分詞器
        Analyzer analyzer = new StandardAnalyzer();
        boolean create = true;
        IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
        if (create) {
            indexWriterConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE);
        } else {
            // lucene是不支持更新的,這裏僅僅是刪除舊索引,而後建立新索引
            indexWriterConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
        }
        IndexWriter indexWriter = new IndexWriter(dir, indexWriterConfig);

        Document doc = new Document();
        // 域值會被索引,可是不會被分詞,即被看成一個完整的token處理,通常用在「國家」或者「ID
        // Field.Store表示是否在索引中存儲原始的域值
        // 若是想在查詢結果裏顯示域值,則須要對其進行存儲
        // 若是內容太大而且不須要顯示域值(整篇文章內容),則不適合存儲到索引中
        doc.add(new StringField("Title", "sean", Field.Store.YES));
        long time = new Date().getTime();
        // LongPoint並不存儲域值
        doc.add(new LongPoint("LastModified", time));
//        doc.add(new NumericDocValuesField("LastModified", time));
        // 會自動被索引和分詞的字段,通常被用在文章的正文部分
        doc.add(new TextField("Content", "this is a test of sean", Field.Store.NO));

        List<Document> docs = new LinkedList<>();
        docs.add(doc);

        indexWriter.addDocuments(docs);
        // 默認會在關閉前提交
        indexWriter.close();
    }
}

對應時序圖:

image

查詢索引代碼:

//查詢索引
public class QueryTest {

    public static void main(String[] args) throws Exception {
        Path indexPath = FileSystems.getDefault().getPath("d:\\index\\");
        Directory dir = FSDirectory.open(indexPath);
        // 分詞器
        Analyzer analyzer = new StandardAnalyzer();

        IndexReader reader = DirectoryReader.open(dir);
        IndexSearcher searcher = new IndexSearcher(reader);

        // 同時查詢多個域
//        String[] queryFields = {"Title", "Content", "LastModified"};
//        QueryParser parser = new MultiFieldQueryParser(queryFields, analyzer);
//        Query query = parser.parse("sean");

        // 一個域按詞查doc
//        Term term = new Term("Title", "test");
//        Query query = new TermQuery(term);

        // 模糊查詢
//        Term term = new Term("Title", "se*");
//        WildcardQuery query = new WildcardQuery(term);

        // 範圍查詢
        Query query1 = LongPoint.newRangeQuery("LastModified", 1L, 1637069693000L);

        // 多關鍵字查詢,必須指定slop(key的存儲方式)
        PhraseQuery.Builder phraseQueryBuilder = new PhraseQuery.Builder();
        phraseQueryBuilder.add(new Term("Content", "test"));
        phraseQueryBuilder.add(new Term("Content", "sean"));
        phraseQueryBuilder.setSlop(10);
        PhraseQuery query2 = phraseQueryBuilder.build();

        // 複合查詢
        BooleanQuery.Builder booleanQueryBuildr = new BooleanQuery.Builder();
        booleanQueryBuildr.add(query1, BooleanClause.Occur.MUST);
        booleanQueryBuildr.add(query2, BooleanClause.Occur.MUST);
        BooleanQuery query = booleanQueryBuildr.build();

        // 返回doc排序
        // 排序域必須存在,不然會報錯
        Sort sort = new Sort();
        SortField sortField = new SortField("Title", SortField.Type.SCORE);
        sort.setSort(sortField);

        TopDocs topDocs = searcher.search(query, 10, sort);
        if(topDocs.totalHits > 0)
            for(ScoreDoc scoreDoc : topDocs.scoreDocs){
                int docNum = scoreDoc.doc;
                Document doc = searcher.doc(docNum);
                System.out.println(doc.toString());
            }
    }
}

對應時序圖:

image

lucene版本信息:

<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-core</artifactId>
    <version>7.4.0</version>
</dependency>

<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-queryparser</artifactId>
    <version>7.4.0</version>
</dependency>

項目應用指南

在實際開發,比較少會直接用lucene,如今主流的搜索框架solr、Elasticsearch都是基於lucene,給咱們提供了更加簡便的API。特別是在分佈式環境中,Elasticsearch能夠問咱們解決單點問題、備份問題、集羣分片等問題,更加符合發展趨勢。

至此,整篇完~

image

java思惟導圖

長按關注,天天java一下,成就架構師

相關文章
相關標籤/搜索