1.
索引流程详解
1.1.
crawl中涉及nutch的部分
1.1.1.
nutch索引产生所需的文件路径以及产生的索引路径
Path linkDb = new Path(dir + "/linkdb");
Path segments = new Path(dir + "/segments");
Path indexes = new Path(dir + "/indexes");
这些都是产生索引文件必需的文件路径,在crawl中的main()方法中被配置。此外,还得配置索引产生的路径,如下:
Path index = new Path(dir + "/index");
1.1.2.
nutch中选择使用何种索引的方式
在nutch的crawl代码刚开始不久,就有以下两行代码:
String indexerName = "lucene";
String solrUrl = null;
boolean isSolrIndex = StringUtils.equalsIgnoreCase(indexerName, "solr");
接下来,在以后的代码中,会根据以上变量的值选择使用何种索引方式,是采用lucene还是solr。也就是说,你可以通过改变这两个变量的值,来选择何种索引方式。
1.1.3.
索引开始
从这段代码开始,索引就开始了。
FileStatus[] fstats = fs.listStatus(segments, HadoopFSUtil.getPassDirectoriesFilter(fs));
//这是FileSystem带有的一个方法,你可以查看Hadoop API来了解详细,该方法的目的旨在提取segments下的各个以时间命名的文件夹的路径。
if (isSolrIndex) { //结合前面讲解的内容,该判断将选择采用何种索引方式。此处将的是lucene索引,所以不再详述solr。
SolrIndexer indexer = new SolrIndexer(conf);
indexer.indexSolr(solrUrl, crawlDb, linkDb,
Arrays.asList(HadoopFSUtil.getPaths(fstats)));
}
else {
//从这里开始,就进行lucene索引了。
DeleteDuplicates dedup = new DeleteDuplicates(conf);
if(indexes != null) {
// Delete old indexes
if (fs.exists(indexes)) {
LOG.info("Deleting old indexes: " + indexes);
fs.delete(indexes, true);
}
// Delete old index
if (fs.exists(index)) {
LOG.info("Deleting old merged index: " + index);
fs.delete(index, true);
}
}
Indexer indexer = new Indexer(conf);
indexer.index(indexes, crawlDb, linkDb,
Arrays.asList(HadoopFSUtil.getPaths(fstats)));
/*从该方法中可以看出,创建索引所需的文件需要哪些了,分别是crawlDb、lindDb、segments中的内容,当然这是只是初步的认识,在后面讲解每个文件中的哪些子文件被用到了,又有什么作用,重点是segments中的内容。*/
//Arrays.asList方法是用来获得诸如E:\out\segments\20120221153925的path数组的。在Indexer
//中的index方法中可以得到体现:
// public void index(Path luceneDir, Path crawlDb,
// Path linkDb, List<Path> segments)
IndexMerger merger = new IndexMerger(conf);
/*上一步产生的indexes文件夹,下面是对indexes的合并,最终产生index索引文件,将重点讲解上面的索引过程,下面的索引合并过程有必要的话自己去了解吧。*/
if(indexes != null) {
dedup.dedup(new Path[] { indexes });
fstats = fs.listStatus(indexes, HadoopFSUtil.getPassDirectoriesFilter(fs));
merger.merge(HadoopFSUtil.getPaths(fstats), index, tmpDir);
}
}
} else {
LOG.warn("No URLs to fetch - check your seed list and URL filters.");
}
if (LOG.isInfoEnabled()) { LOG.info("crawl finished: " + dir); }
}
}
1.1.4.
分析Indexer的index方法
public void index(Path luceneDir, Path crawlDb,
Path linkDb, List<Path> segments)
/*这个方法运行了一个job,该job用来创建indexes。要看懂分布式程序首先得去了解mapreduce*/
throws IOException {
LOG.info("Indexer: starting");
final JobConf job = new NutchJob(getConf());
job.setJobName("index-lucene " + luceneDir);
//该方法中最重要的方法,将在1.1.5中详解
IndexerMapReduce.initMRJob(crawlDb, linkDb, segments, job);
FileOutputFormat.setOutputPath(job, luceneDir);
//下面就涉及到索引管理了。
LuceneWriter.addFieldOptions("segment", LuceneWriter.STORE.YES, LuceneWriter.INDEX.NO, job);
LuceneWriter.addFieldOptions("digest", LuceneWriter.STORE.YES, LuceneWriter.INDEX.NO, job);
LuceneWriter.addFieldOptions("boost", LuceneWriter.STORE.YES, LuceneWriter.INDEX.NO, job);
NutchIndexWriterFactory.addClassToConf(job, LuceneWriter.class);
JobClient.runJob(job);
LOG.info("Indexer: done");
}
1.1.5.
initMRJob方法
public static void initMRJob(Path crawlDb, Path linkDb,
Collection<Path> segments,
JobConf job) {
LOG.info("IndexerMapReduce: crawldb: " + crawlDb);
LOG.info("IndexerMapReduce: linkdb: " + linkDb);
for (final Path segment : segments) {
/*将sgements下的路径作为job的文件添加路径,添加的内容包括(全在segments/xxxxxxx下):
parse_text
parse_data
crawl_fetch
crawl_parse
这些文件夹下的包含的内容如下:
parse_text:包了网页中的文本内容,列如博客中的正文内容
parse_data:包含了网页的一些状态信息,如:标题、外连接等,以crawlDatum的格式存放。
crawl_fetch:包含了每个url的抓取状态信息。Crawldb中状态信息的更新需要它做参数。
crawl_parse:这个不清楚。
*/
LOG.info("IndexerMapReduces: adding segment: " + segment);
FileInputFormat.addInputPath(job, new Path(segment, CrawlDatum.FETCH_DIR_NAME));
FileInputFormat.addInputPath(job, new Path(segment, CrawlDatum.PARSE_DIR_NAME));
FileInputFormat.addInputPath(job, new Path(segment, ParseData.DIR_NAME));
FileInputFormat.addInputPath(job, new Path(segment, ParseText.DIR_NAME));
}
FileInputFormat.addInputPath(job, new Path(crawlDb, CrawlDb.CURRENT_NAME));
FileInputFormat.addInputPath(job, new Path(linkDb, LinkDb.CURRENT_NAME));
job.setInputFormat(SequenceFileInputFormat.class);
//最重要的是MapReduce.class,将在1.1.6中讲解
job.setMapperClass(IndexerMapReduce.class);
job.setReducerClass(IndexerMapReduce.class);
job.setOutputFormat(IndexerOutputFormat.class);
job.setOutputKeyClass(Text.class);
job.setMapOutputValueClass(NutchWritable.class);
job.setOutputValueClass(NutchWritable.class);
}
}
1.1.6.
核心分布式类讲解
public class IndexerMapReduce extends Configured
implements Mapper<Text, Writable, Text, NutchWritable>,
Reducer<Text, NutchWritable, Text, NutchDocument> {
public static final Log LOG = LogFactory.getLog(IndexerMapReduce.class);
private IndexingFilters filters;
private ScoringFilters scfilters;
public void configure(JobConf job) {
setConf(job);
this.filters = new IndexingFilters(getConf());
/*初始化nutch索引插件,这是nutch索引管理的核心,所有Nutch索引都是通过插件机制完成的*/
this.scfilters = new ScoringFilters(getConf());
}
public void map(Text key, Writable value,
OutputCollector<Text, NutchWritable> output, Reporter reporter) throws IOException {
output.collect(key, new NutchWritable(value));
}
/*最重要的是reduce方法*/
public void reduce(Text key, Iterator<NutchWritable> values,
OutputCollector<Text, NutchDocument> output, Reporter reporter)
throws IOException {
Inlinks inlinks = null;
CrawlDatum dbDatum = null;
CrawlDatum fetchDatum = null;
ParseData parseData = null;
ParseText parseText = null;
/*注意values是Iterator类型,同时注意前面FileInputPath添加的路径是有多个的,所以value的值的类型可以是多种类型的。这个循环通过判断每个value的不同类型来将他们分类,在CrawlDatum类中,有一堆final类型的静态常量,每个阶段的value,其状态都在这些静态常量中细分着,每种状态会有一个常量来区分,而每种状态又有多个静态常量来表述。所以对于CrawlDatum,对其又可以进行细分。*/
while (values.hasNext()) {
final Writable value = values.next().get(); // unwrap
if (value instanceof Inlinks) {
inlinks = (Inlinks)value;
} else if (value instanceof CrawlDatum) {
final CrawlDatum datum = (CrawlDatum)value;
if (CrawlDatum.hasDbStatus(datum))
dbDatum = datum;
else if (CrawlDatum.hasFetchStatus(datum)) {
// don't index unmodified (empty) pages
if (datum.getStatus() != CrawlDatum.STATUS_FETCH_NOTMODIFIED)
fetchDatum = datum;
} else if (CrawlDatum.STATUS_LINKED == datum.getStatus() ||
CrawlDatum.STATUS_SIGNATURE == datum.getStatus() ||
CrawlDatum.STATUS_PARSE_META == datum.getStatus()) {
continue;
} else {
throw new RuntimeException("Unexpected status: "+datum.getStatus());
}
} else if (value instanceof ParseData) {
parseData = (ParseData)value;
} else if (value instanceof ParseText) {
parseText = (ParseText)value;
} else if (LOG.isWarnEnabled()) {
LOG.warn("Unrecognized type: "+value.getClass());
}
}
if (fetchDatum == null || dbDatum == null
|| parseText == null || parseData == null) {
return; // only have inlinks
}
if (!parseData.getStatus().isSuccess() ||
fetchDatum.getStatus() != CrawlDatum.STATUS_FETCH_SUCCESS) {
return;
}
//创建索引
NutchDocument doc = new NutchDocument();
final Metadata metadata = parseData.getContentMeta();
//parseData对应一个类来管理,就是parseData,可以通过其中的一些方法获得相应的内容。
// add segment, used to map from merged index back to segment files
doc.add("segment", metadata.get(Nutch.SEGMENT_NAME_KEY));
// add digest, used by dedup
doc.add("digest", metadata.get(Nutch.SIGNATURE_KEY));
final Parse parse = new ParseImpl(parseText, parseData);
try {
// extract information from dbDatum and pass it to
// fetchDatum so that indexing filters can use it
final Text url = (Text) dbDatum.getMetaData().get(Nutch.WRITABLE_REPR_URL_KEY);
if (url != null) {
fetchDatum.getMetaData().put(Nutch.WRITABLE_REPR_URL_KEY, url);
}
// run indexing filters
doc = this.filters.filter(doc, parse, key, fetchDatum, inlinks);
//此处调用插件进行索引建立。关于插件,将在第2节中讲解。
} catch (final IndexingException e) {
if (LOG.isWarnEnabled()) { LOG.warn("Error indexing "+key+": "+e); }
return;
}
// skip documents discarded by indexing filters
if (doc == null) return;
float boost = 1.0f;
// run scoring filters
try {
boost = this.scfilters.indexerScore(key, doc, dbDatum,
fetchDatum, parse, inlinks, boost);
} catch (final ScoringFilterException e) {
if (LOG.isWarnEnabled()) {
LOG.warn("Error calculating score " + key + ": " + e);
}
return;
}
// apply boost to all indexed fields.
doc.setScore(boost);
// store boost for use by explain and dedup
doc.add("boost", Float.toString(boost));
output.collect(key, doc);
}
2.
索引管理详解
2.1.
插件
Nutch的索引都是以插件的形式实现的。nutch-site.xml中添加插件扩展,实现插件使用。如:<name>plugin.includes</name>
<value>analysis-(zh)|protocol-http|urlfilter-regex|parse-(text|html|js|pdf)|index-(basic|anchor|more)|query-(basic|site|url|more|custom)|response-(json|xml)|summary-lucene|scoring-opic|urlnormalizer-(pass|regex|basic)</value>。
以分词为例:
首先,在插件的包中,有个plugin.xml,里面有
<plugin
id="analysis-zh"
name="Chinese Analysis Plug-in"
version="1.0.0"
provider-name="net.paoding.analysis">。nutch-site.xml中的插件扩展要和id相匹配才行。
2.2.
自定义插件
在索引插件中,关键是得到你想添加到索引field中的内容。对于索引中的一些细节,参考《lucene+nutch搜索引擎开发》这本书。