diff options
author | Jon Bratseth <bratseth@oath.com> | 2018-12-05 14:23:12 -0800 |
---|---|---|
committer | gjoranv <gv@oath.com> | 2019-01-21 15:09:30 +0100 |
commit | 879da966c503e1de9e6ab2a80f0a2ff397cff1e3 (patch) | |
tree | 21b4f18186233dd9a176269ebc8081e920fe7834 | |
parent | 0f63b23a4ea60b78ad97f1c19245023fb29545c0 (diff) |
Testing of dynamic summary rendering
9 files changed, 372 insertions, 154 deletions
diff --git a/container-search/src/main/java/com/yahoo/prelude/hitfield/HitField.java b/container-search/src/main/java/com/yahoo/prelude/hitfield/HitField.java index 8778b09934c..2702da099be 100644 --- a/container-search/src/main/java/com/yahoo/prelude/hitfield/HitField.java +++ b/container-search/src/main/java/com/yahoo/prelude/hitfield/HitField.java @@ -24,7 +24,7 @@ public class HitField { private boolean xmlProperty; private List<FieldPart> tokenizedContent = null; - private String content = null; + private String content; private Object original; @@ -48,7 +48,7 @@ public class HitField { /** * @param f The field name * @param c The field content - * @param cjk true if this is a cjk-document + * @param cjk true if the content is CJK text */ public HitField(String f, String c, boolean cjk) { this(f, c, cjk, false); @@ -57,7 +57,7 @@ public class HitField { /** * @param f The field name * @param c The field content - * @param cjk true if this is a cjk-document + * @param cjk true if the content is CJK text */ public HitField(String f, XMLString c, boolean cjk) { this(f, c.toString(), cjk, true); @@ -66,7 +66,7 @@ public class HitField { /** * @param f The field name * @param c The field content - * @param cjk true if this is a cjk-document + * @param cjk true if the content is CJK text * @param xmlProperty true if this should not quote XML syntax */ public HitField(String f, String c, boolean cjk, boolean xmlProperty) { @@ -279,9 +279,8 @@ public class HitField { // Must null content reference _before_ calling getContent() content = null; } - /** - * @return the content of this field - */ + + /** Returns the content of this field */ public String getContent() { if (content == null) { StringBuilder buf = new StringBuilder(); @@ -294,13 +293,8 @@ public class HitField { return content; } - /** - * @return the content of this field, using the arguments as bolding - * tags - */ - public String getContent(String boldOpenTag, - String boldCloseTag, - String separatorTag) { + /** Returns the content of this field, using the arguments as bolding tags */ + public String getContent(String boldOpenTag, String boldCloseTag, String separatorTag) { StringBuilder buf = new StringBuilder(); Iterator<FieldPart> iter = ensureTokenized().iterator(); while(iter.hasNext()) { diff --git a/container-search/src/main/java/com/yahoo/prelude/hitfield/JSONString.java b/container-search/src/main/java/com/yahoo/prelude/hitfield/JSONString.java index 06db012309e..eee7b310d13 100644 --- a/container-search/src/main/java/com/yahoo/prelude/hitfield/JSONString.java +++ b/container-search/src/main/java/com/yahoo/prelude/hitfield/JSONString.java @@ -86,6 +86,7 @@ public class JSONString implements Inspectable { didInitContent = true; } + @Override public String toString() { if (value != null) { return renderFromInspector(); diff --git a/container-search/src/main/java/com/yahoo/prelude/searcher/JuniperSearcher.java b/container-search/src/main/java/com/yahoo/prelude/searcher/JuniperSearcher.java index 521ea83f712..d11b8d86d29 100644 --- a/container-search/src/main/java/com/yahoo/prelude/searcher/JuniperSearcher.java +++ b/container-search/src/main/java/com/yahoo/prelude/searcher/JuniperSearcher.java @@ -77,10 +77,9 @@ public class JuniperSearcher extends Searcher { @Override public void fill(Result result, String summaryClass, Execution execution) { - Result workResult = result; - int worstCase = workResult.getHitCount(); + int worstCase = result.getHitCount(); List<Hit> hits = new ArrayList<>(worstCase); - for (Iterator<Hit> i = workResult.hits().deepIterator(); i.hasNext();) { + for (Iterator<Hit> i = result.hits().deepIterator(); i.hasNext();) { Hit sniffHit = i.next(); if ( ! (sniffHit instanceof FastHit)) continue; @@ -89,26 +88,26 @@ public class JuniperSearcher extends Searcher { hits.add(hit); } - execution.fill(workResult, summaryClass); - highlight(workResult.getQuery().getPresentation().getBolding(), hits.iterator(), summaryClass, + execution.fill(result, summaryClass); + highlight(result.getQuery().getPresentation().getBolding(), hits.iterator(), summaryClass, execution.context().getIndexFacts().newSession(result.getQuery())); } private void highlight(boolean bolding, Iterator<Hit> hitsToHighlight, String summaryClass, IndexFacts.Session indexFacts) { while (hitsToHighlight.hasNext()) { - Hit sniffHit = hitsToHighlight.next(); - if ( ! (sniffHit instanceof FastHit)) continue; + Hit hit = hitsToHighlight.next(); + if ( ! (hit instanceof FastHit)) continue; - FastHit hit = (FastHit) sniffHit; - if (summaryClass != null && ! hit.isFilled(summaryClass)) continue; + FastHit fastHit = (FastHit) hit; + if (summaryClass != null && ! fastHit.isFilled(summaryClass)) continue; - Object searchDefinitionField = hit.getField(MAGIC_FIELD); + Object searchDefinitionField = fastHit.getField(MAGIC_FIELD); if (searchDefinitionField == null) continue; for (Index index : indexFacts.getIndexes(searchDefinitionField.toString())) { if (index.getDynamicSummary() || index.getHighlightSummary()) { - HitField fieldValue = hit.buildHitField(index.getName(), true); + HitField fieldValue = fastHit.buildHitField(index.getName(), true); if (fieldValue != null) insertTags(fieldValue, bolding, index.getDynamicSummary()); } @@ -116,9 +115,9 @@ public class JuniperSearcher extends Searcher { } } - private void insertTags(HitField oldProperty, boolean bolding, boolean dynteaser) { + private void insertTags(HitField field, boolean bolding, boolean dynteaser) { boolean insideHighlight = false; - for (ListIterator<FieldPart> i = oldProperty.listIterator(); i.hasNext();) { + for (ListIterator<FieldPart> i = field.listIterator(); i.hasNext();) { FieldPart f = i.next(); if (f instanceof SeparatorFieldPart) setSeparatorString(bolding, (SeparatorFieldPart) f); @@ -138,8 +137,7 @@ public class JuniperSearcher extends Searcher { break; case RAW_SEPARATOR_CHAR: newFieldParts = initFieldParts(newFieldParts); - addSeparator(bolding, dynteaser, f, toQuote, newFieldParts, - previous, j); + addSeparator(bolding, dynteaser, f, toQuote, newFieldParts, previous, j); previous = j + 1; break; default: diff --git a/container-search/src/main/java/com/yahoo/search/rendering/JsonRenderer.java b/container-search/src/main/java/com/yahoo/search/rendering/JsonRenderer.java index c102d1fa258..e8af150ce25 100644 --- a/container-search/src/main/java/com/yahoo/search/rendering/JsonRenderer.java +++ b/container-search/src/main/java/com/yahoo/search/rendering/JsonRenderer.java @@ -18,6 +18,7 @@ import com.yahoo.document.datatypes.StringFieldValue; import com.yahoo.document.datatypes.TensorFieldValue; import com.yahoo.document.json.JsonWriter; import com.yahoo.lang.MutableBoolean; +import com.yahoo.prelude.hitfield.HitField; import com.yahoo.processing.Response; import com.yahoo.processing.execution.Execution.Trace; import com.yahoo.processing.rendering.AsynchronousSectionedRenderer; diff --git a/container-search/src/main/java/com/yahoo/search/rendering/XmlRenderer.java b/container-search/src/main/java/com/yahoo/search/rendering/XmlRenderer.java index 5f99c531c95..5586fd2f996 100644 --- a/container-search/src/main/java/com/yahoo/search/rendering/XmlRenderer.java +++ b/container-search/src/main/java/com/yahoo/search/rendering/XmlRenderer.java @@ -349,15 +349,12 @@ public final class XmlRenderer extends AsynchronousSectionedRenderer<Result> { } private Result getResult() { - Result r; try { - r = (Result) getResponse(); + return (Result) getResponse(); } catch (ClassCastException e) { - throw new IllegalArgumentException( - "XmlRenderer attempted used outside a search context, got a " - + getResponse().getClass().getName()); + throw new IllegalArgumentException("XmlRenderer attempted used outside a search context, got a " + + getResponse().getClass().getName()); } - return r; } @Override diff --git a/container-search/src/test/java/com/yahoo/prelude/searcher/test/JuniperSearcherTestCase.java b/container-search/src/test/java/com/yahoo/prelude/searcher/test/JuniperSearcherTestCase.java index 9f4a12d24e6..24af91cb5c0 100644 --- a/container-search/src/test/java/com/yahoo/prelude/searcher/test/JuniperSearcherTestCase.java +++ b/container-search/src/test/java/com/yahoo/prelude/searcher/test/JuniperSearcherTestCase.java @@ -47,7 +47,7 @@ public class JuniperSearcherTestCase { DocumentSourceSearcher docsource = new DocumentSourceSearcher(); addResult(new Query("?query=12"), sdName, content, docsource); addResult(new Query("?query=12&bolding=false"), sdName, content, docsource); - return new Chain<Searcher>(searcher, docsource); + return new Chain<>(searcher, docsource); } private void addResult(Query query, String sdName, String content, DocumentSourceSearcher docsource) { diff --git a/container-search/src/test/java/com/yahoo/search/rendering/JsonRendererTestCase.java b/container-search/src/test/java/com/yahoo/search/rendering/JsonRendererTestCase.java index 9e16ddba6fc..10a3b695f64 100644 --- a/container-search/src/test/java/com/yahoo/search/rendering/JsonRendererTestCase.java +++ b/container-search/src/test/java/com/yahoo/search/rendering/JsonRendererTestCase.java @@ -1,14 +1,13 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.search.rendering; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; +import com.yahoo.component.ComponentId; import com.yahoo.component.chain.Chain; +import com.yahoo.container.QrSearchersConfig; import com.yahoo.data.access.simple.Value; import com.yahoo.data.access.slime.SlimeAdapter; import com.yahoo.document.DataType; @@ -19,8 +18,13 @@ import com.yahoo.document.datatypes.StringFieldValue; import com.yahoo.document.datatypes.Struct; import com.yahoo.document.datatypes.TensorFieldValue; import com.yahoo.document.predicate.Predicate; +import com.yahoo.prelude.Index; +import com.yahoo.prelude.IndexFacts; +import com.yahoo.prelude.IndexModel; +import com.yahoo.prelude.SearchDefinition; import com.yahoo.prelude.fastsearch.FastHit; import com.yahoo.prelude.hitfield.JSONString; +import com.yahoo.prelude.searcher.JuniperSearcher; import com.yahoo.search.Query; import com.yahoo.search.Result; import com.yahoo.search.Searcher; @@ -38,6 +42,7 @@ import com.yahoo.search.result.NanNumber; import com.yahoo.search.result.Relevance; import com.yahoo.search.result.StructuredData; import com.yahoo.search.searchchain.Execution; +import com.yahoo.search.searchchain.testutil.DocumentSourceSearcher; import com.yahoo.search.statistics.ElapsedTimeTestCase; import com.yahoo.search.statistics.ElapsedTimeTestCase.CreativeTimeSource; import com.yahoo.search.statistics.ElapsedTimeTestCase.UselessSearcher; @@ -51,32 +56,31 @@ import com.yahoo.yolean.trace.TraceNode; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.mockito.Mockito; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.times; /** * Functional testing of {@link JsonRenderer}. * * @author Steinar Knutsen + * @author bratseth */ public class JsonRendererTestCase { - JsonRenderer originalRenderer; - JsonRenderer renderer; + private JsonRenderer originalRenderer; + private JsonRenderer renderer; public JsonRendererTestCase() { originalRenderer = new JsonRenderer(); @@ -84,23 +88,11 @@ public class JsonRendererTestCase { @Before public void setUp() throws Exception { - // Do the same dance as in production + // Use the shared renderer as a prototype object, as specified in the API contract renderer = (JsonRenderer) originalRenderer.clone(); renderer.init(); } - @After - public void tearDown() throws Exception { - renderer = null; - } - - private static final class Thingie { - @Override - public String toString() { - return "thingie"; - } - } - @Test public void testDocumentId() throws IOException, InterruptedException, ExecutionException { String expected = "{\n" @@ -130,14 +122,6 @@ public class JsonRendererTestCase { assertEqualJson(expected, summary); } - private Result newEmptyResult(String[] args) { - return new Result(new Query("/?" + String.join("&", args))); - } - - private Result newEmptyResult() { - return newEmptyResult(new String[] {"query=a"}); - } - @Test public void testDataTypes() throws IOException, InterruptedException, ExecutionException, JSONException { String expected = "{\n" @@ -188,7 +172,7 @@ public class JsonRendererTestCase { @Test - public final void testTracing() throws IOException, InterruptedException, ExecutionException { + public void testTracing() throws IOException, InterruptedException, ExecutionException { // which clearly shows a trace child is created once too often... String expected = "{\n" + " \"root\": {\n" @@ -243,7 +227,7 @@ public class JsonRendererTestCase { } @Test - public final void testEmptyTracing() throws IOException, InterruptedException, ExecutionException { + public void testEmptyTracing() throws IOException, InterruptedException, ExecutionException { String expected = "{\n" + " \"root\": {\n" + " \"fields\": {\n" @@ -272,7 +256,7 @@ public class JsonRendererTestCase { @SuppressWarnings("unchecked") @Test - public final void testTracingWithEmptySubtree() throws IOException, InterruptedException, ExecutionException { + public void testTracingWithEmptySubtree() throws IOException, InterruptedException, ExecutionException { String expected = "{\n" + " \"root\": {\n" + " \"fields\": {\n" @@ -346,15 +330,8 @@ public class JsonRendererTestCase { assertEqualJson(expected, summary); } - private void subExecution(Execution execution, String color, int traceLevel) { - Execution e2 = new Execution(new Chain<Searcher>(), execution.context()); - Query subQuery = new Query("/?query=b&tracelevel=" + traceLevel); - e2.search(subQuery); - subQuery.trace(color, 1); - } - @Test - public final void testTracingOfNodesWithBothChildrenAndData() throws IOException, InterruptedException, ExecutionException { + public void testTracingOfNodesWithBothChildrenAndData() throws IOException, InterruptedException, ExecutionException { String expected = "{\n" + " \"root\": {\n" + " \"fields\": {\n" @@ -400,7 +377,7 @@ public class JsonRendererTestCase { @Test - public final void testTracingOfNodesWithBothChildrenAndDataAndEmptySubnode() throws IOException, InterruptedException, ExecutionException { + public void testTracingOfNodesWithBothChildrenAndDataAndEmptySubnode() throws IOException, InterruptedException, ExecutionException { String expected = "{\n" + " \"root\": {\n" + " \"fields\": {\n" @@ -441,7 +418,7 @@ public class JsonRendererTestCase { } @Test - public final void testTracingOfNestedNodesWithDataAndSubnodes() throws IOException, InterruptedException, ExecutionException { + public void testTracingOfNestedNodesWithDataAndSubnodes() throws IOException, InterruptedException, ExecutionException { String expected = "{\n" + " \"root\": {\n" + " \"fields\": {\n" @@ -490,7 +467,7 @@ public class JsonRendererTestCase { @Test - public final void test() throws IOException, InterruptedException, ExecutionException { + public void test() throws IOException, InterruptedException, ExecutionException { String expected = "{\n" + " \"root\": {\n" + " \"children\": [\n" @@ -883,7 +860,7 @@ public class JsonRendererTestCase { }); GroupList gl = new GroupList("customer"); Group g = new Group(new DoubleBucketId(1.0, 2.0), new Relevance(1.0)); - g.setField("something()", Integer.valueOf(7)); + g.setField("something()", 7); gl.add(g); rg.add(gl); r.hits().add(rg); @@ -956,7 +933,7 @@ public class JsonRendererTestCase { } @Test - public final void testFieldValueInHit() throws IOException, InterruptedException, ExecutionException, JSONException { + public void testFieldValueInHit() throws IOException, InterruptedException, ExecutionException { String expected = "{\n" + " \"root\": {\n" + " \"children\": [\n" @@ -991,7 +968,7 @@ public class JsonRendererTestCase { } @Test - public final void testHiddenFields() throws IOException, InterruptedException, ExecutionException, JSONException { + public void testHiddenFields() throws IOException, InterruptedException, ExecutionException { String expected = "{\n" + " \"root\": {\n" + " \"children\": [\n" @@ -1015,17 +992,8 @@ public class JsonRendererTestCase { assertEqualJson(expected, summary); } - private Hit createHitWithOnlyHiddenFields() { - Hit h = new Hit("hiddenFields"); - h.setField("NaN", NanNumber.NaN); - h.setField("emptyString", ""); - h.setField("emptyStringFieldValue", new StringFieldValue("")); - h.setField("$vespaImplementationDetail", "Hello, World!"); - return h; - } - @Test - public final void testDebugRendering() throws IOException, InterruptedException, ExecutionException, JSONException { + public void testDebugRendering() throws IOException, InterruptedException, ExecutionException { String expected = "{\n" + " \"root\": {\n" + " \"children\": [\n" @@ -1056,7 +1024,7 @@ public class JsonRendererTestCase { } @Test - public final void testTimingRendering() throws InterruptedException, ExecutionException, JsonParseException, JsonMappingException, IOException { + public void testTimingRendering() throws InterruptedException, ExecutionException, IOException { String expected = "{" + " \"root\": {" + " \"fields\": {" @@ -1091,7 +1059,7 @@ public class JsonRendererTestCase { } @Test - public final void testJsonCallback() throws IOException, InterruptedException, ExecutionException, JSONException { + public void testJsonCallback() throws IOException, InterruptedException, ExecutionException { String expected = "{\n" + " \"root\": {\n" + " \"children\": [\n" @@ -1129,7 +1097,7 @@ public class JsonRendererTestCase { } @Test - public final void testMapInField() throws IOException, InterruptedException, ExecutionException, JSONException { + public void testMapInField() throws IOException, InterruptedException, ExecutionException { String expected = "{\n" + " \"root\": {\n" + " \"children\": [\n" @@ -1185,6 +1153,54 @@ public class JsonRendererTestCase { + "}"; assertEquals("Duplicate key \"duplicate\"", validateJSON(json)); } + + @Test + public void testDynamicSummary() throws Exception { + String content = "\uFFF9Feeding\uFFFAfeed\uFFFB \u001F\uFFF9documents\uFFFAdocument\uFFFB\u001F into Vespa \uFFF9is\uFFFAbe\u001Eincrement of a set of \u001F\uFFF9documents\uFFFAdocument\uFFFB\u001F fed into Vespa \uFFF9is\u001Efloat in XML when \u001Fdocument\u001F attribute \uFFF9is\uFFFAbe\uFFFB int\u001E"; + Result result = createResult("one", content, true); + + String summary = render(result); + + String expected = + "{ \n" + + " \"root\":{ " + + " \"id\":\"toplevel\"," + + " \"relevance\":1.0," + + " \"fields\":{ " + + " \"totalCount\":0" + + " }," + + " \"children\":[ " + + " { " + + " \"id\":\"http://abc.html/\"," + + " \"relevance\":1.0," + + " \"fields\":{ " + + " \"sddocname\":\"one\",\n" + + " \"dynteaser\":\"Feeding <hi>documents</hi> into Vespa is<sep />increment of a set of <hi>documents</hi> fed into Vespa <sep />float in XML when <hi>document</hi> attribute is int<sep />\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + "}\n"; + assertEqualJson(expected, summary); + } + + private Result newEmptyResult(String[] args) { + return new Result(new Query("/?" + String.join("&", args))); + } + + private Result newEmptyResult() { + return newEmptyResult(new String[] {"query=a"}); + } + + private Hit createHitWithOnlyHiddenFields() { + Hit h = new Hit("hiddenFields"); + h.setField("NaN", NanNumber.NaN); + h.setField("emptyString", ""); + h.setField("emptyStringFieldValue", new StringFieldValue("")); + h.setField("$vespaImplementationDetail", "Hello, World!"); + return h; + } + private String render(Result r) throws InterruptedException, ExecutionException { Execution execution = new Execution(Execution.Context.createContextStub()); return render(execution, r); @@ -1207,6 +1223,7 @@ public class JsonRendererTestCase { assertEquals("", validateJSON(expected)); assertEquals("", validateJSON(generated)); } + private String validateJSON(String presumablyValidJson) { try { new JSONObject(presumablyValidJson); @@ -1216,4 +1233,76 @@ public class JsonRendererTestCase { } } + private static final class Thingie { + @Override + public String toString() { + return "thingie"; + } + } + + private Result createResult(String sdName, String content, boolean bolding) { + Chain<Searcher> chain = createSearchChain(sdName, content); + Query query = new Query("?query=12"); + if ( ! bolding) + query = new Query("?query=12&bolding=false"); + Execution execution = createExecution(chain); + Result result = execution.search(query); + execution.fill(result); + return result; + } + + /** + * Creates a search chain which always returns a result with one hit containing information given in this + * + * @param sdName the search definition type of the returned hit + * @param content the content of the "dynteaser" field of the returned hit + */ + private Chain<Searcher> createSearchChain(String sdName, String content) { + JuniperSearcher searcher = new JuniperSearcher(new ComponentId("test"), + new QrSearchersConfig(new QrSearchersConfig.Builder())); + + DocumentSourceSearcher docsource = new DocumentSourceSearcher(); + addResult(new Query("?query=12"), sdName, content, docsource); + addResult(new Query("?query=12&bolding=false"), sdName, content, docsource); + return new Chain<>(searcher, docsource); + } + + private void addResult(Query query, String sdName, String content, DocumentSourceSearcher docsource) { + Result r = new Result(query); + FastHit hit = new FastHit(); + hit.setId("http://abc.html"); + hit.setRelevance(new Relevance(1)); + hit.setField(Hit.SDDOCNAME_FIELD, sdName); + hit.setField("dynteaser", content); + r.hits().add(hit); + docsource.addResult(query, r); + } + + private Execution createExecution(Chain<Searcher> chain) { + Map<String, List<String>> clusters = new LinkedHashMap<>(); + Map<String, SearchDefinition> searchDefs = new LinkedHashMap<>(); + searchDefs.put("one", createSearchDefinitionOne()); + SearchDefinition union = new SearchDefinition("union"); + IndexModel indexModel = new IndexModel(clusters, searchDefs, union); + return new Execution(chain, Execution.Context.createContextStub(new IndexFacts(indexModel))); + } + + private SearchDefinition createSearchDefinitionOne() { + SearchDefinition one = new SearchDefinition("one"); + + Index dynteaser = new Index("dynteaser"); + dynteaser.setDynamicSummary(true); + one.addIndex(dynteaser); + + Index bigteaser = new Index("bigteaser"); + dynteaser.setHighlightSummary(true); + one.addIndex(bigteaser); + + Index otherteaser = new Index("otherteaser"); + otherteaser.setDynamicSummary(true); + one.addIndex(otherteaser); + + return one; + } + } diff --git a/container-search/src/test/java/com/yahoo/search/rendering/XMLRendererTestCase.java b/container-search/src/test/java/com/yahoo/search/rendering/XMLRendererTestCase.java index ea3b46aaaa9..15b6dd1a01d 100644 --- a/container-search/src/test/java/com/yahoo/search/rendering/XMLRendererTestCase.java +++ b/container-search/src/test/java/com/yahoo/search/rendering/XMLRendererTestCase.java @@ -4,9 +4,21 @@ package com.yahoo.search.rendering; import static org.junit.Assert.*; import java.io.ByteArrayOutputStream; - -import org.junit.After; -import org.junit.Before; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.yahoo.component.ComponentId; +import com.yahoo.container.QrSearchersConfig; +import com.yahoo.prelude.Index; +import com.yahoo.prelude.IndexFacts; +import com.yahoo.prelude.IndexModel; +import com.yahoo.prelude.SearchDefinition; +import com.yahoo.prelude.searcher.JuniperSearcher; +import com.yahoo.search.result.Hit; +import com.yahoo.search.result.Relevance; +import com.yahoo.search.searchchain.Execution; +import com.yahoo.search.searchchain.testutil.DocumentSourceSearcher; import org.junit.Test; import com.google.common.util.concurrent.ListenableFuture; @@ -21,50 +33,114 @@ import com.yahoo.search.result.HitGroup; import com.yahoo.search.statistics.ElapsedTimeTestCase; import com.yahoo.search.statistics.TimeTracker; import com.yahoo.search.statistics.ElapsedTimeTestCase.CreativeTimeSource; -import com.yahoo.search.statistics.ElapsedTimeTestCase.UselessSearcher; import com.yahoo.text.Utf8; /** * Test the XML renderer * * @author Steinar Knutsen + * @author bratseth */ public class XMLRendererTestCase { - XmlRenderer d; - - @Before - public void setUp() throws Exception { - d = new XmlRenderer(); - d.init(); + @Test + public void testGetEncoding() { + XmlRenderer renderer = new XmlRenderer(); + renderer.init(); + assertEquals("utf-8", renderer.getEncoding()); } - @After - public void tearDown() throws Exception { + @Test + public void testGetMimeType() { + XmlRenderer renderer = new XmlRenderer(); + renderer.init(); + assertEquals("text/xml", renderer.getMimeType()); } @Test - public final void testGetEncoding() { - assertEquals("utf-8", d.getEncoding()); + public void testXmlRendering() throws Exception { + Query q = new Query("/?query=a"); + + Result result = new Result(q); + result.setCoverage(new Coverage(500, 1)); + + FastHit h = new FastHit("http://localhost/", .95); + h.setField("$a", "Hello, world."); + h.setField("b", "foo"); + result.hits().add(h); + + HitGroup g = new HitGroup("usual"); + h = new FastHit("http://localhost/1", .90); + h.setField("c", "d"); + g.add(h); + result.hits().add(g); + + HitGroup gg = new HitGroup("type grouphit"); + gg.types().add("grouphit"); + gg.setField("e", "f"); + result.hits().add(gg); + result.hits().addError(ErrorMessage.createInternalServerError("message")); + + String summary = render(result); + + String expected = + "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" + + "<result total-hit-count=\"0\" coverage-docs=\"500\" coverage-nodes=\"1\" coverage-full=\"false\" coverage=\"0\" results-full=\"0\" results=\"1\">\n" + + " <error code=\"18\">Internal server error.</error>\n" + + " <errordetails>\n" + + " <error error=\"Internal server error.\" code=\"18\">message</error>\n" + + " </errordetails>\n" + + " <group relevancy=\"1.0\">\n" + + " <hit type=\"summary\" relevancy=\"0.9\">\n" + + " <field name=\"relevancy\">0.9</field>\n" + + " <field name=\"uri\">http://localhost/1</field>\n" + + " <field name=\"c\">d</field>\n" + + " </hit>\n" + + " </group>\n" + + " <hit type=\"grouphit\" relevancy=\"1.0\">\n" + + " <id>type grouphit</id>\n" + + " </hit>\n" + + " <hit type=\"summary\" relevancy=\"0.95\">\n" + + " <field name=\"relevancy\">0.95</field>\n" + + " <field name=\"uri\">http://localhost/</field>\n" + + " <field name=\"b\">foo</field>\n" + + " </hit>\n" + + "</result>\n"; + + assertEquals(expected, summary); } @Test - public final void testGetMimeType() { - assertEquals("text/xml", d.getMimeType()); + public void testXmlRenderingOfDynamicSummary() throws Exception { + String content = "\uFFF9Feeding\uFFFAfeed\uFFFB \u001F\uFFF9documents\uFFFAdocument\uFFFB\u001F into Vespa \uFFF9is\uFFFAbe\u001Eincrement of a set of \u001F\uFFF9documents\uFFFAdocument\uFFFB\u001F fed into Vespa \uFFF9is\u001Efloat in XML when \u001Fdocument\u001F attribute \uFFF9is\uFFFAbe\uFFFB int\u001E"; + Result result = createResult("one", content, true); + + String summary = render(result); + + String expected = + "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" + + "<result total-hit-count=\"0\">\n" + + " <hit relevancy=\"1.0\">\n" + + " <field name=\"relevancy\">1.0</field>\n" + + " <field name=\"sddocname\">one</field>\n" + + " <field name=\"dynteaser\">Feeding <hi>documents</hi> into Vespa is<sep />increment of a set of <hi>documents</hi> fed into Vespa <sep />float in XML when <hi>document</hi> attribute is int<sep /></field>\n" + + " </hit>\n" + + "</result>\n"; + assertEquals(expected, summary); } @Test - public void testImplicitDefaultRender() throws Exception { + public void testXmlRenderingWithTimeTracking() throws Exception { Query q = new Query("/?query=a&tracelevel=5"); q.getPresentation().setTiming(true); - Result r = new Result(q); - r.setCoverage(new Coverage(500, 1)); - - TimeTracker t = new TimeTracker(new Chain<Searcher>( - new UselessSearcher("first"), new UselessSearcher("second"), - new UselessSearcher("third"))); - ElapsedTimeTestCase.doInjectTimeSource(t, new CreativeTimeSource( - new long[] { 1L, 2L, 3L, 4L, 5L, 6L, 7L })); + + Result result = new Result(q); + result.setCoverage(new Coverage(500, 1)); + + TimeTracker t = new TimeTracker(new Chain<Searcher>(new NoopSearcher("first"), + new NoopSearcher("second"), + new NoopSearcher("third"))); + ElapsedTimeTestCase.doInjectTimeSource(t, new CreativeTimeSource(new long[] { 1L, 2L, 3L, 4L, 5L, 6L, 7L })); t.sampleSearch(0, true); t.sampleSearch(1, true); t.sampleSearch(2, true); @@ -72,47 +148,103 @@ public class XMLRendererTestCase { t.sampleSearchReturn(2, true, null); t.sampleSearchReturn(1, true, null); t.sampleSearchReturn(0, true, null); - r.getElapsedTime().add(t); - FastHit h = new FastHit("http://localhost/", .95); - h.setField("$a", "Hello, world."); - h.setField("b", "foo"); - r.hits().add(h); - HitGroup g = new HitGroup("usual"); - h = new FastHit("http://localhost/1", .90); - h.setField("c", "d"); - g.add(h); - r.hits().add(g); - HitGroup gg = new HitGroup("type grouphit"); - gg.types().add("grouphit"); - gg.setField("e", "f"); - r.hits().add(gg); - r.hits().addError(ErrorMessage.createInternalServerError("boom")); + result.getElapsedTime().add(t); + String summary = render(result); + + assertEquals("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<result total-hit-count=\"0\"", + summary.substring(0, 67)); + assertTrue(summary.contains("querytime=")); + assertTrue(summary.contains("summaryfetchtime=")); + assertTrue(summary.contains("searchtime=")); + assertTrue(summary.contains("<meta type=\"context\">")); + } + + private String render(Result result) throws Exception { + XmlRenderer renderer = new XmlRenderer(); + renderer.init(); ByteArrayOutputStream bs = new ByteArrayOutputStream(); - ListenableFuture<Boolean> f = d.render(bs, r, null, null); + ListenableFuture<Boolean> f = renderer.render(bs, result, null, null); assertTrue(f.get()); - String summary = Utf8.toString(bs.toByteArray()); + return Utf8.toString(bs.toByteArray()); + } - assertEquals("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" + - "<result total-hit-count=\"0\"", - summary.substring(0, 67) - ); - assertTrue(summary.contains("<meta type=\"context\">")); - assertTrue(summary.contains("<error code=\"18\">Internal server error.</error>")); - assertTrue(summary.contains("<hit type=\"grouphit\" relevancy=\"1.0\">")); - assertTrue(summary.contains("<hit type=\"summary\" relevancy=\"0.95\">")); - assertEquals(2, occurrences("<error ", summary)); - assertTrue(summary.length() > 900); + private Result createResult(String sdName, String content, boolean bolding) { + Chain<Searcher> chain = createSearchChain(sdName, content); + Query query = new Query("?query=12"); + if ( ! bolding) + query = new Query("?query=12&bolding=false"); + Execution execution = createExecution(chain); + Result result = execution.search(query); + execution.fill(result); + return result; + } + + /** + * Creates a search chain which always returns a result with one hit containing information given in this + * + * @param sdName the search definition type of the returned hit + * @param content the content of the "dynteaser" field of the returned hit + */ + private Chain<Searcher> createSearchChain(String sdName, String content) { + JuniperSearcher searcher = new JuniperSearcher(new ComponentId("test"), + new QrSearchersConfig(new QrSearchersConfig.Builder())); + + DocumentSourceSearcher docsource = new DocumentSourceSearcher(); + addResult(new Query("?query=12"), sdName, content, docsource); + addResult(new Query("?query=12&bolding=false"), sdName, content, docsource); + return new Chain<>(searcher, docsource); + } + + private void addResult(Query query, String sdName, String content, DocumentSourceSearcher docsource) { + Result r = new Result(query); + FastHit hit = new FastHit(); + hit.setId("http://abc.html"); + hit.setRelevance(new Relevance(1)); + hit.setField(Hit.SDDOCNAME_FIELD, sdName); + hit.setField("dynteaser", content); + r.hits().add(hit); + docsource.addResult(query, r); + } + + private Execution createExecution(Chain<Searcher> chain) { + Map<String, List<String>> clusters = new LinkedHashMap<>(); + Map<String, SearchDefinition> searchDefs = new LinkedHashMap<>(); + searchDefs.put("one", createSearchDefinitionOne()); + SearchDefinition union = new SearchDefinition("union"); + IndexModel indexModel = new IndexModel(clusters, searchDefs, union); + return new Execution(chain, Execution.Context.createContextStub(new IndexFacts(indexModel))); } - private int occurrences(String fragment, String string) { - int occurrences = 0; - int cursor = 0; - while ( -1 != (cursor = string.indexOf(fragment, cursor))) { - occurrences++; - cursor += fragment.length(); + private SearchDefinition createSearchDefinitionOne() { + SearchDefinition one = new SearchDefinition("one"); + + Index dynteaser = new Index("dynteaser"); + dynteaser.setDynamicSummary(true); + one.addIndex(dynteaser); + + Index bigteaser = new Index("bigteaser"); + dynteaser.setHighlightSummary(true); + one.addIndex(bigteaser); + + Index otherteaser = new Index("otherteaser"); + otherteaser.setDynamicSummary(true); + one.addIndex(otherteaser); + + return one; + } + + public static class NoopSearcher extends Searcher { + + public NoopSearcher(String name) { + super(new ComponentId(name)); } - return occurrences; + + @Override + public Result search(Query query, Execution execution) { + return execution.search(query); + } + } } diff --git a/container-search/src/test/java/com/yahoo/search/statistics/ElapsedTimeTestCase.java b/container-search/src/test/java/com/yahoo/search/statistics/ElapsedTimeTestCase.java index 15937e77899..7491e970b23 100644 --- a/container-search/src/test/java/com/yahoo/search/statistics/ElapsedTimeTestCase.java +++ b/container-search/src/test/java/com/yahoo/search/statistics/ElapsedTimeTestCase.java @@ -50,6 +50,7 @@ public class ElapsedTimeTestCase { } public static class UselessSearcher extends Searcher { + public UselessSearcher(String name) { super(new ComponentId(name)); } @@ -58,9 +59,11 @@ public class ElapsedTimeTestCase { public Result search(Query query, Execution execution) { return execution.search(query); } + } private static class AlmostUselessSearcher extends Searcher { + AlmostUselessSearcher(String name) { super(new ComponentId(name)); } @@ -73,9 +76,11 @@ public class ElapsedTimeTestCase { r.hits().add(h); return r; } + } private static class NoForwardSearcher extends Searcher { + @Override public Result search(Query query, Execution execution) { Result r = new Result(query); @@ -84,6 +89,7 @@ public class ElapsedTimeTestCase { r.hits().add(h); return r; } + } private class TestingSearcher extends Searcher { |