???某些網站允許軟件開發社團通過發布開發者指南、白皮書、FAQs【常見問題解答】和源代碼以實現信息的共享。隨著信息量的增長,和幾個開發者貢獻出自己的知識庫,于是網站提供搜索引擎來搜索站點上現有的所有信息。雖然這些搜索引擎對文本文件的搜索可以做的很好,但對開發者搜索源代碼做了比較嚴格的限制。搜索引擎認為源代碼就是純文本文件,因此,在這一點上,與成熟的可以處理大量源文件的工具――grep相比沒有什么不同。
在這篇文章中,我推薦使用Lucene,它是基于Java的開源搜索引擎,通過提取和索引相關的源碼元素來搜索源代碼。這里,我僅限定搜索Java源代碼。然而,Lucene同樣可以做到對其他編程語言的源代碼的搜索。
文章給出了在Lucene環境下搜索引擎重點方面的簡短概述。要了解更多細節信息,參考Resources部分。
版權聲明:任何獲得Matrix授權的網站,轉載時請務必保留以下作者信息和鏈接作者:Renuka;
Knightchen(作者的blog:
http://blog.matrix.org.cn/page/Knightchen)
原文:
http://www.matrix.org.cn/resource/article/44/44362_Lucene+Java.html關鍵字:Lucene;Java
概述Lucene是最流行的開源搜索引擎庫之一。它由能文本索引和搜索的核心API組成。Lucene能夠對給出一組文本文件創建索引并且允許你用復雜的查詢來搜索這些索引,例如:+title:Lucene -content:Search、search AND Lucene、+search +code。在進入搜索細節之前,先讓我來介紹一下Lucene的一些功能。
在Lucene中索引文本搜索引擎對所有需要被搜索的數據進行掃描并將其存儲到能有效獲取的一個結構里。這個最有名的結構被稱為倒排索引。例如,現在考慮對一組會議記錄進行索引。首先,每個會議記錄的文件被分為幾個獨立的部分或者域:如標題、作者、email、摘要和內容。其次,每一域的內容被標記化并且提取出關鍵字或者術語。這樣就可以建立如下表所示會議記錄的倒排索引。

????????....???????? ????????
對于域中的每一術語而言,上圖存儲了兩方面的內容:該術語在文件中出現的數量(即頻率【DF】)以及包含該術語的每一文件的ID。對于每個術語保存的其它細節:例如術語在每個文件中出現的次數以及出現的位置也被保存起來。無論如何,對于我們非常重要的一點是要知道:利用Lucene檢索文件意味著將其保存為一種特定格式,該格式允許高效率查詢及獲取。
分析被索引的文本Lucene使用分析器來處理被索引的文本。在將其存入索引之前,分析器用于將文本標記化、摘錄有關的單詞、丟棄共有的單詞、處理派生詞(把派生詞還原到詞根形式,意思是把bowling、bowler和bowls還原為bowl)和完成其它要做的處理。Lucene提供的通用分析器是:
????????SimpleAnalyzer:用字符串標記一組單詞并且轉化為小寫字母。
????????StandardAnalyzer:用字符串標記一組單詞,可識別縮寫詞、email地址、主機名稱等等。并丟棄基于英語的stop words (a, an, the, to)等、處理派生詞。
檢索(搜索索引)索引結構建立后,可以通過指定被搜索的字段和術語構造復雜的查詢來對索引進行檢索。例如,用戶查詢abstract:system AND email:abc@mit.edu得到的結果是所有在摘要中包含system、在email地址中有abc@mit.edu的文件。也就是說,如果在前面倒排索引表的基礎上搜索就返回Doc15。與查詢匹配的文件是按照術語在文件中出現的次數以及包含該術語的文檔的數量進行排列的。Lucene執行一種順序排列機制并且提供了給我們更改它的彈性。
源代碼搜索引擎現在我們知道了關于搜索引擎的基本要點,下面讓我們看一看用于搜索源代碼的搜索引擎應如何實現。下文中展示在搜索Java示例代碼時,開發者主要關注以下Java類:
繼承一個具體類或實現一個接口。
調用特定的方法。
使用特定的Java類。
綜合使用上述部分的組合可以滿足開發者獲取他們正在尋找相關代碼的需要。因此搜索引擎應該允許開發者對這些方面進行單個或組合查詢。IDEs【集成開發環境】有另一個局限性:大部分可使用的工具僅僅基于上述標準之一來支持搜索源代碼。在搜索中,缺乏組合這些標準進行查詢的靈活性。
現在我們開始建立一個支持這些要求的源代碼搜索引擎。
編寫源代碼分析器第一步先寫一個分析器,用來提取或去除源代碼元素,確保建立最佳的索引并且僅包含相關方面的代碼。在Java語言中的關鍵字--public,null,for,if等等,在每個.java文件中它們都出現了,這些關鍵字類似于英語中的普通單詞(the,a,an,of)。因而,分析器必須把這些關鍵字從索引中去掉。
我們通過繼承Lucene的抽象類Analyzer來建立一個Java源代碼分析器。下面列出了JavaSourceCodeAnalyzer類的源代碼,它實現了tokenStream(String,Reader)方法。這個類定義了一組【stop words】,它們能夠在索引過程中,使用Lucene提供的StopFilter類來被去除。tokenStream方法用于檢查被索引的字段。如果該字段是“comment”,首先要利用LowerCaseTokenizer類將輸入項標記化并轉換成小寫字母,然后利用StopFilter類除去英語中的【stop words】(有限的一組英語【stop words】),再利用PorterStemFilter移除通用的語形學以及詞尾后綴。如果被索引的內容不是“comment”,那么分析器就利用LowerCaseTokenizer類將輸入項標記化并轉換成小寫字母,并且利用StopFilter類除去Java關鍵字。
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);
???? }
}
編寫類JavaSourceCodeIndexer第二步生成索引。用來建立索引的非常重要的類有IndexWriter、Analyzer、Document和Field。對每一個源代碼文件建立Lucene的一個Document實例。解析源代碼文件并且摘錄出與代碼相關的語法元素,主要包括:導入聲明、類名稱、所繼承的類、實現的接口、實現的方法、方法使用的參數和每個方法的代碼等。然后把這些句法元素添加到Document實例中每個獨立的Field實例中。然后使用存儲索引的IndexWriter實例將Document實例添加到索引中。
下面列出了JavaSourceCodeIndexer類的源代碼。該類使用了JavaParser類解析Java文件和摘錄語法元素,也可以使用Eclipse3.0 ASTParser。這里就不探究JavaParser類的細節了,因為其它解析器也可以用于提取相關源碼元素。在源代碼文件提取元素的過程中,創建Filed實例并添加到Document實例中。
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有四種不同的字段類型:Keyword,UnIndexed,UnStored和Text,用于指定建立最佳索引。
????????Keyword字段是指不需要分析器解析但需要被編入索引并保存到索引中的部分。JavaSourceCodeIndexer類使用該字段來保存導入類的聲明。
????????UnIndexed字段是既不被分析也不被索引,但是要被逐字逐句的將其值保存到索引中。由于我們一般要存儲文件的位置但又很少用文件名作為關鍵字來搜索,所以用該字段來索引Java文件名。
????????UnStored字段和UnIndexed字段相反。該類型的Field要被分析并編入索引,但其值不會被保存到索引中。由于存儲方法的全部源代碼需要大量的空間。所以用UnStored字段來存儲被索引的方法源代碼。可以直接從Java源文件中取出方法的源代碼,這樣作可以控制我們的索引的大小。
????????Text字段在索引過程中是要被分析、索引并保存的。類名是作為Text字段來保存。下表展示了JavaSourceCodeIndexer類使用Field字段的一般情況。

