diff options
Diffstat (limited to 'container-search/src/main/java/com/yahoo/search/rendering/DefaultRenderer.java')
-rw-r--r-- | container-search/src/main/java/com/yahoo/search/rendering/DefaultRenderer.java | 450 |
1 files changed, 450 insertions, 0 deletions
diff --git a/container-search/src/main/java/com/yahoo/search/rendering/DefaultRenderer.java b/container-search/src/main/java/com/yahoo/search/rendering/DefaultRenderer.java new file mode 100644 index 00000000000..de817d95393 --- /dev/null +++ b/container-search/src/main/java/com/yahoo/search/rendering/DefaultRenderer.java @@ -0,0 +1,450 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.search.rendering; + +import com.yahoo.concurrent.CopyOnWriteHashMap; +import com.yahoo.io.ByteWriter; +import com.yahoo.net.URI; +import com.yahoo.prelude.fastsearch.FastHit; +import com.yahoo.prelude.fastsearch.GroupingListHit; +import com.yahoo.prelude.templates.UserTemplate; +import com.yahoo.processing.rendering.AsynchronousSectionedRenderer; +import com.yahoo.processing.response.Data; +import com.yahoo.processing.response.DataList; +import com.yahoo.search.Query; +import com.yahoo.search.Result; +import com.yahoo.search.grouping.result.HitRenderer; +import com.yahoo.search.query.context.QueryContext; +import com.yahoo.search.result.*; +import com.yahoo.text.Utf8String; +import com.yahoo.text.XMLWriter; +import com.yahoo.yolean.trace.TraceNode; +import com.yahoo.yolean.trace.TraceVisitor; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.util.Iterator; +import java.util.Map; + +// TODO: Rename to XmlRenderer and make this a deprecated empty subclass. + +/** + * XML rendering of search results. This is NOT the default (but it once was). + * + * @author tonytv + */ +@SuppressWarnings({ "rawtypes", "deprecation" }) +public final class DefaultRenderer extends AsynchronousSectionedRenderer<Result> { + + public static final String DEFAULT_MIMETYPE = "text/xml"; + public static final String DEFAULT_ENCODING = "utf-8"; + + private static final Utf8String RESULT = new Utf8String("result"); + private static final Utf8String GROUP = new Utf8String("group"); + private static final Utf8String ID = new Utf8String("id"); + private static final Utf8String FIELD = new Utf8String("field"); + private static final Utf8String HIT = new Utf8String("hit"); + private static final Utf8String ERROR = new Utf8String("error"); + private static final Utf8String TOTAL_HIT_COUNT = new Utf8String("total-hit-count"); + private static final Utf8String QUERY_TIME = new Utf8String("querytime"); + private static final Utf8String SUMMARY_FETCH_TIME = new Utf8String("summaryfetchtime"); + private static final Utf8String SEARCH_TIME = new Utf8String("searchtime"); + private static final Utf8String NAME = new Utf8String("name"); + private static final Utf8String CODE = new Utf8String("code"); + private static final Utf8String COVERAGE_DOCS = new Utf8String("coverage-docs"); + private static final Utf8String COVERAGE_NODES = new Utf8String("coverage-nodes"); + private static final Utf8String COVERAGE_FULL = new Utf8String("coverage-full"); + private static final Utf8String COVERAGE = new Utf8String("coverage"); + private static final Utf8String RESULTS_FULL = new Utf8String("results-full"); + private static final Utf8String RESULTS = new Utf8String("results"); + private static final Utf8String TYPE = new Utf8String("type"); + private static final Utf8String RELEVANCY = new Utf8String("relevancy"); + private static final Utf8String SOURCE = new Utf8String("source"); + + + // this is shared between umpteen threads by design + private final CopyOnWriteHashMap<String, Utf8String> fieldNameMap = new CopyOnWriteHashMap<>(); + + private boolean utf8Output = false; + + private XMLWriter writer; + + @Override + public void init() { + super.init(); + utf8Output = false; + writer = null; + } + + @Override + public String getEncoding() { + + if (getResult() == null + || getResult().getQuery() == null + || getResult().getQuery().getModel().getEncoding() == null) { + return DEFAULT_ENCODING; + } else { + return getResult().getQuery().getModel().getEncoding(); + } + } + + @Override + public String getMimeType() { + return DEFAULT_MIMETYPE; + } + + private XMLWriter wrapWriter(Writer writer) { + return XMLWriter.from(writer, 10, -1); + } + + private void header(XMLWriter writer, Result result) throws IOException { + // TODO: move setting this to Result + utf8Output = "utf-8".equalsIgnoreCase(getRequestedEncoding(result.getQuery())); + writer.xmlHeader(getRequestedEncoding(result.getQuery())); + writer.openTag(RESULT).attribute(TOTAL_HIT_COUNT, String.valueOf(result.getTotalHitCount())); + if (result.getQuery().getPresentation().getReportCoverage()) { + renderCoverageAttributes(result.getCoverage(false), writer); + } + renderTime(writer, result); + writer.closeStartTag(); + } + + private void renderTime(XMLWriter writer, Result result) { + if (!result.getQuery().getPresentation().getTiming()) { + return; + } + + final String threeDecimals = "%.3f"; + final double milli = .001d; + final long now = System.currentTimeMillis(); + final long searchTime = now - result.getElapsedTime().first(); + final double searchSeconds = ((double) searchTime) * milli; + + if (result.getElapsedTime().firstFill() != 0L) { + final long queryTime = result.getElapsedTime().weightedSearchTime(); + final long summaryFetchTime = result.getElapsedTime().weightedFillTime(); + final double querySeconds = ((double) queryTime) * milli; + final double summarySeconds = ((double) summaryFetchTime) * milli; + writer.attribute(QUERY_TIME, String.format(threeDecimals, querySeconds)); + writer.attribute(SUMMARY_FETCH_TIME, String.format(threeDecimals, summarySeconds)); + } + writer.attribute(SEARCH_TIME, String.format(threeDecimals, searchSeconds)); + } + + protected static void renderCoverageAttributes(Coverage coverage, XMLWriter writer) throws IOException { + if (coverage == null) return; + writer.attribute(COVERAGE_DOCS,coverage.getDocs()); + writer.attribute(COVERAGE_NODES,coverage.getNodes()); + writer.attribute(COVERAGE_FULL,coverage.getFull()); + writer.attribute(COVERAGE,coverage.getResultPercentage()); + writer.attribute(RESULTS_FULL,coverage.getFullResultSets()); + writer.attribute(RESULTS,coverage.getResultSets()); + } + + + public void error(XMLWriter writer, Result result) throws IOException { + ErrorMessage error = result.hits().getError(); + writer.openTag(ERROR).attribute(CODE,error.getCode()).content(error.getMessage(),false).closeTag(); + } + + + @SuppressWarnings("UnusedParameters") + protected void emptyResult(XMLWriter writer, Result result) throws IOException {} + + @SuppressWarnings("UnusedParameters") + public void queryContext(XMLWriter writer, QueryContext queryContext, Query owner) throws IOException { + if (owner.getTraceLevel()!=0) { + XMLWriter xmlWriter=XMLWriter.from(writer); + xmlWriter.openTag("meta").attribute("type", QueryContext.ID); + TraceNode traceRoot = owner.getModel().getExecution().trace().traceNode().root(); + traceRoot.accept(new RenderingVisitor(xmlWriter, owner.getStartTime())); + xmlWriter.closeTag(); + } + } + + + private void renderSingularHit(XMLWriter writer, Hit hit) throws IOException { + writer.openTag(HIT); + renderHitAttributes(writer, hit); + writer.closeStartTag(); + renderHitFields(writer, hit); + } + + private void renderHitFields(XMLWriter writer, Hit hit) throws IOException { + renderSyntheticRelevanceField(writer, hit); + for (Iterator<Map.Entry<String, Object>> it = hit.fieldIterator(); it.hasNext(); ) { + renderField(writer, hit, it); + } + } + + private void renderField(XMLWriter writer, Hit hit, Iterator<Map.Entry<String, Object>> it) throws IOException { + Map.Entry<String, Object> entry = it.next(); + boolean isProbablyNotDecoded = false; + if (hit instanceof FastHit) { + FastHit f = (FastHit) hit; + isProbablyNotDecoded = f.fieldIsNotDecoded(entry.getKey()); + } + renderGenericFieldPossiblyNotDecoded(writer, hit, entry, isProbablyNotDecoded); + } + + private void renderGenericFieldPossiblyNotDecoded(XMLWriter writer, Hit hit, Map.Entry<String, Object> entry, boolean probablyNotDecoded) throws IOException { + String fieldName = entry.getKey(); + + // skip depending on hit type + if (fieldName.startsWith("$")) return; // Don't render fields that start with $ // TODO: Move to should render + + writeOpenFieldElement(writer, fieldName); + renderFieldContentPossiblyNotDecoded(writer, hit, probablyNotDecoded, fieldName); + writeCloseFieldElement(writer); + } + + private void renderFieldContentPossiblyNotDecoded(XMLWriter writer, Hit hit, boolean probablyNotDecoded, String fieldName) throws IOException { + boolean dumpedRaw = false; + if (probablyNotDecoded && (hit instanceof FastHit)) { + writer.closeStartTag(); + if ((writer.getWriter() instanceof ByteWriter) && utf8Output) { + dumpedRaw = UserTemplate.dumpBytes((ByteWriter) writer.getWriter(), (FastHit) hit, fieldName); + } + if (dumpedRaw) { + writer.content("", false); // let the xml writer note that this tag had content + } + } + if (!dumpedRaw) { + String xmlval = hit.getFieldXML(fieldName); + if (xmlval == null) { + xmlval = "(null)"; + } + writer.escapedContent(xmlval, false); + } + } + + private void renderSyntheticRelevanceField(XMLWriter writer, Hit hit) throws IOException { + final String relevancyFieldName = "relevancy"; + final Relevance relevance = hit.getRelevance(); + + // skip depending on hit type + if (relevance != null) { + renderSimpleField(writer, relevancyFieldName, relevance); + } + } + + private void renderSimpleField(XMLWriter writer, String relevancyFieldName, Relevance relevance) throws IOException { + writeOpenFieldElement(writer, relevancyFieldName); + writer.content(relevance.toString(), false); + writeCloseFieldElement(writer); + } + + private void writeCloseFieldElement(XMLWriter writer) throws IOException { + writer.closeTag(); + } + + private void writeOpenFieldElement(XMLWriter writer, String relevancyFieldName) throws IOException { + Utf8String utf8 = fieldNameMap.get(relevancyFieldName); + if (utf8 == null) { + utf8 = new Utf8String(relevancyFieldName); + fieldNameMap.put(relevancyFieldName, utf8); + } + writer.openTag(FIELD).attribute(NAME, utf8); + writer.closeStartTag(); + } + + private void renderHitAttributes(XMLWriter writer, Hit hit) throws IOException { + writer.attribute(TYPE, hit.getTypeString()); + if (hit.getRelevance() != null) { + writer.attribute(RELEVANCY, hit.getRelevance().toString()); +} + writer.attribute(SOURCE, hit.getSource()); + } + + private void renderHitGroup(XMLWriter writer, HitGroup hit) throws IOException { + if (HitRenderer.renderHeader(hit, writer)) { + // empty + } else if (hit.types().contains("grouphit")) { + // TODO Keep this? + renderHitGroupOfTypeGroupHit(writer, hit); + } else { + renderGroup(writer, hit); + } + } + + private void renderGroup(XMLWriter writer, HitGroup hit) throws IOException { + writer.openTag(GROUP); + renderHitAttributes(writer, hit); + writer.closeStartTag(); + } + + private void renderHitGroupOfTypeGroupHit(XMLWriter writer, HitGroup hit) throws IOException { + writer.openTag(HIT); + renderHitAttributes(writer, hit); + renderId(writer, hit); + writer.closeStartTag(); + } + + private void renderId(XMLWriter writer, HitGroup hit) throws IOException { + URI uri = hit.getId(); + if (uri != null) { + writer.openTag(ID).content(uri.stringValue(),false).closeTag(); + } + } + + private boolean simpleRenderHit(XMLWriter writer, Hit hit) throws IOException { + if (hit instanceof DefaultErrorHit) { + return simpleRenderDefaultErrorHit(writer, (DefaultErrorHit) hit); + } else if (hit instanceof GroupingListHit) { + return true; + } else { + return false; + } + } + + public static boolean simpleRenderDefaultErrorHit(XMLWriter writer, ErrorHit defaultErrorHit) throws IOException { + writer.openTag("errordetails"); + for (Iterator i = defaultErrorHit.errorIterator(); i.hasNext();) { + ErrorMessage error = (ErrorMessage) i.next(); + renderMessageDefaultErrorHit(writer, error); + } + writer.closeTag(); + return true; + } + + public static void renderMessageDefaultErrorHit(XMLWriter writer, ErrorMessage error) throws IOException { + writer.openTag("error"); + writer.attribute("source", error.getSource()); + writer.attribute("error", error.getMessage()); + writer.attribute("code", Integer.toString(error.getCode())); + writer.content(error.getDetailedMessage(), false); + if (error.getCause()!=null) { + writer.openTag("cause"); + writer.content("\n", true); + StringWriter stackTrace=new StringWriter(); + error.getCause().printStackTrace(new PrintWriter(stackTrace)); + writer.content(stackTrace.toString(), true); + writer.closeTag(); + } + writer.closeTag(); + } + + public static final class RenderingVisitor extends TraceVisitor { + + private static final String tag = "p"; + private final XMLWriter writer; + private long baseTime; + + public RenderingVisitor(XMLWriter writer,long baseTime) { + this.writer=writer; + this.baseTime=baseTime; + } + + @Override + public void entering(TraceNode node) { + if (node.isRoot()) return; + writer.openTag(tag); + } + + @Override + public void leaving(TraceNode node) { + if (node.isRoot()) return; + writer.closeTag(); + } + + @Override + public void visit(TraceNode node) { + if (node.isRoot()) return; + if (node.payload()==null) return; + + writer.openTag(tag); + if (node.timestamp()!=0) + writer.content(node.timestamp()-baseTime,false).content(" ms: ", false); + writer.content(node.payload().toString(),false); + writer.closeTag(); + } + + } + + private Result getResult() { + Result r; + try { + r = (Result) getResponse(); + } catch (ClassCastException e) { + throw new IllegalArgumentException( + "DefaultRenderer attempted used outside a search context, got a " + + getResponse().getClass().getName()); + } + return r; + } + + @Override + public void beginResponse(OutputStream stream) throws IOException { + Charset cs = Charset.forName(getRequestedEncoding(getResult().getQuery())); + CharsetEncoder encoder = cs.newEncoder(); + writer = wrapWriter(new ByteWriter(stream, encoder)); + + header(writer, getResult()); + if (getResult().hits().getError() != null || getResult().hits().getQuery().errors().size() > 0) { + error(writer, getResult()); + } + + if (getResult().getConcreteHitCount() == 0) { + emptyResult(writer, getResult()); + } + + if (getResult().getContext(false) != null) { + queryContext(writer, getResult().getContext(false), getResult().getQuery()); + } + + } + + /** Returns the encoding of the query, or the encoding given by the template if none is set */ + public final String getRequestedEncoding(Query query) { + String encoding = query.getModel().getEncoding(); + if (encoding != null) return encoding; + return getEncoding(); + } + + @Override + public void beginList(DataList<?> list) + throws IOException { + if (getRecursionLevel() == 1) { + return; + } + HitGroup hit = (HitGroup) list; + boolean renderedSimple = simpleRenderHit(writer, hit); + + if (renderedSimple) { + return; + } + renderHitGroup(writer, hit); + } + + @Override + public void data(Data data) throws IOException { + Hit hit = (Hit) data; + boolean renderedSimple = simpleRenderHit(writer, hit); + + if (renderedSimple) { + return; + } + renderSingularHit(writer, hit); + writer.closeTag(); + } + + @Override + public void endList(DataList<?> list) + throws IOException { + if (getRecursionLevel() == 1) { + return; + } + writer.closeTag(); + } + + @Override + public void endResponse() throws IOException { + writer.closeTag(); + writer.close(); + } + +} |