某些网站允许软件开发社团通过发布开发者指南、白皮书、FAQs【常见问题解答】和源代码以实现信息的共享。随着信息量的增长,和几个开发者贡献出自己的知识库,于是网站提供搜索引擎来搜索站点上现有的所有信息。虽然这些搜索引擎对文本文件的搜索可以做的很好,但对开发者搜索源代码做了比较严格的限制。搜索引擎认为源代码就是纯文本文件,因此,在这一点上,与成熟的可以处理大量源文件的工具――grep相比没有什么不同。
在这篇文章中,我推荐使用Lucene,它是基于Java的开源搜索引擎,通过提取和索引相关的源码元素来搜索源代码。这里,我仅限定搜索Java源代码。然而,Lucene同样可以做到对其他编程语言的源代码的搜索。
文章给出了在Lucene环境下搜索引擎重点方面的简短概述。
概述
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实例中。