Search Result Renderers

Vespa provides a default JSON format for queryresults. If a separate format is desired, renderers can be configured to implement custom formats. Both binary and text format may be implemented in a renderer. Renderers should not be used to implement business logic - that should go in Searchers, Handlers or Processors.

The documentation of renderers assumes familiarity with component development.

Renderers are implemented by subclassing com.yahoo.processing.rendering.AsynchronousSectionedRenderer<Result> or com.yahoo.search.rendering.Renderer or com.yahoo.search.rendering.SectionedRenderer. SectionedRenderer differs from Renderer by providing each part to be rendered in separate steps. It is therefore easier to implement a SectionedRenderer than a regular Renderer. AsynchronousSectionedRenderer has a similar API to SectionedRenderer, but supports asynchronously fetched hit contents, so if supporting slow clients or backends is a priority, this offers some advantages. AsynchronousSectionedRenderer also exposes an OutputStream instead of a Writer, so if the backend data contains e.g. data encoded the same way as the output from the container (often UTF-8), notable performance gains are possible.

All renderers are components; They are built and deployed like all other search container components, and supports custom config.

Renderers do not need to be thread safe; They can safely use and store state during rendering in member variables. The container supports this by cloning the renderers just before rendering the search result. To support cloning correctly, the renderers are required to obey the following contract:

  1. At construction time, only final members shall be initialized, and these must refer to immutable data only.
  2. State mutated during rendering shall be initialized in the init method.

SectionedRenderer

To create a SectionedRenderer, you need to subclass it and implement all its abstract methods. For each non-compound entity such as regular hits and query contexts there are an associated method with the same name.

public class DemoRenderer extends SectionedRenderer<Writer> {

    @Override
    public void hit(Writer writer, Hit hit) throws IOException {
        writer.write("Hit: " + hit.getField("documentid") + "\n");
    }
}
For each compound entity such as hit groups and the result itself there are pairs of methods, named begin<name> and end<name>.
public class DemoRenderer extends SectionedRenderer<Writer> {

    private int indentation;

    @Override
    public void beginHitGroup(PrintWriter writer, HitGroup hitGroup) throws IOException {
        writer.write("Begin hit group:" + hitGroup.getId() + "\n");
        ++indentation;
    }

    @Override
    public void endHitGroup(PrintWriter writer, HitGroup hitGroup) throws IOException {
        --indentation;
        writer.write("End hit group:" + hitGroup.getId() + "\n");
    }
}
For a compound entity, a method will be called for each of its members after its begin method and before its end method has been called.
                           Sequence of calls
                          -------------------
Result {                  1. beginResult()
    HitGroup {            2. beginHitGroup()
        Hit               3. hit()
        Hit               4. hit()
        Hit               5. hit()
    }                     6. endHitGroup()
}                         7. endResult()
For grouping results, there is a dedicated set of callbacks available; beginGroup(), endGroup(), beginGroupList(), endGroupList(), beginHitList() and endHitList().

All of GroupList, Group and HitList are subclasses of HitGroup, and the default implementation of the above methods is provided that calls beginHitGroup() and endHitGroup() respectively. Furthermore, since all of the attributes of those classes are regular fields as defined by the root Hit class, you will get reasonable output by simply implementing beginHitGroup(), endHitGroup(), and hit().

JSON example

JSON is the default output format. Read the default JSON result format before implementing custom JSON renderers.

The following example renders a set of fields containing JSON data as a JSON array:

package com.yahoo.mysearcher;

import com.yahoo.search.Result;
import com.yahoo.search.query.context.QueryContext;
import com.yahoo.search.rendering.SectionedRenderer;
import com.yahoo.search.result.ErrorMessage;
import com.yahoo.search.result.Hit;
import com.yahoo.search.result.HitGroup;

import java.io.IOException;
import java.io.Writer;
import java.util.Collection;

public class MyRenderer extends SectionedRenderer<Writer> {
    /**
     * A marker variable for the hit rendering to know whether
     * the hit being rendered is the first one that is rendered.
     */
    boolean firstHit;

    public void init() {
        firstHit = true;
    }

    @Override
    public String getEncoding() {
        return "utf-8";
    }

    @Override
    public String getMimeType() {
        return "application/json";
    }

    @Override
    public void beginResult(Writer writer, Result result) throws IOException {
        writer.write("[");
    }

    @Override
    public void endResult(Writer writer, Result result) throws IOException {
        writer.write("]");
    }

    @Override
    public void error(Writer writer, Collection<ErrorMessage> errorMessages) throws IOException {
        // swallows errors silently
    }

    @Override
    public void emptyResult(Writer writer, Result result) throws IOException {
        //write nothing.
    }

    @Override
    public void queryContext(Writer writer, QueryContext queryContext) throws IOException {
        //write nothing.
    }

    @Override
    public void beginHitGroup(Writer writer, HitGroup hitGroup) throws IOException {
        //write nothing.
    }

    @Override
    public void endHitGroup(Writer writer, HitGroup hitGroup) throws IOException {
        //write nothing.
    }

    @Override
    public void hit(Writer writer, Hit hit) throws IOException {
        if (!firstHit) {
            writer.write(",\n");
        }
        writer.write(hit.toString());
        firstHit = false;
    }
}
In other words, dump a variable length array containing all available data, ignore everything else and silently ignore error states. In other words, this is not suitable for production, but it is enough for a prototype.

To make the above renderer available, add to services.xml:

<?xml version="1.0" encoding="utf-8" ?>
<services version="1.0">
  …
  <container version="1.0">
    <search>
      <renderer id="MyRenderer" class="com.yahoo.mysearcher.MyRenderer" bundle="mybundle" />
    </search>
    …
  </container>
  …
</services>
To use the renderer, add &presentation.format=[id] to queries - in this case &presentation.format=MyRenderer

The renderer itself needs to be distributed to the container nodes as an OSGi bundle. The steps for creating a valid bundle for this use, are the same as for building any other component bundle.

Renderer

SectionedRenderers are feed each part of the result separately. But sometimes this scaffolding is unnecessary; you just want to pull out the parts of the results yourself.

public class SimpleRenderer extends Renderer {
    @Override
    public void render(Writer writer, Result result) throws IOException {
        writer.write("The result contains " + result.getHitCount() + " hits.");
    }

    @Override
    public String getEncoding() {
        return "utf-8";
    }

    @Override
    public String getMimeType() {
        return "text/plain";
    }
}
Here, the render method is expected to do all the work; the derived class is expected to extract all the entities of interest itself and render them.

AsynchronousSectionedRenderer<Result>

This is exactly the same as for the processing framework. It is conceptually very similar to SectionedRenderer, but has no special cases for search results as such. The utility method getResponse() has a parametrized return type, though, so templating the renderer on Result takes away some of the hassle.