Java大數(shù)據(jù)開發(fā)(一)- 搜索引擎 Lucene

Lucene

寫在前面:本文中用到的 Apache Lucene 版本號是 4.10.2 截止到文章發(fā)布時官方的最新版本是 6.5.1 因不同的版本差異較大,請大家在學習過程中確認下版本號是否一致。本文中的所涉及到的源碼分享在在 Gighub 鏈接地址:Part01_Lucene

1.搜索引擎

1.1 - 概述

  • 概述:根據(jù)一定的策略、運用特定的計算機程序從互聯(lián)網(wǎng)上搜集信息,再對信息進行 組織(分詞)處理(添加索引) 后,為用戶提供檢索服務(wù),將用戶檢索相關(guān)的信息展示給用戶的系統(tǒng)。搜索引擎包括 全文索引目錄索引、 元搜索引擎垂直搜索引擎、集合式搜索引擎 、門戶搜索引擎免費鏈接列表等。

1.2 - 搜索原理

搜索引擎原理

1.3 - 應(yīng)用場景

  1. 大型綜合搜索網(wǎng)站
  2. 站內(nèi)搜索
  3. 垂直領(lǐng)域搜索
  4. 軟甲內(nèi)部搜索

1.4 - 搜索技術(shù)

  • SQL 進行模糊查詢:如果沒有前置 % 可以執(zhí)行索引,如果添加前置 % 則全文檢索。
  • Lucene:解決在海量數(shù)據(jù)的情況下,利用 倒排索引 技術(shù),實現(xiàn)快速的 搜索打分 、 排序 等功能

1.5 - 倒排索引

根據(jù)詞條查找文檔

  • 名詞解釋:

    • 文檔(Document):索引庫中的每一條原始數(shù)據(jù)。
    • 詞條(Term):原始數(shù)據(jù)按照算法進行 分詞,得到的每一個詞語。
    • 文檔列表:Lucene 對原始文檔進行編號(DocID),形成的列表就是文檔列表。
  • 創(chuàng)建文檔列表:Lucene 首先對原始文檔數(shù)據(jù)進行編號(DocId),形成文檔列表

文檔編號 ID Title
0 1 谷歌地圖之父跳槽Facebook
1 2 谷歌地圖之父加盟Facebook
2 3 谷歌地圖創(chuàng)始人拉斯離開谷歌加盟Facebook
3 4 谷歌地圖之父跳槽Facebook與Wave項目取消有關(guān)
4 5 谷歌地圖之父拉斯加盟社交網(wǎng)站Facebook
  • 創(chuàng)建倒排索引列表:對文檔中數(shù)據(jù)進行分詞,得到 詞條(Term)。對詞條添加編號并創(chuàng)建索引,并在詞條中記錄包含該詞條的所有文檔編號及其他信息。
詞條ID 詞條 倒排列表(包含該詞條文檔 ID)
1 谷歌 0,1,2,3,4
2 地圖 0,1,2,3,4
3 之父 0,1,3,4
4 跳槽 0,3
5 Facebook 0,1,2,3,4
6 加盟 1,2,4
7 創(chuàng)始人 2
8 拉斯 2,4
9 離開 2
10 3
11 wave 3
12 項目 3
13 取消 3
14 有關(guān) 3
15 社交 4
16 網(wǎng)站 4
  • 搜索過程:
    1. 獲得用戶搜索內(nèi)容,對搜索內(nèi)容進行分詞,得到用戶搜索的所有詞條。
    2. 將詞條在倒排索引列表中進行匹配,得到包含該詞條的所有文檔編號。

2.Lucene

Apache Lucene

Apache Lucene

2.1 - 概述

  • 用于全文檢索和搜尋的開源程序庫,由 Apache 軟件基金會支持和提供。Lucene 提供了簡單強大應(yīng)用程序接口(API),可以進行全文索引和搜索,可以用來制作搜索引擎產(chǎn)品。

2.2 - 全文檢索

  • 計算機索引程序通過掃描文章中的每一個詞,對每一個詞建立一個索引,指明該詞在文章中出現(xiàn)的 次數(shù)位置 ,當用戶查詢時,檢索程序就根據(jù)事先建立的索引進行查找,并將查找的結(jié)果反饋給用戶的檢索方式。
  • 總結(jié):Lucene 全文檢索就是對文檔中全部內(nèi)容進行分詞,然后對所有單詞建立倒排索引的過程。

3.QuickStart

3.1 - 創(chuàng)建索引流程圖

