Lucene就是這麼容易

圖片描述
公衆號閱讀
https://mp.weixin.qq.com/s/M3...css

Lucene

[TOC]git

什麼是Lucene ???

The Apache LuceneTM project develops open-source search software, including:
Lucene Core, our flagship sub-project, provides Java-based indexing and search technology, as well as spellchecking, hit highlighting and advanced analysis/tokenization capabilities.

lucene官網(http://lucene.apache.org/)github

Lucene是apache軟件基金會4 jakarta項目組的一個子項目,是一個開放源代碼的全文檢索引擎工具包,但它不是一個完整的全文檢索引擎,而是一個全文檢索引擎的架構,提供了完整的查詢引擎和索引引擎,部分文本分析引擎(英文與德文兩種西方語言)。Lucene的目的是爲軟件開發人員提供一個簡單易用的工具包,以方便的在目標系統中實現全文檢索的功能,或者是以此爲基礎創建起完整的全文檢索引擎。Lucene是一套用於全文檢索和搜尋的開源程式庫,由Apache軟件基金會支持和提供。Lucene提供了一個簡單卻強大的應用程式接口,可以作全文索引和搜尋。在Java開發環境裏Lucene是一個成熟的免費開源工具。就其自己而言,Lucene是當前以及最近幾年最受歡迎的免費Java信息檢索程序庫。人們常常提到信息檢索程序庫,雖然與搜索引擎有關,但不該該將信息檢索程序庫與搜索引擎相混淆。

爲何使用Lucene

如今Lucene在互聯網行業的用的很是普遍,尤爲是大數據時代的今天,那麼根據本身的理解給你們簡單的介紹一下爲何要學習Lucene。
傳統的sql查詢方式,數據量過多時,數據庫的壓力就會變得很大,查詢速度會變得很是慢。咱們須要使用更好的解決方案來分擔數據庫的壓力。爲了解決數據庫壓力和速度的問題,咱們的數據庫就變成了索引庫,咱們使用Lucene的API的來操做服務器上的索引庫。這樣徹底和數據庫進行了隔離。
索引查詢方式(圖片來源引用)算法

1、快速入門

如今咱們已經瞭解了Lucene。sql

  1. Lucene是一套用於全文檢索和搜尋的開源程序庫,由Apache軟件基金會支持和提供
  2. Lucene提供了一個簡單卻強大的應用程序接口(API),可以作全文索引和搜尋,在Java開發環境裏Lucene是一個成熟的免費開放源代碼工具
  3. Lucene並非現成的搜索引擎產品,但能夠用來製做搜索引擎產品

總結:Lucene全文檢索就是對文檔中所有內容進行分詞,而後對全部單詞創建倒排索引的過程。數據庫

  • 目前最新的版本是7.x系列,可是在企業中仍是用4.x比較多,因此咱們學習4.x的版本。

插入圖片位置(圖片引用來源)
檢索數據須要咱們先分詞,存入索引庫apache

  • 文檔Document:數據庫中一條具體的記錄
  • 字段Field:數據庫中的每一個字段
  • 目錄對象Directory:物理存儲位置
  • 寫出器的配置對象:須要分詞器和lucene的版本
開發須要的jar包
<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.7</maven.compiler.source>
        <maven.compiler.target>1.7</maven.compiler.target>
        <lunece.version>4.10.2</lunece.version>
    </properties>

    <dependencies>
                <!-- 分詞器 -->
        <dependency>
            <groupId>com.janeluo</groupId>
            <artifactId>ikanalyzer</artifactId>
            <version>2012_u6</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>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
  1. 建立文檔對象
  2. 建立存儲目錄
  3. 建立分詞器
  4. 建立索引寫入器的配置對象
  5. 建立索引寫入器對象
  6. 將文檔交給索引寫入器
  7. 提交
  8. 關閉
// 建立索引
    @Test
    public void testCreate() throws Exception {
        // 1 建立文檔對象
        Document document = new Document();
        // 建立並添加字段信息。參數:字段的名稱、字段的值、是否存儲,這裏選Store.YES表明存儲到文檔列表。Store.NO表明不存儲
        document.add(new StringField("id", "1", Store.YES));
        // 這裏咱們title字段須要用TextField,即建立索引又會被分詞。StringField會建立索引,可是不會被分詞
        document.add(new TextField("title", "谷歌地圖之父跳槽facebook", Store.YES));

        // 2 索引目錄類,指定索引在硬盤中的位置
        Directory directory = FSDirectory.open(new File("E:\\luceneTest"));
        // 3 建立分詞器對象
        // Analyzer analyzer = new StandardAnalyzer();
        Analyzer analyzer = new IKAnalyzer();
        // 4 索引寫出工具的配置對象
        IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
        // 是否清空索引庫;設置打開方式:OpenMode.APPEND
        // 會在索引庫的基礎上追加新索引。OpenMode.CREATE會先清空原來數據,再提交新的索引
        conf.setOpenMode(OpenMode.CREATE);
        // 5 建立索引的寫出工具類。參數:索引的目錄和配置信息
        IndexWriter indexWriter = new IndexWriter(directory, conf);

        // 6 把文檔交給IndexWriter
        indexWriter.addDocument(document);
        // 7 提交
        indexWriter.commit();
        // 8 關閉
        indexWriter.close();
    }

索引文件圖片位置

索引查看工具

索引查看工具文件圖片
啓動run.bat
索引效果數組

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

        // 建立查詢解析器,兩個參數:默認要查詢的字段的名稱,分詞器
        QueryParser parser = new QueryParser("title", new IKAnalyzer());
        // 建立查詢解析器,倆個參數:默認要查詢的字段名稱,分詞器
        // MultiFieldQueryParser parser2 = new MultiFieldQueryParser(new
        // String[] {
        // "id", "title" }, new IKAnalyzer());
        // Query query2 = parser2.parse("1");
        // 建立查詢對象
        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);
        }
    }

