Tuesday, September 22, 2015

Find Top K IP Addresses that visited the website using MapReduce

Today, I will demonstrate how to find top K items from given data based on count. The data set that I am using looks like this :

199.72.81.55 - - [01/Jul/1995:00:00:01 -0400] "GET /history/apollo/ HTTP/1.0" 200 6245
unicomp6.unicomp.net - - [01/Jul/1995:00:00:06 -0400] "GET /shuttle/countdown/ HTTP/1.0" 200 3985
199.120.110.21 - - [01/Jul/1995:00:00:09 -0400] "GET /shuttle/missions/sts-73/mission-sts-73.html HTTP/1.0" 200 4085
burger.letters.com - - [01/Jul/1995:00:00:11 -0400] "GET /shuttle/countdown/liftoff.html HTTP/1.0" 304 0
199.120.110.21 - - [01/Jul/1995:00:00:11 -0400] "GET /shuttle/missions/sts-73/sts-73-patch-small.gif HTTP/1.0" 200 4179
burger.letters.com - - [01/Jul/1995:00:00:12 -0400] "GET /images/NASA-logosmall.gif HTTP/1.0" 304 0
burger.letters.com - - [01/Jul/1995:00:00:12 -0400] "GET /shuttle/countdown/video/livevideo.gif HTTP/1.0" 200 0
205.212.115.106 - - [01/Jul/1995:00:00:12 -0400] "GET /shuttle/countdown/countdown.html HTTP/1.0" 200 3985


Here we will write 2 MR jobs to do following:

Job1:
Find (IP Addresses , Total count) that shows how many times that IP Address visited the website .

Job2:
Find Top 10 (IP Address , TotalCount) based on highest count in descending order.

Code Job1:

Mapper Code ->  We are reading line by line and writing context(IPAddress,1)
Reducer Code ->  Iterate values and aggregate all values for each IP Address .

public static class WLMapper1 extends
   Mapper<LongWritable, Text, Text, IntWritable> {
  // HashMap<String, Integer> map = new HashMap<String, Integer>();
  @Override
  protected void map(LongWritable key, Text value, Context context)
    throws java.io.IOException, InterruptedException {
   String IPorServerName = "";
   if (value.toString().contains(" ")) {
    String[] arr = value.toString().split(" ");
    if (arr.length > 0) {
     IPorServerName = arr[0].toLowerCase().trim();
    }
   }
   context.write(new Text(IPorServerName), new IntWritable(1));
  };
 } public static class WLReducer1 extends
   Reducer<Text, IntWritable, Text, IntWritable> {
  @Override
  protected void reduce(Text key, Iterable<IntWritable> values,
    Context context) throws java.io.IOException,
    InterruptedException {
   int count = 0;
   if (values.iterator().hasNext()) {
    for (IntWritable x : values) {
     int val = new Integer(x.toString());
     count += 1;
    }    context.write(key, new IntWritable(count));
   }
  };
 }
When you run this code , following output will be generated for Job1:
007.thegap.com 45
01-dynamic-c.rotterdam.luna.net 1
01-dynamic-c.wokingham.luna.net 28
02-dynamic-c.wokingham.luna.net 13
03-dynamic-c.wokingham.luna.net 15
04-dynamic-c.rotterdam.luna.net 22
04-dynamic-c.wokingham.luna.net 28
05-dynamic-c.rotterdam.luna.net 26
05-dynamic-c.wokingham.luna.net 9
06-dynamic-c.rotterdam.luna.net 22
07-dynamic-c.rotterdam.luna.net 4
0772jela.jelke.rpslmc.edu 3
08-dynamic-c.rotterdam.luna.net 4

Code Job2:

Job2 will take input data from job1 output. This is defined in driver code in following line :
FileInputFormat.setInputPaths(job2, out);
In the mapper part of Job2, we have created a TreeMap object that will hold count  and IPAddress .
Let's try to run this code by uncommenting following line in driver code for job2:

job2.setNumReduceTasks(0);

This will make sure , no reducer code is running for Job2.

public static class WLMapper2 extends
   Mapper<LongWritable, Text, IntWritable, Text> {
  // Our output key and value Writables
  private TreeMap<Integer, String> repToRecordMap = new TreeMap<Integer, String>();

  @Override
  public void map(LongWritable key, Text value, Context context)
    throws IOException, InterruptedException {
   // Parse the input string into a nice map
   if (value.toString().contains("\t")) {
  
    String[] arr = value.toString().split("\t");
    if (arr.length > 1) {
     repToRecordMap.put(Integer.parseInt(arr[1]), arr[0]);     

if (repToRecordMap.size() > 10) {
      repToRecordMap.remove(repToRecordMap.firstKey());
     }
    }
   }

  }

  @Override
  protected void cleanup(Context context) throws IOException,
    InterruptedException {

   for (Map.Entry<Integer, String> entry : repToRecordMap.entrySet()) {
    Integer key = entry.getKey();
    String value = entry.getValue();

    context.write(new IntWritable(key), new Text(value));
   }
  }
 }
Here is the output if we make number of reducers=0 for Job2.

4353 disarray.demon.co.uk
4863 news.ti.com
4906 163.206.89.4
5434 edams.ksc.nasa.gov
5922 piweba2y.prodigy.com
7573 siltb10.orl.mmc.com
7852 alyssa.prodigy.com
9868 piweba1y.prodigy.com
11591 piweba4y.prodigy.com
17572 piweba3y.prodigy.com

Job2's Mapper is splitting the output of Job1 on tab separator. We are putting the count as key and Ipaddress as value in TreeMap.In the cleanup code, we are making sure that count column becomes the key and IPAddress becomes the value. The reason for this operation is that it will sort based on column key (Count column since we want it in descending order) and send it to reducer.

