利用Lucene搜索Java源代码

| No Comments | No TrackBacks

某些网站允许软件开发社团通过发布开发者指南、白皮书、FAQs【常见问题解答】和源代码以实现信息的共享。随着信息量的增长,和几个开发者贡献出自己的知识库,于是网站提供搜索引擎来搜索站点上现有的所有信息。虽然这些搜索引擎对文本文件的搜索可以做的很好,但对开发者搜索源代码做了比较严格的限制。搜索引擎认为源代码就是纯文本文件,因此,在这一点上,与成熟的可以处理大量源文件的工具――grep相比没有什么不同。

在这篇文章中,我推荐使用Lucene,它是基于Java的开源搜索引擎,通过提取和索引相关的源码元素来搜索源代码。这里,我仅限定搜索Java源代码。然而,Lucene同样可以做到对其他编程语言的源代码的搜索。

文章给出了在Lucene环境下搜索引擎重点方面的简短概述。要了解更多细节信息,参考Resources部分。

概述
Lucene是最流行的开源搜索引擎库之一。它由能文本索引和搜索的核心API组成。Lucene能够对给出一组文本文件创建索引并且允许你用复杂的查询来搜索这些索引,例如:+title:Lucene -content:Search、search AND Lucene、+search +code。在进入搜索细节之前,先让我来介绍一下Lucene的一些功能。

在Lucene中索引文本

搜索引擎对所有需要被搜索的数据进行扫描并将其存储到能有效获取的一个结构里。这个最有名的结构被称为倒排索引。例如,现在考虑对一组会议记录进行索引。首先,每个会议记录的文件被分为几个独立的部分或者域:如标题、作者、email、摘要和内容。其次,每一域的内容被标记化并且提取出关键字或者术语。这样就可以建立如下表所示会议记录的倒排索引。
image
        ....                 

对于域中的每一术语而言,上图存储了两方面的内容:该术语在文件中出现的数量(即频率【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,用于指定建立最佳索引。
&#61548;        Keyword字段是指不需要分析器解析但需要被编入索引并保存到索引中的部分。JavaSourceCodeIndexer类使用该字段来保存导入类的声明。
&#61548;        UnIndexed字段是既不被分析也不被索引,但是要被逐字逐句的将其值保存到索引中。由于我们一般要存储文件的位置但又很少用文件名作为关键字来搜索,所以用该字段来索引Java文件名。
&#61548;        UnStored字段和UnIndexed字段相反。该类型的Field要被分析并编入索引,但其值不会被保存到索引中。由于存储方法的全部源代码需要大量的空间。所以用UnStored字段来存储被索引的方法源代码。可以直接从Java源文件中取出方法的源代码,这样作可以控制我们的索引的大小。
&#61548;        Text字段在索引过程中是要被分析、索引并保存的。类名是作为Text字段来保存。下表展示了JavaSourceCodeIndexer类使用Field字段的一般情况。

image

1.
   用Lucene建立的索引可以用Luke预览和修改,Luke是用于理解索引很有用的一个开源工具。图1中是Luke工具的一张截图,它显示了JavaSourceCodeIndexer类建立的索引。

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

No TrackBacks

TrackBack URL: http://www.wujianrong.com/mt-tb.cgi/671

Leave a comment

About this Entry

This page contains a single entry by kevinwu published on May 1, 2006 11:51 AM.

详细介绍在tomcat中配置数据源以及数据源的原理 was the previous entry in this blog.

详细介绍在tomcat中配置数据源以及数据源的原理 is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.