Lucene 基礎知識

1. 數據分類

  • 結構化數據: 指具備固定格式或有限長度的數據,如數據庫等;
  • 非結構化數據: 指不定長或無固定格式的數據, 如郵件,word 文檔等磁盤上的文件;

1.1 非結構化數據查詢方法

  1. 順序掃描法(Serial Scanning)
  2. 全文檢索(Full-text Search)
    • 將非結構化數據中的一部分信息提取出來,從新組織,使其變得有必定結構,而後對此有必定結構的數據進行搜索,
      從而達到搜索相對較快的目的; 這部分從非結構化數據中提取出,而後從新組織的信息,稱之爲索引, 例如字典.
    • 這種先創建索引,而後再對索引進行搜索的過程就叫全文檢索;

2. Lucene 概述

  • Lucene 是 apache 下的一個開放源代碼的全文檢索引擎工具包,提供了完整的查詢引擎和索引引擎,部分文本分析引擎;

2.1 Lucene 實現全文檢索的流程

2.2 建立文檔對象

  • 獲取原始內容的目的是爲了索引,在索引前,須要將原始內容建立成文檔(Document),文檔中包括一個一個的域(Field),
    域中存儲內容;
  • 咱們能夠將磁盤上的一個文件當成一個 document, Document 中包括一些Field(file_name 文件名稱, file_path
    文件路徑, file_size 文件大小, file_content 文件內容);
  • 每個 Document 能夠有多個 Field,同一個Document,能夠有相同的 Field(域名和域值都相同);
  • 每個 Document 都有一個惟一的編號,就是文檔 id;

2.3 分析文檔

  • 將原始內容建立爲包含域(Field)的文檔(document),須要再對域中的內容進行分析,分析的過程是通過對原始文檔提取單詞,
    將字母轉爲小寫,去除標點符號,去除停用詞等過程生成最終的語彙單元,能夠將語彙單元理解爲一個一個的單詞;
  • 每個單詞叫作一個Term,不一樣的域中拆分出來的相同的單詞是不一樣的term; term中包含兩部分,一部分是文檔的域名, 另外一部分
    是單詞的內容;
  • Field 域的屬性
    • 是否分析: 是否對域的內容進行分詞處理;
    • 是否索引: 將 Field 分析後的詞或整個 Field 值進行索引,只有創建索引,才能搜索到;
    • 是否存儲: 存儲在文檔中的 Field 才能夠從 Document 中獲取;

2.4 建立索引

  • 對全部文檔分析得出的語彙單元進行索引,索引的目的是爲了搜索,最終要實現只搜索被索引的語彙單元從而找到 Document;
    這種索引的結構叫倒排索引結構;
  • 傳統方法是根據文件找到該文件的內容,在文件內容中匹配搜索關鍵字,這種方法是順序掃描方法,數據量大,搜索慢;
  • 倒排索引結構是根據內容(詞語)找文檔; 順序掃描方法是根據文檔查找裏面的內容;

// 建立索引庫
// 環境: Lucene 4.10.3
// jar 包
    /*
     * lucene-core-4.10.3
     * lucene-analyzers-common-4.10.3
     * lucene-queryparser-4.10.3
     * commons-io
     * junit
     */

// 測試類
public class FirstLucene{

    // 建立索引
    @Test
    public void testIndex() throws Exception{
        // 1. 建立一個 indexWriter 對象 new IndexWriter(arg0, arg1);
        //     arg0: 指定索引庫的存放位置(Directory 對象)
        //     arg1: config
        // FSDirectory: File System Directory : 磁盤存儲
        // Directory directory = new RAMDirectory(); 保存索引到內存中
        Directory directory = FSDirectory.open(new File("/Users/用戶名/Documents/dic"));

        // 指定一個分詞器
        Analyzer analyzer = new StandardAnalyzer();
        IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3,analyzer);

        // 建立 indexWriter 對象
        IndexWriter indexWriter = new IndexWriter(directory, config);

        // 指定原始文件的目錄
        File f = new File("/Users/用戶名/Documents/searchsource");
        File[] listFiles = f.listFiles();

        for(File file : listFiles){
            // 建立文檔對象
            Document document = new Document();

            // 文件名稱
            String file_name = file.getName();
            Field fileNameField = new TextField("fileName",file_name,Store.YES);

            // 文件大小
            long file_size = FileUtils.sizeOf(file);
            Field fileSizeField = new LongField("fileSize",file_size, Store.YES);

            // 文件路徑
            String file_path = file.getPath();
            Field filePathField = new StoredField("filePath",file_path);

            // 文件內容
            String file_content = FileUtils.readFileToString(file);
            Field fileContentField = new TextField("fileContent",file_content,Store.YES);

            document.add(fileNameField);
            document.add(fileSizeField);
            document.add(filePathField);
            document.add(fileContentField);

            // 使用indexWriter 對象將 document 對象寫入索引庫,此過程將 索引和document 對象寫入索引庫
            indexWriter.addDocument(document);
        }

        // 關閉 IndexWriter 對象
        indexWriter.close();
    }
}


