基于lucene的nutch索引详解

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搜索引擎开发》这本书。


评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值