One trick that I have done in Mapper is following 3 lines of code.As soon as count of records in treeMap is greater than 10, it will remove the item with smallest count from treeMap. This will make sure that at any given time, we will have Top 10 highest count keys in TreeMap.
 
if (repToRecordMap.size() > 10) {
      repToRecordMap.remove(repToRecordMap.firstKey());
     }

Reducer is only changing the key/value pairs while writing it to output. The output we need should have a IPAddress as key and Count as value.

 public static class WLReducer2 extends
   Reducer<IntWritable, Text, Text, IntWritable> {


  @Override
  protected void reduce(IntWritable key, Iterable<Text> values,
    Context context) throws IOException, InterruptedException {

   for (Text x : values) {
    context.write(new Text(x.toString()), key);

   }

  };

 }

Lets run the code and see if we can get the output as expected :

disarray.demon.co.uk 4353

news.ti.com 4863

163.206.89.4 4906

edams.ksc.nasa.gov 5434

piweba2y.prodigy.com 5922

siltb10.orl.mmc.com 7573

alyssa.prodigy.com 7852

piweba1y.prodigy.com 9868

piweba4y.prodigy.com 11591

piweba3y.prodigy.com 17572

We are getting the correct output but the problem is that we wanted to show the output sorted in descending order on count column. To achieve this, we need to write a comparator class:


 public static class KeyComparator extends WritableComparator {
  protected KeyComparator() {
   super(IntWritable.class, true);
  }

  @Override
  public int compare(WritableComparable w1, WritableComparable w2) {
   // TODO Auto-generated method stub

   // Logger.error("--------------------------> writing Keycompare data = ----------->");
   IntWritable ip1 = (IntWritable) w1;
   IntWritable ip2 = (IntWritable) w2;
   int cmp = -1 * ip1.compareTo(ip2);

   return cmp;
  }

The above class can be used by adding following line in driver class.

job2.setSortComparatorClass(KeyComparator.class);

Now the output looks like :

piweba3y.prodigy.com 17572

piweba4y.prodigy.com 11591

piweba1y.prodigy.com 9868

alyssa.prodigy.com 7852

siltb10.orl.mmc.com 7573

piweba2y.prodigy.com 5922

edams.ksc.nasa.gov 5434

163.206.89.4 4906

news.ti.com 4863

disarray.demon.co.uk 4353

I am using following code in driver to make sure that job1 is successful before I start job2:
boolean succ = job.waitForCompletion(true);

  if (!succ) {

   System.out.println("Job1 failed, exiting");

   return -1;

  }


here is the code for driver class that I have used to chain these 2 jobs.


public int run(String[] args) throws Exception {
  // TODO Auto-generated method stub
  Configuration conf = getConf();

  Job job = new Job(conf, "WL Demo");

  job.setJarByClass(WLDemo.class);

  job.setMapperClass(WLMapper1.class);

  job.setReducerClass(WLReducer1.class);

  job.setInputFormatClass(TextInputFormat.class);

  job.setOutputKeyClass(Text.class);

  job.setOutputValueClass(IntWritable.class);

  Path in = new Path(args[0]);

  Path out = new Path(args[1]);

  Path out2 = new Path(args[2]);

  FileInputFormat.setInputPaths(job, in);

  FileOutputFormat.setOutputPath(job, out);

  boolean succ = job.waitForCompletion(true);
  if (!succ) {
   System.out.println("Job1 failed, exiting");
   return -1;
  }
  Job job2 = new Job(conf, "top-k-pass-2");
  FileInputFormat.setInputPaths(job2, out);
  FileOutputFormat.setOutputPath(job2, out2);
  job2.setJarByClass(WLDemo.class);
  job2.setMapperClass(WLMapper2.class);
  job2.setReducerClass(WLReducer2.class);
  // job2.setReducerClass(Reducer1.class);
  job2.setInputFormatClass(TextInputFormat.class);

  job2.setMapOutputKeyClass(IntWritable.class);
  job2.setMapOutputValueClass(Text.class);

  job2.setOutputKeyClass(Text.class);
  job2.setOutputValueClass(IntWritable.class);
  job2.setSortComparatorClass(KeyComparator.class);
  // job2.setNumReduceTasks(0);
  succ = job2.waitForCompletion(true);
  if (!succ) {
   System.out.println("Job2 failed, exiting");
   return -1;
  }
  return 0;
 }
Bug Found and fixed:

One of the issues identified with this approach is that when 2 items have same count, TreeMap will ignore one of the items which will provide incorrect output. To solve this , I have changed the treemap to following :

1) In Mapper 2 , use :
private TreeMap<Integer, List<String>> records = new TreeMap<Integer, List<String>>();

Use following code to restrict total count to less than desired count :

records.put(Integer.parseInt(arr[1]), keyVal);

     if (records.size() > Top_k_Items) {
      records.remove(records.firstKey());

     }

But still total count can be greater than desired count as treemap have a list as value. This will be taken care in Reducer 2 like this :

