1. 程式人生 > >Lucene7.4 初體驗

Lucene7.4 初體驗

前言

本文的簡要內容:

  1. Lucene簡介
  2. 體驗Lucene Demo
  3. Lucene 核心類介紹
  4. Lucene 索引檔案格式

Lucene簡介

Lucene是目前最流行的Java開源搜尋引擎類庫,最新版本為7.4.0。Lucene通常用於全文檢索,Lucene具有簡單高效跨平臺等特點,因此有不少搜尋引擎都是基於Lucene構建的,例如:Elasticsearch,Solr等等。

現代搜尋引擎的兩大核心就是索引和搜尋,建立索引的過程就是對源資料進行處理,例如過濾掉一些特殊字元或詞語,單詞大小寫轉換,分詞,建立倒排索引等支援後續高效準確的搜尋。而搜尋則是直接提供給使用者的功能,儘管面向的使用者不同,諸如百度,谷歌等網際網路公司以及各種企業都提供了各自的搜尋引擎。搜尋過程需要對搜尋關鍵詞進行分詞等處理,然後再引擎內部構建查詢,還要根據相關度對搜尋結果進行排序,最終把命中結果展示給使用者。

Lucene只是一個提供索引和查詢的類庫,並不是一個應用,程式設計師需要根據自己的應用場景進行如資料獲取、資料預處理、使用者介面提供等工作。

搜尋程式的典型元件如下所示:

搜尋程式的典型元件

下圖為Lucene與應用程式的關係:

Lucene與應用程式的關係

體驗Lucene Demo

接下來先來看一個簡單的demo

引入 Maven 依賴

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source
>
1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <lucene.version>7.4.0</lucene.version> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId
>
junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>${lucene.version}</version> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-queryparser</artifactId> <version>${lucene.version}</version> </dependency> </dependencies>

索引類 IndexFiles.java

import org.apache.lucene.analysis.*;
import org.apache.lucene.analysis.standard.*;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.store.*;

import java.io.*;
import java.nio.charset.*;
import java.nio.file.*;
import java.nio.file.attribute.*;

public class IndexFiles {
    public static void main(String[] args) {
        String indexPath = "D:/lucene_test/index"; // 建立索引檔案的目錄
        String docsPath = "D:/lucene_test/docs"; // 讀取文字檔案的目錄

        Path docDir = Paths.get(docsPath);

        IndexWriter writer = null;
        try {
            // 儲存索引資料的目錄
            Directory dir = FSDirectory.open(Paths.get(indexPath));
            // 建立分析器
            Analyzer analyzer = new StandardAnalyzer();
            IndexWriterConfig iwc = new IndexWriterConfig(analyzer);
            iwc.setOpenMode(IndexWriterConfig.OpenMode.CREATE);

            writer = new IndexWriter(dir, iwc);
            indexDocs(writer, docDir);

            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void indexDocs(final IndexWriter writer, Path path) throws IOException {
        if (Files.isDirectory(path)) {
            Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                    try {
                        indexDoc(writer, file);
                    } catch (IOException ignore) {
                        // 不索引那些不能讀取的檔案,忽略該異常
                    }
                    return FileVisitResult.CONTINUE;
                }
            });
        } else {
            indexDoc(writer, path);
        }
    }

    private static void indexDoc(IndexWriter writer, Path file) throws IOException {
        try (InputStream stream = Files.newInputStream(file)) {
            // 建立一個新的空文件
            Document doc = new Document();
            // 新增欄位
            Field pathField = new StringField("path", file.toString(), Field.Store.YES);
            doc.add(pathField);
            Field contentsField = new TextField("contents",
                    new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)));
            doc.add(contentsField);
            System.out.println("adding " + file);
            // 寫文件
            writer.addDocument(doc);
        }
    }
}

查詢類 SearchFiles.java

import org.apache.lucene.analysis.*;
import org.apache.lucene.analysis.standard.*;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.queryparser.classic.*;
import org.apache.lucene.search.*;
import org.apache.lucene.store.*;

