Lucene學習筆記(介紹與代碼實現)

1、需求背景


1.1、普通的數據庫搜索

select * from 表名 where 字段名 like ‘%喫飯睡覺敲代碼%’

1、沒有通過高效的索引方式,查詢的速度在大量數據的情況下是很慢

2、搜索效果比較差,只能對用戶輸入的完整關鍵字首尾位進行模糊匹配。用戶搜索的結果錯誤輸入一個字符,可能就導致查詢出的結果遠離用戶的預期

1.2、新的業務需求

1、即使在相關結果數量百萬時,也能快速得出結果

2、搜索的結果不僅僅侷限於完整的 “喫飯睡覺敲代碼” ,而是將此內容拆分成,“喫飯”,“睡覺”,“敲代碼”,“代碼”等關鍵字進行搜索

3、對拆分後的搜索關鍵字進行特殊標識顯示

 

2、 搜索技術


2.1、搜索引擎的種類

搜索引擎按照功能通常分爲垂直搜索和綜合搜索

1、垂直搜索是指專門針對某一類信息進行搜索。例如:會搜網 主要做商務搜索的,並且提供商務信息。除此之外還有愛看圖標網、職友集等

2、綜合搜索是指對衆多信息進行綜合性的搜索。例如:百度、谷歌、搜狗、360搜索等。


2.2、倒排索引
倒排索引又叫反向索引,以字或詞爲文檔中出現的位置情況

在這裏插入圖片描述

在實際的運用中,我們可以對數據庫中原始的數據結構(左圖),在業務空閒時事先根據左圖內容,創建新的倒排索引結構的數據區域(右圖)

用戶有查詢需求時,先訪問倒排索引數據區域(右圖),得出文檔id後,通過文檔id即可快速,準確的通過左圖找到具體的文檔內容

這一過程,可以通過我們自己寫程序來實現,也可以借用已經抽象出來的通用開源技術來實現

 

3、Lucene概述


3.1、Lucene

Lucene是一套用於全文檢索和搜尋的開源程序庫,由Apache軟件基金會支持和提供,產品官網:http://lucene.apache.org/

Lucene提供了一個簡單卻強大的應用程序接口(API),能夠做全文索引和搜尋,在Java開發環境裏Lucene是一個成熟的免費開放源代碼工具,Lucene並不是現成的搜索引擎產品,但可以用來製作搜索引擎

3.2、全文檢索

計算機索引程序通過掃描文章中的每一個詞,對每一個詞建立一個索引,指明該詞在文章中出現的次數和位置,當用戶查詢時,檢索程序就根據事先建立的索引進行查找,並將查找的結果反饋給用戶

Lucene全文檢索就是對文檔中的全部內容進行分詞,再對索引分詞結果建立倒排索引

3.3、Lucene、Solr、Elasticsearch關係

Lucene:底層的API,工具包

Solr:基於Lucene開發的企業級的搜索引擎產品

Elasticsearch:基於Lucene開發的企業級的搜索引擎產品

 

4、Lucene的基本使用

使用Lucene的API來實現對索引的增(創建索引)、刪(刪除索引)、改(修改索引)、查(搜索數據)

4.1、創建索引

在這裏插入圖片描述

文檔Document:數據庫中一條具體的記錄

字段Field:數據庫中的每個字段

目錄對象Directory:物理存儲位置

寫出器的配置對象:需要分詞器和lucene的版本

 

4.1.1、添加依賴 

<properties>
    <lunece.version>4.10.2</lunece.version>
</properties>
<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
    <!-- lucene核心庫 -->
    <dependency>
        <groupId>org.apache.lucene</groupId>
        <artifactId>lucene-core</artifactId>
        <version>${lunece.version}</version>
    </dependency>
    <!-- Lucene的查詢解析器 -->
    <dependency>
        <groupId>org.apache.lucene</groupId>
        <artifactId>lucene-queryparser</artifactId>
        <version>${lunece.version}</version>
    </dependency>
    <!-- lucene的默認分詞器庫 -->
    <dependency>
        <groupId>org.apache.lucene</groupId>
        <artifactId>lucene-analyzers-common</artifactId>
        <version>${lunece.version}</version>
    </dependency>
    <!-- lucene的高亮顯示 -->
    <dependency>
        <groupId>org.apache.lucene</groupId>
        <artifactId>lucene-highlighter</artifactId>
        <version>${lunece.version}</version>
    </dependency>
