1. 程式人生 > >【Lucene】Apache Lucene全文檢索引擎架構之入門實戰

【Lucene】Apache Lucene全文檢索引擎架構之入門實戰

  Lucene是一套用於全文檢索和搜尋的開源程式庫,由Apache軟體基金會支援和提供。Lucene提供了一個簡單卻強大的應用程式介面,能夠做全文索引和搜尋。在Java開發環境裡Lucene是一個成熟的免費開源工具。就其本身而言,Lucene是當前以及最近幾年最受歡迎的免費Java資訊檢索程式庫。——《百度百科》

  這篇博文主要從兩個方面出發,首先介紹一下Lucene中的全文搜尋原理,其次通過程式示例來展現如何使用Lucene。關於全文搜尋原理部分我上網搜尋了一下,也看了好幾篇文章,最後在寫這篇文章的時候部分參考了其中兩篇(地址我放在文章的末尾),感謝原文作者。

1. 全文檢索

  何為全文檢索?舉個例子,比如現在要在一個檔案中查詢某個字串,最直接的想法就是從頭開始檢索,查到了就OK,這種對於小資料量的檔案來說,很實用,但是對於大資料量的檔案來說,就有點呵呵了。或者說找包含某個字串的檔案,也是這樣,如果在一個擁有幾十個G的硬碟中找那效率可想而知,是很低的。
  檔案中的資料是屬於非結構化資料,也就是說它是沒有什麼結構可言的,要解決上面提到的效率問題,首先我們得即將非結構化資料中的一部分資訊提取出來,重新組織,使其變得有一定結構,然後對此有一定結構的資料進行搜尋,從而達到搜尋相對較快的目的。這就叫全文搜尋。即先建立索引,再對索引進行搜尋的過程。
  那麼lucene中是如何建立索引的呢?假設現在有兩個文件,內容如下:

文章1的內容為:Tom lives in Guangzhou, I live in Guangzhou too.
文章2的內容為:He once lived in Shanghai.

  首先第一步是將文件傳給分片語件(Tokenizer),分片語件會將文件分成一個個單詞,並去除標點符號和停詞。所謂的停詞指的是沒有特別意義的詞,比如英文中的a,the,too等。經過分詞後,得到詞元(Token) 。如下:

文章1經過分詞後的結果:[Tom] [lives] [Guangzhou] [I] [live] [Guangzhou]
文章2經過分詞後的結果:[He] [lives] [Shanghai]

  然後將詞元傳給語言處理元件(Linguistic Processor),對於英語,語言處理元件一般會將字母變為小寫,將單詞縮減為詞根形式,如”lives”到”live”等,將單詞轉變為詞根形式,如”drove”到”drive”等。然後得到詞(Term)。如下:

文章1經過處理後的結果:[tom] [live] [guangzhou] [i] [live] [guangzhou]
文章2經過處理後的結果:[he] [live] [shanghai]

  最後將得到的詞傳給索引元件(Indexer),索引元件經過處理,得到下面的索引結構:

關鍵詞 文章號[出現頻率] 出現位置
guangzhou 1[2] 3,6
he 2[1] 1
i 1[1] 4
live 1[2],2[1] 2,5,2
shanghai 2[1] 3
tom 1[1] 1

  以上就是lucene索引結構中最核心的部分。它的關鍵字是按字元順序排列的,因此lucene可以用二元搜尋演算法快速定位關鍵詞。實現時lucene將上面三列分別作為詞典檔案(Term Dictionary)、頻率檔案(frequencies)和位置檔案(positions)儲存。其中詞典檔案不僅儲存有每個關鍵詞,還保留了指向頻率檔案和位置檔案的指標,通過指標可以找到該關鍵字的頻率資訊和位置資訊。
  搜尋的過程是先對詞典二元查詢、找到該詞,通過指向頻率檔案的指標讀出所有文章號,然後返回結果,然後就可以在具體的文章中根據出現位置找到該詞了。所以lucene在第一次建立索引的時候可能會比較慢,但是以後就不需要每次都建立索引了,就快了。當然了,這是針對英文的檢索,針對中文的規則會有不同,後面我再看看相關資料。

2. 示例程式碼

  根據上文的分析,全文檢索有兩個步驟,先建立索引,再檢索。所以為了測試這個過程,我寫了兩個java類,一個是測試建立索引的,另一個是測試檢索的。首先建立個maven工程,pom.xml如下:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>demo.lucene</groupId>
  <artifactId>Lucene01</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <build/>

  <dependencies>
    <!-- lucene核心包 -->
    <dependency>
        <groupId>org.apache.lucene</groupId>
        <artifactId>lucene-core</artifactId>
        <version>5.3.1</version>
    </dependency>
    <!-- lucene查詢解析包 -->
    <dependency>
        <groupId>org.apache.lucene</groupId>
        <artifactId>lucene-queryparser</artifactId>
        <version>5.3.1</version>
    </dependency>
    <!-- lucene解析器包 -->
    <dependency>
        <groupId>org.apache.lucene</groupId>
        <artifactId>lucene-analyzers-common</artifactId>
        <version>5.3.1</version>
    </dependency>
  </dependencies>
</project>

  在寫程式之前,首先得去弄一些檔案,我隨便找了一些英文的文件(中文的後面再研究),放到了D:\lucene\data\目錄中,如下:
