<?xml version="1.0" encoding="UTF-8"?>
<!--
   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

   https://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
  -->
<!DOCTYPE document PUBLIC "-//APACHE//DTD Documentation V2.0//EN"
   "https://forrest.apache.org/dtd/document-v20.dtd" [
  <!ENTITY % avro-entities PUBLIC "-//Apache//ENTITIES Avro//EN"
	   "../../../../build/avro.ent">
  %avro-entities;
]>
<document>
  <header>
    <title>Apache Avro&#153; &AvroVersion; Hadoop MapReduce guide</title>
  </header>
  <body>
    <p>
      Avro provides a convenient way to represent complex data structures within
      a Hadoop MapReduce job.  Avro data can be used as both input to and output
      from a MapReduce job, as well as the intermediate format.  The example in
      this guide uses Avro data for all three, but it's possible to mix and
      match; for instance, MapReduce can be used to aggregate a particular field
      in an Avro record.
    </p>
    <p>
      This guide assumes basic familiarity with both Hadoop MapReduce and Avro.
      See the <a href="https://hadoop.apache.org/docs/current/">Hadoop
      documentation</a> and the <a href="gettingstartedjava.html">Avro getting
      started guide</a> for introductions to these projects.  This guide uses
      the old MapReduce API (<code>org.apache.hadoop.mapred</code>) and the new
      MapReduce API (<code>org.apache.hadoop.mapreduce</code>).
    </p>
    <section>
      <title>Setup</title>
      <p>
        The code from this guide is included in the Avro docs under
        <em>examples/mr-example</em>.  The example is set up as a Maven project
        that includes the necessary Avro and MapReduce dependencies and the Avro
        Maven plugin for code generation, so no external jars are needed to run
        the example.  In particular, the POM includes the following dependencies:
      </p>
      <source>
&#60;dependency&#62;
  &#60;groupId&#62;org.apache.avro&#60;/groupId&#62;
  &#60;artifactId&#62;avro&#60;/artifactId&#62;
  &#60;version&#62;&AvroVersion;&#60;/version&#62;
&#60;/dependency&#62;
&#60;dependency&#62;
  &#60;groupId&#62;org.apache.avro&#60;/groupId&#62;
  &#60;artifactId&#62;avro-mapred&#60;/artifactId&#62;
  &#60;version&#62;&AvroVersion;&#60;/version&#62;
&#60;/dependency&#62;
&#60;dependency&#62;
  &#60;groupId&#62;org.apache.hadoop&#60;/groupId&#62;
  &#60;artifactId&#62;hadoop-client&#60;/artifactId&#62;
  &#60;version&#62;3.1.2&#60;/version&#62;
&#60;/dependency&#62;
      </source>
      <p>
        And the following plugin:
      </p>
      <source>
&#60;plugin>
  &#60;groupId>org.apache.avro&#60;/groupId>
  &#60;artifactId>avro-maven-plugin&#60;/artifactId>
  &#60;version>&AvroVersion;&#60;/version>
  &#60;executions>
    &#60;execution>
      &#60;phase>generate-sources&#60;/phase>
      &#60;goals>
        &#60;goal>schema&#60;/goal>
      &#60;/goals>
      &#60;configuration>
        &#60;sourceDirectory>${project.basedir}/../&#60;/sourceDirectory>
        &#60;outputDirectory>${project.basedir}/target/generated-sources/&#60;/outputDirectory>
      &#60;/configuration>
    &#60;/execution>
  &#60;/executions>
&#60;/plugin>
      </source>
      <p>
        If you do not configure the <em>sourceDirectory</em> and <em>outputDirectory</em>
        properties, the defaults will be used. The <em>sourceDirectory</em> property
        defaults to <em>src/main/avro</em>. The <em>outputDirectory</em> property
        defaults to <em>target/generated-sources</em>. You can change the paths to
        match your project layout.
      </p>
      <p>
        Alternatively, Avro jars can be downloaded directly from the <a
        href="https://avro.apache.org/releases.html">Apache Avro&#153;
        Releases</a> page.  The relevant Avro jars for this guide are
        <em>avro-&AvroVersion;.jar</em> and
        <em>avro-mapred-&AvroVersion;.jar</em>, as well as
        <em>avro-tools-&AvroVersion;.jar</em> for code generation and viewing
        Avro data files as JSON.  In addition, you will need to install Hadoop
        in order to use MapReduce.
      </p>
    </section>

    <section>
      <title>Example: ColorCount</title>
      <p>
        Below is a simple example of a MapReduce that uses Avro. There is an example
        for both the old (<em>org.apache.hadoop.mapred</em>) and new
        (<em>org.apache.hadoop.mapreduce</em>) APIs under
        <em>examples/mr-example/src/main/java/example/</em>. <em>MapredColorCount</em>
        is the example for the older mapred API while <em>MapReduceColorCount</em> is
        the example for the newer mapreduce API. Both examples are below, but
        we will detail the mapred API in our subsequent examples.
      </p>

      <p>MapredColorCount:</p>
      <source>