</dependencies>

4.1.2、代碼實現

// 創建索引
@Test
public void testCreate() throws Exception{
    // 1 創建文檔對象
    Document document = new Document();
    // 創建並添加字段信息。參數:字段的名稱、字段的值、是否存儲,這裏選Store.YES代表存儲到文檔列表。Store.NO代表不存儲
    document.add(new StringField("id", "1", Field.Store.YES));
    // 這裏我們title字段需要用TextField,即創建索引又會被分詞。StringField會創建索引,但是不會被分詞
    document.add(new TextField("title", "谷歌地圖之父跳槽facebook", Field.Store.YES));

    // 2 索引目錄類,指定索引在硬盤中的位置
    Directory directory = FSDirectory.open(new File("d:\\indexDir"));

    // 3 創建分詞器對象
    Analyzer analyzer = new StandardAnalyzer();

    // 4 索引寫出工具的配置對象
    IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);

    // 5 創建索引的寫出工具類。參數:索引的目錄和配置信息
    IndexWriter indexWriter = new IndexWriter(directory, conf);

    // 6 把文檔交給IndexWriter
    indexWriter.addDocument(document);

    // 7 提交
    indexWriter.commit();

    // 8 關閉
    indexWriter.close();
 }

4.1.3、創建索引的API詳解

4.1.3.1、Document(文檔類)
Document:文檔對象,是一條原始的數據

4.1.3.2、Field(字段類)

一個Document中可以有很多個不同的字段,每一個字段都是一個Field類的對象

一個Document中的字段其類型是不確定的,因此Field類就提供了各種不同的子類,來對應這些不同類型的字段

 

這些子類有一些不同的特性:

1)DoubleField、FloatField、IntField、LongField、StringField這些子類一定會被創建索引,但是不會被分詞,而且不一定會被存儲到文檔列表,查找時一定要匹配所有的內容,否則搜索不到(例如:主鍵id、年齡、生日、日期、價格...)。要通過構造函數中的參數Store來指定:如果Store.YES代表存儲,Store.NO代表不存儲

2)TextField既會創建索引,又會被分詞

      StoreField一定會被存儲,但是一定不創建索引,可以創建各種數據類型的字段(byte、double、float、int、long、String)

      StringField會創建索引,但不會被分詞,如果不分詞,會造成整個字段作爲一個詞條,除非用戶完全匹配,否則搜索不到

如果一個字段要顯示到最終的結果中,那麼一定要存儲,否則就不存儲
如果要根據這個字段進行搜索,那麼這個字段就必須創建索引。
如果一個字段不需要分詞,首先這個字段首先要創建索引,然後如果這個字段的值是不可分割的,那麼就不需要分詞

4.1.3.3、Directory(目錄類)

FSDirectory:文件系統目錄,會把索引庫指向本地磁盤。特點:速度略慢,但是比較安全
RAMDirectory:內存目錄,會把索引庫保存在內存。特點:速度快,但是不安全

4.1.3.4、Analyzer(分詞器類)

提供分詞算法,可以把文檔中的數據按照算法分詞

這些分詞器,並沒有合適的中文分詞器,因此一般我們會用第三方提供的分詞器,一般我們用IK分詞器

4.1.3.5、IK分詞器

IK分詞器官方版本是不支持Lucene4.X的,有人基於IK的源碼做了改造,支持了Lucene4.X,引入jar:

擴展詞典和停用詞典
IK分詞器的詞庫有限,新增加的詞條可以通過配置文件添加到IK的詞庫中,也可以把一些不用的詞條去除:

擴展詞典:用來引入一些自定義的新詞
停止詞典:用來停用一些不必要的詞條

 

4.1.3.6、IndexWriterConfig(索引寫出器配置類)
1) 設置配置信息:Lucene的版本和分詞器類型

2)設置是否清空索引庫中的數據

 

4.1.3.7、IndexWriter(索引寫出器類)
索引寫出工具,作用就是 實現對索引的增(創建索引)、刪(刪除索引)、改(修改索引)

可以一次創建一個,也可以批量創建索引

// 批量創建索引
    @Test
    public void testCreate2() throws Exception{
        // 創建文檔的集合
        Collection<Document> docs = new ArrayList<>();
        // 創建文檔對象
        Document document1 = new Document();
        document1.add(new StringField("id", "1", Field.Store.YES));
        document1.add(new TextField("title", "谷歌地圖之父跳槽facebook", Field.Store.YES));
        docs.add(document1);
        // 創建文檔對象
        Document document2 = new Document();
        document2.add(new StringField("id", "2", Field.Store.YES));
        document2.add(new TextField("title", "谷歌地圖之父加盟FaceBook", Field.Store.YES));
        docs.add(document2);
        // 創建文檔對象
        Document document3 = new Document();
        document3.add(new StringField("id", "3", Field.Store.YES));
        document3.add(new TextField("title", "谷歌地圖創始人拉斯離開谷歌加盟Facebook", Field.Store.YES));
        docs.add(document3);
        // 創建文檔對象
        Document document4 = new Document();
        document4.add(new StringField("id", "4", Field.Store.YES));
        document4.add(new TextField("title", "谷歌地圖之父跳槽Facebook與Wave項目取消有關", Field.Store.YES));
        docs.add(document4);
        // 創建文檔對象
        Document document5 = new Document();
        document5.add(new StringField("id", "5", Field.Store.YES));
        document5.add(new TextField("title", "谷歌地圖之父拉斯加盟社交網站Facebook", Field.Store.YES));
        docs.add(document5);

        // 索引目錄類,指定索引在硬盤中的位置
        Directory directory = FSDirectory.open(new File("d:\\indexDir"));
        // 引入IK分詞器
        Analyzer analyzer = new IKAnalyzer();
        // 索引寫出工具的配置對象
        IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
        // 設置打開方式:OpenMode.APPEND 會在索引庫的基礎上追加新索引。OpenMode.CREATE會先清空原來數據,再提交新的索引
        conf.setOpenMode(IndexWriterConfig.OpenMode.CREATE);

        // 創建索引的寫出工具類。參數:索引的目錄和配置信息
        IndexWriter indexWriter = new IndexWriter(directory, conf);
        // 把文檔集合交給IndexWriter
        indexWriter.addDocuments(docs);
        // 提交
        indexWriter.commit();
        // 關閉
        indexWriter.close();
}

4.2、查詢索引數據
4.2.1、代碼實現

    @Test
    public void testSearch() throws Exception {
        // 創建讀取目錄對象
        Directory directory = FSDirectory.open(new File("d:\\indexDir"));
        // 創建索引讀取工具
        IndexReader reader = DirectoryReader.open(directory);
        // 創建索引搜索工具
        IndexSearcher searcher = new IndexSearcher(reader);

        // 創建查詢解析器,兩個參數:默認要查詢的字段的名稱,分詞器
        QueryParser parser = new QueryParser("title", new IKAnalyzer());
        // 創建查詢對象
        Query query = parser.parse("谷歌");

        // 搜索數據,兩個參數:查詢條件對象要查詢的最大結果條數
        // 返回的結果是 按照匹配度排名得分前N名的文檔信息(包含查詢到的總條數信息、所有符合條件的文檔的編號信息)。
        TopDocs topDocs = searcher.search(query, 10);
        // 獲取總條數
        System.out.println("本次搜索共找到" + topDocs.totalHits + "條數據");
        // 獲取得分文檔對象(ScoreDoc)數組.SocreDoc中包含:文檔的編號、文檔的得分
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (ScoreDoc scoreDoc : scoreDocs) {
            // 取出文檔編號
            int docID = scoreDoc.doc;
            // 根據編號去找文檔
            Document doc = reader.document(docID);
            System.out.println("id: " + doc.get("id"));
            System.out.println("title: " + doc.get("title"));
            // 取出文檔得分
            System.out.println("得分: " + scoreDoc.score);
        }
    }