public static class WLReducer2 extends
   Reducer<IntWritable, Text, Text, IntWritable> {
  int count = 0;

  @Override
  protected void reduce(IntWritable key, Iterable<Text> values,
    Context context) throws IOException, InterruptedException {

   for (Text x : values) {
    if (count < Top_k_Items) {
     context.write(new Text(x), key);
     count = count + 1;
    }
   }

  };


Complete code for solving this problem can be found at this link.

Please provide your feedback in case if you find any issues with this code.

Happy Hadooping !!!

Have a great day.

Varun
 

Read Multiple CSV files using lapply, rbind & reduce functions - R programming

let's do a quick demo on how lapply works in R programming. Suppose you have a number of csv files and each csv file have same structure.
Here is sample data in 1 csv .

"Date","sulfate","nitrate","ID"
"2009-01-01",NA,NA,69
"2009-01-02",NA,NA,69
"2009-01-03",NA,NA,69
"2009-01-04",NA,NA,69
"2009-01-05",NA,NA,69
"2009-01-06",NA,NA,69


Now you want to read all csv files and calculate mean for 1 column. In this case , let's calculate mean  of "sulphate" column across number of different files.

Step 1: read multiple files and create dataframes.
files<- list.file()



 // here files is collection of all files

//  read.csv is the function that you want to apply on all files

dataFrames <- lapply(files, read.csv)

Step 2: combine all dataframes into 1 single dataframe

dataFrames <- Reduce(function(x, y) rbind(x, y), dataFrames)

Step 3: Calculate mean on dataframe column

mean(dataFrame[,sulphate], na.rm = TRUE)

 

Thursday, September 10, 2015

Hive UDF implementation

Consider you have a data set like following:

AccidentId, Date, Day
 
1, 1/1/1979, 5               
 
2, 1/2/1979, 6
 
3, 1/3/1979, 7
 
4, 1/4/1979, 1
 
5, 1/5/1979, 2
 
6, 1/6/1979, 3
 
7, 1/7/1979, 4
 
8, 1/8/1979, 5
 
......
 
.......
 
16, 1/6/1980, 3
 
17, 1/7/1980, 4
 
18, 1/8/1980, 5

This is the query that we want to run on above written input data.

 Select Date , Day_of_Week,count(*) from accidents group by GetYear_FromDate(Date) , Day_of_Week;


The problem is that from Date column we just want Year part.

We have 2 ways to solve this problem :

1) By converting string date to proper format and then using Year function in Hive.

Year(cast(concat(substr(Date,7,4), '-',substr(Date,1,2), '-',substr(Date,4,2))as date)) as DT

2) Write  UDF to get following output:

1979    1    843
1979    2    1180
1979    3    1131
1979    4    1190
1979    5    1231
1979    6    1458
1979    7    1167
1989    7    2

We need to follow 2 rules for HIVE UDFs:
 1) A UDF must be subclass of  org.apache.hadoop.hive.ql.exec.UDF;
 2) A UDF must have 1 evaluate() method.

Here is the UDF code:

import org.apache.hadoop.hive.ql.exec.UDF;
import org.apache.hadoop.io.Text;

public class DateFix extends UDF {

    public Text evaluate(Text str) {
        if (str == null) {
            return null;
        }
       
        return new Text(str.toString().substring(6, 10));
    }
} 






 Now we need to register the UDF :
ADD JAR /home/edureka/Desktop/Projects/HiveUDf/DateDemo.jar;      

We also need to create alias for Java classname:
create temporary function GetYear_FromDate as 'DateFix';    

Finally, we run following query :
Select GetYear_FromDate(Date) , Day_of_Week,count(*) from accidents group by GetYear_FromDate(Date) , Day_of_Week;

That's it. You have just created a UDF in Hive and you can use it on any date column to fetch year part.

Thanks

Wednesday, September 2, 2015

How to implement WritableComparable Interface ?

Today, I decided to write a demo on how to implement customwritable ?

Let's say we have a dataset like following:

AccidentId, Date, Day
1, 1/1/1979, 5               
2, 1/2/1979, 6
3, 1/3/1979, 7
4, 1/4/1979, 1
5, 1/5/1979, 2
6, 1/6/1979, 3
7, 1/7/1979, 4
8, 1/8/1979, 5
......
.......
16, 1/6/1980, 3
17, 1/7/1980, 4
18, 1/8/1980, 5


 Here Day 5 means Thursday, 6 means Friday, 1 means Sunday and so on.

We want to calculate number of accidents on each day of week in each year. The output looks like :

Number of accidents per day in the year 1979

As you can see the reducer is emitting key which is the combination of year and day in the sample data. This is where we need 'WritableComparable' interface.



public class DOW implements WritableComparable<DOW> {
 private Text year;
 private Text day;
 // private int count;
 public DOW() {
  this.year = new Text();
  this.day = new Text();
  // this.count = count;
 }
 public DOW(Text year, Text day) {
  this.year = year;
  this.day = day;
  // this.count = count;
 }
 public Text getYear() {
  return this.year;
 }
 public void setYear(Text year) {
  this.year = year;
 }
 public Text getDay() {
  return this.day;
 }
 public void setDay(Text day) {
  this.day = day;
 }
 @Override
 public void readFields(DataInput in) throws IOException {
  // TODO Auto-generated method stub
  year.readFields(in);
  day.readFields(in);
 }
 @Override
 public void write(DataOutput out) throws IOException {
  // TODO Auto-generated method stub
  year.write(out);
  day.write(out);
 }
 @Override
 public int compareTo(DOW o) {
  // TODO Auto-generated method stub
  int cmp = year.compareTo(o.year);
  if (cmp != 0) {
   return cmp;
  }
  return day.compareTo(o.day);
 }
 @Override
 public String toString() {
  // TODO Auto-generated method stub
  return year + "," + day;
 }
 @Override
 public int hashCode() {
  // TODO Auto-generated method stub
  return year.hashCode() * 163 + day.hashCode();
 }
}


Once we have implemented the WritableComparable interface , we will use the class in Mapper and reducer code as follows.

In the mapper code, I am splitting the value on ',' and getting the year part out of date column. Also, I need day column from dataset .  Once I have both keys, I am writing the context as :

    context.write(new DOW(new Text(Integer.toString(year)), new Text(Integer.toString(day))),
       new IntWritable(1));