import java.io.*;
import java.nio.charset.*;
import java.nio.file.*;

public class SearchFiles {
    public static void main(String[] args) throws Exception {
        String indexPath = "D:/lucene_test/index"; // 建立索引檔案的目錄
        String field = "contents";
        IndexReader reader = DirectoryReader.open(FSDirectory.open(Paths.get(indexPath)));
        IndexSearcher searcher = new IndexSearcher(reader);
        Analyzer analyzer = new StandardAnalyzer();

        BufferedReader in = null;
        in = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
        QueryParser parser = new QueryParser(field, analyzer);
        System.out.println("Enter query:");
        // 從Console讀取要查詢的語句
        String line = in.readLine();
        if (line == null || line.length() == -1) {
            return;
        }
        line = line.trim();
        if (line.length() == 0) {
            return;
        }

        Query query = parser.parse(line);
        System.out.println("Searching for:" + query.toString(field));
        doPagingSearch(searcher, query);
        in.close();
        reader.close();
    }

    private static void doPagingSearch(IndexSearcher searcher, Query query) throws IOException {
        // TopDocs儲存搜尋結果
        TopDocs results = searcher.search(query, 10);
        ScoreDoc[] hits = results.scoreDocs;
        int numTotalHits = Math.toIntExact(results.totalHits);
        System.out.println(numTotalHits + " total matching documents");
        for (ScoreDoc hit : hits) {
            Document document = searcher.doc(hit.doc);
            System.out.println("文件:" + document.get("path"));
            System.out.println("相關度:" + hit.score);
            System.out.println("================================");
        }

    }
}

測試

首先建立資料夾 D:\lucene_test,在 lucene_test 下再建立 docs 資料夾,用來儲存要索引的測試檔案

docs 下建立3個檔案 test1.txt, test2.txt, test3.txt,分別寫入 hello world、 hello lucene、 hello elasticsearch

執行索引類 IndexFiles.java,可看到Console輸出

adding D:\lucene_test\docs\test1.txt
adding D:\lucene_test\docs\test2.txt
adding D:\lucene_test\docs\test3.txt

Lucene的索引檔案

執行查詢類 SearchFiles.java,搜尋 hello ,三個檔案相關度一樣

Enter query:
hello
Searching for:hello
3 total matching documents
文件:D:\lucene_test\docs\test1.txt
相關度:0.13353139
================================
文件:D:\lucene_test\docs\test2.txt
相關度:0.13353139
================================
文件:D:\lucene_test\docs\test3.txt
相關度:0.13353139
================================

搜尋 hello lucene,test2.txt的相關度比其他兩個高

Enter query:
hello lucene
Searching for:hello lucene
3 total matching documents
文件:D:\lucene_test\docs\test2.txt
相關度:1.1143606
================================
文件:D:\lucene_test\docs\test1.txt
相關度:0.13353139
================================
文件:D:\lucene_test\docs\test3.txt
相關度:0.13353139
================================

Lucene 核心類介紹

核心索引類

IndexWriter

進行索引寫操作的一箇中心元件
不能進行讀取和搜尋

Directory

Directory代表Lucene索引的存放位置
常用的實現:
    FSDerectory:表示一個儲存在檔案系統中的索引的位置
    RAMDirectory:表示一個儲存在記憶體當中的索引的位置
作用:
    IndexWriter通過獲取Directory的一個具體實現,在Directory指向的位置中操作索引

Analyzer

Analyzer,分析器,相當於篩子,對內容進行過濾,分詞,轉換等
作用:把過濾之後的資料交給indexWriter進行索引

Document

用來存放文件(資料),該文件為非結構化資料中抓取的相關資料
通過Field(域)組成Document,類似於mysql中的一個個欄位組成的一條記錄

Field

Document中的一個欄位

核心搜尋類

IndexSearcher

IndexSearcher在建立好的索引上進行搜尋
它只能以 只讀 的方式開啟一個索引,所以可以有多個IndexSearcher的例項在一個索引上進行操作

Term

