基於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搜索引擎開發》這本書。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章