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——文本搜索引擎,其可以通过加载分析器及多字段索引来实现源代码搜索。文章只介绍了代码搜索引擎的基本功能,同时在源码检索中使用愈加完善的分析器可以提高检索性能并获得更好的查询结果。这种搜索引擎可以允许用户在软件开发社区搜索和共享源代码。