org.apache.nutch.indexwriter.cloudsearch.CloudSearchIndexWriter.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.nutch.indexwriter.cloudsearch.CloudSearchIndexWriter.java

Source

/*
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.
 */
package org.apache.nutch.indexwriter.cloudsearch;

import java.lang.invoke.MethodHandles;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.AbstractMap;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.nutch.indexer.IndexWriter;
import org.apache.nutch.indexer.IndexWriterParams;
import org.apache.nutch.indexer.NutchDocument;
import org.apache.nutch.indexer.NutchField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.amazonaws.regions.RegionUtils;
import com.amazonaws.services.cloudsearchdomain.AmazonCloudSearchDomainClient;
import com.amazonaws.services.cloudsearchdomain.model.ContentType;
import com.amazonaws.services.cloudsearchdomain.model.UploadDocumentsRequest;
import com.amazonaws.services.cloudsearchdomain.model.UploadDocumentsResult;
import com.amazonaws.services.cloudsearchv2.AmazonCloudSearchClient;
import com.amazonaws.services.cloudsearchv2.model.DescribeDomainsRequest;
import com.amazonaws.services.cloudsearchv2.model.DescribeDomainsResult;
import com.amazonaws.services.cloudsearchv2.model.DescribeIndexFieldsRequest;
import com.amazonaws.services.cloudsearchv2.model.DescribeIndexFieldsResult;
import com.amazonaws.services.cloudsearchv2.model.DomainStatus;
import com.amazonaws.services.cloudsearchv2.model.IndexFieldStatus;
import com.amazonaws.util.json.JSONException;
import com.amazonaws.util.json.JSONObject;

/**
 * Writes documents to CloudSearch.
 */
public class CloudSearchIndexWriter implements IndexWriter {
    private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

    private static final int MAX_SIZE_BATCH_BYTES = 5242880;
    private static final int MAX_SIZE_DOC_BYTES = 1048576;

    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");

    private AmazonCloudSearchDomainClient client;

    private int maxDocsInBatch = -1;

    private StringBuffer buffer;

    private int numDocsInBatch = 0;

    private boolean dumpBatchFilesToTemp = false;

    private Configuration conf;

    private Map<String, String> csfields = new HashMap<String, String>();

    private String endpoint;
    private String regionName;

    @Override
    public void open(Configuration conf, String name) throws IOException {
        //Implementation not required
    }

    @Override
    public void open(IndexWriterParams parameters) throws IOException {
        //    LOG.debug("CloudSearchIndexWriter.open() name={} ", name);

        endpoint = parameters.get(CloudSearchConstants.ENDPOINT);
        dumpBatchFilesToTemp = parameters.getBoolean(CloudSearchConstants.BATCH_DUMP, false);
        this.regionName = parameters.get(CloudSearchConstants.REGION);

        if (StringUtils.isBlank(endpoint) && !dumpBatchFilesToTemp) {
            String message = "Missing CloudSearch endpoint. Should set it set via -D "
                    + CloudSearchConstants.ENDPOINT + " or in nutch-site.xml";
            message += "\n" + describe();
            LOG.error(message);
            throw new RuntimeException(message);
        }

        maxDocsInBatch = parameters.getInt(CloudSearchConstants.MAX_DOCS_BATCH, -1);

        buffer = new StringBuffer(MAX_SIZE_BATCH_BYTES).append('[');

        if (dumpBatchFilesToTemp) {
            // only dumping to local file
            // no more config required
            return;
        }

        if (StringUtils.isBlank(endpoint)) {
            throw new RuntimeException("endpoint not set for CloudSearch");
        }

        AmazonCloudSearchClient cl = new AmazonCloudSearchClient();
        if (StringUtils.isNotBlank(regionName)) {
            cl.setRegion(RegionUtils.getRegion(regionName));
        }

        String domainName = null;

        // retrieve the domain name
        DescribeDomainsResult domains = cl.describeDomains(new DescribeDomainsRequest());

        Iterator<DomainStatus> dsiter = domains.getDomainStatusList().iterator();
        while (dsiter.hasNext()) {
            DomainStatus ds = dsiter.next();
            if (ds.getDocService().getEndpoint().equals(endpoint)) {
                domainName = ds.getDomainName();
                break;
            }
        }

        // check domain name
        if (StringUtils.isBlank(domainName)) {
            throw new RuntimeException("No domain name found for CloudSearch endpoint");
        }

        DescribeIndexFieldsResult indexDescription = cl
                .describeIndexFields(new DescribeIndexFieldsRequest().withDomainName(domainName));
        for (IndexFieldStatus ifs : indexDescription.getIndexFields()) {
            String indexname = ifs.getOptions().getIndexFieldName();
            String indextype = ifs.getOptions().getIndexFieldType();
            LOG.info("CloudSearch index name {} of type {}", indexname, indextype);
            csfields.put(indexname, indextype);
        }

        client = new AmazonCloudSearchDomainClient();
        client.setEndpoint(endpoint);
    }

    @Override
    public void delete(String url) throws IOException {

        try {
            JSONObject doc_builder = new JSONObject();

            doc_builder.put("type", "delete");

            // generate the id from the url
            String ID = CloudSearchUtils.getID(url);
            doc_builder.put("id", ID);

            // add to the batch
            addToBatch(doc_builder.toString(2), url);

        } catch (JSONException e) {
            LOG.error("Exception caught while building JSON object", e);
        }

    }

    @Override
    public void update(NutchDocument doc) throws IOException {
        write(doc);
    }