Lucene創(chuàng)建索引流程圖
  1. 創(chuàng)建文檔對象(Document),并添加索引Field字段(Field)

    • 索引字段:Field
  2. 創(chuàng)建目錄對象(Directory)并指定索引在硬盤中存儲位置

  3. 創(chuàng)建分詞器對象(Analyzer)

  4. 創(chuàng)建索引寫出器配置對象(IndexWriterConfig)

    • 索引分詞器:Analyzer
    • 版本:Version
  5. 創(chuàng)建索引寫出器(IndexWriter)

    • 目錄:Directory
    • 索引寫出器配置:IndexWriterConfig
  6. 索引寫出器,添加文檔對象

    • 文檔:Document
  7. 提交并關(guān)閉索引寫出器

3.2 - 創(chuàng)建索引

  • 導入依賴 jar:

    <dependency>
        <groupId>org.apache.lucene</groupId>
        <artifactId>lucene-core</artifactId>
        <version>${lucene.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.lucene</groupId>
        <artifactId>lucene-analyzers-common</artifactId>
        <version>${lucene.version}</version>
    </dependency>
    
  • 代碼示例:

    public class QuickStartTest {
    
        @Test
        public void createTest() throws IOException {
            /* 1.創(chuàng)建文檔對象 */
            Document document = new Document();
            /*
            * 添加字段
            *
            * StringField: Field.Store.YES 表示存儲到文檔列表
            * TestField: 既創(chuàng)建索引 又分詞
            * */
            document.add(new StringField("id", "1", Field.Store.YES));
            document.add(new TextField("title", "谷歌地圖之父跳槽facebook", Field.Store.YES));
    
            /*
            * 2.創(chuàng)建目錄類 指定索引在硬盤中位置
            * */
            Directory directory = FSDirectory.open(new File("/Users/zhangsiyao1/Desktop/indexDir"));
    
            /*
            * 3.創(chuàng)建分詞器對象 analyzer
            * */
            Analyzer analyzer = new StandardAnalyzer();
    
            /*
            * 4.索引寫出工具配置對象 config
            * */
            IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, analyzer);
    
            /*
            * 5.創(chuàng)建索引寫出工具類
            * */
            IndexWriter writer = new IndexWriter(directory, config);
    
            /*
            * 6.將文檔添加到寫出器工具類中
            * */
            writer.addDocument(document);
    
            /*
            * 7.提交 & 關(guān)閉 寫出工具
            * */
            writer.commit();
            writer.close();
        }
    }
    

3.3 - 使用 lukeall 工具查看索引

LuceneData

4.創(chuàng)建索引詳解

4.1 - Document

Document詳解圖
  • Document:代表一行數(shù)據(jù)
  • Field:代表 Document 中的一個字段

4.2 - Field

Field
  1. 存儲 :StoreField 支持(byte[]、BytesRef、double、float、int、long、String)
  2. 創(chuàng)建索引 + 可選存儲 :DoubleField、FloatField、IntField、LongField、StringField
  3. 創(chuàng)建索引 + 可選存儲 + 分詞 :TestField
  • 是否存儲?:如果字段需要顯示到最終結(jié)果中,則需要存儲。
  • 是否創(chuàng)建索引?:如果根據(jù)該字段進行索引,則需要創(chuàng)建索引。
  • 是否分詞?:前提需要創(chuàng)建索引,如果字段是不可分割的則不需要分詞。

4.3 - Directory

Directory
  • FSDirectory:文件系統(tǒng)目錄,將索引庫指向本地磁盤。
    • 特點:速度略慢,較安全,節(jié)約內(nèi)存。
  • RAMDirectory:內(nèi)存目錄,將索引保存在內(nèi)存中。
    • 特點:速度快,不安全,占用內(nèi)存。

4.4 - Analyzer

Analyzer
  • 沒有適合中文的分詞器,ChineseAnalyzer(棄用),需要使用第三方分詞器

  • maven 導入 IKAnalyzer

    <dependency>
        <groupId>com.janeluo</groupId>
        <artifactId>ikanalyzer</artifactId>
        <version>2012_u6</version>
    </dependency>
    
  • 擴展詞典和停用詞典:在 resources 下創(chuàng)建 IKAnalyzer.cfg.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
    <properties>
        <comment>IK Analyzer 擴展配置</comment>
        <!-- 配置擴展字典 -->
        <entry key="ext_dict">ext.dic</entry>
        <!-- 配置擴展停止分詞字典 -->
        <entry key="ext_stopwords">stopword.dic</entry>
    </properties>
    