文件
文件裡面都是密密麻麻的英文,我就不截圖了。
接下來開始寫建立索引的java程式:

/**
 * 建立索引的類
 * @author Ni Shengwu
 *
 */
public class Indexer {

    private IndexWriter writer; //寫索引例項

    //構造方法,例項化IndexWriter
    public Indexer(String indexDir) throws Exception {
        Directory dir = FSDirectory.open(Paths.get(indexDir));
        Analyzer analyzer = new StandardAnalyzer(); //標準分詞器,會自動去掉空格啊,is a the等單詞
        IndexWriterConfig config = new IndexWriterConfig(analyzer); //將標準分詞器配到寫索引的配置中
        writer = new IndexWriter(dir, config); //例項化寫索引物件
    }

    //關閉寫索引
    public void close() throws Exception {
        writer.close();
    }

    //索引指定目錄下的所有檔案
    public int indexAll(String dataDir) throws Exception {
        File[] files = new File(dataDir).listFiles(); //獲取該路徑下的所有檔案
        for(File file : files) {
            indexFile(file); //呼叫下面的indexFile方法,對每個檔案進行索引
        }
        return writer.numDocs(); //返回索引的檔案數
    }

    //索引指定的檔案
    private void indexFile(File file) throws Exception {
        System.out.println("索引檔案的路徑:" + file.getCanonicalPath());
        Document doc = getDocument(file); //獲取該檔案的document
        writer.addDocument(doc); //呼叫下面的getDocument方法,將doc新增到索引中
    }

    //獲取文件,文件裡再設定每個欄位,就類似於資料庫中的一行記錄
    private Document getDocument(File file) throws Exception{
        Document doc = new Document();
        //新增欄位
        doc.add(new TextField("contents", new FileReader(file))); //新增內容
        doc.add(new TextField("fileName", file.getName(), Field.Store.YES)); //新增檔名,並把這個欄位存到索引檔案裡
        doc.add(new TextField("fullPath", file.getCanonicalPath(), Field.Store.YES)); //新增檔案路徑
        return doc;
    }

    public static void main(String[] args) {
        String indexDir = "D:\\lucene"; //將索引儲存到的路徑
        String dataDir = "D:\\lucene\\data"; //需要索引的檔案資料存放的目錄
        Indexer indexer = null;
        int indexedNum = 0;
        long startTime = System.currentTimeMillis(); //記錄索引開始時間
        try {
            indexer = new Indexer(indexDir);
            indexedNum = indexer.indexAll(dataDir);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                indexer.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        long endTime = System.currentTimeMillis(); //記錄索引結束時間
        System.out.println("索引耗時" + (endTime-startTime) + "毫秒");
        System.out.println("共索引了" + indexedNum + "個檔案");
    }
}

  我是按照建立索引的過程來寫的程式,在註釋中已經解釋的很清楚了,這裡就不再贅述了。然後執行一下main方法看一下結果,如下:
索引結果
  共索引了7個檔案,耗時649毫秒,還是蠻快的,而且索引檔案的路徑也是對的,然後可以看一下D:\lucene\會生成一些檔案,這些就是生成的索引。
索引
  現在有了索引了,我們可以檢索想要查詢的字元了,我隨便打開了一個檔案,在裡面找了個比較醜的字串“generate-maven-artifacts”來作為檢索的物件。在檢索之前先看一下檢索的java程式碼:

public class Searcher {

    public static void search(String indexDir, String q) throws Exception {

        Directory dir = FSDirectory.open(Paths.get(indexDir)); //獲取要查詢的路徑,也就是索引所在的位置
        IndexReader reader = DirectoryReader.open(dir);
        IndexSearcher searcher = new IndexSearcher(reader);
        Analyzer analyzer = new StandardAnalyzer(); //標準分詞器,會自動去掉空格啊,is a the等單詞
        QueryParser parser = new QueryParser("contents", analyzer); //查詢解析器
        Query query = parser.parse(q); //通過解析要查詢的String,獲取查詢物件

        long startTime = System.currentTimeMillis(); //記錄索引開始時間
        TopDocs docs = searcher.search(query, 10);//開始查詢,查詢前10條資料,將記錄儲存在docs中
        long endTime = System.currentTimeMillis(); //記錄索引結束時間
        System.out.println("匹配" + q + "共耗時" + (endTime-startTime) + "毫秒");
        System.out.println("查詢到" + docs.totalHits + "條記錄");

        for(ScoreDoc scoreDoc : docs.scoreDocs) { //取出每條查詢結果
            Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相當於docID,根據這個docID來獲取文件
            System.out.println(doc.get("fullPath")); //fullPath是剛剛建立索引的時候我們定義的一個欄位
        }
        reader.close();
    }

    public static void main(String[] args) {
        String indexDir = "D:\\lucene";
        String q = "generate-maven-artifacts"; //查詢這個字串
        try {
            search(indexDir, q);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

執行一下main方法,看一下結果:
檢索
  Lucene已經正確的幫我們檢索到了,然後我把中間的“-”去掉,它也能幫我們檢索到,但是我把前面的字元都去掉,只留下“rtifacts”就檢索不到了,這也能說明Lucene中建立索引是以單詞來劃分的,但是這個問題是可以解決的,我會在後續的文章中寫到。

文末福利:“程式設計師私房菜”,一個有溫度的公眾號~
程式設計師私房菜