Key ->    new DOW(new Text(Integer.toString(year)), new Text(Integer.toString(day))
Value -> new IntWritable(1)

protected void map(LongWritable key, Text value, Context context)
    throws java.io.IOException, InterruptedException {
   if (value.toString().contains(",")) {
    String[] array = value.toString().split(",");
    if (!array[9].equals("Date")) {
     Date dt = null;
     try {
      dt = new SimpleDateFormat("dd/mm/yyyy").parse(array[9]);
     } catch (ParseException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
     }
     int year = dt.getYear();
     int day = Integer.parseInt(array[10].toString());
     sLogger.info("year=" + year);
     sLogger.info("array[10]=" + array[10]);
     context.write(new DOW(new Text(Integer.toString(year)),
       new Text(Integer.toString(day))),
       new IntWritable(1));
    }
   }

Reducer is simply calculating the number of accidents based on key.


@Override
  protected void reduce(DOW key, Iterable<IntWritable> values,
    Context context) throws java.io.IOException,
    InterruptedException {
   int count = 0;
   sLogger.info("key =" + key);
   for (IntWritable x : values) {
    int val = Integer.parseInt(x.toString());
    count = count + val;
   }
   context.write(key, new IntWritable(count));
  };

Once you run this code, you will be able to see the output based on the key year and day.

And that is it .

Happy Hadooping !!

Varun

Tuesday, September 1, 2015

User Defined Counters in Hadoop

Friends,

Today, we will learn few concepts about User Defined Counters. Hadoop have many counters such as :
1) MR Task Counters
2) File System Counters
3) File input & File Output format counters
4) Job Counters
5) User Defined Counters

Let's try to understand why do we need counters ?
Consider a scenario where you have many bad records in your input file. You want to find out how many such bad records exists . Ex: you are writing MR job for the query:
Select name,count(*) from User but some of the records are bad as the name do not exists in the columns . You would want to filter out such records and at the same time want to see how many such records exists. Here is the sample data :

Id,Name
1,Tom
2,John
3,
4,null
5,
6,Tom
7,Abi
8,Jack
9,Jack
10,

Let's write some code to create the counters. We will start by writing an enum to define the categories of records.

public enum RecordType {
  Good, Bad
 }

Here is the mapper that will increment the counter when it gets Good/Bad records.

protected void map(LongWritable key, Text value, Context context)
    throws java.io.IOException, InterruptedException {
   if (value.toString() != "" && value.toString().contains(",")) {

    String[] array = value.toString().split(",");

    if (array.length < 2) {
     context.getCounter(RecordType.Bad).increment(1);
    } else {
     if (array[1].isEmpty()) {

      context.getCounter(RecordType.Bad).increment(1);
     } else {
      context.getCounter(RecordType.Good).increment(1);
      context.write(new Text(array[1]), new IntWritable(1));
     }
    }

   }

  };


The reducer code is simple  and needs no explanation.
protected void reduce(Text key, Iterable<IntWritable> values,
    Context context) throws java.io.IOException,
    InterruptedException {
   int count = 0;

   for (IntWritable x : values) {
    int val = Integer.parseInt(x.toString());

    count += val;

   }
   context.write(key, new IntWritable(count));
  };

Once you run the code on the sample dataset . You should be able to see the following counters:


This looks good but the only issue is the name (CDemo$RecordType) is not too user friendly.
Lets see how we can change the names to better user friendly names.

1) Create a file with the name ClassName_Enum.properties . ex: CDemo_RecordType.properties
2) place the file at same location where your class exists
3) Enter following lines :

CounterGroupName= Counter records
Good.name= GOOD Records
Bad.name=BAD Records


4) Save the file and create the jar and run it using hadoop command.
5) Now you can see counters with the names supplied in your file.


Here is the source code :

https://github.com/varunkhatri/HadoopCounters/blob/master/CountersDemo/src/CDemo.java

Hope you enjoyed the demo.

Happy Hadooping !!!

Varun

Monday, August 31, 2015

Multiple Output Format - Hadoop

Friends,

Hadoop have many output formats  such as :

1) TextOutput Format
2) Sequence File Output Format
3) MapFile Output Format
4) Multiple Output Format

Out of all the available formats, we will talk about Multiple Output formats. I will implement a simple example to demonstrate how it works.

In this example, we will use a sample data which is used for traffic accidents analysis.
We want to calculate following :
1)   Number of accidents per weather condition
2)   Number of accidents per  light condition.

The sample data looks like :

AccidentId, Weather_Condition_Id,Light_Condition_Id
1,3,5,
2,4,3
3,5,6

Explanation of sample data :
Here accident id =1 occurred due to Weather Condition = 3 and Light Condition =5 and so on.

How do we make sure that reducer creates 2 files , one for weather condition and another for Light condition.

I am splitting my csv file on "," and in my scenario 25th column in row is weather condition id and 24th column is Light condition id .

In map method , before writing intermediate key value pairs , I am making sure to attach characters "W" and "L" to distinguish between 2 different columns .

public static class AccidentWeatherMapper extends
            Mapper<LongWritable, Text, Text, IntWritable> {

        @Override
        protected void map(LongWritable key, Text value, Context context)
                throws java.io.IOException, InterruptedException {

            if (value.toString().contains(",")) {
                String[] array = value.toString().split(",");

                context.write(new Text("W" + array[25]), new IntWritable(1));
                context.write(new Text("L" + array[24]), new IntWritable(1));
            }

        };
    }

Now in reducer code, we will create  Multiple Output format in setup method. In reduce method , I used  the character that I attached earlier and based on it writing to 2 different files.

public static class AccidentWeatherReducer extends
            Reducer<Text, IntWritable, Text, IntWritable> {

        MultipleOutputs<Text, IntWritable> mos;

        @Override
        protected void setup(Context context) throws java.io.IOException,
                InterruptedException {
            mos = new MultipleOutputs<Text, IntWritable>(context);
        };

        @Override
        protected void reduce(Text key, Iterable<IntWritable> values,
                Context context) throws java.io.IOException,
                InterruptedException {
            int count = 0;
            for (IntWritable x : values) {
                int val = Integer.parseInt(x.toString());
                count += val;
            }
            if (key.toString().contains("W")) {
                mos.write("Weather",
                        key.toString().substring(1, key.toString().length()),
                        new IntWritable(count));
            } else if (key.toString().contains("L")) {
                mos.write("Light",
                        key.toString().substring(1, key.toString().length()),
                        new IntWritable(count));
            }
        };

        @Override
        protected void cleanup(Context context) throws java.io.IOException,
                InterruptedException {
            mos.close();
        };
    }