Term是搜尋的基本單元,一個Term由 key:value 組成(類似於mysql中的  欄位名稱=查詢的內容)
例子: Query query = new TermQuery(new Term("filename", "lucene"));

Query

Query是一個抽象類,用來將使用者輸入的查詢字串封裝成Lucene能夠識別的Query

TermQuery

Query子類,Lucene支援的最基本的一個查詢類
例子:TermQuery termQuery = new TermQuery(new Term("filename", "lucene"));

BooleanQuery

BooleanQUery,布林查詢,是一個組合Query(多個查詢條件的組合)
BooleanQuery是可以巢狀的

栗子:
BooleanQuery query = new BooleanQuery();
BooleanQuery query2 = new BooleanQuery();
TermQuery termQuery1 = new TermQuery(new Term("fileName", "lucene"));
TermQuery termQuery2 = new TermQuery(new Term("fileName", "name"));
query2.add(termQuery1, Occur.SHOULD);
query.add(termQuery2, Occur.SHOULD);
query.add(query2, Occur.SHOULD);;       //BooleanQuery是可以巢狀的

Occur列舉:
    MUST
    SHOULD
    FILTER
    MUST_NOT

NumericRangeQuery

數字區間查詢
栗子:
Query newLongRange = NumericRangeQuery.newLongRange("fileSize",0l, 100l, true, true);

PrefixQuery

字首查詢,查詢分詞中含有指定字元開頭的內容
栗子:
PrefixQuery query = new PrefixQuery(new Term("fileName","hell"));

PhraseQuery

短語查詢
栗子1PhraseQuery query = new PhraseQuery();
    query.add(new Term("fileName","lucene"));

FuzzyQuery

模糊查詢
栗子:
FuzzyQuery query = new FuzzyQuery(new Term("fileName","lucene"));

WildcardQuery

萬用字元查詢:
* :任意字元(0或多個)
? : 一個字元

栗子:
WildcardQuery query = new WildcardQuery(new Term("fileName","*"));

RegexQuery

正則表示式查詢
栗子:搜尋含有最少1個字元,最多6個字元的
RegexQuery query = new RegexQuery(new Term("fileName","[a-z]{1,6}"));

MultiFieldQueryParser

查詢多個field
栗子:
String[] fields = {"fileName","fileContent"};
MultiFieldQueryParser queryParser = new MultiFieldQueryParser(fields, new StandardAnalyzer());
Query query = queryParser.parse("fileName:lucene AND filePath:a");

TopDocs

TopDocs類是一個簡單的指標容器,指標一般指向前N個排名的搜尋結果,搜尋結果即匹配條件的文件
TopDocs會記錄前N個結果中每個結果的int docID和浮點數型分數(反映相關度)

栗子:
    TermQuery searchingBooks = new TermQuery(new Term("subject","search")); 
    Directory dir = TestUtil.getBookIndexDirectory();
    IndexSearcher searcher = new IndexSearcher(dir);
    TopDocs matches = searcher.search(searchingBooks, 10);

Lucene 6.0 索引檔案格式

倒排索引

談到倒排索引,那麼首先看看正排是什麼樣子的呢?假設文件1包含【中文、英文、日文】,文件2包含【英文、日文、韓文】,文件3包含【韓文,中文】,那麼根據文件去查詢內容的話

文件1->【中文、英文、日文】
文件2->【英文、日文、韓文】
文件3->【韓文,中文】

反過來,根據內容去查詢文件

中文->【文件1、文件3】
英文->【文件1、文件2】
日文->【文件1、文件2】
韓文->【文件2、文件3

這就是倒排索引,而Lucene擅長的也正在於此

段(Segments)

Lucene的索引可能是由多個子索引或Segments組成。每個Segment是一個完全獨立的索引,可以單獨用於搜尋,索引涉及

  1. 為新新增的documents建立新的segments
  2. 合併已經存在的segments

搜尋可能涉及多個segments或多個索引,每個索引可能由一組segments組成

文件編號

