1. 程式人生 > >lucene 思維導圖,讓搜尋引擎不再難懂

lucene 思維導圖,讓搜尋引擎不再難懂

image

   (公眾號回覆“lucene”獲取源導圖)

今天,我們來講講lucene,同學們搬好板凳坐好啦。

(lucene幹嘛的呀?)

首先我們來看張思維導圖:

image

以上是我們java常用的全文搜尋引擎框架,很多專案的搜尋功能都是基於以上4個框架完成的。

所以lucene到底是幹啥的?

Lucene是一套用於全文檢索和搜尋的開放原始碼程式庫,一個能夠輕鬆集新增搜尋功能到一個應用程式中的簡單卻強大的核心程式碼庫和API。

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

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

Elasticsearch也使用Java開發並使用Lucene作為其核心來實現所有索引和搜尋的功能,但是它的目的是通過簡單的RESTful API來隱藏Lucene的複雜性,從而讓全文搜尋變得簡單。

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-1、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

檢索檔案之前先要建立索引,所以上圖得從“待檢索檔案”節點開始看。

構建索引過程:

1、為每一個待檢索的檔案構建Document類物件,將檔案中各部分內容作為Field類物件。

2、使用Analyzer類實現對文件中的自然語言文字進行分詞處理,並使用IndexWriter類構建索引。

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

檢索索引過程:

4、使用IndexReader類讀取索引。

5、使用Term類表示使用者所查詢的關鍵字以及關鍵字所在的欄位,使用QueryParser類表示使用者的查詢條件。

6、使用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一下,成就架構師