2、工具類

查詢

注:代碼中加了必要註釋服務器

public void search(Query query) throws Exception {
        // 索引目錄對象
        Directory directory = FSDirectory.open(new File("E:\\luceneTest"));
        // 索引讀取工具
        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);
        }
    }

注:普通詞條查詢架構

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

注:通配符查詢

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

注:模糊查詢;數組範圍查詢;布爾查詢

/*
     * 測試模糊查詢
     */
    @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"), 2);
        search(query);
    }

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

        search(query);
    }

    /*****************************************************************
     * 布爾查詢: 布爾查詢自己沒有查詢條件,能夠把其它查詢經過邏輯運算進行組合! 交集: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);
    // }

修改-刪除

注:修改和刪除操做

/*
     * 測試:修改索引 注意: A:Lucene修改功能底層會先刪除,再把新的文檔添加。
     * B:修改功能會根據Term進行匹配,全部匹配到的都會被刪除。這樣很差 C:所以,通常咱們修改時,都會根據一個惟一不重複字段進行匹配修改。例如ID
     * D:可是詞條搜索,要求ID必須是字符串。若是不是,這個方法就不能用。
     * 若是ID是數值類型,咱們不能直接去修改。能夠先手動刪除deleteDocuments(數值範圍查詢鎖定ID),再添加。
     */
    @Test
    public void testUpdate() throws Exception {
        // 建立目錄對象
        Directory directory = FSDirectory.open(new File("E:\\luceneTest"));
        // 建立配置對象
        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();
    }

    /*
     * 演示:刪除索引 注意: 通常,爲了進行精確刪除,咱們會根據惟一字段來刪除。好比ID 若是是用Term刪除,要求ID也必須是字符串類型!
     */
    @Test
    public void testDelete() throws Exception {
        // 建立目錄對象
        Directory directory = FSDirectory.open(new File("E:\\luceneTest"));
        // 建立配置對象
        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();
    }

高亮顯示

  1. 建立目錄 對象
  2. 建立索引讀取工具
  3. 建立索引搜索工具
  4. 建立查詢解析器
  5. 建立查詢對象
  6. 建立格式化器
  7. 建立查詢分數工具
  8. 準備高亮工具
  9. 搜索
  10. 獲取結果
  11. 用高亮工具處理普通的查詢結果

百度搜索圖片
插入圖片位置

  • 關鍵字增長css樣式
// 高亮顯示
    @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);
        }

    }

排序

// 排序
    @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"));
        }
    }

分頁

// 分頁
    @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"));
        }
    }

3、優化

Lucene打分算法

  1. 當談論到查詢的相關性,很重要的一件事就是對於給定的查詢語句,如何計算文檔得分。文檔得分是一個用來描述查詢語句和文檔之間匹配程度的變量。若是你但願經過干預Lucene查詢來改變查詢結果的排序,你就須要對Lucene的得分計算有所理解。
  1. 當一個文檔出如今了搜索結果中,這就意味着該文檔與用戶給定的查詢語句是相匹配的。Lucene會對匹配成功的文檔給定一個分數。至少從Lucene這個層面,從打分公式的結果來看,分數值越高,表明文檔相關性越高。 天然而然,咱們能夠得出:兩個不一樣的查詢語句對同一個文檔的打分將會有所不一樣,可是比較這兩個得分是沒有意義的。用戶須要記住的是:咱們不只要避免去比較不一樣查詢語句對同一個文檔的打分結果,還要避免比較不一樣查詢語句對文檔打分結果的最大值。這是由於文檔的得分是多個因素共同影響的結果,不只有權重(boosts)和查詢語句的結構起做用,還有匹配關鍵詞的個數,關鍵詞所在的域,查詢歸一化因子中用到的匹配類型……。在極端狀況下,只是由於咱們用了自定義打分的查詢對象或者因爲倒排索引中詞的動態變化,類似的查詢表達式對於同一個文檔都會產生大相徑庭的打分。

