// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.prelude.searcher;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import com.yahoo.component.annotation.Inject;
import com.yahoo.component.ComponentId;
import com.yahoo.component.chain.dependencies.After;
import com.yahoo.component.chain.dependencies.Before;
import com.yahoo.component.chain.dependencies.Provides;
import com.yahoo.data.access.ArrayTraverser;
import com.yahoo.data.access.Inspectable;
import com.yahoo.data.access.Inspector;
import com.yahoo.data.access.Type;
import com.yahoo.data.access.simple.Value;
import com.yahoo.prelude.Index;
import com.yahoo.prelude.IndexFacts;
import com.yahoo.search.Searcher;
import com.yahoo.container.QrSearchersConfig;
import com.yahoo.prelude.fastsearch.FastHit;
import com.yahoo.prelude.hitfield.BoldCloseFieldPart;
import com.yahoo.prelude.hitfield.BoldOpenFieldPart;
import com.yahoo.prelude.hitfield.FieldPart;
import com.yahoo.prelude.hitfield.HitField;
import com.yahoo.prelude.hitfield.SeparatorFieldPart;
import com.yahoo.prelude.hitfield.StringFieldPart;
import com.yahoo.search.Query;
import com.yahoo.search.Result;
import com.yahoo.search.result.Hit;
import com.yahoo.search.searchchain.Execution;
import com.yahoo.search.searchchain.PhaseNames;
/**
* Converts juniper highlighting to XML style
*
* Note: This searcher only converts backend binary highlighting and separators
* to the configured highlighting and separator tags.
*
* @author Steinar Knutsen
*/
@After(PhaseNames.RAW_QUERY)
@Before(PhaseNames.TRANSFORMED_QUERY)
@Provides(JuniperSearcher.JUNIPER_TAG_REPLACING)
public class JuniperSearcher extends Searcher {
public final static char RAW_HIGHLIGHT_CHAR = '\u001F';
public final static char RAW_SEPARATOR_CHAR = '\u001E';
private static final String ELLIPSIS = "...";
public static final String JUNIPER_TAG_REPLACING = "JuniperTagReplacing";
private final String boldOpenTag;
private final String boldCloseTag;
private final String separatorTag;
@Inject
public JuniperSearcher(ComponentId id, QrSearchersConfig config) {
super(id);
boldOpenTag = config.tag().bold().open();
boldCloseTag = config.tag().bold().close();
separatorTag = config.tag().separator();
}
/**
* Convert Juniper style property highlighting to XML style.
*/
@Override
public Result search(Query query, Execution execution) {
Result result = execution.search(query);
highlight(query.getPresentation().getBolding(), result.hits().deepIterator(), null,
execution.context().getIndexFacts().newSession(query));
return result;
}
@Override
public void fill(Result result, String summaryClass, Execution execution) {
int worstCase = result.getHitCount();
List hits = new ArrayList<>(worstCase);
for (Iterator i = result.hits().deepIterator(); i.hasNext();) {
Hit hit = i.next();
if ( ! (hit instanceof FastHit)) continue;
FastHit fastHit = (FastHit)hit;
if (fastHit.isFilled(summaryClass)) continue;
hits.add(fastHit);
}
execution.fill(result, summaryClass);
highlight(result.getQuery().getPresentation().getBolding(), hits.iterator(), summaryClass,
execution.context().getIndexFacts().newSession(result.getQuery()));
}
private void highlight(boolean bolding, Iterator hitsToHighlight,
String summaryClass, IndexFacts.Session indexFacts) {
while (hitsToHighlight.hasNext()) {
Hit hit = hitsToHighlight.next();
if ( ! (hit instanceof FastHit)) continue;
FastHit fastHit = (FastHit) hit;
if (summaryClass != null && ! fastHit.isFilled(summaryClass)) continue;
Object searchDefinitionField = fastHit.getField(Hit.SDDOCNAME_FIELD);
if (searchDefinitionField == null) continue;
for (Index index : indexFacts.getIndexes(searchDefinitionField.toString())) {
if (index.getDynamicSummary() || index.getHighlightSummary()) {
var field = fastHit.getField(index.getName());
if (StringArrayConverter.shouldHandleField(field)) {
new StringArrayConverter(fastHit, index, field, bolding);
} else {
HitField fieldValue = fastHit.buildHitField(index.getName(), true);
if (fieldValue != null) {
insertTags(fieldValue, bolding, index.getDynamicSummary());
}
}
}
}
}
}
private class StringArrayConverter implements ArrayTraverser {
private Index index;
private boolean bolding;
private Value.ArrayValue convertedField = new Value.ArrayValue();
/**
* This converts the backend binary highlighting of each item in an array of string field,
* and creates a new field that replaces the original.
*/
StringArrayConverter(FastHit hit, Index index, Object field, boolean bolding) {
this.index = index;
this.bolding = bolding;
((Inspectable)field).inspect().traverse(this);
hit.setField(index.getName(), convertedField);
}
static boolean shouldHandleField(Object field) {
return (field instanceof Inspectable) &&
(((Inspectable)field).inspect().type() == Type.ARRAY);
}
@Override
public void entry(int idx, Inspector inspector) {
// This is how HitField is instantiated in Hit.buildHitField() when forceNoPreTokenize=true.
var hitField = new HitField(index.getName(), inspector.asString(), false);
insertTags(hitField, bolding, index.getDynamicSummary());
convertedField.add(hitField.getContent());
}
}
private void insertTags(HitField field, boolean bolding, boolean dynteaser) {
boolean insideHighlight = false;
for (ListIterator i = field.listIterator(); i.hasNext();) {
FieldPart f = i.next();
if (f instanceof SeparatorFieldPart)
setSeparatorString(bolding, (SeparatorFieldPart) f);
if (f.isFinal()) continue;
String toQuote = f.getContent();
List newFieldParts = null;
int previous = 0;
for (int j = 0; j < toQuote.length(); j++) {
char key = toQuote.charAt(j);
switch (key) {
case RAW_HIGHLIGHT_CHAR:
newFieldParts = initFieldParts(newFieldParts);
addBolding(bolding, insideHighlight, f, toQuote, newFieldParts, previous, j);
previous = j + 1;
insideHighlight = !insideHighlight;
break;
case RAW_SEPARATOR_CHAR:
newFieldParts = initFieldParts(newFieldParts);
addSeparator(bolding, dynteaser, f, toQuote, newFieldParts, previous, j);
previous = j + 1;
break;
default:
// no action
break;
}
}
if (previous > 0 && previous < toQuote.length()) {
newFieldParts.add(new StringFieldPart(toQuote.substring(previous), f.isToken()));
}
if (newFieldParts != null) {
i.remove();
for (Iterator j = newFieldParts.iterator(); j.hasNext();) {
i.add(j.next());
}
}
}
}
private void setSeparatorString(boolean bolding, SeparatorFieldPart f) {
if (bolding)
f.setContent(separatorTag);
else
f.setContent(ELLIPSIS);
}
private void addSeparator(boolean bolding, boolean dynteaser, FieldPart f, String toQuote,
List newFieldParts, int previous, int j) {
if (previous != j)
newFieldParts.add(new StringFieldPart(toQuote.substring(previous, j), f.isToken()));
if (dynteaser)
newFieldParts.add(bolding ? new SeparatorFieldPart(separatorTag) : new SeparatorFieldPart(ELLIPSIS));
}
private void addBolding(boolean bolding, boolean insideHighlight, FieldPart f, String toQuote,
List newFieldParts, int previous, int j) {
if (previous != j) {
newFieldParts.add(new StringFieldPart(toQuote.substring(previous, j), f.isToken()));
}
if (bolding) {
if (insideHighlight) {
newFieldParts.add(new BoldCloseFieldPart(boldCloseTag));
} else {
if (newFieldParts.size() > 0
&& newFieldParts.get(newFieldParts.size() - 1) instanceof BoldCloseFieldPart) {
newFieldParts.remove(newFieldParts.size() - 1);
} else {
newFieldParts.add(new BoldOpenFieldPart(boldOpenTag));
}
}
}
}
private List initFieldParts(List newFieldParts) {
if (newFieldParts == null)
newFieldParts = new ArrayList<>();
return newFieldParts;
}
}