Finally, in the driver code, we will configure the naming conventions of output files and output format class for key and value .

public int run(String[] args) throws Exception {
        // TODO Auto-generated method stub
        Configuration conf = getConf();

        Job job = new Job(conf, "MI Demo");

        job.setJarByClass(AnalysisDemo.class);

        job.setMapperClass(AccidentWeatherMapper.class);

        job.setReducerClass(AccidentWeatherReducer.class);


        job.setInputFormatClass(TextInputFormat.class);


        job.setMapOutputKeyClass(Text.class);

        job.setMapOutputValueClass(IntWritable.class);

//  Here set the name of file and key/value types
        MultipleOutputs.addNamedOutput(job, "Weather", TextOutputFormat.class,
                Text.class, IntWritable.class);

        MultipleOutputs.addNamedOutput(job, "Light", TextOutputFormat.class,
                Text.class, IntWritable.class);

        Path in = new Path(args[0]);

        Path out = new Path(args[1]);

        FileInputFormat.setInputPaths(job, in);

        FileOutputFormat.setOutputPath(job, out);

        System.exit(job.waitForCompletion(true) ? 0 : 1);

        return 0;
    }

And that's it. Now you can use a command like this to see data in 2 different files.
 
hadoop jar /home/Desktop/Projects/Accident/AADemo.jar AnalysisDemo /Accident_Project/Input/Accidents.csv /Accident_Project/Output/Out12


Hope you enjoyed the quick demo of the Multiple Output Format.

Happy Hadooping !!!

Varun

Thursday, August 27, 2015

User Defined Functions ( UDFs) - PIG

Hi friends ,

Today I wanted to give a simple demo of some of the UDFs available in PIG. We will try to understand few UDFs with the help of some code . Let's quickly jump on 1st UDF:

Note: I have read and tried to understand the concepts from Hadoop - The definitive Guide. If you have time , please go through this book. You won't regret it.

Let's Suppose you have data like this :
1;"nyc, new york, usa";NULL
2;"stockton, california, usa";18
3;"moscow, yukon territory, russia";NULL
4;"porto, v.n.gaia, portugal";17
5;"farnborough, hants, united kingdom";NULL
6;"santa monica, california, usa";61

And you want to run a query which looks like :

Select * from table where Id > 2;
In PIG you will use following commands :
A = Load '/homeDesktop/Projects/Book/UserTest.csv' USING  PigStorage(';') 
>> AS (Id:int,Location:chararray,Age:chararray);                                      


 C = FILTER A BY Id >2;                                                             


 Dump C;


And here is the output :
(3,"moscow, yukon territory, russia",NULL)
(4,"porto, v.n.gaia, portugal",17)
(5,"farnborough, hants, united kingdom",NULL)
(6,"santa monica, california, usa",61)


This is Simple. Now consider this filter (Id > 2) is used many times. We can always write a simple UDF to do so.

1) Filter UDF
Create a java project to write MR code. Here name of function is the name of class that we will use in Pig Command. This class will extend FilterFunc.

Note: Filter UDFs are all subclasses of FilterFunc, which itself is a subclass of EvalFunc
public class FilterById extends FilterFunc {

// We will override exec method

// It will accept a tuple from which we will get object

// This object is used for creating filter clause that we used earlier


    @Override
    public Boolean exec(Tuple tuple) throws IOException {
        // TODO Auto-generated method stub

        if (tuple == null || tuple.size() == 0) {
            return false;
        }
        try {
            Object object = tuple.get(0);
            if (object == null) {
                return false;
            }
            int i = (Integer) object;
            return i >= 2;
        } catch (ExecException e) {
            // TODO: handle exception
            throw new IOException(e);
        }
        // return null;
    }





Once this is done , create the jar and register it using following command.
 REGISTER /home/edureka/Desktop/Projects/PigDemo/PDemo1.jar;   
 

Again , you can try following commands :
A = Load '/home/Desktop/Projects/Book/UserTest.csv' USING  PigStorage(';') 
>> AS (Id:int,Location:chararray,Age:chararray);                                     



 REGISTER /home/edureka/Desktop/Projects/PigDemo/PDemo1.jar;  

                     
 C = FILTER A BY FilterById(Id);                                                    
 Dump C; 



And here is the output :
(3,"moscow, yukon territory, russia",NULL)
(4,"porto, v.n.gaia, portugal",17)
(5,"farnborough, hants, united kingdom",NULL)
(6,"santa monica, california, usa",61)
  2) Now let's say you want to add some string to location column. We can use EvalFunc to write this UDF.
public class AddString extends EvalFunc<String> {

 @Override
 public String exec(Tuple tuple) throws IOException {
  // TODO Auto-generated method stub
  if (tuple == null || tuple.size() == 0) {
   return null;
  }
  try {
   Object object = tuple.get(0);
   if (object == null) {
    return null;
   }
   return ((String) object) + "- Added String";
  } catch (ExecException e) {
   throw new IOException(e);
  }
 }

}
  and then use following commands .
grunt> A = Load '/home/Desktop/Projects/Book/UserTest.csv' USING  PigStorage(';') 
>> AS (Id:int,Location:chararray,Age:chararray);                                      
grunt> REGISTER /home/Desktop/Projects/PigDemo/PDemo1.jar; 
C = FOREACH A GENERATE Id,AddString(Location),Age; 
Dump C;
 
 And this is the output.
