1. 程式人生 > >全文檢索Lucene使用方式

全文檢索Lucene使用方式

在說全文檢索之前,先說說資料庫搜尋,資料庫中的搜尋很容易實現,通常都是使用sql語句進行查詢,而且能很快的得到查詢結果。

為什麼資料庫搜尋很容易?

因為資料庫中的資料儲存是有規律的,有行有列而且資料格式、資料長度都是固定的。

 

資料分類

我們生活中的資料總體分為兩種:結構化資料和非結構化資料。

結構化資料:指具有固定格式或有限長度的資料,如資料庫,元資料等。

非結構化資料:指不定長或無固定格式的資料,如郵件,word文件等磁碟上的檔案

 

非結構化資料查詢方法

1.順序掃描法(Serial Scanning)

所謂順序掃描,比如要找內容包含某一個字串的檔案,就是一個文件一個文件的看,對於每一個文件,從頭看到尾,如果此文件包含此字串,則此文件為我們要找的檔案,接著看下一個檔案,直到掃描完所有的檔案。如利用
windows的搜尋也可以搜尋檔案內容,只是相當的慢。

2.全文檢索(Full-text Search)

將非結構化資料中的一部分資訊提取出來,重新組織,使其變得有一定結構,然後對此有一定結構的資料進行搜尋,從而達到搜尋相對較快的目的。這部分從非結構化資料中提取出的然後重新組織的資訊,我們稱之索引

例如:字典。字典的拼音表和部首檢字表就相當於字典的索引,對每一個字的解釋是非結構化的,如果字典沒有音節表和部首檢字表,在茫茫辭海中找一個字只能順序掃描。然而字的某些資訊可以提取出來進行結構化處理,比如讀音,就比較結構化,分聲母和韻母,分別只有幾種可以一一列舉,於是將讀音拿出來按一定的順序排列,每一項讀音都指向此字的詳細解釋的頁數。我們搜尋時按結構化的拼音搜到讀音,然後按其指向的頁數,便可找到我們的非結構化資料
——也即對字的解釋。

這種先建立索引,再對索引進行搜尋的過程就叫全文檢索(Full-text Search)

雖然建立索引的過程也是非常耗時的,但是索引一旦建立就可以多次使用,全文檢索主要處理的是查詢,所以耗時間建立索引是值得的。

 

如何實現全文檢索

可以使用Lucene實現全文檢索。Lucene是apache下的一個開放原始碼的全文檢索引擎工具包。提供了完整的查詢引擎和索引引擎,部分文字分析引擎。Lucene的目的是為軟體開發人員提供一個簡單易用的工具包,以方便的在目標系統中實現全文檢索的功能。

全文檢索的應用場景

對於資料量大、資料結構不固定的資料可採用全文檢索方式搜尋,比如百度、Google等搜尋引擎、論壇站內搜尋、電商網站站內搜尋等。但是百度是收錢的,給錢就往前面拍.

但是我們使用肯定不是做百度這型別的搜尋,主要做的是電商專案種種的搜尋.

 

Lucene官網:http://lucene.apache.org/

索引和搜尋流程圖,在經過左邊建立索引的過程後,索引庫種會把原始文件和索引存到索引庫當中去,後期就算把原始文件刪掉也能搜到結果.

 

建立索引

對文件索引的過程,將使用者要搜尋的文件內容進行索引,索引儲存在索引庫(index)中。

這裡我們要搜尋的文件是磁碟上的文字檔案,根據案例描述:凡是檔名或檔案內容包括關鍵字的檔案都要找出來,這裡要對檔名和檔案內容建立索引。

 

獲得原始文件

原始文件是指要索引和搜尋的內容。原始內容包括網際網路上的網頁、資料庫中的資料、磁碟上的檔案等。

本案例中的原始內容就是磁碟上的檔案,如下圖:

從網際網路上、資料庫、檔案系統中等獲取需要搜尋的原始資訊,這個過程就是資訊採集,資訊採集的目的是為了對原始內容進行索引。