    @Override
    public void write(NutchDocument doc) throws IOException {
        try {
            JSONObject doc_builder = new JSONObject();

            doc_builder.put("type", "add");

            String url = doc.getField("url").toString();

            // generate the id from the url
            String ID = CloudSearchUtils.getID(url);
            doc_builder.put("id", ID);

            JSONObject fields = new JSONObject();

            for (final Entry<String, NutchField> e : doc) {
                String fieldname = cleanFieldName(e.getKey());
                String type = csfields.get(fieldname);

                // undefined in index
                if (!dumpBatchFilesToTemp && type == null) {
                    LOG.info("Field {} not defined in CloudSearch domain for {} - skipping.", fieldname, url);
                    continue;
                }

                List<Object> values = e.getValue().getValues();
                // write the values
                for (Object value : values) {
                    // Convert dates to an integer
                    if (value instanceof Date) {
                        Date d = (Date) value;
                        value = DATE_FORMAT.format(d);
                    }
                    // normalise strings
                    else if (value instanceof String) {
                        value = CloudSearchUtils.stripNonCharCodepoints((String) value);
                    }

                    fields.accumulate(fieldname, value);
                }
            }

            doc_builder.put("fields", fields);

            addToBatch(doc_builder.toString(2), url);

        } catch (JSONException e) {
            LOG.error("Exception caught while building JSON object", e);
        }
    }

    private void addToBatch(String currentDoc, String url) throws IOException {
        int currentDocLength = currentDoc.getBytes(StandardCharsets.UTF_8).length;

        // check that the doc is not too large -> skip it if it does
        if (currentDocLength > MAX_SIZE_DOC_BYTES) {
            LOG.error("Doc too large. currentDoc.length {} : {}", currentDocLength, url);
            return;
        }

        int currentBufferLength = buffer.toString().getBytes(StandardCharsets.UTF_8).length;

        LOG.debug("currentDoc.length {}, buffer length {}", currentDocLength, currentBufferLength);

        // can add it to the buffer without overflowing?
        if (currentDocLength + 2 + currentBufferLength < MAX_SIZE_BATCH_BYTES) {
            if (numDocsInBatch != 0)
                buffer.append(',');
            buffer.append(currentDoc);
            numDocsInBatch++;
        }
        // flush the previous batch and create a new one with this doc
        else {
            commit();
            buffer.append(currentDoc);
            numDocsInBatch++;
        }

        // have we reached the max number of docs in a batch after adding
        // this doc?
        if (maxDocsInBatch > 0 && numDocsInBatch == maxDocsInBatch) {
            commit();
        }
    }

    @Override
    public void commit() throws IOException {

        // nothing to do
        if (numDocsInBatch == 0) {
            return;
        }

        // close the array
        buffer.append(']');

        LOG.info("Sending {} docs to CloudSearch", numDocsInBatch);

        byte[] bb = buffer.toString().getBytes(StandardCharsets.UTF_8);

        if (dumpBatchFilesToTemp) {
            try {
                File temp = File.createTempFile("CloudSearch_", ".json");
                FileUtils.writeByteArrayToFile(temp, bb);
                LOG.info("Wrote batch file {}", temp.getName());
            } catch (IOException e1) {
                LOG.error("Exception while generating batch file", e1);
            } finally {
                // reset buffer and doc counter
                buffer = new StringBuffer(MAX_SIZE_BATCH_BYTES).append('[');
                numDocsInBatch = 0;
            }
            return;
        }
        // not in debug mode
        try (InputStream inputStream = new ByteArrayInputStream(bb)) {
            UploadDocumentsRequest batch = new UploadDocumentsRequest();
            batch.setContentLength((long) bb.length);
            batch.setContentType(ContentType.Applicationjson);
            batch.setDocuments(inputStream);
            @SuppressWarnings("unused")
            UploadDocumentsResult result = client.uploadDocuments(batch);
        } catch (Exception e) {
            LOG.error("Exception while sending batch", e);
            LOG.error(buffer.toString());
        } finally {
            // reset buffer and doc counter
            buffer = new StringBuffer(MAX_SIZE_BATCH_BYTES).append('[');
            numDocsInBatch = 0;
        }
    }

    @Override
    public void close() throws IOException {
        // This will flush any unsent documents.
        commit();
        // close the client
        if (client != null) {
            client.shutdown();
        }
    }

    public Configuration getConf() {
        return this.conf;
    }

    @Override
    public void setConf(Configuration conf) {
        this.conf = conf;
    }

    /**
     * Returns {@link Map} with the specific parameters the IndexWriter instance can take.
     *
     * @return The values of each row. It must have the form <KEY,<DESCRIPTION,VALUE>>.
     */
    @Override
    public Map<String, Entry<String, Object>> describe() {
        Map<String, Map.Entry<String, Object>> properties = new LinkedHashMap<>();

        properties.put(CloudSearchConstants.ENDPOINT, new AbstractMap.SimpleEntry<>(
                "Endpoint where service requests should be submitted.", this.endpoint));
        properties.put(CloudSearchConstants.REGION, new AbstractMap.SimpleEntry<>("Region name.", this.regionName));
        properties.put(CloudSearchConstants.BATCH_DUMP, new AbstractMap.SimpleEntry<>(
                "true to send documents to a local file.", this.dumpBatchFilesToTemp));
        properties.put(CloudSearchConstants.MAX_DOCS_BATCH, new AbstractMap.SimpleEntry<>(
                "Maximum number of documents to send as a batch to CloudSearch.", this.maxDocsInBatch));

        return properties;
    }

    /**
     * Remove the non-cloudSearch-legal characters. Note that this might convert
     * two fields to the same name.
     *
     * @param name
     * @return
     */
    String cleanFieldName(String name) {
        String lowercase = name.toLowerCase();
        return lowercase.replaceAll("[^a-z_0-9]", "_");
    }

}