(1,"nyc, new york, usa"- Added String,NULL)
(2,"stockton, california, usa"- Added String,18)
(3,"moscow, yukon territory, russia"- Added String,NULL)
(4,"porto, v.n.gaia, portugal"- Added String,17)
(5,"farnborough, hants, united kingdom"- Added String,NULL)
(6,"santa monica, california, usa"- Added String,61)
 

Wednesday, August 26, 2015

How to use MultipleInputFormat in Map Reduce

Hi Friends ,
Today I wanted to give a demo on MultipleInputFormat.In an ideal world we expect our data to come from one source and that too in particular format. for example - if the data exists in  csv file , we expect it to be comma separated and complete data in 1 file . That makes life easy ... isn't it ?

But what if the data exists in multiple different formats and coming from multiple different sources . Lets try to understand this with an example.

Lets say we need to run the following query :

Select name, count(*) from user .
This is a simple query if we need to run this on database but what if the data exists in 2 different formats and 2 different files.
1) CSV Format

Id,Name,Gender
1,Krishna,M
2,Tina,F
3,Umesh,M
4,Nakeeran,F
5,Varun,M
6,Varun,M


2) Tab Format

Id    Name    Gender
7    Tom     M
8    Harry    F
9    Paul    M
10    Nakeeran    F
11    Gopi    M
12    Varun    M



If we want to get the output similar to above written query's output using Mapreduce , we need to look into the different input formats  provided by hadoop.

Lets break the problem in small parts.
1) read the csv file -> we need to create a mapper for this
InputFormat-> TextInputFormat

public static class MiCsvMapper extends
            Mapper<LongWritable, Text, Text, IntWritable> {

        @Override
        protected void map(LongWritable key, Text value, Context context)
                throws java.io.IOException, InterruptedException {
            if (value.toString() != "" && value.toString().contains(",")) {
                String[] array = value.toString().split(",");

                String userInfo = array[0] + "," + array[1] + "," + array[2];

                context.write(new Text(array[1]), new IntWritable(1));

            }
        };
    }





2) read the tab file -> we need to create a mapper for this
InputFormat-> KeyValueTextInputFormat

public static class MiTabMapper extends
            Mapper<Text, Text, Text, IntWritable> {
        @Override
        protected void map(Text key, Text value, Context context)
                throws java.io.IOException, InterruptedException {
            if (value.toString() != "" && value.toString().contains("\t")) {
                String[] array = value.toString().split("\t");
                context.write(new Text(array[0]), new IntWritable(1));
            }
        }
    }





3) Now a simple reducer as if we have only 1 file. Reducer do not care about different sources as long as its getting data in desired format that it can process.

public static class MiReducer extends
            Reducer<Text, IntWritable, Text, IntWritable> {

        @Override
        protected void reduce(Text key, java.lang.Iterable<IntWritable> values,
                Context context) throws java.io.IOException,
                InterruptedException {

            int count = 0;

            for (IntWritable x : values) {
                int val = Integer.parseInt(x.toString());
                count += val;
            }

            context.write(key, new IntWritable(count));

        };
    }





4) But how do we tell Hadoop that we have 2 different formats of input file.
let's take  a look at Driver code.
 MultipleInputs.addInputPath method will take care of multiple files by passing the 4 parameters :
a) job Object
b) Input File path
c) Input    File Format
d) Mapper class

public int run(String[] args) throws Exception {
        // TODO Auto-generated method stub
        Configuration conf = getConf();

        Job job = new Job(conf, "MI Demo");

        job.setJarByClass(MIDemo.class);


        MultipleInputs.addInputPath(job, new Path(args[0]),
                TextInputFormat.class, MiCsvMapper.class);

        MultipleInputs.addInputPath(job, new Path(args[1]),
                KeyValueTextInputFormat.class, MiTabMapper.class);

        job.setReducerClass(MiReducer.class);


        job.setOutputFormatClass(TextOutputFormat.class);

        job.setOutputKeyClass(Text.class);

        job.setOutputValueClass(IntWritable.class);

        // Path in = new Path(args[0]);

        Path out = new Path(args[2]);

        // FileInputFormat.addInputPath(job, in);

        FileOutputFormat.setOutputPath(job, out);

        System.exit(job.waitForCompletion(true) ? 0 : 1);

        return 0;
    }




5) Now you can run the code with following command. Notice 2 input files and 1 output file.

hadoop jar /home/Desktop/Projects/MultipleInputDemo/MDemo.jar  MIDemo /MultiDemoInput/Users.csv /MultiDemoInput/UsersTab  /MultiDemoOutput/out2

6) Finally the output looks like :
Gopi    1
Harry    1
Krishna    1
Nakeeran    2
Name    2
Paul    1
Tina    1
Tom     1
Umesh    1
Varun    3
 
And that is how you can use MultipleInputFormat . Hope you understood the concept of MultipleInputFormat.

Have a great day...
Happy Hadooping !!!

Varun

Monday, August 24, 2015

TextInputFormat Vs KeyValueTextInputFormat

Lets try to understand the difference with the help of an example. Suppose we have following data .
Id,Name,Gender

1,Krishna,M
2,Tina,F
3,Umesh,M
4,Nakeeran,F
5,Varun,M

Now lets write Mapreduce code to print the input as output using TextInputFormat .

By default TextInputFormat have the key value pair in this format:

Key -> LongWritable
Value -> Text

Here is the mapper code.

Please note here my K1-> LongWritable line offset
                                 V1-> Text
We don't need any reducers and we will print all data as K2->Text and V2-> Text

public static class SMapperDemo extends
            Mapper<LongWritable, Text, Text, Text> {

        @Override
        protected void map(LongWritable key, Text value, Context context)
                throws java.io.IOException, InterruptedException {
            if (value.toString() != "" && value.toString().contains(",")) {
                String[] array = value.toString().split(",");

                String userInfo = array[0] + "," + array[1] + "," + array[2];

                context.write(new Text(key.toString()), new Text(userInfo));
            }
        };
    }