package example;

import java.io.IOException;

import org.apache.avro.*;
import org.apache.avro.Schema.Type;
import org.apache.avro.mapred.*;
import org.apache.hadoop.conf.*;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.mapred.*;
import org.apache.hadoop.util.*;

import example.avro.User;

public class MapredColorCount extends Configured implements Tool {

  public static class ColorCountMapper extends AvroMapper&#60;User, Pair&#60;CharSequence, Integer>> {
    @Override
    public void map(User user, AvroCollector&#60;Pair&#60;CharSequence, Integer>> collector, Reporter reporter)
        throws IOException {
      CharSequence color = user.getFavoriteColor();
      // We need this check because the User.favorite_color field has type ["string", "null"]
      if (color == null) {
        color = "none";
      }
      collector.collect(new Pair&#60;CharSequence, Integer>(color, 1));
    }
  }

  public static class ColorCountReducer extends AvroReducer&#60;CharSequence, Integer,
                                                            Pair&#60;CharSequence, Integer>> {
    @Override
    public void reduce(CharSequence key, Iterable&#60;Integer> values,
                       AvroCollector&#60;Pair&#60;CharSequence, Integer>> collector,
                       Reporter reporter)
        throws IOException {
      int sum = 0;
      for (Integer value : values) {
        sum += value;
      }
      collector.collect(new Pair&#60;CharSequence, Integer>(key, sum));
    }
  }

  public int run(String[] args) throws Exception {
    if (args.length != 2) {
      System.err.println("Usage: MapredColorCount &#60;input path> &#60;output path>");
      return -1;
    }

    JobConf conf = new JobConf(getConf(), MapredColorCount.class);
    conf.setJobName("colorcount");

    FileInputFormat.setInputPaths(conf, new Path(args[0]));
    FileOutputFormat.setOutputPath(conf, new Path(args[1]));

    AvroJob.setMapperClass(conf, ColorCountMapper.class);
    AvroJob.setReducerClass(conf, ColorCountReducer.class);

    // Note that AvroJob.setInputSchema and AvroJob.setOutputSchema set
    // relevant config options such as input/output format, map output
    // classes, and output key class.
    AvroJob.setInputSchema(conf, User.getClassSchema());
    AvroJob.setOutputSchema(conf, Pair.getPairSchema(Schema.create(Type.STRING),
        Schema.create(Type.INT)));

    JobClient.runJob(conf);
    return 0;
  }

  public static void main(String[] args) throws Exception {
    int res = ToolRunner.run(new Configuration(), new MapredColorCount(), args);
    System.exit(res);
  }
}
      </source>

      <p>MapReduceColorCount:</p>
      <source>
package example;

import java.io.IOException;

import org.apache.avro.Schema;
import org.apache.avro.mapred.AvroKey;
import org.apache.avro.mapred.AvroValue;
import org.apache.avro.mapreduce.AvroJob;
import org.apache.avro.mapreduce.AvroKeyInputFormat;
import org.apache.avro.mapreduce.AvroKeyValueOutputFormat;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;

import example.avro.User;

public class MapReduceColorCount extends Configured implements Tool {

  public static class ColorCountMapper extends
      Mapper&lt;AvroKey&lt;User&gt;, NullWritable, Text, IntWritable&gt; {

    @Override
    public void map(AvroKey&lt;User&gt; key, NullWritable value, Context context)
        throws IOException, InterruptedException {

      CharSequence color = key.datum().getFavoriteColor();
      if (color == null) {
        color = &quot;none&quot;;
      }
      context.write(new Text(color.toString()), new IntWritable(1));
    }
  }

  public static class ColorCountReducer extends
      Reducer&lt;Text, IntWritable, AvroKey&lt;CharSequence&gt;, AvroValue&lt;Integer&gt;&gt; {

    @Override
    public void reduce(Text key, Iterable&lt;IntWritable&gt; values,
        Context context) throws IOException, InterruptedException {

      int sum = 0;
      for (IntWritable value : values) {
        sum += value.get();
      }
      context.write(new AvroKey&lt;CharSequence&gt;(key.toString()), new AvroValue&lt;Integer&gt;(sum));
    }
  }