在Internet上採集資訊的軟體通常稱為爬蟲或蜘蛛,也稱為網路機器人,爬蟲訪問網際網路上的每一個網頁,將獲取到的網頁內容儲存起來。 Lucene不提供資訊採集的類庫,需要自己編寫一個爬蟲程式實現資訊採集,也可以通過一些開源軟體實現資訊採集,如下:

 Nutch(http://lucene.apache.org/nutch), Nutch是apache的一個子專案,包括大規模爬蟲工具,能夠抓取和分辨web網站資料。

 jsoup(http://jsoup.org/ ),jsoup 是一款Java 的HTML解析器,可直接解析某個URL地址、HTML文字內容。它提供了一套非常省力的API,可通過DOM,CSS以及類似於jQuery的操作方法來取出和操作資料。

 heritrix(http://sourceforge.net/projects/archive-crawler/files/),Heritrix 是一個由 java 開發的、開源的網路爬蟲,使用者可以使用它來從網上抓取想要的資源。其最出色之處在於它良好的可擴充套件性,方便使用者實現自己的抓取邏輯。

本案例我們要獲取磁碟上檔案的內容,可以通過檔案流來讀取文字檔案的內容,對於pdf、doc、xls等檔案可通過第三方提供的解析工具讀取檔案內容,比如Apache POI讀取doc和xls的檔案內容。

從網上爬取資料只是爬裡面的文字資訊,從而建立索引,而不爬圖片,這幾個爬蟲都是java寫的

 

建立文件物件

獲取原始內容的目的是為了索引,在索引前需要將原始內容建立成文件(Document),文件中包括一個一個的域(Field),域中儲存內容。

這裡我們可以將磁碟上的一個檔案當成一個document,Document中包括一些Field(file_name檔名稱、file_path檔案路徑、file_size檔案大小、file_content檔案內容),如下圖:每個詳細資訊都可以建立一個域,這裡的域就相當於資料庫的欄位,java物件的屬性,只是叫法不一樣而已.

注意:每個Document可以有多個Field,不同的Document可以有不同的Field,同一個Document可以有相同的Field(域名和域值都相同)

每個文件都有一個唯一的編號,就是文件id。id的增長方式是自增的從0開始自增1.這個編號不是域.這個編號我們不能進行操作,而其它域可以.

 

分析文件

將原始內容建立為包含域(Field)的文件(document),需要再對域中的內容進行分析,分析的過程是經過對原始文件提取單詞、將字母轉為小寫、去除標點符號、去除停用詞等過程生成最終的語彙單元,可以將語彙單元理解為一個一個的單詞。

比如下邊的文件經過分析如下:

原文件內容:

Lucene is a Java full-text search engine.  Lucene is not a complete

application, but rather a code library and API that can easily be used

to add search capabilities to applications.

分析後得到的語彙單元:

lucene、java、full、search、engine。。。。

每個單詞叫做一個Term,不同的域中拆分出來的相同的單詞是不同的term。term中包含兩部分一部分是文件的域名,另一部分是單詞的內容。

例如:檔名中包含apache和檔案內容中包含的apache是不同的term。

建立索引

如下圖,每個文件建立索引都會走一遍左邊的流程,如果兩個文件分析的時候有相同的term的時候,會在索引庫只儲存一份,但是在索引後面會跟上文件物件的編號,這樣就能減少索引庫的壓力,當查詢的時候,會直接找到索引後面的編號找文件物件然後反饋回去.而不是整個查一遍.

注意:建立索引是對語彙單元索引,通過詞語找文件,這種索引的結構叫倒排索引結構

傳統方法是根據檔案找到該檔案的內容,在檔案內容中匹配搜尋關鍵字,這種方法是順序掃描方法,資料量大、搜尋慢。

倒排索引結構是根據內容(詞語)找文件,倒排索引結構也叫反向索引結構,包括索引和文件兩部分,索引即詞彙表,它的規模較小,而文件集合較大。

 

下載Lucene   Jdk要求:1.7以上

解壓後開啟,找到這麼幾個包,io的和junit的自己去maven官網下吧

下面這個是Field的實現類,為什麼會分這麼多呢?因為根據實際情況,比如檔案大小,這個域你分析的話就沒必要,因為分出來什麼也不是,沒人根據這個東西來查詢,再比如商品編號,這個東西不能進行分析,但是得儲存成索引,這又是一種情況,所以得根據不同情況自定義與物件.

下面開始一個小demo,自己先準備好一些檔案,這個是建立索引的demo

 

/**
 * 
 */
package com.buba.lucene;

import java.io.File;

import org.apache.commons.io.FileUtils;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.LongField;
import org.apache.lucene.document.StoredField;
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.apache.lucene.util.Version;
import org.junit.Test;

public class FirstLucene {

    @Test
    public void testIndex() throws Exception{
//        //建立一個索引庫位置隨便寫
        Directory directory = FSDirectory.open(new File("F:\\temp\\index"));
        //Directory directory2 = new RAMDirectory(); 這是記憶體索引庫存到記憶體中,但是一關機就沒了,不建議使用
        //這個是官方推薦的標準分詞器,對英文分的準對中文不行,
        Analyzer analyzer = new StandardAnalyzer();
        //指定一個分析器,對文件內容進行分析。第一個引數是版本號,如果導了多個lucene的話使用最新的
        IndexWriterConfig config = new IndexWriterConfig(Version.LATEST,analyzer );
        //建立一個流往索引庫存資料時用,
        IndexWriter indexWriter = new IndexWriter(directory, config);
//    

        
//        第三步:建立field物件,將field新增到document物件中。 域物件放到文件物件
        File f = new File("F:\\searchsource\\searchsource");//我這個路徑是文字位置
        File[] listFiles = f.listFiles();
        for(File file : listFiles) {
//            建立document物件。
            Document document = new Document();
            //這裡面獲取的具體資訊可以寫別的自己加就可以
            //檔名稱
            String file_name = file.getName();
            Field fileNameField = new TextField("fileName",file_name,Store.YES);//第一個引數是域名字,第二個域值,第三個是否儲存
            //檔案大小
            long fiel_size = FileUtils.sizeOf(file);
            Field fileSizeField = new LongField("fileSize", fiel_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();
    }
}
 

執行完後去索引庫可以看到生成的索引檔案 ,是打不開的需要藉助工具才能看到裡面的東西

下載地址https://download.csdn.net/download/kxj19980524/10867071

解壓後點擊bat檔案,就可了

選中索引庫目錄

英文單詞也不是太難的應該能理解,可以按著我框的點一下看看

搜尋索引過程:

根據查詢語法在倒排索引詞典表中分別找出對應搜尋詞的索引,從而找到索引所連結的文件連結串列。

比如搜尋語法為“fileName:lucene”表示搜尋出fileName域中包含Lucene的文件。

搜尋過程就是在索引上查詢域為fileName,並且關鍵字為Lucene的term,並根據term找到文件id列表。

渲染結果

以一個友好的介面將查詢結果展示給使用者,使用者根據搜尋結果找自己想要的資訊,為了幫助使用者很快找到自己的結果,提供了很多展示的效果,比如搜尋結果中將關鍵字高亮顯示,百度提供的快照等。

搜尋類的實現類,也是根據不同情況寫不同實現類

@Test
    public void testSearch()throws Exception{
//        第一步:建立一個Directory物件,也就是索引庫存放的位置。
        Directory directory = FSDirectory.open(new File("F:\\temp\\index"));
//        第二步:建立一個indexReader物件,需要指定Directory物件。
        IndexReader indexReader = DirectoryReader.open(directory);
//        第三步:建立一個indexsearcher物件,需要指定IndexReader物件  這是搜尋物件
        IndexSearcher indexsearcher = new IndexSearcher(indexReader);
//        第四步:建立一個TermQuery物件,指定查詢的域和查詢的關鍵詞。
        Query query = new TermQuery(new Term("fileName","apache"));//這個是精準查詢,域名和域值
//        第五步:執行查詢。
        TopDocs topDocs = indexsearcher.search(query, 2);  //2是查詢個數
//        第六步:返回查詢結果。遍歷查詢結果並輸出。
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for(ScoreDoc s:scoreDocs) {
            int doc = s.doc;   //這個返回的是term後面跟的id值
            Document doc2 = indexsearcher.doc(doc);  //轉成dom物件後獲取出來當初存的域值
            //檔名稱
            String fileName = doc2.get("fileName");
            System.out.println(fileName);
            //檔案大小
            String fileContent = doc2.get("fileContent");
            System.out.println(fileContent);
            //檔案大小
            String fileSize = doc2.get("fileSize");
            System.out.println(fileSize);
            //檔案路徑
            String filePath = doc2.get("filePath");
            System.out.println(filePath);
            System.out.println("------------------");
            
        }
//        第七步:關閉IndexReader物件
        indexReader.close();
    }

接下來說說這個標準分詞器,因為畢竟是外國人開發的,肯定是按照英文分的,如果分中文的話就一個字一個字分了所以很不好.

//檢視標準分析器的分詞效果
        @Test
        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.");
            TokenStream tokenStream = analyzer.tokenStream("test", "全文檢索概念\r\n" + 
                    "全文檢索是將整本書java、整篇文章中的任意內容資訊");
            //新增一個引用,可以獲得每個關鍵詞
            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();
        }

換成支援中日韓的試試,然而效果也不好

Analyzer analyzer = new CJKAnalyzer();

有一個提供給apache的,效果還是但是擴充套件性差,不能與時俱進,不能新增

最後使用一個IKAnalyzer分詞器下載地址https://code.google.com/p/ik-analyzer/貌似現在谷歌上不去了,反正我是訪問不了,所以我自己上傳了一個https://download.csdn.net/download/kxj19980524/10867301

匯入包後替換就行,把下面這句替換舊行

Analyzer analyzer = new    IKAnalyzer();

它最強大的功能就是可以擴充套件,引入jar包和配置檔案後,把這注釋開啟,然後建立兩個檔案,一個裡面寫擴充套件的詞彙一個裡面寫,禁止的詞彙

要記住一點,建立索引和查詢索引必須使用同一個分詞器不然查出來的結果不在預料之內

刪除全部索引

public IndexWriter getIndexWriter() throws Exception{
        //建立一個索引庫位置隨便寫
        Directory directory = FSDirectory.open(new File("F:\\temp\\index"));
        //Directory directory2 = new RAMDirectory(); 這是記憶體索引庫存到記憶體中,但是一關機就沒了,不建議使用
        //這個是官方推薦的標準分詞器,對英文分的準對中文不行,
        Analyzer analyzer = new StandardAnalyzer();
        //指定一個分析器,對文件內容進行分析。第一個引數是版本號,如果導了多個lucene的話使用最新的
        IndexWriterConfig config = new IndexWriterConfig(Version.LATEST,analyzer );
        //建立一個流往索引庫存資料時用,
        return new IndexWriter(directory, config);
    }
    //刪除全部索引
    @Test
    public void deleteAllIndex() 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();
    }

修改索引,修改就是,刪一個新增一個 ,它這個刪除修改不會把id也刪掉,只會把文件內容清空,可以在工具裡看看.

    //修改
    @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));
        indexWriter.updateDocument(new Term("fileName","apache"), doc,new IKAnalyzer());
        indexWriter.close();
    }