4.5 - IndexWriterConfig

  • 設(shè)置寫出時,是否先清除索引庫中數(shù)據(jù):

public IndexWriterConfig setOpenMode(OpenMode openMode)

  • 打開索引庫類型:
public static enum OpenMode {
    /** 
    * Creates a new index or overwrites an existing one. 
    */
    CREATE,
        
    /** 
    * Opens an existing index. 
    */
    APPEND,
        
    /** 
    * Creates a new index if one does not exist,
    * otherwise it opens the index and documents will be appended. 
    */
    CREATE_OR_APPEND 
}

4.6 - IndexWriter

  • 批量創(chuàng)建索引:
public void addDocuments(Iterable<? extends Iterable<? extends IndexableField>> docs)

5.查詢索引

5.1 - 基本查詢

public class QueryTest {

    private static final File INDEX_DIR_FILE = new File("/Users/zhangsiyao1/Desktop/indexDir");

    @Test
    public void baseSearchTest() throws IOException, ParseException {
        /* 索引目錄對象 */
        Directory directory = FSDirectory.open(INDEX_DIR_FILE);
        /* 索引讀取工具 */
        DirectoryReader directoryReader = DirectoryReader.open(directory);
        /* 索引搜索工具 */
        IndexSearcher indexSearcher = new IndexSearcher(directoryReader);

        /*
        * 創(chuàng)建查詢解析器
        * 1.查詢字段名稱
        * 2.分詞解析器
        * */
        QueryParser queryParser = new QueryParser("title", new IKAnalyzer());
        /* 獲取查詢對象 */
        Query query = queryParser.parse("谷歌地圖之父拉斯");

        /*
        * 搜索數(shù)據(jù)
        * 1.查詢解析器解析后的查詢結(jié)果
        * 2.查詢結(jié)果的最大條數(shù)
        * */
        TopDocs topDocs = indexSearcher.search(query, 10);

        /* 獲取總條數(shù) */
        int totalHits = topDocs.totalHits;
        System.out.println("本地搜索共查詢到 " + totalHits + " 匹配記錄");

        /*
        * 得分文檔數(shù)組
        * 1.doc 文檔編號(ID)
        * 2.score 文檔得分數(shù)
        * */
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;

        for (ScoreDoc scoreDoc : scoreDocs) {
            /* 文檔編號 */
            int docID = scoreDoc.doc;
            /* 通過索引讀取器 根據(jù)文檔編號獲取文檔 */
            Document document = directoryReader.document(docID);

            System.out.println("DocID: " + document.get("id"));
            System.out.println("Title: " + document.get("title"));
            
            /* 文檔得分 */
            System.out.println("Score: " + scoreDoc.score);
        }
    }
}

6.查詢索引詳解

  • 封裝 Lucene 查詢工具類 LuceneQueryUtils
public class LuceneQueryUtils {

    private static final File INDEX_DIR_FILE = new File("/Users/zhangsiyao1/Desktop/indexDir");

    public static void queryByQuery(Query query, int maxResult) throws IOException {
        /* 索引目錄對象 */
        Directory directory = FSDirectory.open(INDEX_DIR_FILE);
        /* 索引讀取工具 */
        DirectoryReader directoryReader = DirectoryReader.open(directory);
        /* 索引搜索工具 */
        IndexSearcher indexSearcher = new IndexSearcher(directoryReader);
        /* 搜索數(shù)據(jù) */
        TopDocs topDocs = indexSearcher.search(query, maxResult);

        int totalHits = topDocs.totalHits;
        System.out.println("本地搜索共查詢到 " + totalHits + " 匹配記錄");
        System.out.println("=======================================");
        /*
        * 得分文檔數(shù)組
        * */
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;

        for (ScoreDoc scoreDoc : scoreDocs) {
            /* 文檔編號 */
            int docID = scoreDoc.doc;
            /* 通過索引讀取器 根據(jù)文檔編號獲取文檔 */
            Document document = directoryReader.document(docID);
            System.out.println("DocID: " + document.get("id"));
            System.out.println("Title: " + document.get("title"));
            System.out.println("Score: " + scoreDoc.score);
            System.out.println("=======================================");
        }
    }
}

6.1 - MultiFieldQueryParser

  • 根據(jù)多字段查詢:MultiFieldQueryParser

    MultiFieldQueryParser queryParser = new MultiFieldQueryParser(
            new String[]{"id", "title"},
            new IKAnalyzer()
    );
    