Lucene通過一個整型的文件編號指向每個文件,第一個被加入索引的文件編號為0,後續加入的文件編號依次遞增。
注意文件編號是可能發生變化的,所以在Lucene外部儲存這些值時需要格外小心。

索引結構概述

每個segment索引包括資訊

  • Segment info:包含有關segment的元資料,例如文件編號,使用的檔案
  • Field names:包含索引中使用的欄位名稱集合
  • Stored Field values:對於每個document,它包含屬性-值對的列表,其中屬性是欄位名稱。這些用於儲存有關文件的輔助資訊,例如其標題、url或訪問資料庫的識別符號
  • Term dictionary:包含所有文件的所有索引欄位中使用的所有terms的字典。字典還包括包含term的文件編號,以及指向term的頻率和接近度的指標
  • Term Frequency data:對於字典中的每個term,包含該term的所有文件的數量以及該term在該文件中的頻率,除非省略頻率(IndexOptions.DOCS)
  • Term Proximity data:對於字典中的每個term,term在每個文件中出現的位置。注意,如果所有文件中的所有欄位都省略位置資料,則不會存在
  • Normalization factors:對於每個文件中的每個欄位,儲存一個值,該值將乘以該欄位上的匹配的分數
  • Term Vectors:對於每個文件中的每個欄位,可以儲存term vector,term vector由term文字和term頻率組成
  • Per-document values:與儲存的值類似,這些也以文件編號作為key,但通常旨在被載入到主儲存器中以用於快速訪問。儲存的值通常用於彙總來自搜尋的結果,而每個文件值對於諸如評分因子是有用的
  • Live documents:一個可選檔案,指示哪些文件是活動的
  • Point values:可選的檔案對,記錄索引欄位尺寸,以實現快速數字範圍過濾和大數值(例如BigInteger、BigDecimal(1D)、地理形狀交集(2D,3D))

檔案命名

屬於一個段的所有檔案具有相同的名稱和不同的副檔名。當使用複合索引檔案,這些檔案(除了段資訊檔案、鎖檔案和已刪除的文件檔案)將壓縮成單個.cfs檔案。當任何索引檔案被儲存到目錄時,它被賦予一個從未被使用過的檔名字

複合索引檔案

副檔名摘要

名稱 副檔名 簡短描述
Segments File segments_N 儲存了一個提交點(a commit point)的資訊
Lock File write.lock 防止多個IndexWriter同時寫到一份索引檔案中
Segment Info .si 儲存了索引段的元資料資訊
Compound File .cfs,.cfe 一個可選的虛擬檔案,把所有索引資訊都儲存到複合索引檔案中
Fields .fnm 儲存fields的相關資訊
Field Index .fdx 儲存指向field data的指標
Field Data .fdt 文件儲存的欄位的值
Term Dictionary .tim term詞典,儲存term資訊
Term Index .tip 到Term Dictionary的索引
Frequencies .doc 由包含每個term以及頻率的docs列表組成
Positions .pos 儲存出現在索引中的term的位置資訊
Payloads .pay 儲存額外的per-position元資料資訊,例如字元偏移和使用者payloads
Norms .nvd,.nvm .nvm檔案儲存索引欄位加權因子的元資料,.nvd檔案儲存索引欄位加權資料
Per-Document Values .dvd,.dvm .dvm檔案儲存索引文件評分因子的元資料,.dvd檔案儲存索引文件評分資料
Term Vector Index .tvx 將偏移儲存到文件資料檔案中
Term Vector Documents .tvd 包含有term vectors的每個文件資訊
Term Vector Fields .tvf 欄位級別有關term vectors的資訊
Live Documents .liv 哪些是有效檔案的資訊
Point values .dii,.dim 保留索引點,如果有的話

鎖檔案

預設情況下,儲存在索引目錄中的鎖檔名為 write.lock。如果鎖目錄與索引目錄不同,則鎖檔案將命名為“XXXX-write.lock”,其中XXXX是從索引目錄的完整路徑匯出的唯一字首。此鎖檔案確保每次只有一個寫入程式在修改索引。