全文檢索工具包Lucene

什麼是全文檢索

數據的分類

結構化數據:指的是格式固定、長度固定、數據類型固定的數據,例如數據庫中的數據。html

非結構化數據:指的是格式不固定、長度不固定、數據類型不固定的數據,例如 word 文檔、pdf 文檔、郵件、html。java

數據的查詢

結構化數據的查詢:像數據庫中的數據咱們能夠經過 SQL 語句來進行查詢,簡單且速度快。git

非結構化數據的查詢:以「從多個文本文件中查詢出包含 spring 單詞的文件」爲例,咱們能夠經過一個一個打開經過目測瀏覽文本內容進行查找,也能夠經過將文檔讀到內存中,依次匹配字符串進行查找,再或者能夠將非結構化數據轉換爲結構化數據,如將其保存到數據庫中。github

全文檢索

先建立索引,而後經過索引查找的過程就叫作全文檢索。spring

全文檢索的應用場景

  • 搜索引擎,如百度、360 搜索、google、搜狗等。
  • 站內搜索,如論壇搜索年、微博、文章搜索等。
  • 電商搜索,如淘寶商品搜索、京東商品搜索。

有搜索的地方就可使用到全文檢索技術。數據庫

Lucene介紹

簡述

Lucene 是一個基於 Java 開發的全文檢索工具包。apache

實現全文檢索的流程

  • 左方框中表示建立索引的過程,對要搜索的原始內容進行索引構建一個索引庫。
  • 右邊框中表示搜索的過程,從索引庫中搜索內容。

建立索引

  1. 得到文檔。

    原始文檔:要基於哪些數據來進行搜索,那麼這些數據就是原始文檔。mybatis

    搜索引擎:使用爬蟲得到原始文檔。ide

    站內搜索:數據庫中的數據。工具

    案例:直接使用 IO 流讀取磁盤上的文件。

  2. 構建文檔對象。

    對應的每一個原始文檔就是一個 Document 對象,每一個 Document 對象包含多個域(Field),域中保存的就是原始文檔數據(域的名稱及域的值),每一個文檔對象都有一個惟一的編號,就是文檔 id。

  3. 分析文檔。

    就是分詞的過程。如:

    a) 根據空格對字符串進行拆分,獲得一個單詞列表。

    b) 把單詞統一轉換成小寫。

    c) 去除標點符號。

    d) 去除停用詞(無心義的詞)。

    e) 最後將拆分出來的每個關鍵詞封裝成一個 Term 對象(Term 中包含兩部份內容,一是關鍵詞所在的域,二是關鍵詞自己,不一樣的域中拆分出來的相同的關鍵詞是不一樣的 Term)。

  4. 建立索引。

    基於關鍵詞列表建立一個索引保存到索引庫中,索引庫中包含了索引、Document 對象、關鍵詞和文檔的對應關係。

查詢索引

  1. 用戶查詢接口。

    其實就是用戶輸入查詢條件的地方,如百度的搜索框。

  2. 把關鍵詞封裝爲一個對象,對象中包含了要查詢的域和要搜索的關鍵詞。
  3. 查詢索引。

    根據查詢的關鍵詞對象到對應的域上搜索,找到關鍵詞,即也能經過關鍵詞與文檔的對應關係找到對應的文檔。

  4. 渲染結果。

    根據文檔的 id 找到文檔對象,對關鍵詞進行高亮顯示、分頁處理等操做,最終顯示給用戶。

Lucene的使用

準備

一、官網下載 Lucene:http://lucene.apache.org

二、最低要求 jdk1.8。

三、依賴 jar 以下:

四、在 E:/temp/searchsouce 有以下測試文件:

do you like spring
document1.txt
do you like show you 
show.txt
do you like spring  mybatis
test2
do you like spring  mybatisdeawdffeef  show eyour resource
wna.txt

入門程序

建立索引庫

import org.apache.commons.io.FileUtils;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.junit.Test;

import java.io.File;