The driver code is simple as well.

@Override
    public int run(String[] args) throws Exception {
        // TODO Auto-generated method stub

        Configuration conf = getConf();

        

        Job job = new Job(conf, "Sql Demo");

        job.setJarByClass(SDemo.class);

        job.setMapperClass(SMapperDemo.class);

        job.setNumReduceTasks(0);

        job.setInputFormatClass(TextInputFormat.class);

        job.setOutputFormatClass(TextOutputFormat.class);

        job.setOutputKeyClass(Text.class);

        job.setOutputValueClass(Text.class);

        Path in = new Path(args[0]);

        Path out = new Path(args[1]);

        FileInputFormat.addInputPath(job, in);

        FileOutputFormat.setOutputPath(job, out);

        System.exit(job.waitForCompletion(true) ? 0 : 1);

        return 0;
    }



On running this code, it will print line offset as Key and Line itself as value. The output looks like :

0    Id,Name,Gender
15    1,Krishna,M
27    2,Tina,F
36    3,Umesh,M
46    4,Nakeeran,F
59    5,Varun,M


Now lets write Mapreduce code to print the input as output using KeyValueTextInputFormat . By default KeyValueTextInputFormat is tab separated  and its K1 and V1 is of type Text.

This is the driver code. I have done 2 changes here. Changed the default line separator to "," and set the input format as KeyValueTextInputFormat.

public int run(String[] args) throws Exception {
        // TODO Auto-generated method stub

        Configuration conf = getConf();

        conf.set("key.value.separator.in.input.line", ",");

        Job job = new Job(conf, "Sql Demo");

        job.setJarByClass(SDemo.class);

        job.setMapperClass(SMapperDemo.class);

        job.setNumReduceTasks(0);

        job.setInputFormatClass(KeyValueTextInputFormat.class);

        job.setOutputFormatClass(TextOutputFormat.class);

        job.setOutputKeyClass(Text.class);

        job.setOutputValueClass(Text.class);

        Path in = new Path(args[0]);

        Path out = new Path(args[1]);

        FileInputFormat.addInputPath(job, in);

        FileOutputFormat.setOutputPath(job, out);

        System.exit(job.waitForCompletion(true) ? 0 : 1);

        return 0;
    }


The mapper code is reading the input file . Since the separator is ",", the key will be the text before 1st comma and the value will be remaining line. ex:
Input Line -> 1,Krishna,M
Output Key -> 1
Output Value -> Krishna,M

@Override
        protected void map(Text key, Text value, Context context)
                throws java.io.IOException, InterruptedException {
            if (value.toString() != "" && value.toString().contains(",")) {
                String[] array = value.toString().split(",");




// array[2] will give index out of bound error since the value only has remaining // part of line after 1st comma 

                String userInfo = array[0] + "," + array[1];// + "," + array[2];

                context.write(new Text(key.toString()), new Text(userInfo));
            }
        };



And the output looks like :
Id    Name,Gender
1    Krishna,M
2    Tina,F
3    Umesh,M
4    Nakeeran,F
5    Varun,M


Here Id became key and remaining line became value.

Happy Hadooping!!

have a great day.

Varun

Pig Basics

Hey guys,

I just thought of writing few commands for some one who is beginner in PIG .
So let's get started. We will consider the data in  file in this format.

year,product,quantity
2000, iphone, 1000
2001, iphone, 1500
2002, iphone, 2000
2000, nokia,  1200
2001, nokia,  1500
2002, nokia,  900

1)  Load Command
A = LOAD '/home/Desktop/Projects/Book/UserTest' USING PigStorage(',') AS (year:chararray,product:chararray,quantity:int);

2) If you want to see the records.
Dump A;

3) To check the schema.
Describe A;

4) Filter :
B = Filter A BY year == '2000';';

5) group :

C = Group A BY year;
This command will give Tuples.

(2000,{(2000, nokia,  1200),(2000, iphone, 1000)})
(2001,{(2001, nokia,  1500),(2001, iphone, 1500 )})
(2002,{(2002, nokia,  900),(2002, iphone, 2000)})


 // here 1st field Key on which data is grouped and
// 2nd field is Bag of Tuples

You can also try this :

describe C;

C: {group: chararray, A: {(year: chararray,product: chararray,quantity: chararray)}}

Here "group" is the alias given to grouping field by Pig.
A is the data on which grouping is done. 

6)  max_quant = foreach C GENERATE group, MAX (A.quantity);

Dump max_quant;
(2000,1200)
(2001,1500)
(2002,2000)










7) ILLUSTRATE: ILLUSTRATE A; // TO GET IDEA OF DATA AND QUERY 

----------------------------------------------------------------------------
| A     | year:chararray    | product:chararray    | quantity:int    |
----------------------------------------------------------------------------
|       | 2001              |  iphone              |  1500                 |
----------------------------------------------------------------------------



That's it for the day. I think these commands are good enough to get anyone started with PIG..

Thanks
Varun


Sunday, August 23, 2015

Replicated Join Demo

Problem : How to do replicated join in Hadoop ?

Recently I learnt a new concept in Hadoop . By definition, Replicated Join means when one data set is too big and other data set is small enough that can be stored in memory , we can use Replicated Joins. Using this method, the join can be performed in the map side.

Here is the example of replicated join using Distributed Cache . I am using very simple xml files as my input data . Consider User data as small data that we will store in memory .


User Data

User Data
 












Comments Data










Please refer  link to create XmlInputFormat class . This is useful for reading XML data.
Now we can write Mapper Code. In the mapper's setup method, we are reading User xml file and storing in HashMap object.


