Skip to content

Commit b4fab9c

Browse files
authored
[7.17] Add a highlighter unit test base class (#85719) (#87413)
The vast majority of our highlighter tests are integration or rest tests, which exercise the full ES stack but take a long time to run and are difficult to debug. We have a few unit tests but they are testing very low-level behaviour, and don't interact with the fetch phase or hit contexts. This commit adds a new HighlighterTestCase base class with some helper methods that should fill the gap between these two sets of tests. It includes a method that takes a MapperService, ParsedDocument and SearchSourceBuilder, and then runs the appropriate highlighter fetch subphase over the resulting hit.
1 parent 659e818 commit b4fab9c

File tree

3 files changed

+133
-1
lines changed

3 files changed

+133
-1
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.search.fetch.subphase.highlight;
10+
11+
import org.elasticsearch.index.mapper.MapperService;
12+
import org.elasticsearch.index.mapper.ParsedDocument;
13+
import org.elasticsearch.index.query.QueryBuilders;
14+
import org.elasticsearch.search.builder.SearchSourceBuilder;
15+
import org.elasticsearch.search.fetch.HighlighterTestCase;
16+
17+
import java.io.IOException;
18+
import java.util.Map;
19+
20+
public class CustomUnifiedHighlighterTests extends HighlighterTestCase {
21+
22+
public void testSimpleTermHighlighting() throws IOException {
23+
24+
MapperService mapperService = createMapperService(mapping(b -> {
25+
b.startObject("field");
26+
b.field("type", "text");
27+
b.endObject();
28+
}));
29+
30+
ParsedDocument doc = mapperService.documentMapper().parse(source(b -> b.field("field", "this is some text")));
31+
32+
SearchSourceBuilder search = new SearchSourceBuilder().query(QueryBuilders.termQuery("field", "some"))
33+
.highlighter(new HighlightBuilder().field("field"));
34+
35+
Map<String, HighlightField> highlights = highlight(mapperService, doc, search);
36+
assertHighlights(highlights, "field", "this is <em>some</em> text");
37+
}
38+
39+
}

test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,10 @@ protected final void withAggregationContext(
547547
}
548548

549549
protected SearchExecutionContext createSearchExecutionContext(MapperService mapperService) {
550+
return createSearchExecutionContext(mapperService, null);
551+
}
552+
553+
protected SearchExecutionContext createSearchExecutionContext(MapperService mapperService, IndexSearcher searcher) {
550554
final SimilarityService similarityService = new SimilarityService(
551555
mapperService.getIndexSettings(),
552556
null,
@@ -567,7 +571,7 @@ protected SearchExecutionContext createSearchExecutionContext(MapperService mapp
567571
xContentRegistry(),
568572
writableRegistry(),
569573
null,
570-
null,
574+
searcher,
571575
() -> nowInMillis,
572576
null,
573577
null,
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.search.fetch;
10+
11+
import org.apache.lucene.search.IndexSearcher;
12+
import org.elasticsearch.common.text.Text;
13+
import org.elasticsearch.index.mapper.MapperService;
14+
import org.elasticsearch.index.mapper.MapperServiceTestCase;
15+
import org.elasticsearch.index.mapper.ParsedDocument;
16+
import org.elasticsearch.index.query.ParsedQuery;
17+
import org.elasticsearch.index.query.SearchExecutionContext;
18+
import org.elasticsearch.search.SearchHit;
19+
import org.elasticsearch.search.builder.SearchSourceBuilder;
20+
import org.elasticsearch.search.fetch.subphase.highlight.FastVectorHighlighter;
21+
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
22+
import org.elasticsearch.search.fetch.subphase.highlight.HighlightPhase;
23+
import org.elasticsearch.search.fetch.subphase.highlight.Highlighter;
24+
import org.elasticsearch.search.fetch.subphase.highlight.PlainHighlighter;
25+
import org.elasticsearch.search.fetch.subphase.highlight.UnifiedHighlighter;
26+
27+
import java.io.IOException;
28+
import java.util.Arrays;
29+
import java.util.HashMap;
30+
import java.util.HashSet;
31+
import java.util.Map;
32+
import java.util.Set;
33+
import java.util.stream.Collectors;
34+
35+
import static org.mockito.Mockito.mock;
36+
import static org.mockito.Mockito.when;
37+
38+
public class HighlighterTestCase extends MapperServiceTestCase {
39+
40+
protected Map<String, Highlighter> getHighlighters() {
41+
Map<String, Highlighter> highlighters = new HashMap<>();
42+
highlighters.put("unified", new UnifiedHighlighter());
43+
highlighters.put("fvh", new FastVectorHighlighter(getIndexSettings()));
44+
highlighters.put("plain", new PlainHighlighter());
45+
return highlighters;
46+
}
47+
48+
/**
49+
* Runs the highlight phase for a search over a specific document
50+
* @param mapperService the Mappings to use for highlighting
51+
* @param doc a parsed document to highlight
52+
* @param search the search to highlight
53+
*/
54+
protected final Map<String, HighlightField> highlight(MapperService mapperService, ParsedDocument doc, SearchSourceBuilder search)
55+
throws IOException {
56+
Map<String, HighlightField> highlights = new HashMap<>();
57+
withLuceneIndex(mapperService, iw -> iw.addDocument(doc.rootDoc()), ir -> {
58+
SearchExecutionContext context = createSearchExecutionContext(mapperService, new IndexSearcher(ir));
59+
HighlightPhase highlightPhase = new HighlightPhase(getHighlighters());
60+
FetchSubPhaseProcessor processor = highlightPhase.getProcessor(fetchContext(context, search));
61+
FetchSubPhase.HitContext hitContext = new FetchSubPhase.HitContext(
62+
new SearchHit(0, "id", new Text("_doc"), null, null),
63+
ir.leaves().get(0),
64+
0
65+
);
66+
processor.process(hitContext);
67+
highlights.putAll(hitContext.hit().getHighlightFields());
68+
});
69+
return highlights;
70+
}
71+
72+
/**
73+
* Given a set of highlights, assert that any particular field has the expected fragments
74+
*/
75+
protected static void assertHighlights(Map<String, HighlightField> highlights, String field, String... fragments) {
76+
assertNotNull(highlights.get(field));
77+
Set<String> actualFragments = Arrays.stream(highlights.get(field).getFragments()).map(Text::toString).collect(Collectors.toSet());
78+
Set<String> expectedFragments = new HashSet<>(Arrays.asList(fragments));
79+
assertEquals(expectedFragments, actualFragments);
80+
}
81+
82+
private static FetchContext fetchContext(SearchExecutionContext context, SearchSourceBuilder search) throws IOException {
83+
FetchContext fetchContext = mock(FetchContext.class);
84+
when(fetchContext.highlight()).thenReturn(search.highlighter().build(context));
85+
when(fetchContext.parsedQuery()).thenReturn(new ParsedQuery(search.query().toQuery(context)));
86+
when(fetchContext.getSearchExecutionContext()).thenReturn(context);
87+
return fetchContext;
88+
}
89+
}

0 commit comments

Comments
 (0)