4.2.2、核心API

4.2.2.1、QueryParser(查詢解析器)

1)QueryParser(單一字段的查詢解析器)

2)MultiFieldQueryParser(多字段的查詢解析器)

4.2.2.2、Query(查詢對象,包含要查詢的關鍵詞信息)

1)通過QueryParser解析關鍵字,得到查詢對象

2)自定義查詢對象(高級查詢),我們可以通過Query的子類,直接創建查詢對象,實現高級查詢

4.2.2.3、IndexSearch(索引搜索對象,執行搜索功能)

IndexSearch可以幫助我們實現:快速搜索、排序、打分等功能

IndexSearch需要依賴IndexReader類

查詢後得到的結果,就是打分排序後的前N名結果。N可以通過第2個參數來指定:

4.2.2.4、TopDocs(查詢結果對象)

通過IndexSearcher對象,我們可以搜索,獲取結果:TopDocs對象

4.2.2.5、ScoreDoc(得分文檔對象)

ScoreDoc是得分文檔對象,包含兩部分數據(文檔編號、文檔的得分信息)

4.2.3、特殊查詢

public void search(Query query) throws Exception {
        // 索引目錄對象
        Directory directory = FSDirectory.open(new File("indexDir"));
        // 索引讀取工具
        IndexReader reader = DirectoryReader.open(directory);
        // 索引搜索工具
        IndexSearcher searcher = new IndexSearcher(reader);

        // 搜索數據,兩個參數:查詢條件對象要查詢的最大結果條數
        // 返回的結果是 按照匹配度排名得分前N名的文檔信息(包含查詢到的總條數信息、所有符合條件的文檔的編號信息)。
        TopDocs topDocs = searcher.search(query, 10);
        // 獲取總條數
        System.out.println("本次搜索共找到" + topDocs.totalHits + "條數據");
        // 獲取得分文檔對象(ScoreDoc)數組.SocreDoc中包含:文檔的編號、文檔的得分
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;

        for (ScoreDoc scoreDoc : scoreDocs) {
            // 取出文檔編號
            int docID = scoreDoc.doc;
            // 根據編號去找文檔
            Document doc = reader.document(docID);
            System.out.println("id: " + doc.get("id"));
            System.out.println("title: " + doc.get("title"));
            // 取出文檔得分
            System.out.println("得分: " + scoreDoc.score);
        }
    }

4.2.3.1、TermQuery(詞條查詢)

    /*
     * 普通詞條查詢
     * 注意:Term(詞條)是搜索的最小單位,不可再分詞。值必須是字符串!
     */
    @Test
    public void testTermQuery() throws Exception {
        // 創建詞條查詢對象
        Query query = new TermQuery(new Term("title", "谷歌地圖"));
        search(query);
    }

 

4.2.3.2、WildcardQuery(通配符查詢)

    /*
     * 通配符查詢
     * 	? 可以代表任意一個字符
     * 	* 可以任意多個任意字符
     */
    @Test
    public void testWildCardQuery() throws Exception {
        // 創建查詢對象
        Query query = new WildcardQuery(new Term("title", "*歌*"));
        search(query);
    }

4.2.3.3 FuzzyQuery(模糊查詢)

    /*
     * 模糊查詢
     */
    @Test
    public void testFuzzyQuery() throws Exception {
        // 創建模糊查詢對象:允許用戶輸錯。但是要求錯誤的最大編輯距離不能超過2
        // 編輯距離:一個單詞到另一個單詞最少要修改的次數 facebool --> facebook 需要編輯1次,編輯距離就是1
//    Query query = new FuzzyQuery(new Term("title","fscevool"));
        // 可以手動指定編輯距離,但是參數必須在0~2之間
        Query query = new FuzzyQuery(new Term("title","facevool"),1);
        search(query);
    }