public static class ReplicatedJoinMapper extends

   Mapper<LongWritable, Text, Text, Text> {

  private static final Log Logger = LogFactory

    .getLog(ReplicatedJoinMapper.class);

  private final String Empty_String = "null";


  private String joinType = null;

  private final HashMap<String, String> userInfo = new HashMap<String, String>();


  @Override

  protected void setup(Context context) throws java.io.IOException,

    InterruptedException {



   Path[] files = DistributedCache.getLocalCacheFiles(context

     .getConfiguration());



   if (files != null && files.length > 0) {

    try {

     if (files[0].getName().equals("User")) {

      // InputStream is = new

      // FileInputStream(files[0].toString());

      File is = new File(files[0].toString());


      DocumentBuilderFactory dbFactory = DocumentBuilderFactory

        .newInstance();


      DocumentBuilder dBuilder = dbFactory

        .newDocumentBuilder();

      Logger.info("-------------------------->2 = ----------->");

      Document doc = dBuilder.parse(is.toString().replaceAll(

        "[^\\x20-\\x7e\\x0A]", ""));



      doc.getDocumentElement().normalize();


      NodeList nList = doc.getElementsByTagName("employee");


      for (int temp = 0; temp < nList.getLength(); temp++) {


       Node nNode = nList.item(temp);


       if (nNode.getNodeType() == Node.ELEMENT_NODE) {


        Element eElement = (Element) nNode;


        String id = eElement.getElementsByTagName("id")

          .item(0).getTextContent();

      


        String name = eElement

          .getElementsByTagName("name").item(0)

          .getTextContent();

   

        String gender = eElement

          .getElementsByTagName("gender").item(0)

          .getTextContent();

   


        userInfo.put(id, id + "," + name + "," + gender);

        joinType = "inner";

       }

      }

     }


    } catch (Exception e) {

     // TODO: handle exception

     Logger.error(e.getMessage());

    }


   } else {

    Logger.info("-------------------------->file not found----------->");

   }

  }





// Here in map method we will read Comments XML file data


  @Override

  protected void map(LongWritable key, Text value, Context context)

    throws IOException, InterruptedException {

   try {


    InputStream is = new ByteArrayInputStream(value.toString()

      .getBytes());

    DocumentBuilderFactory dbFactory = DocumentBuilderFactory

      .newInstance();

    DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();

    Document doc = dBuilder.parse(is);


    doc.getDocumentElement().normalize();


    NodeList nList = doc.getElementsByTagName("employee");


    for (int temp = 0; temp < nList.getLength(); temp++) {


     Node nNode = nList.item(temp);


     if (nNode.getNodeType() == Node.ELEMENT_NODE) {


      Element eElement = (Element) nNode;


      String id = eElement.getElementsByTagName("id").item(0)

        .getTextContent();

     


      String comment = eElement

        .getElementsByTagName("comment").item(0)

        .getTextContent();

    

      String userInfoLine = userInfo.get(id);

  

      if (userInfoLine != null) {


// Inner Join

       Logger.info("--------------------------> writing context----------->");

       context.write(new Text(id), new Text(userInfoLine

         + "," + comment));

      } else {


// Outer join

       context.write(new Text(id), new Text(Empty_String));

      }

     }

    }


   } catch (Exception e) {

    // TODO: handle exception

    Logger.error(e.getCause());

   }

  };

 }





Finally we can write Driver code. Here we are using DistributedCache.addCacheFile method to provide User xml to each map tasks. The same file is read in set up method above.





@Override

 public int run(String[] args) throws Exception {


  final Log Logger = LogFactory.getLog(RJDemo.class);


  Configuration conf = getConf();


  String[] arg = new GenericOptionsParser(conf, args).getRemainingArgs();


  


// These tags are in my xml file


  conf.set("START_TAG_KEY", "<employee>");

  conf.set("END_TAG_KEY", "</employee>");





  Job job = new Job(conf, "SO DEMO");


   try {

   DistributedCache.addCacheFile(new URI("/SODemoInput/User"),

   job.getConfiguration());

   } catch (Exception e) {

   e.printStackTrace();

   System.out.println(e);

   }

  job.setJarByClass(RJDemo.class);


  job.setMapperClass(ReplicatedJoinMapper.class);


  job.setNumReduceTasks(0);


  job.setInputFormatClass(XmlInputFormat.class);


  job.setOutputFormatClass(TextOutputFormat.class);


  // job.setOutputKeyClass(Text.class);


  job.setOutputValueClass(Text.class);


  Path in = new Path(args[0]);


  Path out = new Path(args[1]);

  out.getFileSystem(conf).delete(out);


  FileInputFormat.addInputPath(job, in);


  FileOutputFormat.setOutputPath(job, out);


  System.exit(job.waitForCompletion(true) ? 0 : 1);


  return 0;

 }





Finally run this command to see output as:

hadoop jar  /home/Desktop/Projects/ReplicatedJoinDemo/RJDemo.jar RJDemo  /SODemoInput/Comments  /SODemoOutput/Out32



And the output looks like :










And that's it :)

 Have a good day ..
Happy Hadooping !!




Monday, May 18, 2015

Log4j in Map Reduce programming

Problem : How to use Log4j in Map Reduce programming

Solution :

Include Log4j jar file in your project.
In your mapper class, use log4j as :



private static final Logger sLogger = Logger.getLogger(TwitterMapper.class); 
   protected void map(LongWritable key, Text value, Context context)
           throws java.io.IOException ,InterruptedException
           {
                sLogger.info("          ");
            }  
 

When the program is executed , look for following line :

15/05/19 07:06:08 INFO mapreduce.Job: The url to track the job: http://localhost:8088/proxy/application_1431707941929_0002/

This is telling the path where logs are created. 

In the browser, http://localhost:8088

Tools > Logs > UserLogs > application_1431707941929_0002 (id will be different in your scenario )

Browse through the files to find the logs you have written in your map reduce code.

And that's it :)

Have a good day ..
Happy Hadooping !!