Lucene使用心得

Lucene中兩個最重要的概念,索引和搜索
索引:一個比較經典的例子:Eclipse中搜索帶有指定字符串“aaa”的所有文件。如果順序的掃描文件查找,這會是相當的鬱悶。這時就出現了索引:爲了快速搜索大量的文本,首先索引那個文本然後把它轉化爲一個可以快速搜索的格式,因此可以除去緩慢的順序地掃描過程。這個轉化過程稱爲索引,它的輸出稱爲一條索引。索引就可以認爲是一個快速隨機訪問存於其內部的詞的數據結構。
搜索:搜索是在一個索引中查找指定字符串來找出它們所出現的文檔的過程。

一些基本的類說明:
Document:Document相當於一個要進行索引的單元,任何可以想要被索引的文件都必須轉化爲Document對象才能進行索引,可以把它看成數據庫裏面的一行記錄。
Field:就像數據庫中的字段,它有三種屬性,isStored(是否被存儲),isIndexed(是否被索引),isTokenized(是否分詞)。
IndexWriter:它主要是用來將Document加入索引,同時控制索引過程中的一些參數使用。
IndexSearcher:lucene中最基本的檢索工具,所有的檢索都會用到IndexSearcher工具。
Analyzer:分析器,主要用於分析搜索引擎遇到的各種文本。
Directory:索引存放的位置;lucene提供了兩種索引存放的位置,一種是磁盤,一種是內存。一般情況將索引放在磁盤上;相應地lucene提供了FSDirectory和RAMDirectory兩個類。
Query:查詢,lucene中支持模糊查詢,語義查詢,短語查詢,組合查詢等等,如有TermQuery,BooleanQuery,RangeQuery,WildcardQuery等一些類。
QueryParser: 是一個解析用戶輸入的工具,可以通過掃描用戶輸入的字符串,生成Query對象。
Hits:在搜索完成之後,需要把搜索結果返回並顯示給用戶,只有這樣纔算是完成搜索的目的。在lucene中,搜索的結果的集合是用Hits類的實例來表示的。

如下的例子,是對指定目錄下所有java文件建立索引,然後搜索帶“String”字符串的所有java文件。
public class FileSearch {
private static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static void main(String[] args) throws Exception {
File indexDir = new File("e:\\lucene");
File dataDir = new File("e:\\luceneData");

long start = System.currentTimeMillis();
int numIndexed = index(indexDir, dataDir);
long end = System.currentTimeMillis();
System.out.println("Indexing " + numIndexed + " files took "
+ (end - start) + " milliseconds");

search(indexDir,"String");
}

/**
* 對dataDir目錄下所有文件生成索引文件並存放到indexDir目錄下
*
* @param indexDir
* @param dataDir
* @return
* @throws IOException
*/
public static int index(File indexDir, File dataDir) throws IOException {

if (!dataDir.exists() || !dataDir.isDirectory()) {
throw new IOException(dataDir
+ " does not exist or is not a directory");
}
/*
* StandardAnalyzer表示用lucene自帶的標準分詞機制 false表示不覆蓋原來該目錄的索引,true表示覆蓋
*/
IndexWriter writer = new IndexWriter(indexDir, new StandardAnalyzer(),
true);
writer.setUseCompoundFile(false);
indexDirectory(writer, dataDir);
int numIndexed = writer.docCount();
//Optimize的過程就是要減少剩下的Segment的數量,儘量讓它們處於一個文件中
writer.optimize();
writer.close();
return numIndexed;
}

public static void search(File indexDir, String q) throws Exception {
Directory fsDir = FSDirectory.getDirectory(indexDir, false);
IndexSearcher is = new IndexSearcher(fsDir);//打開索引
QueryParser qp = new QueryParser("contents", new StandardAnalyzer());
Query query = qp.parse(q);
long start = System.currentTimeMillis();
Hits hits = is.search(query); //搜索索引
long end = System.currentTimeMillis();

System.err.println("Found " + hits.length() + " document(s) (in "
+ (end - start) + " milliseconds) that matched query ‘" + q
+ "’:");

for (int i = 0; i < hits.length(); i++) {
Document doc = hits.doc(i); //得到匹配的文檔
System.out.println(doc.get("filename"));
}
}

/**
* 遞歸建立索引
*
* @param writer
* @param dir
* @throws IOException
*/
private static void indexDirectory(IndexWriter writer, File dir)
throws IOException {
for (File f : dir.listFiles()) {
if (f.isDirectory()) {
indexDirectory(writer, f);
} else if (f.getName().endsWith(".java")) {
indexFile(writer, f);
}
}
}

/**
* 爲文件內容建立索引
*
* @param writer
* @param f
* @throws IOException
*/
private static void indexFile(IndexWriter writer, File f)
throws IOException {
if (f.isHidden() || !f.exists() || !f.canRead()) {
return;
}
/* 創建一份文件 */
Document doc = new Document();
/*
* 創建一個域filename,文件路徑作爲域裏面的內容,並且添加到Document
* Field.Store.YES表示域裏面的內容將被存儲到索引
* Field.Index.TOKENIZED表示域裏面的內容將被索引,以便用來搜索
*/
doc.add(new Field("filename", f.getCanonicalPath(), Field.Store.YES,
Field.Index.TOKENIZED));
doc.add(new Field("filedate", format.format(f.lastModified()), Field.Store.YES,
Field.Index.TOKENIZED));
/*
* 創建一個域contents,文件的實體數據作爲域裏面的內容,並且添加到Document
*/
doc.add(new Field("contents", new BufferedReader(new FileReader(f)))); // 索引文件內容
/* 添加到索引 */
writer.addDocument(doc);
}
}