查詢所有

//IndexReader  IndexSearcher
    public IndexSearcher getIndexSearcher() throws Exception{
        // 第一步:建立一個Directory物件,也就是索引庫存放的位置。
        Directory directory = FSDirectory.open(new File("D:\\temp\\index"));// 磁碟
        // 第二步:建立一個indexReader物件,需要指定Directory物件。
        IndexReader indexReader = DirectoryReader.open(directory);
        // 第三步:建立一個indexsearcher物件,需要指定IndexReader物件
        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 = document.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();
        System.out.println(query);
        printResult(indexSearcher, query);
        //關閉資源
        indexSearcher.getIndexReader().close();
    }

根據數值範圍查詢,運用到按照價格篩選

    //根據數值範圍查詢
    @Test
    public void testNumericRangeQuery() throws Exception {
        IndexSearcher indexSearcher = getIndexSearcher();
        
        Query query = NumericRangeQuery.newLongRange("fileSize", 47L, 200L, false, true);//這兩個boolean的意思是包含47,200麼?
        System.out.println(query);
        printResult(indexSearcher, query);
        //關閉資源
        indexSearcher.getIndexReader().close();
    }

 組合查詢,多條件進行篩選

//可以組合查詢條件
    @Test
    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"));
        //  select * from user where id =1 or name = 'safdsa'
        booleanQuery.add(query1, Occur.MUST);  //這的引數看下面,意思就是必須或者,不必須的意思
        booleanQuery.add(query2, Occur.SHOULD);
        System.out.println(booleanQuery);
        printResult(indexSearcher, booleanQuery);
        //關閉資源
        indexSearcher.getIndexReader().close();
    }