  public int run(String[] args) throws Exception {
    if (args.length != 2) {
      System.err.println(&quot;Usage: MapReduceColorCount &lt;input path&gt; &lt;output path&gt;&quot;);
      return -1;
    }

    Job job = new Job(getConf());
    job.setJarByClass(MapReduceColorCount.class);
    job.setJobName(&quot;Color Count&quot;);

    FileInputFormat.setInputPaths(job, new Path(args[0]));
    FileOutputFormat.setOutputPath(job, new Path(args[1]));

    job.setInputFormatClass(AvroKeyInputFormat.class);
    job.setMapperClass(ColorCountMapper.class);
    AvroJob.setInputKeySchema(job, User.getClassSchema());
    job.setMapOutputKeyClass(Text.class);
    job.setMapOutputValueClass(IntWritable.class);

    job.setOutputFormatClass(AvroKeyValueOutputFormat.class);
    job.setReducerClass(ColorCountReducer.class);
    AvroJob.setOutputKeySchema(job, Schema.create(Schema.Type.STRING));
    AvroJob.setOutputValueSchema(job, Schema.create(Schema.Type.INT));

    return (job.waitForCompletion(true) ? 0 : 1);
  }

  public static void main(String[] args) throws Exception {
    int res = ToolRunner.run(new MapReduceColorCount(), args);
    System.exit(res);
  }
}
      </source>

