【大數據】Lucene全文搜索引擎入門篇(零基礎小白也適用)

前言:Lucene是apache老爹開源的一款全文搜索引擎,雖然目前已被市面上一些更好用的搜索引擎逐步替代,但作爲搜索引擎的鼻祖,仍然有必要學習一番,而且有了Lucene的基礎之後,學習solr,elastichSearch也會更容易理解。

看完本篇,你將瞭解到Lucene是什麼,Lucene的使用場景,原理,什麼是倒排索引,如何分詞,以及如何使用lucene實戰構建簡易的搜索引擎等。


目錄

1.Lucene可以用來做什麼?

2.Lucene的原理

3.Lucene的倒排索引

4.Lucene分詞

5.Lucene索引建立與窺探

6.Lucene的實際使用

7.索引編輯(更新)

 8.拓展功能

9.總結


 

1.Lucene可以用來做什麼?

我們知道,傳統關係型數據庫在數據量大到一定量級後(一般是單表超過500W行或者2G左右),查詢速度會變得非常緩慢,也比較喫資源,加上需求中經常會有一些模糊查詢,模糊查詢可能會導致傳統數據庫的索引失效,這時候如果再用傳統數據庫就呵呵了。所以在好的電商系統中,搜素引擎都是標配。通過搜素引擎,可以很好的減輕數據庫的讀壓力。除此之外,像我們熟知的百度,谷歌等搜索大佬,也都有用到搜索引擎,搜索引擎還可以用於搭建文庫,總之很強大,可以做很多事情。

2.Lucene的原理

Lucene的原理我說白話一點,就是先把被搜索內容按照一定規則進行分詞並存儲,生成一個目錄,然後把你輸入的搜索關鍵詞也進行分詞,然後與前面生成的目錄中的關鍵詞進行匹配,如果匹配到,就可以快速定位到該被搜索內容的存儲位置,從而實現搜索。說簡單點就是這樣,當然裏面有很多細節,先不着急,等看完倒排索引之後你會對Lucene的原理有更深的瞭解。

如圖,比如我搜索了程序員禿頭,搜索引擎一般會根據一定的拆分規則,把我輸入的“程序員禿頭”拆分成:程序員,禿頭,頭等關鍵字。然後與爬蟲爬取到的大量文檔裏的關鍵詞進行匹配(這些大量文檔會按照一定規則預先分詞,建立索引,然後存儲起來),如果匹配到了就可以返回給用戶了,返回的結果可以對關鍵詞添加一些“高亮”,標紅等特效,也可以按照一定規則對搜索結果進行排序。


3.Lucene的倒排索引

這裏我援引別人已經畫好的圖來解釋下倒排索引,描述也力求簡潔,幫助快速理解:

下面第一張圖是文檔的原始內容和編號,第二張圖是建立的倒排索引。

倒排索引的建立步驟:

①先對原始文檔中的內容按照一定規則進行分詞,至於如何分先不用管,我後面會講到。

②然後對分詞後的單詞在原始文檔中進行定位,比如“谷歌”在原始文檔中的1-5號文檔都有出現過,所以谷歌的倒排列表就是:1,2,3,4,5. 其它倒排列表以此類推。

有了這樣一套對應列表之後,下次用戶如果輸入了“網站”這個關鍵詞,系統就可以通過倒排列表快速定位到原始文檔中的第5項數據,類似於生成了一本書的一個目錄,告訴你幾個關鍵詞,可以在目錄中快速定位具體的文檔在多少頁,不用再一頁一頁翻了。

看到這裏你應該明白倒排索引的原理了,可以先思考下爲啥叫“倒排”索引,而不是別的名字?

回答這個問題前,先看下什麼是正向索引:正向索引建立的關係是由文檔->關鍵詞的,也就是給定一篇文檔,記錄關鍵詞在文檔中出現的位置,通過文檔來關聯關鍵詞的,這種方式在數據量小的情況下完全OK,但互聯網這片大海中,文檔的量及是宇宙級的,這種方式要掃描的文檔驢輩子都掃不完。倒排索引正好是逆向思維,把文檔的關鍵詞提取,然後通過關鍵詞取關聯文檔,這樣就極大的減輕了掃描的量級。