public class LuceneFirst {
    @Test
    public void createIndex() throws Exception{
        //一、建立一個 Directory 對象,指定索引庫保存的位置。
        // 把索引庫保存在內存中
        // Directory directory = new RAMDirectory();
        // 把索引庫保存在磁盤中
        Directory directory = FSDirectory.open(new File("E:/temp/index").toPath());
        //二、基於 Directory 對象建立一個 IndexWriter 對象。
        IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig());
        //三、讀取磁盤上的文件,對應每一個文件來建立一個文檔(Document)對象。
        File dir = new File("E:/temp/searchsouce");
        File[] files = dir.listFiles();
        for (File file : files) {
            // 取文件名
            String fileName = file.getName();
            // 文件路徑
            String filePath = file.getPath();
            // 文件內容
            String fileContent = FileUtils.readFileToString(file, "utf-8");
            // 文件大小
            long fileSize = FileUtils.sizeOf(file);
            // 建立 field
            // 參數1:域名稱 參數2:域的內容 參數3:是否保存
            Field nameField = new TextField("name", fileName, Field.Store.YES);
            Field pathField = new TextField("path", filePath, Field.Store.YES);
            Field contentField = new TextField("content", fileContent, Field.Store.YES);
            Field sizeField = new TextField("size", fileSize + "", Field.Store.YES);
            // 建立文檔對象
            Document document = new Document();
            //四、向文檔對象中添加域(Field)。
            document.add(nameField);
            document.add(pathField);
            document.add(contentField);
            document.add(sizeField);
            //五、把文檔對象寫入索引庫。
            indexWriter.addDocument(document);
        }
        //六、關閉 IndexWriter 對象。
        indexWriter.close();
    }
}

執行完上述單元測試後,在 E:/temp/index 下就會建立以下索引文件:

查詢索引庫

import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.*;
import org.apache.lucene.store.FSDirectory;
import org.junit.Test;

import java.io.File;

public class LuceneFirst {
    @Test
    public void searchIndex() throws Exception{
        // 一、建立一個 Directory 對象,指定索引庫位置。
        FSDirectory directory = FSDirectory.open(new File("E:/temp/index").toPath());
        // 二、建立一個 IndexReader 對象。
        IndexReader indexReader = DirectoryReader.open(directory);
        // 三、建立一個 IndexSearcher 對象,構造參數爲 IndexReader 對象。
        IndexSearcher indexSearcher = new IndexSearcher(indexReader);
        // 四、建立一個 Query 對象(TermQuery),在 content 域中查詢 spring 關鍵詞。
        Query query = new TermQuery(new Term("content", "spring"));
        // 五、執行查詢,獲得一個 TopDocs 對象。
        // 參數1:查詢對象 參數2:返回的記錄最大條數
        TopDocs topDocs = indexSearcher.search(query, 10);
        // 六、取查詢結果的總記錄數。
        System.out.println(topDocs.totalHits);
        // 七、取文檔列表。
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (ScoreDoc scoreDoc : scoreDocs) {
            // 取文檔 id
            int id = scoreDoc.doc;
            // 根據 id 取文檔對象
            Document document = indexSearcher.doc(id);
            // 八、打印文檔的內容。
            System.out.println(document.get("name"));
            System.out.println(document.get("path"));
            System.out.println(document.get("content"));
            System.out.println(document.get("size"));
        }
        // 九、關閉 IndexReader 對象
        indexReader.close();
    }
}

控制檯輸出:

查看索引庫內容

若是咱們想要查看建立好的索引庫內容,能夠經過一個工具——luke 查看,下載地址 https://github.com/DmitryKey/luke/releases

下載解壓後經過 luke.bat 批處理文件打開以下可視化界面:

分析器

在建立索引的代碼中建立 IndexWriter 對象時傳入的了 IndexWriterConfig 對象:

new IndexWriter(directory, new IndexWriterConfig());

在該對象中其實就建立了默認的分析器,看它的構造方法:

public IndexWriterConfig() {
    this(new StandardAnalyzer());
}

即默認的分析器就是 StandardAnalyzer 。

默認標準分析器

使用 Analyzer 對象的 tokenStream() 方法返回一個 TokenStream 對象,該對象中包含了最終的分詞結果。

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.junit.Test;