StandardAnalyzer是Lucene自帶的分詞,它的主要功能有
1.對原有句子以空格進行了分詞。
2.所有的大寫字母都轉換爲小寫、。
3.可以去掉一些沒有用處的單詞,例如"is","am","are"等單詞,也刪除了所有的標點。
它對中文的支持並不好,需要中文搜索的,有其他的分詞包支持。

在這個例子裏面只是搜索了文件內容,也就是單一Field搜索。如果我既要搜索單詞還要指定日期,那就需要用到多個Field搜索了。如果索引文件存放在多個目錄下,就需要用到多個目錄的搜索,以下是這兩者結合
public static void searchMore(File indexDir, String q,String date) throws Exception {
Directory fsDir = FSDirectory.getDirectory(indexDir, false);
IndexSearcher is = new IndexSearcher(fsDir);//打開索引
/* 多目錄,這隻有一個目錄 */
IndexSearcher indexSearchers[] = { is };
//搜索 filename,filedate兩個字段
String[] fields = { "filename", "filedate" };
String[] queries = {q,date};
//兩者都必須滿足
BooleanClause.Occur[] clauses = { BooleanClause.Occur.MUST, BooleanClause.Occur.MUST };
Query query = MultiFieldQueryParser.parse(queries, fields, clauses, new StandardAnalyzer());
/* 多目錄搜索,這裏只有一個目錄 */
MultiSearcher searcher = new MultiSearcher(indexSearchers);

long start = System.currentTimeMillis();
Hits hits = searcher.search(query); //搜索索引
long end = System.currentTimeMillis();

System.err.println("Found " + hits.length() + " document(s) (in "
+ (end - start) + " milliseconds) that matched query ‘" + q
+ "’:");

for (int i = 0; i < hits.length(); i++) {
Document doc = hits.doc(i); //得到匹配的文檔
System.out.println(doc.get("filename"));
}
}

這樣就可以搜索出所有 內容和日期都包含指定字符的所有文件。
如果需要模糊查詢某個字段,可以這樣
WildcardQuery query2 = new WildcardQuery(new Term("contents", "*er*"));


現在只能搜索txt或者類txt的文件,比如.java,.cpp,.properties等。比較常見的文件如word,excel,pdf,html之類的,又如何搜索呢。因爲lucene索引的時候是將String型的信息建立索引的,所以必須是將word/pdf/html/pdf等文件的內容轉化爲String.
定義了一個轉化接口
public interface FileConvert {
public String read(String path) throws Exception;
}


先是一般情況下類txt轉換。
public class TxtFileReader implements FileConvert {

public String read(String path) throws Exception {
StringBuffer content = new StringBuffer("");// 文檔內容
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(path));
String s1 = null;
while ((s1 = br.readLine()) != null) {
content.append(s1 + "\r");
}
} finally {
if(br != null) {
br.close();
}
}
return content.toString().trim();
}
}