計算文檔得分,考慮因素以下:

  • 文檔權重(Document boost):在索引時給某個文檔設置的權重值。
  • 域權重(Field boost):在查詢的時候給某個域設置的權重值。
  • 調整因子(Coord):基於文檔中包含查詢關鍵詞個數計算出來的調整因子。通常而言,若是一個文檔中相比其它的文檔出現了更多的查詢關鍵詞,那麼其值越大。
  • 逆文檔頻率(Inerse document frequency):基於Term的一個因子,存在的意義是告訴打分公式一個詞的稀有程度。其值越低,詞越稀有(這裏的值是指單純的頻率,即多少個文檔中出現了該詞;而非指Lucene中idf的計算公式)。打分公式利用這個因子提高包含稀有詞文檔的權重。
  • 長度歸一化(Length norm):基於域的一個歸一化因子。其值由給定域中Term的個數決定(在索引文檔的時候已經計算出來了,而且存儲到了索引中)。域越的文本越長,因子的權重越低。這代表Lucene打分公式偏向於域包含Term少的文檔。
  • 詞頻(Term frequency):基於Term的一個因子。用來描述給定Term在一個文檔中出現的次數,詞頻越大,文檔的得分越大。
  • 查詢歸一化因子(Query norm):基於查詢語句的歸一化因子。其值爲查詢語句中每個查詢詞權重的平方和。查詢歸一化因子使得比較不一樣查詢語句的得分變得可行,固然比較不一樣查詢語句得分並不老是那麼易於實現和可行的。

Lucene打分公式

Lucene概念上的打分公式是這樣的:(TF/IDF公式的概念版)
tf-idf公式圖片
上面的公式展現了布爾信息檢索模型和向量空間信息檢索模型的組合。咱們暫時不去討論它,直接見識下Lucene實際應用的打分公式:
lucene-tf-idf
能夠看到,文檔的分數其實是由查詢語句q和文檔d做爲變量的一個函數值。打分公式中有兩部分不直接依賴於查詢詞,它們是coord和queryNorm。公式的值是這樣計算的,coord和queryNorm兩大部分直接乘以查詢語句中每一個查詢詞計算值的總和。另外一方面,這個總和也是由每一個查詢詞的詞頻(tf),逆文檔頻率(idf),查詢詞的權重,還有norm,也就是前面說的length norm相乘而得的結果。聽上去有些複雜吧?不用擔憂,這些東西不須要所有記住。你只須要知道在進行文檔打分的時候,哪些因素是起決定做用的就能夠了。基本上,從前面的公式中能夠提煉出如下的幾個規則:

  • 匹配到的關鍵詞越稀有,文檔的得分就越高。
  • 文檔的域越小(包含比較少的Term),文檔的得分就越高。
  • 設置的權重(索引和搜索時設置的均可以)越大,文檔得分越高。

正如咱們所看到的那樣,Lucene會給具備這些特徵的文檔打最高分:文檔內容可以匹配到較多的稀有的搜索關鍵詞,文檔的域包含較少的Term,而且域中的Term可能是稀有的。
簡而言之
圖片插入位置
圖片插入位置

停用詞和擴展詞加載

將IKAnalyzer.cfg.xml和stopword.dic和xxx.dic文件複製到MyEclipse的src目錄下,再進行配置
IK Analyzer默認的停用詞詞典爲IKAnalyzer2012_u6/stopword.dic,這個停用詞詞典並不完整,只有30多個英文停用詞。能夠擴展停用詞字典,新增ext_stopword.dic,文件和IKAnalyzer.cfg.xml在同一目錄,編輯IKAnalyzer.cfg.xml把新增的停用詞字典寫入配置文件,多個停用詞字典用逗號隔開,以下所示。

<entry  key="ext_stopwords">stopword.dic;ext_stopword.dic</entry>

目錄結構圖

接下來就能夠構建本身的搜索引擎了。

上面展現了lucene一些基本操做,更詳細的的工具類能夠訪問https://github.com/wangshiyu777/usefulDemo,分享了詳細Demo。

相關文章
相關標籤/搜索