1.
?? 用Lucene建立的索引可以用Luke預覽和修改,Luke是用于理解索引很有用的一個開源工具。圖1中是Luke工具的一張截圖,它顯示了JavaSourceCodeIndexer類建立的索引。

圖1:在Luke中索引截圖
如圖所見,導入類的聲明沒有標記化或分析就被保存了。類名和方法名被轉換為小寫字母后,才保存的。
查詢Java源代碼建立多字段索引后,可以使用Lucene來查詢這些索引。它提供了這兩個重要類分別是IndexSearcher和QueryParser,用于搜索文件。QueryParser類則用于解析由用戶輸入的查詢表達式,同時IndexSearcher類在文件中搜索滿足查詢條件的結果。下列表格顯示了一些可能發生的查詢及它的含義:

用戶通過索引不同的語法元素組成有效的查詢條件并搜索代碼。下面列出了用于搜索的示例代碼。
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實例用FSDirectory來打開包含索引的目錄。然后使用Analyzer實例分析搜索用的查詢字符串,以確保它與索引(還原詞根,轉換小寫字母,過濾掉,等等)具有同樣的形式。為了避免在查詢時將Field作為一個關鍵字索引,Lucene做了一些限制。Lucene用Analyzer分析在QueryParser實例里傳給它的所有字段。為了解決這個問題,可以用Lucene提供的PerFieldAnalyzerWrapper類為查詢中的每個字段指定必要的分析。因此,查詢字符串import:org.w3c.* AND code:Document將用KeywordAnalyzer來解析字符串org.w3c.*并且用JavaSourceCodeAnalyzer來解析Document。QueryParser實例如果查詢沒有與之相符的字段,就使用默認的字段:code,使用PerFieldAnalyzerWrapper來分析查詢字符串,并返回分析后的Query實例。IndexSearcher實例使用Query實例并返回一個Hits實例,它包含了滿足查詢條件的文件。
結束語這篇文章介紹了Lucene——文本搜索引擎,其可以通過加載分析器及多字段索引來實現源代碼搜索。文章只介紹了代碼搜索引擎的基本功能,同時在源碼檢索中使用愈加完善的分析器可以提高檢索性能并獲得更好的查詢結果。這種搜索引擎可以允許用戶在軟件開發社區搜索和共享源代碼。
資源這篇文章的示例Sample codeMatrix:
http://www.matrix.org.cnOnjava:
http://www.onjava.com/Lucene home page
"Introduction to Text Indexing with Apache Jakarta Lucene:" 這是篇簡要介紹使用Lucene的文章。
Lucene in Action: 在使用Lucene方面進行了深入地講解。