最近公司想搭一套代码搜索系统,所以调研了一下Oracle的开源代码搜索系统opengrok的索引建立机制。
opengrok建立索引主要架构图如下所示:
入口在Indexer类中,其中Indexer会对每个project建立一个索引类IndexDatabase,IndexDatabase通过遍历代码目录下的所有文件对每个文件生成索引。IndexDatabase中的indexDown是建立索引的核心方法,对每个文件进行三层解析,第一层解析出文件本身的一些属性(文件名,目录等),第二层用ctags解析出代码中的所有变量定义,第三层用源文件对应对解析器解析出源文件中的所有变量引用,最终把所有解析出来的属性存在一个lucene的Document对象中。
第一层解析关键代码:
doc.add(new Field(QueryBuilder.U, Util.path2uid(path, date),
string_ft_stored_nanalyzed_norms));
doc.add(new Field(QueryBuilder.FULLPATH, file.getAbsolutePath(),
string_ft_nstored_nanalyzed_norms));
doc.add(new SortedDocValuesField(QueryBuilder.FULLPATH, new BytesRef(file.getAbsolutePath())));
try {
HistoryReader hr = HistoryGuru.getInstance().getHistoryReader(file);
if (hr != null) {
doc.add(new TextField(QueryBuilder.HIST, hr));
// date = hr.getLastCommentDate() //RFE
}
} catch (HistoryException e) {
LOGGER.log(Level.WARNING, "An error occurred while reading history: ", e);
}
doc.add(new Field(QueryBuilder.DATE, date, string_ft_stored_nanalyzed_norms));
doc.add(new SortedDocValuesField(QueryBuilder.DATE, new BytesRef(date)));
if (path != null) {
doc.add(new TextField(QueryBuilder.PATH, path, Store.YES));
Project project = Project.getProject(path);
if (project != null) {
doc.add(new TextField(QueryBuilder.PROJECT, project.getPath(), Store.YES));
}
}
其中doc就是lucene的Document,可以看到第一层是解析出了文件的通用的一些信息。
第二和第三层的解析代码:
doc.add(new TextField(QueryBuilder.FULL, getReader(src.getStream())));
String fullpath = doc.get(QueryBuilder.FULLPATH);
if (fullpath != null && ctags != null) {
defs = ctags.doCtags(fullpath + "\n");
if (defs != null && defs.numberOfSymbols() > 0) {
doc.add(new TextField(QueryBuilder.DEFS, new IteratorReader(defs.getSymbols())));
//this is to explicitly use appropriate analyzers tokenstream to workaround #1376 symbols search works like full text search
TextField ref=new TextField(QueryBuilder.REFS,this.SymbolTokenizer);
this.SymbolTokenizer.setReader(getReader(src.getStream()));
doc.add(ref);
byte[] tags = defs.serialize();
doc.add(new StoredField(QueryBuilder.TAGS, tags));
}
}
if (scopesEnabled && xrefOut == null) {
/*
* Scopes are generated during xref generation. If xrefs are
* turned off we still need to run writeXref to produce scopes,
* we use a dummy writer that will throw away any xref output.
*/
xrefOut = new NullWriter();
}
if (xrefOut != null) {
try (Reader in = getReader(src.getStream())) {
writeXref(in, xrefOut);
}
Scopes scopes = xref.getScopes();
if (scopes.size() > 0) {
byte[] scopesSerialized = scopes.serialize();
doc.add(new StoredField(QueryBuilder.SCOPES, scopesSerialized));
}
}
可以看到其中区分了代码中的引用(ref)和定义(def)。
最终生成的doc中其实就包含了一个源文件中的若干属性字段,每个字段都可以暴露给客户端进行代码搜索的条件。
结语:代码搜索系统的核心就是根据代码文件的各种属性建立索引,提供给客户端查询,如果做深入的话需要结合语法进行索引,比如有哪些类定义哪些枚举定义有哪些变量定义都可以单独建立索引,opengrok中还有一个很重要的部分是处理文件历史版本以及变化更新索引的机制,这部分暂时还看的不太懂,有时间再继续研究。