public class AnalyzerTest {
    @Test
    public void test() throws Exception {
        // 一、建立一個 Analyzer 對象(StandardAnalyzer)。
        Analyzer analyzer = new StandardAnalyzer();
        // 二、使用分析器對象的 tokenStream() 方法獲得一個 TokenStream 對象。
        TokenStream tokenStream = analyzer.tokenStream("", "do you like spring mybatis");
        // 三、向 TokenStream 對象中設置一個引用,至關於一個指針。
        CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
        // 四、調用 TokenStream 對象的 reset() ,若是不調用則會拋異常。
        tokenStream.reset();
        // 五、遍歷 TokenStream 對象。
        while (tokenStream.incrementToken()){
            System.out.println(charTermAttribute.toString());
        }
        // 六、關閉 TokenStream 對象。
        tokenStream.close();
    }
}

執行上述單元測試控制檯輸出以下:

即默認的 StandardAnalyzer 分析器是按空格分詞,最終獲得的關鍵詞列表就是分析結果。(它是不支持中文的,當要分析的內容包含中文時,會將每個中文字符當作一個關鍵詞。)

IK中文分析器

咱們已經知道默認的分析器是不支持中文的,因此咱們可使用第三方提供的支持中文分析的分析器工具——IK中文分析器,點擊下載(提取碼:t9yg)。

在工程中加入下載下來的 jar 包,在 classpath 下添加相應配置文件:

<?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>
IKAnalyzer.cfg.xml
a
an
and
are
as
at
be
but
by
for
if
in
into
is
it
no
not
of
on
or
such
that
the
their
then
there
these
they
this
to
was
will
with
stopword.dic

而後只須要將原來的分析器實例( StandardAnalyzer )替換爲用 IK 分析器( IKAnalyzer )的實例便可,以下:

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.junit.Test;
import org.wltea.analyzer.lucene.IKAnalyzer;

public class IKAnalyzerTest {
    @Test
    public void test() throws Exception{
        // 一、建立一個 Analyzer 對象(StandardAnalyzer)。
        Analyzer analyzer = new IKAnalyzer();
        // 二、使用分析器對象的 tokenStream() 方法獲得一個 TokenStream 對象。
        TokenStream tokenStream = analyzer.tokenStream("", "小張今天很開心");
        // 三、向 TokenStream 對象中設置一個引用,至關於一個指針。
        CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
        // 四、調用 TokenStream 對象的 reset() ,若是不調用則會拋異常。
        tokenStream.reset();
        // 五、遍歷 TokenStream 對象。
        while (tokenStream.incrementToken()){
            System.out.println(charTermAttribute.toString());
        }
        // 六、關閉 TokenStream 對象。
        tokenStream.close();
    }
}

執行上述單元測試,控制檯輸出結果以下:

回頭看一下 IKAnalyzer.cfg.xml 配置文件,在其中配置了兩個 entry 節點,它們分別有不一樣的 key 屬性值。

key 名稱爲 ext_dict 對應的節點用來指定一個擴展詞文件路徑,該擴展詞文件中能夠定義一些咱們本身但願識別爲關鍵詞的詞語,以換行隔開便可;

key 名稱爲 ext_stopwords 對應的節點用來指定一個停用詞文件路徑,該停用詞文件中能夠定義一些咱們指定的無心義詞語。

若是想要在建立索引庫的時候使用 IKAnalyzer 進行分詞,那麼只須要在建立 IndexWriterConfig 實例時指定其構造參數爲 IKAnalyzer 的實例便可,如:

IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig(new IKAnalyzer()));

索引庫的維護

Field域的屬性

Field 域通常有三個屬性:

  • 是否分析:是否對該域的內容進行分詞處理,前提是咱們要對該域的內容進行查詢。
  • 是否索引:將 Field 分析後的詞或整個 Field 值進行索引,只有索引後的 Filed 內容才能被搜索到。

    如:商品名稱、商品簡介需分析後進行索引,而訂單號和身份證號不用分析但也須要索引,由於它們都有可能做爲查詢條件。

  • 是否存儲:將 Field 值存儲在文檔中,存儲在文檔中的 Field 才能夠從 Document 中獲取。

    如商品名稱、訂單號等凡是未來要從 Document 中獲取的 Field 都要存儲。

 Field 域的經常使用實現有以下幾種:

Field 存儲數據類型 是否分析(Analyzed) 是否索引(Indexed) 是否存儲(Stored) 說明
StringField(FieldName,FiledValue,Store) 字符串類型 N Y Y或N 這個 Field 用來構建一個字符串 Field,可是不會進行分析,會將整個串直接索引,一般用來存儲如身份證號、姓名等字段內容,是否存儲經過第三個參數指定 Store.YES 或 Store.NO 決定。
LongPoint(FieldName,FieldValue) Long 類型 Y Y N 可使用 LongPoint、IntPoint 等類型存儲數值類型的數據來讓數值類型進行索引,但僅使用它是不能存儲數據的,要同時存儲數據須要搭配 StoredField 一塊兒使用。
StoredField(FieldName,FieldValue) 重載構造方法,支持多種類型。 N N Y 這個 Field 用來構建不一樣類型的 Field,不分析、不索引,但會讓 Field 存儲在文檔中。
TextField(FieldName,FieldValue,Store) 字符串或流 Y Y Y或N 若是是一個 Reader,Lucene 會猜想內容比較多,採用 UnStored 即不存儲的策略。

文檔管理

文檔管理指的就是針對索引庫中文檔的 CRUD 操做,基本的操做方式大體以下:

import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.store.FSDirectory;
import org.junit.Before;
import org.junit.Test;
import org.wltea.analyzer.lucene.IKAnalyzer;

import java.io.File;

public class DocumentManagerTest {
    IndexWriter indexWriter;

    @Before
    public void init() throws Exception {
        // 建立一個 IndexWriter 對象,使用 IKAnalyzer 做爲分析器
        indexWriter = new IndexWriter(FSDirectory.open(new File("E:/temp/index").toPath()), new IndexWriterConfig(new IKAnalyzer()));
    }

    /**
     * 添加文檔到索引庫
     */
    @Test
    public void addDocument() throws Exception {
        // 建立一個 Document 對象
        Document document = new Document();
        // 向 Document 對象中添加域
        document.add(new TextField("name", "新添加的文件名", Field.Store.YES));
        document.add(new TextField("content", "新添加的文件內容", Field.Store.YES));
        document.add(new StoredField("path", "E:/temp/searchsource/test1.txt"));
        // 把文檔寫入索引庫
        indexWriter.addDocument(document);
        // 釋放 IndexWriter
        indexWriter.close();
    }

    /**
     * 刪除所有文檔
     */
    @Test
    public void deleteAllDocument() throws Exception {
        // 刪除所有文檔
        indexWriter.deleteAll();
        // 釋放 IndexWriter
        indexWriter.close();
    }

    /**
     * 根據查詢對象刪除指定文檔
     */
    @Test
    public void deleteDocumentByQuery() throws Exception {
        // 刪除 content 域中包含關鍵詞 Spring 的文檔
        indexWriter.deleteDocuments(new Term("content", "Spring"));
        // 釋放 IndexWriter
        indexWriter.close();
    }

    /**
     * 更新文檔 實際上就是先刪除再新增
     */
    @Test
    public void updateDocument() throws Exception{
        // 建立一個新的文檔
        Document document = new Document();
        // 向 Document 對象中添加域
        document.add(new TextField("name", "要更新的文件名", Field.Store.YES));
        document.add(new TextField("content", "要更新的文件內容", Field.Store.YES));
        document.add(new StoredField("path", "E:/temp/searchsource/test2.txt"));
        // 更新操做 其實是先刪除 name 域中包含關鍵詞 Spring 的文檔,再添加 document 文檔到索引庫
        indexWriter.updateDocument(new Term("name","Spring"),document);
        // 釋放 IndexWriter
        indexWriter.close();
    }
}

索引庫的查詢

索引庫的經常使用查詢方式有以下幾種:

一、使用 Query 的子類:

  •  TermQuery ,根據關鍵詞進行查詢,需指定要查詢的域及要查詢的關鍵詞,在上面入門程序的查詢索引庫中已經使用過了就再也不贅述。
  •  RangeQuery ,針對數值類型的域進行範圍查詢,例:
    import org.apache.lucene.document.Document;
    import org.apache.lucene.document.LongPoint;
    import org.apache.lucene.index.DirectoryReader;
    import org.apache.lucene.index.IndexReader;
    import org.apache.lucene.search.IndexSearcher;
    import org.apache.lucene.search.Query;
    import org.apache.lucene.search.ScoreDoc;
    import org.apache.lucene.search.TopDocs;
    import org.apache.lucene.store.FSDirectory;
    import org.junit.Before;
    import org.junit.Test;
    
    import java.io.File;
    
    public class RangeQueryTest {
    
        IndexSearcher indexSearcher;
        IndexReader indexReader;
    
        @Before
        public void init() throws Exception {
            // 一、建立一個 Directory 對象,指定索引庫位置。
            FSDirectory directory = FSDirectory.open(new File("E:/temp/index").toPath());
            // 二、建立一個 IndexReader 對象。
            indexReader = DirectoryReader.open(directory);
            // 三、建立一個 IndexSearcher 對象,構造參數爲 IndexReader 對象。
            indexSearcher = new IndexSearcher(indexReader);
        }
    
        @Test
        public void test() throws Exception {
            // 四、建立查詢對象
            // 參數1:要查詢的域名稱 參數 2:查詢內容值的最小值 3:查詢內容值的最大值
            Query query = LongPoint.newRangeQuery("size", 1, 1000);
            // 五、執行查詢,獲得一個 TopDocs 對象。
            // 參數1:查詢對象 參數2:返回的記錄最大條數
            TopDocs topDocs = indexSearcher.search(query, 10);
            // 六、取查詢結果的總記錄數。
            System.out.println(topDocs.totalHits);
            // 七、取文檔列表。
            ScoreDoc[] scoreDocs = topDocs.scoreDocs;
            for (ScoreDoc scoreDoc : scoreDocs) {
                // 取文檔 id
                int id = scoreDoc.doc;
                // 根據 id 取文檔對象
                Document document = indexSearcher.doc(id);
                // 八、打印文檔的內容。
                System.out.println(document.get("name"));
                System.out.println(document.get("path"));
                System.out.println(document.get("content"));
                System.out.println(document.get("size"));
            }
            // 九、關閉 IndexReader 對象
            indexReader.close();
        }
    }
    例:

二、使用 QueryParser 進行查詢:

使用 QueryParser 其實是對要查詢的內容先分詞,而後基於分詞的結果進行查詢。要使用它須要再添加以下 jar 包:

lucene-queryparser-8.1.1.jar
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.FSDirectory;
import org.junit.Before;
import org.junit.Test;
import org.wltea.analyzer.lucene.IKAnalyzer;

import java.io.File;

public class QueryParserTest {
    IndexReader indexReader;
    IndexSearcher indexSearcher;

    @Before
    public void init() throws Exception {
        // 一、建立一個 Directory 對象,指定索引庫位置。
        FSDirectory directory = FSDirectory.open(new File("E:\\temp\\index").toPath());
        // 二、建立一個 IndexReader 對象。
        indexReader = DirectoryReader.open(directory);
        // 三、建立一個 IndexSearcher 對象,構造參數爲 IndexReader 對象。
        indexSearcher = new IndexSearcher(indexReader);
    }

    @Test
    public void test() throws Exception {
        // 建立一個 QueryParser 對象,參數1:要查詢的域名稱 參數2:要使用的分析器對象
        QueryParser queryParser = new QueryParser("content", new IKAnalyzer());
        // 使用 QueryParser 對象建立一個 Query 對象
        Query query = queryParser.parse("spring is the season after winter and before summer");
        // 執行查詢
        // 五、執行查詢,獲得一個 TopDocs 對象。
        // 參數1:查詢對象 參數2:返回的記錄最大條數
        TopDocs topDocs = indexSearcher.search(query, 10);
        // 六、取查詢結果的總記錄數。
        System.out.println(topDocs.totalHits);
        // 七、取文檔列表。
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (ScoreDoc scoreDoc : scoreDocs) {
            // 取文檔 id
            int id = scoreDoc.doc;
            // 根據 id 取文檔對象
            Document document = indexSearcher.doc(id);
            // 八、打印文檔的內容。
            System.out.println(document.get("name"));
            System.out.println(document.get("path"));
            System.out.println(document.get("content"));
            System.out.println(document.get("size"));
        }
        // 九、關閉 IndexReader 對象
        indexReader.close();
    }
}
例:
相關文章
相關標籤/搜索