6.2 - Query

  • 方式一:通過 QueryParser 解析關(guān)鍵字,得到查詢對象。
  • 方式二:自定義查詢對象,通過 Query 子類,創(chuàng)建查詢對象,實現(xiàn)高級查詢。

6.3 - IndexSearch

  • 功能:快速搜索、排序、打分等功能,其依賴于 IndexReader 對象。

  • 基本創(chuàng)建過程:

    /* 索引目錄對象 */
    Directory directory = FSDirectory.open(INDEX_DIR_FILE);
    /* 索引讀取工具 */
    DirectoryReader directoryReader = DirectoryReader.open(directory);
    /* 索引搜索工具 */
    IndexSearcher indexSearcher = new IndexSearcher(directoryReader);
    
  • 根據(jù)打分排序指定位置結(jié)果:

    TopDocs topDocs = indexSearcher.search(query, 10);
    

6.4 - TopDocs

  • 獲取對象:

    TopDocs topDocs = indexSearcher.search(query, 10);
    
  • 包含內(nèi)容:

    • int totalHints:查詢的總條數(shù)
    • ScoreDoc[] scoreDocs:得分文檔對象數(shù)組

6.5 - ScoreDoc

  • 包含內(nèi)容:
    • int doc:文檔編號(ID),根據(jù)文檔 ID 獲取指定文檔

      Document document = directoryReader.document(docID);
      
    • float score:文檔得分

7.高級查詢

7.1 - 詞條查詢

  • 概述:詞條是搜索的最小單位 不可再分割 且值必須是字符串
public void termQueryTest() throws IOException {
   TermQuery termQuery = new TermQuery(new Term("title", "谷歌地圖"));
   LuceneQueryUtils.queryByQuery(termQuery, 10);
}

7.2 - 通配符查詢

  • ?:任意 1 個字符
  • *:任意 n 字符
public void wildcardQuery() throws IOException {
   WildcardQuery query = new WildcardQuery(new Term("title", "*歌"));
   LuceneQueryUtils.queryByQuery(query, 10);
}

7.3 - 模糊查詢

  • maxEdits:最大編輯距離 一個單詞到另一個單詞最少修改次數(shù) [0,2]
public void fuzzyQueryTest() throws IOException {
   FuzzyQuery fuzzyQuery = new FuzzyQuery(new Term("title", "facebool"), 1);
   LuceneQueryUtils.queryByQuery(fuzzyQuery, 10);
}

7.4 - 數(shù)值范圍查詢

  • 應(yīng)用:id[2L,2L] 對非 String 類型的 ID 進行精確查找
public void numericRangeQueryTest() throws IOException {
   NumericRangeQuery<Long> numericRangeQuery = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
   LuceneQueryUtils.queryByQuery(numericRangeQuery, 10);
}

7.5 - 組合查詢

  • 交集: Occur.MUST + Occur.MUST
  • 并集: Occur.SHOULD + Occur.SHOULD
  • 補集: Occur.MUST_NOT
public void booleanQueryTest() throws IOException {
   NumericRangeQuery<Long> numericRangeQuery1 = NumericRangeQuery.newLongRange("id", 1L, 3L, true, true);
   NumericRangeQuery<Long> numericRangeQuery2 = NumericRangeQuery.newLongRange("id", 2L, 4L, true, true);
   BooleanQuery booleanQuery = new BooleanQuery();
   booleanQuery.add(numericRangeQuery1, BooleanClause.Occur.MUST_NOT);
   booleanQuery.add(numericRangeQuery2, BooleanClause.Occur.SHOULD);
   LuceneQueryUtils.queryByQuery(booleanQuery, 10);
}

8.修改索引

  • 問題:修改索引時,只能指定詞條(Term)進行更新,詞條只能是 String 類型,如果 id 為數(shù)值類型怎么更新?
  • 答案:先刪除,再更新
public class UpdateIndexTest {

    private static final File INDEX_DIR_FILE = new File("/Users/zhangsiyao1/Desktop/indexDir");