關於倒排索引就提這麼多,實際上要比這個複雜一些,感興趣的建議讀完之後可以回頭來深入研究,以免打斷思路,這裏直接給出我援引的博主圖片的博客地址:https://blog.csdn.net/csdnliuxin123524/article/details/91581209

4.Lucene分詞

Lucene提供了幾種分詞的實現,在4.0+的版本後還提供了對中文分詞的支持,據說效果不太好,於是後來有人開源了更好用的中文分詞器:IK Analysis,詞庫可以自定義,不太友好的地方就是這款基於Lucene的中文分詞器好像因爲Lucene的落寞而年久失修了,版本還停留在2012年的版本,對新版本的Lucene無法兼容,據說當年創造了每秒160萬分詞的神話。

5.Lucene索引建立與窺探

這裏我直接通過代碼來實現Lucene的分詞,分詞器我採用lucene提供的代碼其實很簡單,不要被其篇幅所蠱惑。

第一步,引入依賴,這裏我建立一個Maven工程爲例:

pom.xml中引入如下依賴:

   <dependencies>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>${org.apache.lucene}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>${org.apache.lucene}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-highlighter</artifactId>
            <version>${org.apache.lucene}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queries</artifactId>
            <version>${org.apache.lucene}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queryparser</artifactId>
            <version>${org.apache.lucene}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-memory</artifactId>
            <version>${org.apache.lucene}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-smartcn</artifactId>
            <version>${org.apache.lucene}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-join</artifactId>
            <version>${org.apache.lucene}</version>
        </dependency>
    </dependencies>

版本號我這裏使用截至目前2019年11月18日最新版本:8.3.0

    <properties>
        <org.apache.lucene>8.3.0</org.apache.lucene>
    </properties>

 實現代碼如下:

public class Test {
    //生成索引的存放位置
    private final static String IDX_DIR = "D:\\lucene";
    public static void main(String[] args) {
        IndexWriter indexWriter = null;
        try {
            //創建索引庫
            Directory dir = FSDirectory.open(Paths.get(IDX_DIR));
            //新建分析器對象
            Analyzer analyzer = new SmartChineseAnalyzer();
            //新建配置對象
            IndexWriterConfig config = new IndexWriterConfig(analyzer);
            //創建一個IndexWriter對象
            indexWriter = new IndexWriter(dir, config);
            //創建文檔對象
            Document document1 = new Document();
            //添加文檔字段,字段名可以自己指定,Store.YES 說明該字段要被存儲,false則不需要被存儲到文檔列表
            document1.add(new StringField("id", "1L", Store.YES));
            document1.add(new TextField("title", "三旬老漢隔人暴扣", Store.YES));
            document1.add(new TextField("content", "某23號三旬老漢一記搶斷之後運球過人後隔人劈扣拿下2分!", Store.YES));

            Document document2 = new Document();
            document2.add(new StringField("id", "2L", Store.YES));
            document2.add(new TextField("title", "濃眉哥怒砍3雙", Store.YES));
            document2.add(new TextField("content", "濃眉哥本場狀態佳,僅前三節比賽就拿下3雙,將比賽勝局牢牢鎖定!", Store.YES));

            //寫出索引數據
            indexWriter.addDocument(document1);
            indexWriter.addDocument(document2);
            indexWriter.commit();

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                indexWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

點擊main方法運行後,我們可以在對應的目錄(我這裏是D盤的lucene目錄)下發現被新建的幾個文件

到這裏說明索引文件已被創建,那麼索引裏到底有啥玩意,想窺探一下但發現根本打不開,這個時候lukeall大哥就登場了:下載地址:https://github.com/DmitryKey/luke/releases

一定要匹配你使用的lucene版本,否則可能解析報錯。

下載後解壓打開,然後選擇你索引所在目錄(我這裏是D盤的lucene目錄),就可以看到分詞詳情了:

點擊OK後,在show top terms可以看到被分詞的內容:

藉助Luke我們可以看到被Lucene分詞後建立的倒排索引,再結合3中提到的倒排索引原理,應該可以很好的理解倒排索引和Lucene的索引建立.

6.Lucene的實際使用

有了索引之後,接下來就是要藉助索引去實現搜索了.Luncene提供了多種搜索方式,以此來滿足不同的業務需求,我們先從最普通的關鍵詞搜索來看,在上面已經建好的Maven工程下新增TestQuery類:

public class TestQuery {
    private final static String IDX_DIR = "D:\\lucene";

    public static void main(String[] args) {
        try {
            //創建索引庫對象
            Directory directory = FSDirectory.open(Paths.get(IDX_DIR));
            //索引讀取工具
            IndexReader indexReader = DirectoryReader.open(directory);
            //索引搜索工具
            IndexSearcher indexSearcher = new IndexSearcher(indexReader);

            //創建查詢解析器
            QueryParser parser = new QueryParser("content", new SmartChineseAnalyzer());
            //創建查詢對象
            Query query = parser.parse("濃眉");
            //獲取搜索結果 第二個參數n是返回多少條,可以根據情況限制
            TopDocs docs = indexSearcher.search(query, 10);

            //獲取總條數
            System.out.println("本次查詢共搜索到:" + docs.totalHits + " 條相關數據");
            //獲取得分對象
            ScoreDoc[] scoreDocs = docs.scoreDocs;
            for (ScoreDoc scoreDoc : scoreDocs) {
                //獲取文檔編號
                int docId = scoreDoc.doc;
                //根據文檔編號獲取文檔內容
                Document document = indexReader.document(docId);
                System.out.println("id: " + docId);
                System.out.println("title: " + document.getField("title"));
                System.out.println("content: " + document.getField("content"));
                System.out.println("搜索得分: " + scoreDoc.score);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}

運行上述代碼,效果如圖:

以上便是最普通的根據關鍵詞查詢,查詢時Lucene會根據匹配度算出一個得分,得分越高排名越靠前.

Lucene還提供了:

WildcardQuery(通配符查詢)

// 創建查詢對象
Query query = new WildcardQuery(new Term("title", "*三*"));

FuzzyQuery(模糊查詢)

// 創建模糊查詢對象:允許用戶輸錯。但是要求錯誤的最大編輯距離不能超過2
// 編輯距離:一個單詞到另一個單詞最少要修改的次數 loohan --> laohan 需要編輯1次,編輯距離就是1
// 可以手動指定編輯距離,區間[0,2]
Query query = new FuzzyQuery(new Term("title","laohan"),1);

NumericRangeQuery(數值範圍查詢) 

// 針對數值字段的範圍查詢,參數:字段名稱,最小值、最大值、是否包含最小值、是否包含最大值
Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);

 BooleanQuery(組合查詢)

/**
 * 布爾查詢:
 * 布爾查詢本身沒有查詢條件,可以把其它查詢通過邏輯運算進行組合
 * 交集:Occur.MUST + Occur.MUST
 * 並集:Occur.SHOULD + Occur.SHOULD
 * 非:Occur.MUST_NOT
 */
Query query1 = NumericRangeQuery.newLongRange("id", 1L, 3L, true, true);
Query query2 = NumericRangeQuery.newLongRange("id", 2L, 4L, true, true);
// 創建布爾查詢的對象
BooleanQuery query = new BooleanQuery();

// 組合其它查詢
query.add(query1, BooleanClause.Occur.MUST_NOT);
query.add(query2, BooleanClause.Occur.SHOULD);

search(query);

7.索引編輯(更新)

Lucene提供的索引是可以編輯的,否則一旦數據發生變化,搜出來的東西可能並不是我們想要的,特別是在電商場景下,商品的列表可能會經常發生變化,如果不能及時的更新索引,會給用戶和商家帶來很大不便.

public class TestUpdate {
    private final static String IDX_DIR = "D:\\lucene";

    public static void main(String[] args) {
        IndexWriter indexWriter = null;
        try {
            //創建目錄
            Directory directory = FSDirectory.open(Paths.get(IDX_DIR));
            //創建配置對象
            IndexWriterConfig config = new IndexWriterConfig(new SmartChineseAnalyzer());
            //創建索引寫出工具
            indexWriter = new IndexWriter(directory, config);

            //創建新的文檔數據
            Document document = new Document();
            document.add(new StringField("id","1L", Store.YES));
            document.add(new TextField("title","湖人三大核心全面爆發,僅兩節比賽就領先對手三十分!",Store.YES));

            //修改索引,修改指定文檔中所有的匹配字段的索引,一般選id,因爲id唯一
            indexWriter.updateDocument(new Term("id","1L"),document);
            indexWriter.commit();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                indexWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Lucene也支持索引的刪除:

//根據匹配關鍵詞來刪除
writer.deleteDocuments(new Term("id", "1L"));

//根據匹配數字範圍來刪除
Query query = NumericRangeQuery.newLongRange("id", 1L, 2L, true, true);
writer.deleteDocuments(query);

//刪除全部
write.deleteAll();

 8.拓展功能

Lucene除了可以對建立的索引進行增刪改查之外,還可以讓查詢結果更"用戶體驗友好",內置提供了查詢關鍵詞高亮顯示功能,查詢結果排序功能等。

高亮功能:

// 設置HTML格式化樣式
Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>");
QueryScorer scorer = new QueryScorer(query);
// 創建高亮工具
Highlighter highlighter = new Highlighter(formatter, scorer);

//省略對socre[]循環部分...

//通過分詞器確定需要高亮顯示的關鍵詞
String highlight = highlighter.getBestFragment(new SmartChineseAnalyzer(), "content", "濃眉哥");
//展示效果
System.out.println("高亮展示的關鍵詞:" + highlight);

排序功能:

//老版本的Lucene可以採用這種方式排序
//設置排序字段
Sort sort = new Sort(new SortField("id", Type.LONG,true));
//獲取搜索結果
TopDocs docs = indexSearcher.search(query, 10,sort);

如果你是在新版本下按照這種方式排序會拋下面這種異常:

 網上的解決方法是爲需要排序的字段設置Numeric值,確實可以解決:

document.add(new NumericDocValuesField("title",10L));

但又發現了新的問題: 

一用排序字段得到的搜索得分就變成NAN了.

效果如下圖: 

網上說是BUG,於是我嘗試着把Lucene的版本從7一直升到8,發現這個問題依舊存在,而且網友說是從4開始就有這個問題,考慮到Apache的影響力,我覺得這樣的bug肯定活不過這麼多版本,於是看了下Sort的源碼,最終找到了解決方案,原來是因爲搜索得分算法是比較耗性能的,特別是自定義排序字段後,所以在高版本中,使用自定義排序字段後,搜索得分是默認被關閉的,如果需要計算得分需要手動開啓:

//設置排序字段
Sort sort = new Sort(new SortField("title", Type.LONG,true));
//獲取搜索結果 兩個布爾類型參數,第一個是:doDocScores,第二個是doMaxScore
TopDocs docs = indexSearcher.search(query,10,sort,true,true);

第一個參數doDocScores的意思就是開啓搜索得分. 第二個參數是在搜索過程中,如果搜到了搜索得分最高的那條數據,就自動終止不再繼續搜了,直接返回,以此來提高性能.,對於這個功能背後的算法,感興趣的可以繼續研究這篇博主總結的:

《Lucene之MaxScorer算法簡介》https://blog.csdn.net/wzhg0508/article/details/12953119

遺憾的時,Lucene並沒提供分頁功能,需要我們手動實現,思路與在Mysql下手動分頁查詢差不多,這裏不再贅述。

另外對得分算法感興趣的同學可以參考這篇:

《深入理解Lucene默認打分算法》https://blog.csdn.net/huaishu/article/details/77648377

本人學識淺薄,就不班門弄斧自己總結了,魯迅老爹的拿來主義真香!

9.總結

本篇主要介紹了Lucene的使用場景,原理,以及實操,適合新手入門,如果要喫透Lucene的話沒個幾個月還真搞不定,裏面的源碼和算法都比較厲害,現在市面上有很多基於Lucene二次開發或拓展的搜索引擎,比如solr,elasticsearch等,正是因爲有了Lucene底層強有力的支持,才讓我們今天在互聯網上獲取信息變得如此便捷與輕鬆,最後,感謝你的閱讀,文中若有不正之處歡迎留言斧正。

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