4.2.3.4、NumericRangeQuery(數值範圍查詢)

    /*
    * 數值範圍查詢
    * 可以用來對非String類型的ID進行精確的查找
    */
    @Test
    public void testNumericRangeQuery() throws Exception{
        // 數值範圍查詢對象,參數:字段名稱,最小值、最大值、是否包含最小值、是否包含最大值
        Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
        search(query);
    }

4.2.3.5、BooleanQuery(組合查詢)

    /*
     * 布爾查詢:
     * 布爾查詢本身沒有查詢條件,可以把其它查詢通過邏輯運算進行組合
     * 交集:Occur.MUST + Occur.MUST
     * 並集:Occur.SHOULD + Occur.SHOULD
     * 非:Occur.MUST_NOT
     */
    @Test
    public void testBooleanQuery() throws Exception{

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

4.4 修改索引

/*
* 修改索引
* 注意:
* A:Lucene修改功能底層會先刪除,再把新的文檔添加。
* B:修改功能會根據Term進行匹配,所有匹配到的都會被刪除。這樣不好
* C:因此,一般我們修改時,都會根據一個唯一不重複字段進行匹配修改。例如ID
* D:但是詞條搜索,要求ID必須是字符串。如果不是,這個方法就不能用。
* 如果ID是數值類型,我們不能直接去修改。可以先手動刪除deleteDocuments(數值範圍查詢鎖定ID),再添加。
*/
@Test
public void testUpdate() throws Exception{
    // 創建目錄對象
    Directory directory = FSDirectory.open(new File("indexDir"));
    // 創建配置對象
    IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
    // 創建索引寫出工具
    IndexWriter writer = new IndexWriter(directory, conf);

    // 創建新的文檔數據
    Document doc = new Document();
    doc.add(new StringField("id","1",Store.YES));
    doc.add(new TextField("title","谷歌地圖之父跳槽facebook ",Store.YES));
    /* 修改索引。參數:
    * 	詞條:根據這個詞條匹配到的所有文檔都會被修改
    * 	文檔信息:要修改的新的文檔數據
    */
    writer.updateDocument(new Term("id","1"), doc);
    // 提交
    writer.commit();
    // 關閉
    writer.close();
}

4.5 刪除索引

/*
* 刪除索引
* 注意:
* 一般,爲了進行精確刪除,我們會根據唯一字段來刪除。比如ID
* 如果是用Term刪除,要求ID也必須是字符串類型!
*/
@Test
public void testDelete() throws Exception {
    // 創建目錄對象
    Directory directory = FSDirectory.open(new File("indexDir"));
    // 創建配置對象
    IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
    // 創建索引寫出工具
    IndexWriter writer = new IndexWriter(directory, conf);

    // 根據詞條進行刪除
    // writer.deleteDocuments(new Term("id", "1"));

    // 根據query對象刪除,如果ID是數值類型,那麼我們可以用數值範圍查詢鎖定一個具體的ID
    // Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
    // writer.deleteDocuments(query);

    // 刪除所有
    writer.deleteAll();
    // 提交
    writer.commit();
    // 關閉
    writer.close();
}

5、Lucene的高級使用
5.1、高亮顯示

    // 高亮顯示
    @Test
    public void testHighlighter() throws Exception {
        // 創建目錄對象
        Directory directory = FSDirectory.open(new File("indexDir"));
        // 創建索引讀取工具
        IndexReader reader = DirectoryReader.open(directory);
        // 創建索引搜索工具
        IndexSearcher searcher = new IndexSearcher(reader);

        // 創建查詢解析器
        QueryParser parser = new QueryParser("title", new IKAnalyzer());
        // 創建查詢對象
        Query query = parser.parse("谷歌地圖");

        // 創建格式化器
        Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>");
        // 創建查詢分數工具
        QueryScorer scorer = new QueryScorer(query);
        // 準備高亮工具
        Highlighter highlighter = new Highlighter(formatter, scorer);
        // 搜索
        TopDocs topDocs = searcher.search(query, 10);
        System.out.println("本次搜索共" + topDocs.totalHits + "條數據");

        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (ScoreDoc scoreDoc : scoreDocs) {
            // 獲取文檔編號
            int docID = scoreDoc.doc;
            Document doc = reader.document(docID);
            System.out.println("id: " + doc.get("id"));

            String title = doc.get("title");
            // 用高亮工具處理普通的查詢結果,參數:分詞器,要高亮的字段的名稱,高亮字段的原始值
            String hTitle = highlighter.getBestFragment(new IKAnalyzer(), "title", title);

            System.out.println("title: " + hTitle);
            // 獲取文檔的得分
            System.out.println("得分:" + scoreDoc.score);
        }

    }

5.2、排序

    // 排序
    @Test
    public void testSortQuery() throws Exception {
        // 目錄對象
        Directory directory = FSDirectory.open(new File("indexDir"));
        // 創建讀取工具
        IndexReader reader = DirectoryReader.open(directory);
        // 創建搜索工具
        IndexSearcher searcher = new IndexSearcher(reader);

        QueryParser parser = new QueryParser("title", new IKAnalyzer());
        Query query = parser.parse("谷歌地圖");

        // 創建排序對象,需要排序字段SortField,參數:字段的名稱、字段的類型、是否反轉如果是false,升序。true降序
        Sort sort = new Sort(new SortField("id", SortField.Type.LONG, true));
        // 搜索
        TopDocs topDocs = searcher.search(query, 10,sort);
        System.out.println("本次搜索共" + topDocs.totalHits + "條數據");

        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (ScoreDoc scoreDoc : scoreDocs) {
            // 獲取文檔編號
            int docID = scoreDoc.doc;
            Document doc = reader.document(docID);
            System.out.println("id: " + doc.get("id"));
            System.out.println("title: " + doc.get("title"));
        }
    }

5.3、分頁

    // 分頁
	@Test
	public void testPageQuery() throws Exception {
		// 實際上Lucene本身不支持分頁。因此我們需要自己進行邏輯分頁。我們要準備分頁參數:
		int pageSize = 2;// 每頁條數
		int pageNum = 3;// 當前頁碼
		int start = (pageNum - 1) * pageSize;// 當前頁的起始條數
		int end = start + pageSize;// 當前頁的結束條數(不能包含)
		
		// 目錄對象
		Directory directory = FSDirectory.open(new File("indexDir"));
		// 創建讀取工具
		IndexReader reader = DirectoryReader.open(directory);
		// 創建搜索工具
		IndexSearcher searcher = new IndexSearcher(reader);
		
		QueryParser parser = new QueryParser("title", new IKAnalyzer());
		Query query = parser.parse("谷歌地圖");
		
		// 創建排序對象,需要排序字段SortField,參數:字段的名稱、字段的類型、是否反轉如果是false,升序。true降序
		Sort sort = new Sort(new SortField("id", Type.LONG, false));
		// 搜索數據,查詢0~end條
		TopDocs topDocs = searcher.search(query, end,sort);
		System.out.println("本次搜索共" + topDocs.totalHits + "條數據");
		
		ScoreDoc[] scoreDocs = topDocs.scoreDocs;
		for (int i = start; i < end; i++) {
			ScoreDoc scoreDoc = scoreDocs[i];
			// 獲取文檔編號
			int docID = scoreDoc.doc;
			Document doc = reader.document(docID);
			System.out.println("id: " + doc.get("id"));
			System.out.println("title: " + doc.get("title"));
		}
	}


5.4、得分算法
Lucene會對搜索結果打分,用來表示文檔數據與詞條關聯性的強弱,得分越高,表示查詢的匹配度就越高,排名就越靠前

在這裏插入圖片描述

Field field = new StoredField("url", "www.amigo.com");
// 設置激勵因子
field.setBoost(10.0f);  
document.add(field);

 

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