blob: 4c6ddf3b34ddbf5e514a88ef2f4765369cdbf32a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.search.vectorhighlight;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.FieldInfo;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.StoredFieldVisitor;
import org.apache.lucene.search.highlight.DefaultEncoder;
import org.apache.lucene.search.highlight.Encoder;
import org.apache.lucene.search.vectorhighlight.FieldFragList.WeightedFragInfo;
import org.apache.lucene.search.vectorhighlight.FieldFragList.WeightedFragInfo.SubInfo;
import org.apache.lucene.search.vectorhighlight.FieldPhraseList.WeightedPhraseInfo.Toffs;
/**
* Base FragmentsBuilder implementation that supports colored pre/post tags and multivalued fields.
*
* <p>Uses {@link BoundaryScanner} to determine fragments.
*/
public abstract class BaseFragmentsBuilder implements FragmentsBuilder {
protected String[] preTags, postTags;
public static final String[] COLORED_PRE_TAGS = {
"<b style=\"background:yellow\">", "<b style=\"background:lawngreen\">",
"<b style=\"background:aquamarine\">",
"<b style=\"background:magenta\">", "<b style=\"background:palegreen\">",
"<b style=\"background:coral\">",
"<b style=\"background:wheat\">", "<b style=\"background:khaki\">",
"<b style=\"background:lime\">",
"<b style=\"background:deepskyblue\">", "<b style=\"background:deeppink\">",
"<b style=\"background:salmon\">",
"<b style=\"background:peachpuff\">", "<b style=\"background:violet\">",
"<b style=\"background:mediumpurple\">",
"<b style=\"background:palegoldenrod\">", "<b style=\"background:darkkhaki\">",
"<b style=\"background:springgreen\">",
"<b style=\"background:turquoise\">", "<b style=\"background:powderblue\">"
};
public static final String[] COLORED_POST_TAGS = {"</b>"};
private char multiValuedSeparator = ' ';
private final BoundaryScanner boundaryScanner;
private boolean discreteMultiValueHighlighting = false;
protected BaseFragmentsBuilder() {
this(new String[] {"<b>"}, new String[] {"</b>"});
}
protected BaseFragmentsBuilder(String[] preTags, String[] postTags) {
this(preTags, postTags, new SimpleBoundaryScanner());
}
protected BaseFragmentsBuilder(BoundaryScanner boundaryScanner) {
this(new String[] {"<b>"}, new String[] {"</b>"}, boundaryScanner);
}
protected BaseFragmentsBuilder(
String[] preTags, String[] postTags, BoundaryScanner boundaryScanner) {
this.preTags = preTags;
this.postTags = postTags;
this.boundaryScanner = boundaryScanner;
}
static Object checkTagsArgument(Object tags) {
if (tags instanceof String) {
return tags;
} else if (tags instanceof String[]) {
return tags;
}
throw new IllegalArgumentException("type of preTags/postTags must be a String or String[]");
}
public abstract List<WeightedFragInfo> getWeightedFragInfoList(List<WeightedFragInfo> src);
private static final Encoder NULL_ENCODER = new DefaultEncoder();
@Override
public String createFragment(
IndexReader reader, int docId, String fieldName, FieldFragList fieldFragList)
throws IOException {
return createFragment(reader, docId, fieldName, fieldFragList, preTags, postTags, NULL_ENCODER);
}
@Override
public String[] createFragments(
IndexReader reader,
int docId,
String fieldName,
FieldFragList fieldFragList,
int maxNumFragments)
throws IOException {
return createFragments(
reader, docId, fieldName, fieldFragList, maxNumFragments, preTags, postTags, NULL_ENCODER);
}
@Override
public String createFragment(
IndexReader reader,
int docId,
String fieldName,
FieldFragList fieldFragList,
String[] preTags,
String[] postTags,
Encoder encoder)
throws IOException {
String[] fragments =
createFragments(reader, docId, fieldName, fieldFragList, 1, preTags, postTags, encoder);
if (fragments == null || fragments.length == 0) return null;
return fragments[0];
}
@Override
public String[] createFragments(
IndexReader reader,
int docId,
String fieldName,
FieldFragList fieldFragList,
int maxNumFragments,
String[] preTags,
String[] postTags,
Encoder encoder)
throws IOException {
if (maxNumFragments < 0) {
throw new IllegalArgumentException(
"maxNumFragments(" + maxNumFragments + ") must be positive number.");
}
List<WeightedFragInfo> fragInfos = fieldFragList.getFragInfos();
Field[] values = getFields(reader, docId, fieldName);
if (values.length == 0) {
return null;
}
if (discreteMultiValueHighlighting && values.length > 1) {
fragInfos = discreteMultiValueHighlighting(fragInfos, values);
}
fragInfos = getWeightedFragInfoList(fragInfos);
int limitFragments = maxNumFragments < fragInfos.size() ? maxNumFragments : fragInfos.size();
List<String> fragments = new ArrayList<>(limitFragments);
StringBuilder buffer = new StringBuilder();
int[] nextValueIndex = {0};
for (int n = 0; n < limitFragments; n++) {
WeightedFragInfo fragInfo = fragInfos.get(n);
fragments.add(
makeFragment(buffer, nextValueIndex, values, fragInfo, preTags, postTags, encoder));
}
return fragments.toArray(new String[fragments.size()]);
}
protected Field[] getFields(IndexReader reader, int docId, final String fieldName)
throws IOException {
// according to javadoc, doc.getFields(fieldName) cannot be used with lazy loaded field???
final List<Field> fields = new ArrayList<>();
reader.document(
docId,
new StoredFieldVisitor() {
@Override
public void stringField(FieldInfo fieldInfo, String value) {
Objects.requireNonNull(value, "String value should not be null");
FieldType ft = new FieldType(TextField.TYPE_STORED);
ft.setStoreTermVectors(fieldInfo.hasVectors());
fields.add(new Field(fieldInfo.name, value, ft));
}
@Override
public Status needsField(FieldInfo fieldInfo) {
return fieldInfo.name.equals(fieldName) ? Status.YES : Status.NO;
}
});
return fields.toArray(new Field[fields.size()]);
}
protected String makeFragment(
StringBuilder buffer,
int[] index,
Field[] values,
WeightedFragInfo fragInfo,
String[] preTags,
String[] postTags,
Encoder encoder) {
StringBuilder fragment = new StringBuilder();
final int s = fragInfo.getStartOffset();
int[] modifiedStartOffset = {s};
String src =
getFragmentSourceMSO(
buffer, index, values, s, fragInfo.getEndOffset(), modifiedStartOffset);
int srcIndex = 0;
for (SubInfo subInfo : fragInfo.getSubInfos()) {
for (Toffs to : subInfo.getTermsOffsets()) {
fragment
.append(
encoder.encodeText(
src.substring(srcIndex, to.getStartOffset() - modifiedStartOffset[0])))
.append(getPreTag(preTags, subInfo.getSeqnum()))
.append(
encoder.encodeText(
src.substring(
to.getStartOffset() - modifiedStartOffset[0],
to.getEndOffset() - modifiedStartOffset[0])))
.append(getPostTag(postTags, subInfo.getSeqnum()));
srcIndex = to.getEndOffset() - modifiedStartOffset[0];
}
}
fragment.append(encoder.encodeText(src.substring(srcIndex)));
return fragment.toString();
}
protected String getFragmentSourceMSO(
StringBuilder buffer,
int[] index,
Field[] values,
int startOffset,
int endOffset,
int[] modifiedStartOffset) {
while (buffer.length() < endOffset && index[0] < values.length) {
buffer.append(values[index[0]++].stringValue());
buffer.append(getMultiValuedSeparator());
}
int bufferLength = buffer.length();
// we added the multi value char to the last buffer, ignore it
if (values[index[0] - 1].fieldType().tokenized()) {
bufferLength--;
}
int eo =
bufferLength < endOffset ? bufferLength : boundaryScanner.findEndOffset(buffer, endOffset);
modifiedStartOffset[0] = boundaryScanner.findStartOffset(buffer, startOffset);
return buffer.substring(modifiedStartOffset[0], eo);
}
protected String getFragmentSource(
StringBuilder buffer, int[] index, Field[] values, int startOffset, int endOffset) {
while (buffer.length() < endOffset && index[0] < values.length) {
buffer.append(values[index[0]].stringValue());
buffer.append(multiValuedSeparator);
index[0]++;
}
int eo = buffer.length() < endOffset ? buffer.length() : endOffset;
return buffer.substring(startOffset, eo);
}
protected List<WeightedFragInfo> discreteMultiValueHighlighting(
List<WeightedFragInfo> fragInfos, Field[] fields) {
Map<String, List<WeightedFragInfo>> fieldNameToFragInfos = new HashMap<>();
for (Field field : fields) {
fieldNameToFragInfos.put(field.name(), new ArrayList<WeightedFragInfo>());
}
fragInfos:
for (WeightedFragInfo fragInfo : fragInfos) {
int fieldStart;
int fieldEnd = 0;
for (Field field : fields) {
if (field.stringValue().isEmpty()) {
fieldEnd++;
continue;
}
fieldStart = fieldEnd;
fieldEnd += field.stringValue().length() + 1; // + 1 for going to next field with same name.
if (fragInfo.getStartOffset() >= fieldStart
&& fragInfo.getEndOffset() >= fieldStart
&& fragInfo.getStartOffset() <= fieldEnd
&& fragInfo.getEndOffset() <= fieldEnd) {
fieldNameToFragInfos.get(field.name()).add(fragInfo);
continue fragInfos;
}
if (fragInfo.getSubInfos().isEmpty()) {
continue fragInfos;
}
Toffs firstToffs = fragInfo.getSubInfos().get(0).getTermsOffsets().get(0);
if (fragInfo.getStartOffset() >= fieldEnd || firstToffs.getStartOffset() >= fieldEnd) {
continue;
}
int fragStart = fieldStart;
if (fragInfo.getStartOffset() > fieldStart && fragInfo.getStartOffset() < fieldEnd) {
fragStart = fragInfo.getStartOffset();
}
int fragEnd = fieldEnd;
if (fragInfo.getEndOffset() > fieldStart && fragInfo.getEndOffset() < fieldEnd) {
fragEnd = fragInfo.getEndOffset();
}
List<SubInfo> subInfos = new ArrayList<>();
Iterator<SubInfo> subInfoIterator = fragInfo.getSubInfos().iterator();
// The boost of the new info will be the sum of the boosts of its SubInfos
float boost = 0.0f;
while (subInfoIterator.hasNext()) {
SubInfo subInfo = subInfoIterator.next();
List<Toffs> toffsList = new ArrayList<>();
Iterator<Toffs> toffsIterator = subInfo.getTermsOffsets().iterator();
while (toffsIterator.hasNext()) {
Toffs toffs = toffsIterator.next();
if (toffs.getStartOffset() >= fieldEnd) {
// We've gone past this value so its not worth iterating any more.
break;
}
boolean startsAfterField = toffs.getStartOffset() >= fieldStart;
boolean endsBeforeField = toffs.getEndOffset() < fieldEnd;
if (startsAfterField && endsBeforeField) {
// The Toff is entirely within this value.
toffsList.add(toffs);
toffsIterator.remove();
} else if (startsAfterField) {
/*
* The Toffs starts within this value but ends after this value
* so we clamp the returned Toffs to this value and leave the
* Toffs in the iterator for the next value of this field.
*/
toffsList.add(new Toffs(toffs.getStartOffset(), fieldEnd - 1));
} else if (endsBeforeField) {
/*
* The Toffs starts before this value but ends in this value
* which means we're really continuing from where we left off
* above. Since we use the remainder of the offset we can remove
* it from the iterator.
*/
toffsList.add(new Toffs(fieldStart, toffs.getEndOffset()));
toffsIterator.remove();
} else {
/*
* The Toffs spans the whole value so we clamp on both sides.
* This is basically a combination of both arms of the loop
* above.
*/
toffsList.add(new Toffs(fieldStart, fieldEnd - 1));
}
}
if (!toffsList.isEmpty()) {
subInfos.add(
new SubInfo(subInfo.getText(), toffsList, subInfo.getSeqnum(), subInfo.getBoost()));
boost += subInfo.getBoost();
}
if (subInfo.getTermsOffsets().isEmpty()) {
subInfoIterator.remove();
}
}
WeightedFragInfo weightedFragInfo =
new WeightedFragInfo(fragStart, fragEnd, subInfos, boost);
fieldNameToFragInfos.get(field.name()).add(weightedFragInfo);
}
}
List<WeightedFragInfo> result = new ArrayList<>();
for (List<WeightedFragInfo> weightedFragInfos : fieldNameToFragInfos.values()) {
result.addAll(weightedFragInfos);
}
Collections.sort(
result,
new Comparator<WeightedFragInfo>() {
@Override
public int compare(
FieldFragList.WeightedFragInfo info1, FieldFragList.WeightedFragInfo info2) {
return info1.getStartOffset() - info2.getStartOffset();
}
});
return result;
}
public void setMultiValuedSeparator(char separator) {
multiValuedSeparator = separator;
}
public char getMultiValuedSeparator() {
return multiValuedSeparator;
}
public boolean isDiscreteMultiValueHighlighting() {
return discreteMultiValueHighlighting;
}
public void setDiscreteMultiValueHighlighting(boolean discreteMultiValueHighlighting) {
this.discreteMultiValueHighlighting = discreteMultiValueHighlighting;
}
protected String getPreTag(int num) {
return getPreTag(preTags, num);
}
protected String getPostTag(int num) {
return getPostTag(postTags, num);
}
protected String getPreTag(String[] preTags, int num) {
int n = num % preTags.length;
return preTags[n];
}
protected String getPostTag(String[] postTags, int num) {
int n = num % postTags.length;
return postTags[n];
}
}