對於HTML文件,我們只需要它實際的內容,那些tr,td之類的標籤是沒有意義的。lucene的demo中有個現成的HTMLParser可以去掉這些無用信息。
public class HTMLFileReader implements FileConvert {

/* (non-Javadoc)
* @see lucene.reader.FileConvert#read(java.lang.String)
*/
@Override
public String read(String path) throws Exception {
StringBuffer content = new StringBuffer("");
BufferedReader reader = null;
try {
FileInputStream fis = new FileInputStream(path);
//這裏的字符編碼要對上html頭文件,否則會出亂碼
HTMLParser htmlParser = new HTMLParser(new InputStreamReader(fis,"utf-8"));
reader = new BufferedReader(htmlParser.getReader());
String line = null;
while ((line = reader.readLine()) != null) {
content.append(line + "\n");
}
} finally {
if(reader != null) {
reader.close();
}
}
String contentString = content.toString();
return contentString;
}

}

PDF文件則可以用PDFBox來做。
public class PDFFileReader implements FileConvert{

@Override
public String read(String path) throws Exception {
StringBuffer content = new StringBuffer("");// 文檔內容
FileInputStream fis = null;
try{
fis = new FileInputStream(path);
PDFParser p = new PDFParser(fis);
p.parse();
PDFTextStripper ts = new PDFTextStripper();
content.append(ts.getText(p.getPDDocument()));
}finally {
if(fis != null) {
fis.close();
}
}
return content.toString().trim();
}
}

word文件就可以用POI來解決了
public class DocFileReader implements FileConvert {

/* (non-Javadoc)
* @see lucene.reader.FileConvert#read(java.lang.String)
*/
@Override
public String read(String path) throws Exception {
StringBuffer content = new StringBuffer("");// 文檔內容
HWPFDocument doc = new HWPFDocument(new FileInputStream(path));
Range range = doc.getRange();
int paragraphCount = range.numParagraphs();
for (int i = 0; i < paragraphCount; i++) {// 遍歷段落讀取數據
Paragraph pp = range.getParagraph(i);
content.append(pp.text());
}
return content.toString().trim();
}
}

簡單的封裝以下:
public class StrategyReader implements FileConvert{
private Map<String,FileConvert> convertMap = new HashMap<String,FileConvert>();

public StrategyReader() {
}

//外部調用初始化
public void init() {
FileConvert docFileReader = new DocFileReader();
FileConvert htmlFileReader = new HTMLFileReader();
FileConvert pdfFileReader = new PDFFileReader();
FileConvert txtFileReader = new TxtFileReader();
convertMap.put("txt", txtFileReader);
convertMap.put("java", txtFileReader);
convertMap.put("pdf", pdfFileReader);
convertMap.put("html", htmlFileReader);
convertMap.put("doc", docFileReader);
}
public void setConvertMap(Map<String, FileConvert> convertMap) {
//可以由配置文件配置,IOC容器refrence
this.convertMap = convertMap;
}

@Override
public String read(String path) throws Exception {
int suffixIndex = path.lastIndexOf(".");
if(suffixIndex < 0) {
return convertMap.get("txt").read(path);
} else {
String suffix = path.substring(suffixIndex);
FileConvert convert = convertMap.get(suffix);
if(convert != null) {
return convert.read(path);
} else {
throw new Exception("can not convert " + path);
}
}
}

}

這樣,上面的示例程序,引入StrategyReader,只需要在indexFile和indexDirectory方法裏面做一點小小的修改,就可以轉換這些指定的文件爲索引了。

一些其他的操作
// 刪除索引
public void deleteIndex(String indexDir){
try {
long start = System.currentTimeMillis();
IndexReader reader = IndexReader.open(indexDir);
int numFiles = reader.numDocs();
for (int i = 0; i < numFiles; i++) {
// 這裏的刪除只是做一個刪除標記,可以看到執行deleteDocument後會產生一個del後綴的文件用來記錄這些標記過的文件
reader.deleteDocument(i);
}
reader.close();
long end = System.currentTimeMillis();
System.out.println("delete index: " + (end - start) + " total milliseconds");
} catch (IOException e) {
System.out.println(" caught a " + e.getClass() + "\n with message: " + e.getMessage());
}
}

// 恢復已刪除的索引
public void unDeleteIndex(String indexDir){
try {
IndexReader reader = IndexReader.open(indexDir);
reader.undeleteAll();
reader.close();
} catch (IOException e) {
System.out.println(" caught a " + e.getClass() + "\n with message: " + e.getMessage());
}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章