    /*
    * 1.Lucene 底層先刪除所有匹配的索引 再添加新文檔
    * 2.一般修改功能會根據(jù) Term 詞條進行匹配
    * 3.根據(jù)一個唯一不重復(fù)字段進行匹配(ID)
    *
    * 問題: update 時 Term 詞條搜索 要求 ID 必須是字符串 如果不是則不能使用這個方法
    * 解決: 先刪除該詞條 再添加更新后的詞條
    * */
    @Test
    public void updateTest() throws IOException {
        /* 創(chuàng)建目錄對象 */
        FSDirectory directory = FSDirectory.open(INDEX_DIR_FILE);
        /* 創(chuàng)建索引寫出器配置對象 */
        IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
        /* 創(chuàng)建索引寫出器 */
        IndexWriter writer = new IndexWriter(directory, config);

        Document document = new Document();
        document.add(new StringField("id", "1", Field.Store.YES));
        document.add(new TextField("title", "谷歌地圖之父跳槽facebook為了加入Amazon", Field.Store.YES));
        writer.updateDocument(new Term("id", "1"), document);

        writer.commit();
        writer.close();
    }
}

9.刪除索引

  • 方式一:根據(jù) Term 刪除,只能根據(jù) String 類型的詞條進行匹配刪除。
  • 方式二:根據(jù) Query 刪除,可以是任意類型的詞條進行匹配(更新 ID 非 String 類型文檔的解決方案)。
  • 方式三:刪除所有。
public class UpdateIndexTest {

    /*
    * 刪除索引
    * */
    @Test
    public void deleteTest() throws IOException {
        /* 創(chuàng)建目錄對象 */
        FSDirectory directory = FSDirectory.open(INDEX_DIR_FILE);
        /* 創(chuàng)建索引寫出器配置對象 */
        IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
        /* 創(chuàng)建索引寫出器 */
        IndexWriter writer = new IndexWriter(directory, config);

        /*
        * 1.根據(jù)詞條 Term 進行刪除 只能匹配 字符串類型 字段
        * */
        writer.deleteDocuments(new Term("id", "1"));

        /*
        * 2.根據(jù) Query 刪除 可以匹配任何類型的字段
        * */
        NumericRangeQuery<Long> numericRangeQuery = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
        writer.deleteDocuments(numericRangeQuery);

        /* 3.刪除所有 */
        writer.deleteAll();

        writer.commit();
        writer.close();
    }
}

10.Lucene 高級使用

10.1 - 高亮顯示

  1. SimpleHTMLFormatter:HTML 格式化工具
  2. Highlighter:高亮工具
@Test
public void highLightTest() throws IOException, ParseException, InvalidTokenOffsetsException {
    /* 目錄對象 */
    FSDirectory directory = FSDirectory.open(INDEX_DIR_FILE);
    /* 讀取工具 */
    DirectoryReader reader = DirectoryReader.open(directory);
    /* 搜索工具 */
    IndexSearcher searcher = new IndexSearcher(reader);

    /* parse 方式獲得 Query 對象 */
    QueryParser queryParser = new QueryParser("title", new IKAnalyzer());
    Query query = queryParser.parse("谷歌地圖");

    /* HTML 格式化器 */
    Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>");
    QueryScorer queryScorer = new QueryScorer(query);
    /* 準備高亮工具 */
    Highlighter highlighter = new Highlighter(formatter, queryScorer);

    /* 搜索 */
    TopDocs topDocs = searcher.search(query, 10);
    System.out.println("TotalHits: " + topDocs.totalHits);

    for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
        Document document = reader.document(scoreDoc.doc);
        /*
        * 高亮工具處理普通查詢結(jié)果
        * 參數(shù)一: 分詞器
        * 參數(shù)二: 高亮字段名
        * 參數(shù)三: 高亮字段原始值
        * */
        String highLightTitle = highlighter.getBestFragment(new IKAnalyzer(), "title", document.get("title"));
        System.out.println(highLightTitle);
    }
}
  • 導入依賴 jar:
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-highlighter</artifactId>
    <version>${lucene.version}</version>
</dependency>

10.2 - 排序

Sort sortArray = new Sort(new SortField("id", SortField.Type.LONG, true));
TopDocs topDocs = searcher.search(query, 10, sortArray);

悄悄話 ??

  • 最近項目進度比較緊,基本是有時間學習技術(shù),沒時間寫出來的樣子,這兩天趁著休息時間將之前的一些學習內(nèi)容按照先后順序陸續(xù)整理一下與大家分享。

彩蛋 ??

  • 最近開通了文集的同名專題 《Java大數(shù)據(jù)開發(fā)》 并會從大數(shù)據(jù)開發(fā)的基礎(chǔ)技術(shù)向下延伸至云服務(wù),有興趣的朋友可以來一同交流進步。

如果你覺得我的分享對你有幫助的話,請在下面??隨手點個喜歡 ??,你的肯定才是我最大的動力,感謝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容