2007年3月5日星期一

利用Lucene搜索Java源代码 (ZZ)

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 code
Matrix:http://www.matrix.org.cn/
Onjava:http://www.onjava.com/
Lucene home page
"Introduction to Text Indexing with Apache Jakarta Lucene:" 这是篇简要介绍使用Lucene的文章。
Lucene in Action: 在使用Lucene方面进行了深入地讲解。

没有评论: