某些網(wǎng)站允許軟件開(kāi)發(fā)社團通過(guò)發(fā)布開(kāi)發(fā)者指南、白皮書(shū)、FAQs【常見(jiàn)問(wèn)題解答】和源代碼以實(shí)現信息的共享。隨著(zhù)信息量的增長(cháng),和幾個(gè)開(kāi)發(fā)者貢獻出自己的知識庫,于是網(wǎng)站提供搜索引擎來(lái)搜索站點(diǎn)上現有的所有信息。雖然這些搜索引擎對文本文件的搜索可以做的很好,但對開(kāi)發(fā)者搜索源代碼做了比較嚴格的限制。搜索引擎認為源代碼就是純文本文件,因此,在這一點(diǎn)上,與成熟的可以處理大量源文件的工具――grep相比沒(méi)有什么不同。
在這篇文章中,我推薦使用Lucene,它是基于Java的開(kāi)源搜索引擎,通過(guò)提取和索引相關(guān)的源碼元素來(lái)搜索源代碼。這里,我僅限定搜索Java源代碼。然而,Lucene同樣可以做到對其他編程語(yǔ)言的源代碼的搜索。
文章給出了在Lucene環(huán)境下搜索引擎重點(diǎn)方面的簡(jiǎn)短概述。要了解更多細節信息,參考Resources部分。
版權聲明:任何獲得Matrix授權的網(wǎng)站,轉載時(shí)請務(wù)必保留以下作者信息和鏈接作者:Renuka;
Knightchen(作者的blog:
http://blog.matrix.org.cn/page/Knightchen)
原文:
http://www.matrix.org.cn/resource/article/44/44362_Lucene+Java.html關(guān)鍵字:Lucene;Java
概述Lucene是最流行的開(kāi)源搜索引擎庫之一。它由能文本索引和搜索的核心API組成。Lucene能夠對給出一組文本文件創(chuàng )建索引并且允許你用復雜的查詢(xún)來(lái)搜索這些索引,例如:+title:Lucene -content:Search、search AND Lucene、+search +code。在進(jìn)入搜索細節之前,先讓我來(lái)介紹一下Lucene的一些功能。
在Lucene中索引文本搜索引擎對所有需要被搜索的數據進(jìn)行掃描并將其存儲到能有效獲取的一個(gè)結構里。這個(gè)最有名的結構被稱(chēng)為倒排索引。例如,現在考慮對一組會(huì )議記錄進(jìn)行索引。首先,每個(gè)會(huì )議記錄的文件被分為幾個(gè)獨立的部分或者域:如標題、作者、email、摘要和內容。其次,每一域的內容被標記化并且提取出關(guān)鍵字或者術(shù)語(yǔ)。這樣就可以建立如下表所示會(huì )議記錄的倒排索引。
....
對于域中的每一術(shù)語(yǔ)而言,上圖存儲了兩方面的內容:該術(shù)語(yǔ)在文件中出現的數量(即頻率【DF】)以及包含該術(shù)語(yǔ)的每一文件的ID。對于每個(gè)術(shù)語(yǔ)保存的其它細節:例如術(shù)語(yǔ)在每個(gè)文件中出現的次數以及出現的位置也被保存起來(lái)。無(wú)論如何,對于我們非常重要的一點(diǎn)是要知道:利用Lucene檢索文件意味著(zhù)將其保存為一種特定格式,該格式允許高效率查詢(xún)及獲取。
分析被索引的文本Lucene使用分析器來(lái)處理被索引的文本。在將其存入索引之前,分析器用于將文本標記化、摘錄有關(guān)的單詞、丟棄共有的單詞、處理派生詞(把派生詞還原到詞根形式,意思是把bowling、bowler和bowls還原為bowl)和完成其它要做的處理。Lucene提供的通用分析器是:
SimpleAnalyzer:用字符串標記一組單詞并且轉化為小寫(xiě)字母。
StandardAnalyzer:用字符串標記一組單詞,可識別縮寫(xiě)詞、email地址、主機名稱(chēng)等等。并丟棄基于英語(yǔ)的stop words (a, an, the, to)等、處理派生詞。
檢索(搜索索引)索引結構建立后,可以通過(guò)指定被搜索的字段和術(shù)語(yǔ)構造復雜的查詢(xún)來(lái)對索引進(jìn)行檢索。例如,用戶(hù)查詢(xún)abstract:system AND email:abc@mit.edu得到的結果是所有在摘要中包含system、在email地址中有abc@mit.edu的文件。也就是說(shuō),如果在前面倒排索引表的基礎上搜索就返回Doc15。與查詢(xún)匹配的文件是按照術(shù)語(yǔ)在文件中出現的次數以及包含該術(shù)語(yǔ)的文檔的數量進(jìn)行排列的。Lucene執行一種順序排列機制并且提供了給我們更改它的彈性。
源代碼搜索引擎現在我們知道了關(guān)于搜索引擎的基本要點(diǎn),下面讓我們看一看用于搜索源代碼的搜索引擎應如何實(shí)現。下文中展示在搜索Java示例代碼時(shí),開(kāi)發(fā)者主要關(guān)注以下Java類(lèi):
繼承一個(gè)具體類(lèi)或實(shí)現一個(gè)接口。
調用特定的方法。
使用特定的Java類(lèi)。
綜合使用上述部分的組合可以滿(mǎn)足開(kāi)發(fā)者獲取他們正在尋找相關(guān)代碼的需要。因此搜索引擎應該允許開(kāi)發(fā)者對這些方面進(jìn)行單個(gè)或組合查詢(xún)。IDEs【集成開(kāi)發(fā)環(huán)境】有另一個(gè)局限性:大部分可使用的工具僅僅基于上述標準之一來(lái)支持搜索源代碼。在搜索中,缺乏組合這些標準進(jìn)行查詢(xún)的靈活性。
現在我們開(kāi)始建立一個(gè)支持這些要求的源代碼搜索引擎。
編寫(xiě)源代碼分析器第一步先寫(xiě)一個(gè)分析器,用來(lái)提取或去除源代碼元素,確保建立最佳的索引并且僅包含相關(guān)方面的代碼。在Java語(yǔ)言中的關(guān)鍵字--public,null,for,if等等,在每個(gè).java文件中它們都出現了,這些關(guān)鍵字類(lèi)似于英語(yǔ)中的普通單詞(the,a,an,of)。因而,分析器必須把這些關(guān)鍵字從索引中去掉。
我們通過(guò)繼承Lucene的抽象類(lèi)Analyzer來(lái)建立一個(gè)Java源代碼分析器。下面列出了JavaSourceCodeAnalyzer類(lèi)的源代碼,它實(shí)現了tokenStream(String,Reader)方法。這個(gè)類(lèi)定義了一組【stop words】,它們能夠在索引過(guò)程中,使用Lucene提供的StopFilter類(lèi)來(lái)被去除。tokenStream方法用于檢查被索引的字段。如果該字段是“comment”,首先要利用LowerCaseTokenizer類(lèi)將輸入項標記化并轉換成小寫(xiě)字母,然后利用StopFilter類(lèi)除去英語(yǔ)中的【stop words】(有限的一組英語(yǔ)【stop words】),再利用PorterStemFilter移除通用的語(yǔ)形學(xué)以及詞尾后綴。如果被索引的內容不是“comment”,那么分析器就利用LowerCaseTokenizer類(lèi)將輸入項標記化并轉換成小寫(xiě)字母,并且利用StopFilter類(lèi)除去Java關(guān)鍵字。
package com.infosys.lucene.code JavaSourceCodeAnalyzer.;
import java.io.Reader;
import java.util.Set;
import org.apache.lucene.analysis.*;
public class JavaSourceCodeAnalyzer extends Analyzer {
private Set javaStopSet;
private Set englishStopSet;
private static final String[] JAVA_STOP_WORDS = {
"public","private","protected","interface",
"abstract","implements","extends","null""new",
"switch","case", "default" ,"synchronized" ,
"do", "if", "else", "break","continue","this",
"assert" ,"for","instanceof", "transient",
"final", "static" ,"void","catch","try",
"throws","throw","class", "finally","return",
"const" , "native", "super","while", "import",
"package" ,"true", "false" };
private static final String[] ENGLISH_STOP_WORDS ={
"a", "an", "and", "are","as","at","be" "but",
"by", "for", "if", "in", "into", "is", "it",
"no", "not", "of", "on", "or", "s", "such",
"that", "the", "their", "then", "there","these",
"they", "this", "to", "was", "will", "with" };
public SourceCodeAnalyzer(){
super();
javaStopSet = StopFilter.makeStopSet(JAVA_STOP_WORDS);
englishStopSet = StopFilter.makeStopSet(ENGLISH_STOP_WORDS);
}
public TokenStream tokenStream(String fieldName, Reader reader) {
if (fieldName.equals("comment"))
return new PorterStemFilter(new StopFilter(
new LowerCaseTokenizer(reader),englishStopSet));
else
return new StopFilter(
new LowerCaseTokenizer(reader),javaStopSet);
}
}
編寫(xiě)類(lèi)JavaSourceCodeIndexer第二步生成索引。用來(lái)建立索引的非常重要的類(lèi)有IndexWriter、Analyzer、Document和Field。對每一個(gè)源代碼文件建立Lucene的一個(gè)Document實(shí)例。解析源代碼文件并且摘錄出與代碼相關(guān)的語(yǔ)法元素,主要包括:導入聲明、類(lèi)名稱(chēng)、所繼承的類(lèi)、實(shí)現的接口、實(shí)現的方法、方法使用的參數和每個(gè)方法的代碼等。然后把這些句法元素添加到Document實(shí)例中每個(gè)獨立的Field實(shí)例中。然后使用存儲索引的IndexWriter實(shí)例將Document實(shí)例添加到索引中。
下面列出了JavaSourceCodeIndexer類(lèi)的源代碼。該類(lèi)使用了JavaParser類(lèi)解析Java文件和摘錄語(yǔ)法元素,也可以使用Eclipse3.0 ASTParser。這里就不探究JavaParser類(lèi)的細節了,因為其它解析器也可以用于提取相關(guān)源碼元素。在源代碼文件提取元素的過(guò)程中,創(chuàng )建Filed實(shí)例并添加到Document實(shí)例中。
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import com.infosys.lucene.code.JavaParser.*;
public class JavaSourceCodeIndexer {
private static JavaParser parser = new JavaParser();
private static final String IMPLEMENTS = "implements";
private static final String IMPORT = "import";
...
public static void main(String[] args) {
File indexDir = new File("C:\\Lucene\\Java");
File dataDir = new File("C:\\JavaSourceCode ");
IndexWriter writer = new IndexWriter(indexDir,
new JavaSourceCodeAnalyzer(), true);
indexDirectory(writer, dataDir);
writer.close();
}
public static void indexDirectory(IndexWriter writer, File dir){
File[] files = dir.listFiles();
for (int i = 0; i < files.length; i++) {
File f = files[i];
// Create a Lucene Document
Document doc = new Document();
// Use JavaParser to parse file
parser.setSource(f);
addImportDeclarations(doc, parser);
addComments(doc, parser);
// Extract Class elements Using Parser
JClass cls = parser.getDeclaredClass();
addClass(doc, cls);
// Add field to the Lucene Document
doc.add(Field.UnIndexed(FILENAME, f.getName()));
writer.addDocument(doc);
}
}
private static void addClass(Document doc, JClass cls) {
//For each class add Class Name field
doc.add(Field.Text(CLASS, cls.className));
String superCls = cls.superClass;
if (superCls != null)
//Add the class it extends as extends field
doc.add(Field.Text(EXTENDS, superCls));
// Add interfaces it implements
ArrayList interfaces = cls.interfaces;
for (int i = 0; i < interfaces.size(); i++)
doc.add(Field.Text(IMPLEMENTS, (String) interfaces.get(i)));
//Add details on methods declared
addMethods(cls, doc);
ArrayList innerCls = cls.innerClasses;
for (int i = 0; i < innerCls.size(); i++)
addClass(doc, (JClass) innerCls.get(i));
}
private static void addMethods(JClass cls, Document doc) {
ArrayList methods = cls.methodDeclarations;
for (int i = 0; i < methods.size(); i++) {
JMethod method = (JMethod) methods.get(i);
// Add method name field
doc.add(Field.Text(METHOD, method.methodName));
// Add return type field
doc.add(Field.Text(RETURN, method.returnType));
ArrayList params = method.parameters;
for (int k = 0; k < params.size(); k++)
// For each method add parameter types
doc.add(Field.Text(PARAMETER, (String)params.get(k)));
String code = method.codeBlock;
if (code != null)
//add the method code block
doc.add(Field.UnStored(CODE, code));
}
}
private static void addImportDeclarations(Document doc, JavaParser parser) {
ArrayList imports = parser.getImportDeclarations();
if (imports == null) return;
for (int i = 0; i < imports.size(); i++)
//add import declarations as keyword
doc.add(Field.Keyword(IMPORT, (String) imports.get(i)));
}
}
Lucene有四種不同的字段類(lèi)型:Keyword,UnIndexed,UnStored和Text,用于指定建立最佳索引。
Keyword字段是指不需要分析器解析但需要被編入索引并保存到索引中的部分。JavaSourceCodeIndexer類(lèi)使用該字段來(lái)保存導入類(lèi)的聲明。
UnIndexed字段是既不被分析也不被索引,但是要被逐字逐句的將其值保存到索引中。由于我們一般要存儲文件的位置但又很少用文件名作為關(guān)鍵字來(lái)搜索,所以用該字段來(lái)索引Java文件名。
UnStored字段和UnIndexed字段相反。該類(lèi)型的Field要被分析并編入索引,但其值不會(huì )被保存到索引中。由于存儲方法的全部源代碼需要大量的空間。所以用UnStored字段來(lái)存儲被索引的方法源代碼??梢灾苯訌腏ava源文件中取出方法的源代碼,這樣作可以控制我們的索引的大小。
Text字段在索引過(guò)程中是要被分析、索引并保存的。類(lèi)名是作為T(mén)ext字段來(lái)保存。下表展示了JavaSourceCodeIndexer類(lèi)使用Field字段的一般情況。
1.
用Lucene建立的索引可以用Luke預覽和修改,Luke是用于理解索引很有用的一個(gè)開(kāi)源工具。圖1中是Luke工具的一張截圖,它顯示了JavaSourceCodeIndexer類(lèi)建立的索引。
圖1:在Luke中索引截圖
如圖所見(jiàn),導入類(lèi)的聲明沒(méi)有標記化或分析就被保存了。類(lèi)名和方法名被轉換為小寫(xiě)字母后,才保存的。
查詢(xún)Java源代碼建立多字段索引后,可以使用Lucene來(lái)查詢(xún)這些索引。它提供了這兩個(gè)重要類(lèi)分別是IndexSearcher和QueryParser,用于搜索文件。QueryParser類(lèi)則用于解析由用戶(hù)輸入的查詢(xún)表達式,同時(shí)IndexSearcher類(lèi)在文件中搜索滿(mǎn)足查詢(xún)條件的結果。下列表格顯示了一些可能發(fā)生的查詢(xún)及它的含義:
用戶(hù)通過(guò)索引不同的語(yǔ)法元素組成有效的查詢(xún)條件并搜索代碼。下面列出了用于搜索的示例代碼。
public class JavaCodeSearch {
public static void main(String[] args) throws Exception{
File indexDir = new File(args[0]);
String q = args[1]; //parameter:JGraph code:insert
Directory fsDir = FSDirectory.getDirectory(indexDir,false);
IndexSearcher is = new IndexSearcher(fsDir);
PerFieldAnalyzerWrapper analyzer = new
PerFieldAnalyzerWrapper( new
JavaSourceCodeAnalyzer());
analyzer.addAnalyzer("import", new KeywordAnalyzer());
Query query = QueryParser.parse(q, "code", analyzer);
long start = System.currentTimeMillis();
Hits hits = is.search(query);
long end = System.currentTimeMillis();
System.err.println("Found " + hits.length() +
" docs in " + (end-start) + " millisec");
for(int i = 0; i < hits.length(); i++){
Document doc = hits.doc(i);
System.out.println(doc.get("filename")
+ " with a score of " + hits.score(i));
}
is.close();
}
}IndexSearcher實(shí)例用FSDirectory來(lái)打開(kāi)包含索引的目錄。然后使用Analyzer實(shí)例分析搜索用的查詢(xún)字符串,以確保它與索引(還原詞根,轉換小寫(xiě)字母,過(guò)濾掉,等等)具有同樣的形式。為了避免在查詢(xún)時(shí)將Field作為一個(gè)關(guān)鍵字索引,Lucene做了一些限制。Lucene用Analyzer分析在QueryParser實(shí)例里傳給它的所有字段。為了解決這個(gè)問(wèn)題,可以用Lucene提供的PerFieldAnalyzerWrapper類(lèi)為查詢(xún)中的每個(gè)字段指定必要的分析。因此,查詢(xún)字符串import:org.w3c.* AND code:Document將用KeywordAnalyzer來(lái)解析字符串org.w3c.*并且用JavaSourceCodeAnalyzer來(lái)解析Document。QueryParser實(shí)例如果查詢(xún)沒(méi)有與之相符的字段,就使用默認的字段:code,使用PerFieldAnalyzerWrapper來(lái)分析查詢(xún)字符串,并返回分析后的Query實(shí)例。IndexSearcher實(shí)例使用Query實(shí)例并返回一個(gè)Hits實(shí)例,它包含了滿(mǎn)足查詢(xún)條件的文件。
結束語(yǔ)這篇文章介紹了Lucene——文本搜索引擎,其可以通過(guò)加載分析器及多字段索引來(lái)實(shí)現源代碼搜索。文章只介紹了代碼搜索引擎的基本功能,同時(shí)在源碼檢索中使用愈加完善的分析器可以提高檢索性能并獲得更好的查詢(xún)結果。這種搜索引擎可以允許用戶(hù)在軟件開(kāi)發(fā)社區搜索和共享源代碼。
資源這篇文章的示例Sample codeMatrix:
http://www.matrix.org.cnOnjava:
http://www.onjava.com/Lucene home page
"Introduction to Text Indexing with Apache Jakarta Lucene:" 這是篇簡(jiǎn)要介紹使用Lucene的文章。
Lucene in Action: 在使用Lucene方面進(jìn)行了深入地講解。
Renuka Sindhgatta 是一位資深的構架師,現在在印度班加羅爾市【 in the Software Engineering and Technology labs of Infosys Technologies Limited 】工作。