使用queryparser查詢,上面的所有查詢都是通過使用query的子類查詢的,這個查詢是通過寫表示式查詢.

    //條件解釋的物件查詢
    @Test
    public void testQueryParser() throws Exception {
        IndexSearcher indexSearcher = getIndexSearcher();
        //引數1: 預設查詢的域  
        //引數2:採用的分析器
        QueryParser queryParser = new QueryParser("fileName",new IKAnalyzer());
        // *:*   域:值
        Query query = queryParser.parse("fileName:lucene is apache OR fileContent:lucene is apache");//這填寫的表示式在上面的query物件中都有,不妨把上面子類查詢的query物件列印一下看看是什麼東西,但是這個表示式不支援查詢數值範圍,只支援字串,solr中支援數值,下一篇中會講到.上面的每一種查詢都可以使用表示式的形式進行查詢.
        
        printResult(indexSearcher, query);
        //關閉資源
        indexSearcher.getIndexReader().close();
    }

 

查詢語法

1、基礎的查詢語法,關鍵詞查詢:

域名+“:”+搜尋的關鍵字

例如:content:java

  1. 範圍查詢

域名+“:”+[最小值 TO 最大值]

例如:size:[1 TO 1000]

範圍查詢在lucene中支援數值型別,不支援字串型別。在solr中支援字串型別。

  1. 組合條件查詢

1)+條件1 +條件2:兩個條件之間是並且的關係and

例如:+filename:apache +content:apache

  1. +條件1 條件2:必須滿足第一個條件,應該滿足第二個條件

例如:+filename:apache content:apache

  1. 條件1 條件2:兩個條件滿足其一即可。

例如:filename:apache content:apache

4)-條件1 條件2:必須不滿足條件1,要滿足條件2

例如:-filename:apache content:apache

條件解析的物件查詢   多個默唸域,就是多個條件而已,使用上面的單個的只要改改語法也能實現多條件的效果

//條件解析的物件查詢   多個默唸域
    @Test
    public void testMultiFieldQueryParser() throws Exception {
        IndexSearcher indexSearcher = getIndexSearcher();
        
        String[] fields = {"fileName","fileContent"};
        //引數1: 預設查詢的域  
        //引數2:採用的分析器
        MultiFieldQueryParser queryParser = new MultiFieldQueryParser(fields,new IKAnalyzer());
        // *:*   域:值
        Query query = queryParser.parse("lucene is apache");
        
        printResult(indexSearcher, query);
        //關閉資源
        indexSearcher.getIndexReader().close();
    }

 下一篇使用solr框架實現全文檢索https://blog.csdn.net/kxj19980524/article/details/85202324