Index: src/java/org/apache/nutch/searcher/NutchSearchServlet.java
===================================================================
--- src/java/org/apache/nutch/searcher/NutchSearchServlet.java	(revision 0)
+++ src/java/org/apache/nutch/searcher/NutchSearchServlet.java	(revision 0)
@@ -0,0 +1,155 @@
+package org.apache.nutch.searcher;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.util.StringUtils;
+import org.apache.nutch.searcher.formatter.ResultsFormatter;
+import org.apache.nutch.searcher.formatter.XMLResultsFormatter;
+import org.apache.nutch.util.NutchConfiguration;
+
+public class NutchSearchServlet
+  extends HttpServlet {
+
+  private Map<String, ResultsFormatter> formatters = new HashMap<String, ResultsFormatter>();
+  private NutchBean bean;
+  private Configuration conf;
+
+  public void init(ServletConfig config)
+    throws ServletException {
+
+    super.init(config);
+
+    XMLResultsFormatter xmlFrmt = new XMLResultsFormatter();
+    formatters.put(xmlFrmt.getTypeIdentifier(), xmlFrmt);
+
+    String formatStr = this.getInitParameter("formatters");
+    if (formatStr != null && formatStr.length() > 0) {
+      String[] frmtClasses = formatStr.split(",");
+      try {
+        for (int i = 0; i < frmtClasses.length; i++) {
+          String frmtName = frmtClasses[i].trim();
+          Class frmtCls = Class.forName(frmtName);
+          ResultsFormatter formatter = (ResultsFormatter) frmtCls.newInstance();
+          String typeId = formatter.getTypeIdentifier();
+          formatters.put(typeId, formatter);
+        }
+      } catch (Exception e) {
+        NutchBean.LOG.info("Error creating formatter:\n. "
+          + StringUtils.stringifyException(e));
+      }
+    }
+
+    try {
+      this.conf = NutchConfiguration.get(config.getServletContext());
+      bean = NutchBean.get(config.getServletContext(), this.conf);
+    } catch (IOException e) {
+      throw new ServletException(e);
+    }
+  }
+
+  public void doGet(HttpServletRequest request, HttpServletResponse response)
+    throws ServletException, IOException {
+
+    if (NutchBean.LOG.isInfoEnabled()) {
+      NutchBean.LOG.info("Query request from " + request.getRemoteAddr());
+    }
+
+    // get parameters from request
+    request.setCharacterEncoding("UTF-8");
+    String queryString = request.getParameter("query");
+    if (queryString == null)
+      queryString = "";
+
+    String formatType = request.getParameter("type");
+    if (formatType == null)
+      formatType = "xml";
+
+    // the query language
+    String queryLang = request.getParameter("lang");
+    if (queryLang == null)
+      queryLang = "en";
+
+    int start = 0; // first hit to display
+    String startString = request.getParameter("start");
+    if (startString != null)
+      start = Integer.parseInt(startString);
+
+    int hitsPerPage = 10; // number of hits to display
+    String hitsString = request.getParameter("hitsPerPage");
+    if (hitsString != null)
+      hitsPerPage = Integer.parseInt(hitsString);
+
+    String sort = request.getParameter("sort");
+    boolean reverse = sort != null
+      && "true".equals(request.getParameter("reverse"));
+
+    // De-Duplicate handling. Look for duplicates field and for how many
+    // duplicates per results to return. Default duplicates field is 'site'
+    // and duplicates per results default is '2'.
+    String dedupField = request.getParameter("dedupField");
+    if (dedupField == null || dedupField.length() == 0) {
+      dedupField = "site";
+    }
+    int hitsPerDup = 2;
+    String hitsPerDupString = request.getParameter("hitsPerDup");
+    if (hitsPerDupString != null && hitsPerDupString.length() > 0) {
+      hitsPerDup = Integer.parseInt(hitsPerDupString);
+    } else {
+      // If 'hitsPerSite' present, use that value.
+      String hitsPerSiteString = request.getParameter("hitsPerSite");
+      if (hitsPerSiteString != null && hitsPerSiteString.length() > 0) {
+        hitsPerDup = Integer.parseInt(hitsPerSiteString);
+      }
+    }
+
+    Query query = Query.parse(queryString, queryLang, this.conf);
+    if (NutchBean.LOG.isInfoEnabled()) {
+      NutchBean.LOG.info("query: " + queryString);
+      NutchBean.LOG.info("lang: " + queryLang);
+    }
+
+    // execute the query
+    Hits hits;
+    try {
+      hits = bean.search(query, start + hitsPerPage, hitsPerDup, dedupField,
+        sort, reverse);
+    } catch (IOException e) {
+      if (NutchBean.LOG.isWarnEnabled()) {
+        NutchBean.LOG.warn("Search Error", e);
+      }
+      hits = new Hits(0, new Hit[0]);
+    }
+
+    if (NutchBean.LOG.isInfoEnabled()) {
+      NutchBean.LOG.info("total hits: " + hits.getTotal());
+    }
+
+    // generate results
+    int end = (int) Math.min(hits.getLength(), start + hitsPerPage);
+
+    Hit[] show = hits.getHits(start, end - start);
+    HitDetails[] details = bean.getDetails(show);
+    Summary[] summaries = bean.getSummary(details, query);
+
+    ResultsFormatter formatter = formatters.get(formatType);
+    if (formatter == null) {
+      formatter = formatters.get("xml");
+    }
+
+    byte[] output = formatter.formatSearchResults(show, details, summaries,
+      queryLang, queryString, sort, dedupField, hitsPerDup, reverse, start,
+      end, hits.getTotal());
+    response.setContentType(formatter.getContentType());
+    response.getOutputStream().write(output);
+    response.flushBuffer();
+  }
+}
Index: src/java/org/apache/nutch/searcher/formatter/JSONResultsFormatter.java
===================================================================
--- src/java/org/apache/nutch/searcher/formatter/JSONResultsFormatter.java	(revision 0)
+++ src/java/org/apache/nutch/searcher/formatter/JSONResultsFormatter.java	(revision 0)
@@ -0,0 +1,114 @@
+package org.apache.nutch.searcher.formatter;
+
+import java.net.URLEncoder;
+
+import org.apache.nutch.html.Entities;
+import org.apache.nutch.searcher.Hit;
+import org.apache.nutch.searcher.HitDetails;
+import org.apache.nutch.searcher.Summary;
+
+public class JSONResultsFormatter
+  implements ResultsFormatter {
+
+  private String printStartNode(String name) {
+    return ("\"" + name + "\": {");
+  }
+
+  private String printEndNode() {
+    return "}";
+  }
+
+  private String printString(String name, String value, boolean last) {
+    return ("\"" + name + "\": \"" + Entities.encode(value) + "\"" + (last ? "" : ","));
+  }
+
+  private String printLong(String name, long value, boolean last) {
+    return ("\"" + name + "\": " + String.valueOf(value) + (last ? "" : ","));
+  }
+
+  private String printInt(String name, int value, boolean last) {
+    return ("\"" + name + "\": " + String.valueOf(value) + (last ? "" : ","));
+  }
+
+  private String printArray(String name, String[] values, boolean last) {
+    StringBuilder arrayBuilder = new StringBuilder();
+    arrayBuilder.append("\"" + name + "\": [");
+    for (int i = 0; i < values.length; i++) {
+      arrayBuilder.append("\"" + Entities.encode(values[i]) + "\"");
+      if (i < (values.length - 1)) {
+        arrayBuilder.append(",");
+      }
+    }
+    arrayBuilder.append("]" + (last ? "" : ","));
+    return arrayBuilder.toString();
+  }
+
+  public byte[] formatSearchResults(Hit[] hits, HitDetails[] details,
+    Summary[] summaries, String lang, String query, String sort, String dedup,
+    int hitsPerDup, boolean reverse, int start, int end, long total) {
+
+    try {
+      StringBuilder json = new StringBuilder();
+      json.append("{\n");
+      json.append("\t" + printStartNode("search") + "\n");
+      json.append("\t\t" + printString("query", query, false) + "\n");
+      json.append("\t\t"
+        + printString("queryString", URLEncoder.encode(query, "UTF-8"), false)
+        + "\n");
+      json.append("\t\t" + printLong("numberOfHits", total, false) + "\n");
+
+      if (lang != null) {
+        json.append("\t\t" + printString("lang", lang, false) + "\n");
+      }
+      if (sort != null) {
+        json.append("\t\t" + printString("sortField", sort, false) + "\n");
+      }
+      if (dedup != null) {
+        json.append("\t\t" + printString("dedupField", dedup, false) + "\n");
+      }
+
+      json.append("\t\t" + printInt("hitsPerDup", hitsPerDup, false) + "\n");
+      json.append("\t\t"
+        + printString("reverse", String.valueOf(reverse), false) + "\n");
+      json.append("\t\t" + printInt("start", start, false) + "\n");
+      json.append("\t\t" + printInt("end", end, false) + "\n");
+
+      json.append("\t\t" + printStartNode("documents") + "\n");
+      for (int i = 0; i < details.length; i++) {
+
+        json.append("\t\t\t" + printStartNode("document") + "\n");
+        json.append("\t\t\t\t" + printInt("indexNo", hits[i].getIndexNo(), false) + "\n");
+        json.append("\t\t\t\t" + printInt("indexDocumentNo", hits[i].getIndexDocNo(), false) + "\n");
+        json.append("\t\t\t\t" + printString("summary", summaries[i].toString(), false) + "\n");
+
+        json.append("\t\t\t\t" + printStartNode("fields") + "\n");
+        HitDetails detail = details[i];
+        for (int j = 0; j < detail.getLength(); j++) {
+          String fieldName = detail.getField(j);
+          String[] fieldValues = detail.getValues(fieldName);
+          json.append("\t\t\t\t\t" + printArray(fieldName, fieldValues,
+            j == (detail.getLength() - 1)) + "\n");
+        }
+        json.append("\t\t\t\t" + printEndNode() + "\n"); // fields
+        json.append("\t\t\t" + printEndNode() + (i < (details.length - 1) ? "," : "") + "\n"); // document
+      }
+      json.append("\t\t" + printEndNode() + "\n"); // documents
+      json.append("\t" + printEndNode() + "\n"); // search
+      json.append("}" + "\n");
+
+      return ("processJSON(" + json.toString() + ")").getBytes();
+
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public String getContentType() {
+    return "application/x-javascript";
+  }
+
+  public String getTypeIdentifier() {
+    return "json";
+  }
+
+}
Index: src/java/org/apache/nutch/searcher/formatter/ResultsFormatter.java
===================================================================
--- src/java/org/apache/nutch/searcher/formatter/ResultsFormatter.java	(revision 0)
+++ src/java/org/apache/nutch/searcher/formatter/ResultsFormatter.java	(revision 0)
@@ -0,0 +1,17 @@
+package org.apache.nutch.searcher.formatter;
+
+import org.apache.nutch.searcher.Hit;
+import org.apache.nutch.searcher.HitDetails;
+import org.apache.nutch.searcher.Summary;
+
+public interface ResultsFormatter {
+
+  public String getContentType();
+
+  public String getTypeIdentifier();
+
+  public byte[] formatSearchResults(Hit[] hit, HitDetails[] details,
+    Summary[] summaries, String lang, String query, String sort, String dedup,
+    int hitsPerDup, boolean reverse, int start, int end, long total);
+
+}
Index: src/java/org/apache/nutch/searcher/formatter/XMLResultsFormatter.java
===================================================================
--- src/java/org/apache/nutch/searcher/formatter/XMLResultsFormatter.java	(revision 0)
+++ src/java/org/apache/nutch/searcher/formatter/XMLResultsFormatter.java	(revision 0)
@@ -0,0 +1,151 @@
+package org.apache.nutch.searcher.formatter;
+
+import java.io.ByteArrayOutputStream;
+import java.net.URLEncoder;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.apache.nutch.html.Entities;
+import org.apache.nutch.searcher.Hit;
+import org.apache.nutch.searcher.HitDetails;
+import org.apache.nutch.searcher.Summary;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+public class XMLResultsFormatter
+  implements ResultsFormatter {
+
+  public String getContentType() {
+    return "text/xml";
+  }
+
+  public String getTypeIdentifier() {
+    return "xml";
+  }
+
+  private static Element addNode(Document doc, Node parent, String name) {
+    Element child = doc.createElement(name);
+    parent.appendChild(child);
+    return child;
+  }
+
+  private static void addNode(Document doc, Node parent, String name,
+    String text) {
+    Element child = doc.createElement(name);
+    child.appendChild(doc.createTextNode(getLegalXml(text)));
+    parent.appendChild(child);
+  }
+
+  private static void addAttribute(Document doc, Element node, String name,
+    String value) {
+    Attr attribute = doc.createAttribute(name);
+    attribute.setValue(getLegalXml(value));
+    node.getAttributes().setNamedItem(attribute);
+  }
+
+  protected static String getLegalXml(final String text) {
+    if (text == null) {
+      return null;
+    }
+    StringBuffer buffer = null;
+    for (int i = 0; i < text.length(); i++) {
+      char c = text.charAt(i);
+      if (!isLegalXml(c)) {
+        if (buffer == null) {
+          // Start up a buffer. Copy characters here from now on
+          // now we've found at least one bad character in original.
+          buffer = new StringBuffer(text.length());
+          buffer.append(text.substring(0, i));
+        }
+      } else {
+        if (buffer != null) {
+          buffer.append(c);
+        }
+      }
+    }
+    return (buffer != null) ? buffer.toString() : text;
+  }
+
+  private static boolean isLegalXml(final char c) {
+    return c == 0x9 || c == 0xa || c == 0xd || (c >= 0x20 && c <= 0xd7ff)
+      || (c >= 0xe000 && c <= 0xfffd) || (c >= 0x10000 && c <= 0x10ffff);
+  }
+
+  public byte[] formatSearchResults(Hit[] hits, HitDetails[] details,
+    Summary[] summaries, String lang, String query, String sort, String dedup,
+    int hitsPerDup, boolean reverse, int start, int end, long total) {
+
+    try {
+      
+      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+      Document xmldoc = factory.newDocumentBuilder().newDocument();
+
+      Element results = addNode(xmldoc, xmldoc, "results");
+      Element search = addNode(xmldoc, results, "search");
+
+      addNode(xmldoc, search, "query", query);
+      addNode(xmldoc, search, "queryString", URLEncoder.encode(query, "UTF-8"));
+      addNode(xmldoc, search, "numberOfHits", String.valueOf(total));
+      
+      if (lang != null) {
+        addNode(xmldoc, search, "lang", lang);
+      }      
+      if (sort != null) {
+        addNode(xmldoc, search, "sortField", sort);
+      }
+      if (dedup != null) {
+        addNode(xmldoc, search, "dedupField", dedup);
+      }
+      addNode(xmldoc, search, "hitsPerDup", String.valueOf(hitsPerDup));
+      addNode(xmldoc, search, "reverse", String.valueOf(reverse));
+      addNode(xmldoc, search, "start", String.valueOf(start));
+      addNode(xmldoc, search, "end", String.valueOf(end));
+      
+      Element documents = addNode(xmldoc, results, "documents");
+      for (int i = 0; i < details.length; i++) {
+        
+        Element document = addNode(xmldoc, documents, "document");
+        addAttribute(xmldoc, document, "indexNo", 
+          String.valueOf(hits[i].getIndexNo()));
+        addAttribute(xmldoc, document, "indexDocumentNo", 
+          String.valueOf(hits[i].getIndexDocNo()));
+        
+        addNode(xmldoc, document, "summary", Entities.encode(summaries[i].toString()));
+        
+        Element fields = addNode(xmldoc, document, "fields");
+        HitDetails detail = details[i];
+        for (int j = 0; j < detail.getLength(); j++) {
+          String fieldName = detail.getField(j);
+          String[] fieldValues = detail.getValues(fieldName);       
+          Element field = addNode(xmldoc, fields, "field");
+          addAttribute(xmldoc, field, "name", fieldName);
+          for (int k = 0; k < fieldValues.length; k++) {
+            addNode(xmldoc, field, "value", Entities.encode(fieldValues[k]));
+          }
+        }
+      }
+
+      DOMSource source = new DOMSource(xmldoc);
+      TransformerFactory transFactory = TransformerFactory.newInstance();
+      Transformer transformer = transFactory.newTransformer();
+      transformer.setOutputProperty("indent", "yes");
+      ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      StreamResult result = new StreamResult(baos);
+      transformer.transform(source, result);
+      baos.flush();
+      baos.close();
+      
+      return baos.toByteArray();
+      
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    } 
+  }
+
+}
Index: src/web/web.xml
===================================================================
--- src/web/web.xml	(revision 604035)
+++ src/web/web.xml	(working copy)
@@ -36,6 +36,18 @@
   <servlet-class>org.apache.nutch.searcher.OpenSearchServlet</servlet-class>
 </servlet>
 
+<servlet>
+  <servlet-name>NutchSearch</servlet-name>
+  <servlet-class>org.apache.nutch.searcher.NutchSearchServlet</servlet-class>
+  <init-param>
+    <param-name>formatters</param-name>
+    <param-value>
+      org.apache.nutch.searcher.formatter.XMLResultsFormatter,
+      org.apache.nutch.searcher.formatter.JSONResultsFormatter
+  </param-value>
+  </init-param>
+</servlet>
+
 <servlet-mapping>
   <servlet-name>Cached</servlet-name>
   <url-pattern>/servlet/cached</url-pattern>
@@ -46,6 +58,11 @@
   <url-pattern>/opensearch</url-pattern>
 </servlet-mapping>
 
+<servlet-mapping>
+  <servlet-name>NutchSearch</servlet-name>
+  <url-pattern>/nutchsearch</url-pattern>
+</servlet-mapping>
+
 <welcome-file-list>
   <welcome-file>search.html</welcome-file>
   <welcome-file>index.html</welcome-file>