      <p>
        ColorCount reads in data files containing <code>User</code> records,
        defined in <em>examples/user.avsc</em>, and counts the number of
        instances of each favorite color.  (This example draws inspiration from
        the canonical WordCount MapReduce application.)  This example uses the
        old MapReduce API.  See MapReduceAvroWordCount, found under
        <em>doc/examples/mr-example/src/main/java/example/</em> to see the new MapReduce
        API example.  The <code>User</code>
        schema is defined as follows:
      </p>
      <source>
{"namespace": "example.avro",
 "type": "record",
 "name": "User",
 "fields": [
     {"name": "name", "type": "string"},
     {"name": "favorite_number",  "type": ["int", "null"]},
     {"name": "favorite_color", "type": ["string", "null"]}
 ]
}
      </source>
      <p>
        This schema is compiled into the <code>User</code> class used by
        ColorCount via the Avro Maven plugin (see
        <em>examples/mr-example/pom.xml</em> for how this is set up).
      </p>
      <p>
        ColorCountMapper essentially takes a <code>User</code> as input and
        extracts the <code>User</code>'s favorite color, emitting the key-value
        pair <code>&#60;</code><em>favoriteColor</em><code>, 1></code>.
        ColorCountReducer then adds up how many occurrences of a particular
        favorite color were emitted, and outputs the result as a
        <code>Pair</code> record.  These <code>Pair</code>s are serialized to an
        Avro data file.
      </p>
      <section>
        <title>Running ColorCount</title>
        <p>
          The ColorCount application is provided as a Maven project in the Avro
          docs under <em>examples/mr-example</em>.  To build the project,
          including the code generation of the User schema, run:
        </p>
        <source>
mvn compile
        </source>
        <p>
          Next, run GenerateData from examples/mr-examples to create an Avro data
          file, <em>input/users.avro</em>, containing 20 <code>User</code>s with
          favorite colors chosen randomly from a list:
        </p>
        <source>
mvn exec:java -q -Dexec.mainClass=example.GenerateData
        </source>
        <p>
          Besides creating the data file, GenerateData prints the JSON
          representations of the Users generated to stdout, for example:
        </p>
        <source>
{"name": "user", "favorite_number": null, "favorite_color": "red"}
{"name": "user", "favorite_number": null, "favorite_color": "green"}
{"name": "user", "favorite_number": null, "favorite_color": "purple"}
{"name": "user", "favorite_number": null, "favorite_color": null}
...
        </source>
        <p>
          Now we're ready to run ColorCount.  We specify our freshly-generated
          <em>input</em> folder as the input path and <em>output</em> as our
          output folder (note that MapReduce will not start a job if the output
          folder already exists):
        </p>
        <source>
mvn exec:java -q -Dexec.mainClass=example.MapredColorCount -Dexec.args="input output"
        </source>
        <p>
          Once ColorCount completes, checking the contents of the new
          <em>output</em> directory should yield the following:
        </p>
        <source>
$ ls output/
part-00000.avro  _SUCCESS
        </source>
        <p>
          You can check the contents of the generated Avro file using the avro-tools jar:
        </p>
        <source>
$ java -jar /path/to/avro-tools-&AvroVersion;.jar tojson output/part-00000.avro
{"value": 3, "key": "blue"}
{"value": 7, "key": "green"}
{"value": 1, "key": "none"}
{"value": 2, "key": "orange"}
{"value": 3, "key": "purple"}
{"value": 2, "key": "red"}
{"value": 2, "key": "yellow"}
        </source>
      </section>
    </section>
    <p>Now let's go over the ColorCount example in detail.</p>
    <section>
      <title>Mapper - org.apache.hadoop.mapred API</title>
      <p>
        The easiest way to use Avro data files as input to a MapReduce job is to
        subclass <code>AvroMapper</code>.  An <code>AvroMapper</code> defines a
        map function that takes an Avro datum as input and outputs a key/value
        pair represented as a <code>Pair</code> record.  In the ColorCount
        example, <code>ColorCountMapper</code> is an <code>AvroMapper</code>
        that takes a <code>User</code> as input and outputs a
        <code>Pair&#60;CharSequence, Integer>></code>, where the
        <code>CharSequence</code> key is the user's favorite color and the
        <code>Integer</code> value is 1.
      </p>
      <source>
public static class ColorCountMapper extends AvroMapper&#60;User, Pair&#60;CharSequence, Integer>> {
  @Override
  public void map(User user, AvroCollector&#60;Pair&#60;CharSequence, Integer>> collector, Reporter reporter)
      throws IOException {
    CharSequence color = user.getFavoriteColor();
    // We need this check because the User.favorite_color field has type ["string", "null"]
    if (color == null) {
      color = "none";
    }
    collector.collect(new Pair&#60;CharSequence, Integer>(color, 1));
  }
}
      </source>
      <p>
        In order to use our <code>AvroMapper</code>, we must call
        <code>AvroJob.setMapperClass</code> and
        <code>AvroJob.setInputSchema</code>.
      </p>
      <source>
AvroJob.setMapperClass(conf, ColorCountMapper.class);
AvroJob.setInputSchema(conf, User.getClassSchema());
      </source>
      <p>
        Note that <code>AvroMapper</code> does not implement the
        <code>Mapper</code> interface.  Under the hood, the specified Avro data
        files are deserialized into <code>AvroWrapper</code>s containing the
        actual data, which are processed by a <code>Mapper</code> that calls the
        configured <code>AvroMapper</code>'s map function.
        <code>AvroJob.setInputSchema</code> sets up the relevant configuration
        parameters needed to make this happen, thus you should not need to call
        <code>JobConf.setMapperClass</code>,
        <code>JobConf.setInputFormat</code>,
        <code>JobConf.setMapOutputKeyClass</code>,
        <code>JobConf.setMapOutputValueClass</code>, or
        <code>JobConf.setOutputKeyComparatorClass</code>.
      </p>
    </section>
    <section>
      <title>Mapper - org.apache.hadoop.mapreduce API</title>
      <p>
        This document will not go into all the differences between the mapred and mapreduce APIs,
        however will describe the main differences. As you can see, ColorCountMapper is now a
        subclass of the Hadoop Mapper class and is passed an AvroKey as it's key.

        Additionally, the AvroJob method calls were slightly changed.
      </p>
      <source>
  public static class ColorCountMapper extends
      Mapper&lt;AvroKey&lt;User&gt;, NullWritable, Text, IntWritable&gt; {

    @Override
    public void map(AvroKey&lt;User&gt; key, NullWritable value, Context context)
        throws IOException, InterruptedException {

      CharSequence color = key.datum().getFavoriteColor();
      if (color == null) {
        color = &quot;none&quot;;
      }
      context.write(new Text(color.toString()), new IntWritable(1));
    }
  }
      </source>
    </section>
    <section>
      <title>Reducer - org.apache.hadoop.mapred API</title>
      <p>
        Analogously to <code>AvroMapper</code>, an <code>AvroReducer</code>
        defines a reducer function that takes the key/value types output by an
        <code>AvroMapper</code> (or any mapper that outputs <code>Pair</code>s)
        and outputs a key/value pair represented a <code>Pair</code> record.  In
        the ColorCount example, <code>ColorCountReducer</code> is an
        <code>AvroReducer</code> that takes the <code>CharSequence</code> key
        representing a favorite color and the <code>Iterable&#60;Integer></code>
        representing the counts for that color (they should all be 1 in this
        example) and adds up the counts.
      </p>
      <source>
public static class ColorCountReducer extends AvroReducer&#60;CharSequence, Integer,
                                                          Pair&#60;CharSequence, Integer>> {
  @Override
  public void reduce(CharSequence key, Iterable&#60;Integer> values,
                     AvroCollector&#60;Pair&#60;CharSequence, Integer>> collector,
                     Reporter reporter)
      throws IOException {
    int sum = 0;
    for (Integer value : values) {
      sum += value;
    }
    collector.collect(new Pair&#60;CharSequence, Integer>(key, sum));
  }
}
      </source>
      <p>
        In order to use our <code>AvroReducer</code>, we must call
        <code>AvroJob.setReducerClass</code> and
        <code>AvroJob.setOutputSchema</code>.
      </p>
      <source>
AvroJob.setReducerClass(conf, ColorCountReducer.class);
AvroJob.setOutputSchema(conf, Pair.getPairSchema(Schema.create(Type.STRING),
                                                 Schema.create(Type.INT)));
      </source>
      <p>
        Note that <code>AvroReducer</code> does not implement the
        <code>Reducer</code> interface.  The intermediate <code>Pair</code>s
        output by the mapper are split into <code>AvroKey</code>s and
        <code>AvroValue</code>s, which are processed by a <code>Reducer</code>
        that calls the configured <code>AvroReducer</code>'s reduce function.
        <code>AvroJob.setOutputSchema</code> sets up the relevant configuration
        parameters needed to make this happen, thus you should not need to call
        <code>JobConf.setReducerClass</code>,
        <code>JobConf.setOutputFormat</code>,
        <code>JobConf.setOutputKeyClass</code>,
        <code>JobConf.setMapOutputKeyClass</code>,
        <code>JobConf.setMapOutputValueClass</code>, or
        <code>JobConf.setOutputKeyComparatorClass</code>.
      </p>
    </section>
    <section>
      <title>Reduce - org.apache.hadoop.mapreduce API</title>
      <p>
        As before we not detail every difference between the APIs. As with the Mapper
        change ColorCountReducer is now a subclass of Reducer and AvroKey and AvroValue
        are emitted.

        Additionally, the AvroJob method calls were slightly changed.
      </p>
      <source>
  public static class ColorCountReducer extends
      Reducer&lt;Text, IntWritable, AvroKey&lt;CharSequence&gt;, AvroValue&lt;Integer&gt;&gt; {

    @Override
    public void reduce(Text key, Iterable&lt;IntWritable&gt; values,
        Context context) throws IOException, InterruptedException {

      int sum = 0;
      for (IntWritable value : values) {
        sum += value.get();
      }
      context.write(new AvroKey&lt;CharSequence&gt;(key.toString()), new AvroValue&lt;Integer&gt;(sum));
    }
  }
      </source>
    </section>
    <section>
      <title>Learning more</title>
      <p>
        The mapred API allows users to mix Avro <code>AvroMapper</code>s and
        <code>AvroReducer</code>s with non-Avro <code>Mapper</code>s and
        <code>Reducer</code>s and the mapreduce API allows users input Avro
        and output non-Avro or vice versa.
      </p>

      <p>
        The mapred package has API <a
        href="https://avro.apache.org/docs/current/api/java/org/apache/avro/mapred/package-summary.html">
        <code>org.apache.avro.mapred</code> documentation</a> as does the <a
        href="https://avro.apache.org/docs/current/api/java/org/apache/avro/mapreduce/package-summary.html">
        <code>org.apache.avro.mapreduce</code> package</a>.
        MapReduce API (<code>org.apache.hadoop.mapreduce</code>). Similarily to the mapreduce package,
        it's possible with the mapred API to implement your own <code>Mapper</code>s and
        <code>Reducer</code>s directly using the public classes provided in
        these libraries.  See the AvroWordCount application, found under
        <em>examples/mr-example/src/main/java/example/AvroWordCount.java</em> in
        the Avro documentation, for an example of implementing a
        <code>Reducer</code> that outputs Avro data using the old MapReduce API.
        See the MapReduceAvroWordCount application, found under
        <em>examples/mr-example/src/main/java/example/MapReduceAvroWordCount.java</em> in
        the Avro documentation, for an example of implementing a
        <code>Reducer</code> that outputs Avro data using the new MapReduce API.
      </p>
    </section>
  </body>
</document>