// 查看分詞完成後的文件: Luke
java -jar lukeall-4.10.3.jar

3. 查詢索引

3.1 建立查詢

  • 用戶輸入查詢關鍵字執行搜索前,須要先建立一個查詢對象,查詢對象中能夠指定查詢要搜索的 Field 文檔域,查詢關鍵字等,
    查詢對象會生成具體的查詢語法;
  • 例如: fileName:lucene: 表示要搜索Field域的內容爲"lucene"的文檔;

3.2 執行查詢

  • 根據查詢語法在倒排索引詞典表中分別找出對應搜索詞的索引,從而找到索引所連接的文檔鏈表;
  • 好比: fileName:lucene 的搜索過程: 在索引上查找域爲 fileName, 而且關鍵字爲Lucene的term, 並根據 term 找到
    文檔 id 列表;

3.3 渲染結果

3.4 IndexSearcher 搜索方法

// 查詢索引
/*
 * 步驟:
 *    1. 建立一個 Directory 對象,用於指定索引庫存放的位置;
 *    2. 建立一個 indexReader 對象, 須要指定 Directory 對象, 用於讀取索引庫中的文件;
 *    3. 建立一個 indexSearcher 對象, 須要指定 indexReader 對象;
 *    4. 建立一個 TermQuery 對象,指定查詢的域和查詢的關鍵詞
 *    5. 執行查詢
 *    6. 返回查詢結果,遍歷查詢結果並輸出;
 *    7. 關閉 indexReader
 */

 public class IndexSearchTest{

    @Test
    public void testIndexSearch() throws Exception{

        Directory directory = FSDirectory.open(new File("/Users/用戶名/Documents/dic"));

        IndexReader indexReader = DirectoryReader.open(directory);

        IndexSearcher indexSearcher = new IndexSearcher(indexReader);

        // 建立一個 TermQuery 對象,指定查詢的域和查詢的關鍵詞
        Query query = new TermQuery(new Term("fileName","java"));
        // 執行查詢
        TopDocs topDocs = indexSearcher.search(query,2);

        SocreDoc[] scoreDocs = topDocs.scoreDocs;
        for(ScoreDoc scoreDoc : scoreDocs){
            // 獲取文檔 id
            int docID = scoreDoc.doc;

            // 經過id,從索引中讀取出對應的文檔
            Document document = indexReader.document(docID);
            // 獲取文件名稱
            System.out.println(document.get("fileName"));
            // 獲取文件內容
            System.out.println(document.get("fileContent"));
            // 文件路徑
            System.out.println(document.get("filePath"));
            // 文件大小
            System.out.println(document.get("fileSize"));

            System.out.println("=======================");
        }

        indexReader.close();
    }
}

4. 支持中文分詞器(IKAnalyzer)

4.1 分詞器(Analyzer)的執行過程

  • 從一個 Reader 字符流開始,建立一個基於 Reader 的 Tokenizer分詞器,通過三個 TokenFilter,生成語彙單元 Tokens;
  • 若是要查看分詞器的分詞效果,只須要看Tokenstream中的內容就能夠了,每一個分詞器都有一個方法tokenStream,返回一個
    tokenStream 對象;

// 查看標準分詞器的分詞效果
public void testTokenStream() throws Exception {
        //建立一個標準分析器對象
        Analyzer analyzer = new StandardAnalyzer();
        //得到tokenStream對象
        //第一個參數:域名,能夠隨便給一個
        //第二個參數:要分析的文本內容
        TokenStream tokenStream = analyzer.tokenStream("test",
                                "The Spring Framework provides a comprehensive"
                                      +"programming and configuration model.");

        //添加一個引用,能夠得到每一個關鍵詞
        CharTermAttribute charTermAttribute = tokenStream.addAttribute(CharTermAttribute.class);
        //添加一個偏移量的引用,記錄了關鍵詞的開始位置以及結束位置
        OffsetAttribute offsetAttribute = tokenStream.addAttribute(OffsetAttribute.class);
        //將指針調整到列表的頭部
        tokenStream.reset();
        //遍歷關鍵詞列表,經過incrementToken方法判斷列表是否結束
        while(tokenStream.incrementToken()) {
            //關鍵詞的起始位置
            System.out.println("start->" + offsetAttribute.startOffset());
            //取關鍵詞
            System.out.println(charTermAttribute);
            //結束位置
            System.out.println("end->" + offsetAttribute.endOffset());
        }
        tokenStream.close();
    }

5.索引庫的維護

// 索引庫維護: 就是索引的增刪改查

public class LuceneManager{

    public IndexWriter getIndexWriter(){
        Directory directory = FSDirectory.open(new File("/Users/用戶名/Documents/dic"));
        Analyzer analyzer = new StandardAnalyzer();
        IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer);

