利用Lucene搜尋Java原始碼

wangjun_pfc發表於2007-10-13

在這篇文章中,我推薦使用Lucene,它是基於Java的開源搜尋引擎,通過提取和索引相關的原始碼元素來搜尋原始碼。這裡,我僅限定搜尋Java原始碼。然而,Lucene同樣可以做到對其他程式語言的原始碼的搜尋。

文章工具

某些網站允許軟體開發社團通過釋出開發者指南、白皮書、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:Searchsearch AND Lucene+search +code。在進入搜尋細節之前,先讓我來介紹一下Lucene的一些功能。

Lucene中索引文字

搜尋引擎對所有需要被搜尋的資料進行掃描並將其儲存到能有效獲取的一個結構裡。這個最有名的結構被稱為倒排索引。例如,現在考慮對一組會議記錄進行索引。首先,每個會議記錄的檔案被分為幾個獨立的部分或者域:如標題、作者、email、摘要和內容。其次,每一域的內容被標記化並且提取出關鍵字或者術語。這樣就可以建立如下表所示會議記錄的倒排索引。

....

對於域中的每一術語而言,上圖儲存了兩方面的內容:該術語在檔案中出現的數量(即頻率【DF】)以及包含該術語的每一檔案的ID。對於每個術語儲存的其它細節:例如術語在每個檔案中出現的次數以及出現的位置也被儲存起來。無論如何,對於我們非常重要的一點是要知道:利用Lucene檢索檔案意味著將其儲存為一種特定格式,該格式允許高效率查詢及獲取。

分析被索引的文字

Lucene
使用分析器來處理被索引的文字。在將其存入索引之前,分析器用於將文字標記化、摘錄有關的單詞、丟棄共有的單詞、處理派生詞(把派生詞還原到詞根形式,意思是把bowlingbowlerbowls還原為bowl)和完成其它要做的處理。Lucene提供的通用分析器是:
SimpleAnalyzer
:用字串標記一組單詞並且轉化為小寫字母。
l
StandardAnalyzer
:用字串標記一組單詞,可識別縮寫詞、email地址、主機名稱等等。並丟棄基於英語的stopl words (a, an, the, to)等、處理派生詞。

檢索(搜尋索引)
索引結構建立後,可以通過指定被搜尋的欄位和術語構造複雜的查詢來對索引進行檢索。例如,使用者查詢abstractsystem 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類的原始碼,它實現了tokenStreamStringReader)方法。這個類定義了一組【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
第二步生成索引。用來建立索引的非常重要的類有IndexWriterAnalyzerDocumentField。對每一個原始碼檔案建立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;
// 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 detailson 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
有四種不同的欄位型別:KeywordUnIndexedUnStoredText,用於指定建立最佳索引。
Keyword
欄位是指不需要分析器解析但需要被編入索引並儲存到索引中的部分。JavaSourceCodeIndexer類使用該欄位來儲存匯入類的宣告。
l
UnIndexed
欄位是既不被分析也不被索引,但是要被逐字逐句的將其值儲存到索引中。由於我們一般要儲存檔案的位置但又很少用檔名作為關鍵字來搜尋,所以用該欄位來索引lJava檔名。
UnStored
欄位和UnIndexed欄位相反。該型別的Field要被分析並編入索引,但其值不會被儲存到索引中。由於儲存方法的全部
l原始碼需要大量的空間。所以用UnStored欄位來儲存被索引的方法原始碼。可以直接從Java原始檔中取出方法的原始碼,這樣作可以控制我們的索引的大小。
Text
欄位在索引過程中是要被分析、索引並儲存的。類名是作為Text欄位來儲存。下表展示了JavaSourceCodeIndexer類使用Field欄位的一般情況。
l



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


1:在Luke中索引截圖

如圖所見,匯入類的宣告沒有標記化或分析就被儲存了。類名和方法名被轉換為小寫字母后,才儲存的。

查詢Java原始碼
建立多欄位索引後,可以使用Lucene來查詢這些索引。它提供了這兩個重要類分別是IndexSearcherQueryParser,用於搜尋檔案。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做了一些限制。LuceneAnalyzer分析在QueryParser例項裡傳給它的所有欄位。為了解決這個問題,可以用Lucene提供的PerFieldAnalyzerWrapper類為查詢中的每個欄位指定必要的分析。因此,查詢字串importorg.w3c.* AND code:Document將用KeywordAnalyzer來解析字串org.w3c.*並且用JavaSourceCodeAnalyzer來解析DocumentQueryParser例項如果查詢沒有與之相符的欄位,就使用預設的欄位:code,使用PerFieldAnalyzerWrapper來分析查詢字串,並返回分析後的Query例項。IndexSearcher例項使用Query例項並返回一個Hits例項,它包含了滿足查詢條件的檔案。

結束語

這篇文章介紹了Lucene——文字搜尋引擎,其可以通過載入分析器及多欄位索引來實現原始碼搜尋。文章只介紹了程式碼搜尋引擎的基本功能,同時在原始碼檢索中使用愈加完善的分析器可以提高檢索效能並獲得更好的查詢結果。這種搜尋引擎可以允許使用者在軟體開發社群搜尋和共享原始碼

 

 

相關文章