        return new IndexWriter(directory,config);
    }

    // 全刪除
    @Test
    public void testAllDelete() throws Exception{
        IndexWriter indexWriter = getIndexWriter();
        indexWriter.deleteAll();
        indexWriter.close();
    }

    // 根據條件刪除
    @Test
    public void testDelete() throws Exception{
        IndexWriter indexWriter = getIndexWriter();
        Query query = new TermQuery(new Term("fileName","apache"));
        indexWriter.deleteDocuments(query);
        indexWriter.close();
    }

    // 修改
    @Test
    public void testUpdate() throws Exception{
        IndexWriter indexWriter = getIndexWriter();
        Document doc = new Document();
        doc.add(new TextField("fileN","測試文件名",Store.YES));
        doc.add(new TextField("fileC","測試文件內容",Store.YES));

        // 將 lucene 刪除,而後添加 doc
        indexWriter.updateDocument(new Term("fileName","lucene"),doc, new IKAnalyzer());
        indexWriter.close();
    }
}

6. 索引庫查詢

  1. 對要搜索的信息建立 Query 查詢對象,Lucene會根據 Query 查詢對象生成最終的查詢語法;
  2. 可經過兩種方法建立查詢對象:
    • 使用 Lucene 提供的 Query子類;
    • 使用 QueryParse 解析查詢表達式, 須要加入lucene-queryparser-4.10.3.jar
public class LuceneManager{

    // 獲取 IndexSearcher
    public IndexSearcher getIndexSearcher() throws Exception{
        Directory directory = FSDirectory.open(new File("/Users/用戶名/Documents/dic"));
        IndexReader indexReader = DirectoryReader.open(directory);

        return new IndexSearcher(indexReader);
    }

    // 獲取執行結果
    public void printResult(IndexSearcher indexSearcher, Query query) throws Exception{
        TopDocs topDocs = indexSearcher.search(query,10);

        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for(ScoreDoc scoreDoc : scoreDocs){
            int doc = scoreDoc.doc;
            Document document = indexSearcher.doc(doc);

            String fileName = docment.get("fileName");
            System.out.println(fileName);

            String fileContent = document.get("fileContent");
            System.out.println(fileContent);

            String fileSize = document.get("fileSize");
            System.out.println(fileSize);

            String filePath = document.get("filePath");
            System.out.println(filePath);

            System.out.println("======================");
        }
    }

    // 查詢全部
    @Test
    public void testMatchAllDocsQuery() throws Exception{
        IndexSearcher indexSearcher = getIndexSearcher();

        Query query = new MatchAllDocsQUery();

        printResult(indexSearcher,query);
        // 關閉資源
        indexSearcher.getIndexReader().close();
    }

    // 精準查詢(TermQuery)


    // NumericRangeQuery(按數值範圍查詢)
    @Test
    public void testNumericRangeQuery() throws Exception{
        IndexSearcher indexSearcher = getIndexSearcher();

        /*
         * 建立查詢
         *     參數: 域名, 最小值, 最大值, 是否包含最小值, 是否包含最大值
         */
         Query query = NumericRangeQuery.newLongRange("fileSize",100L,200L,true,true);

         // 執行查詢
         printResult(query,indexSearcher);
    }

    // BooleanQuery(組合查詢)
    public void testBooleanQuery() throws Exception{
        IndexSearcher indexSearcher = getIndexSearcher();

        BooleanQuery booleanQuery = new BooleanQuery();

        Query query1 = new TermQuery(new Term("fileName","apache"));
        Query query2 = new TermQuery(new Term("fileName","lucene"));

        // Occur.MUST: 必須知足此條件, 至關於 and
        // Occur.SHOULD: 應該知足此條件, 可是不知足也能夠, 至關於 or
        // Occur.MUST_NOT: 必須不知足, 至關於 not
        booleanQuery.add(query1,Occur.SHOULD);
        booleanQuery.add(query2,Occur.SHOULD);

        printResult(indexSearcher,booleanQuery);
        // 關閉資源
        indexSearcher.getIndexReader().close();
    }


    // 使用 QueryParse 解析查詢表達式
    @Test
    public void testQueryParser() throws Exception{
        IndexSearcher indexSearcher = getIndexSearcher();

        // 建立 QueryParser 對象, 其中 arg0: 表示默認查詢域, arg1: 分詞器
        QueryParser queryParser = new QueryParser("fileName",new IKAnalyzer());

        // 此時,表示使用默認域: fileName
        // Query query = queryParser.parse("apache");
        // 表示查詢 fileContent 域
        Query query = queryParser.parse("fileContent:apache");

        printResult(indexSearcher, query);
        // 關閉資源
        indexSearcher.getIndexReader().close();
    }

    // 指定多個默認搜索域
    @Test
    public void testMultiFieldQueryParser() throws Exception{
        IndexSearcher indexSearcher = getIndexSearcher();
        // 指定多個默認搜索域
        String[] fields = {"fileName", "fileContent"};

        // 建立 MultiFiledQueryParser 對象
        MultiFieldQueryParser queryParser = new MultiFieldQueryParser(fields, new IKAnalyzer());
        Query query = queryParser.parse("apache");

        // 輸出查詢條件
        System.out.println(query);

        // 執行查詢
        printResult(indexSearcher, query);
        // 關閉資源
        indexSearcher.getIndexReader().close();
    }
}
相關文章
相關標籤/搜索