diff --git a/src/main/java/org/codelibs/fess/Constants.java b/src/main/java/org/codelibs/fess/Constants.java index 9e17c864a6d663fc1a9a618f0b83930f6dfcfbed..a686227ba246fec1beadcde2bc541287b1d6744a 100644 --- a/src/main/java/org/codelibs/fess/Constants.java +++ b/src/main/java/org/codelibs/fess/Constants.java @@ -471,4 +471,11 @@ public class Constants extends CoreLibConstants { public static final String EXECUTE_TYPE_SUGGEST = "suggest"; public static final String DEFAULT_SCRIPT = "groovy"; + + public static final String TEXT_FRAGMENTS = "text_fragments"; + + public static final String TEXT_FRAGMENT_TYPE_QUERY = "query"; + + public static final String TEXT_FRAGMENT_TYPE_HIGHLIGHT = "highlight"; + } diff --git a/src/main/java/org/codelibs/fess/helper/ViewHelper.java b/src/main/java/org/codelibs/fess/helper/ViewHelper.java index 61212c2d3c019e860fdeeb7fd6552e919af9fd10..563eb4077f33ba8dc99889bc8f17081a0b34aac0 100644 --- a/src/main/java/org/codelibs/fess/helper/ViewHelper.java +++ b/src/main/java/org/codelibs/fess/helper/ViewHelper.java @@ -57,6 +57,8 @@ import org.codelibs.core.io.CloseableUtil; import org.codelibs.core.lang.StringUtil; import org.codelibs.core.misc.DynamicProperties; import org.codelibs.core.stream.StreamUtil; +import org.codelibs.fesen.common.text.Text; +import org.codelibs.fesen.search.fetch.subphase.highlight.HighlightField; import org.codelibs.fess.Constants; import org.codelibs.fess.app.web.base.SearchForm; import org.codelibs.fess.app.web.base.login.FessLoginAssist; @@ -114,6 +116,8 @@ public class ViewHelper { protected static final Pattern SHARED_FOLDER_PATTERN = Pattern.compile("^file:/+[^/]\\."); + protected static final String ELLIPSIS = "..."; + protected boolean encodeUrlLink = false; protected String urlLinkEncoding = Constants.UTF_8; @@ -154,6 +158,12 @@ public class ViewHelper { protected long facetCacheDuration = 60 * 10L; // 10min + protected int textFragmentPrefixLength; + + protected int textFragmentSuffixLength; + + protected int textFragmentSize; + @PostConstruct public void init() { if (logger.isDebugEnabled()) { @@ -197,6 +207,10 @@ public class ViewHelper { })); facetCache = CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(facetCacheDuration, TimeUnit.SECONDS).build(); + + textFragmentPrefixLength = fessConfig.getQueryHighlightTextFragmentPrefixLengthAsInteger(); + textFragmentSuffixLength = fessConfig.getQueryHighlightTextFragmentSuffixLengthAsInteger(); + textFragmentSize = fessConfig.getQueryHighlightTextFragmentSizeAsInteger(); } public String getContentTitle(final Map<String, Object> document) { @@ -438,14 +452,39 @@ public class ViewHelper { } final String mimetype = DocumentUtil.getValue(document, fessConfig.getIndexFieldMimetype(), String.class); - if (StringUtil.isNotBlank(mimetype) && "application/pdf".equals(mimetype)) { - return appendPDFSearchWord(url); + if (StringUtil.isNotBlank(mimetype)) { + switch (mimetype) { + case "text/html": + return appendHTMLSearchWord(document, url); + case "application/pdf": + return appendPDFSearchWord(document, url); + default: + break; + } } } return url; } + protected String appendHTMLSearchWord(final Map<String, Object> document, final String url) { + final TextFragment[] textFragments = (TextFragment[]) document.get(Constants.TEXT_FRAGMENTS); + if (textFragments != null) { + final StringBuilder buf = new StringBuilder(1000); + buf.append(url).append("#:~:"); + for (int i = 0; i < textFragmentSize && i < textFragments.length; i++) { + buf.append(textFragments[i].toURLString()).append('&'); + } + return buf.toString(); + } + return url; + } + + @Deprecated protected String appendPDFSearchWord(final String url) { + return appendPDFSearchWord(null, url); + } + + protected String appendPDFSearchWord(final Map<String, Object> document, final String url) { final String queries = (String) LaRequestUtil.getRequest().getAttribute(Constants.REQUEST_QUERIES); if (queries != null) { try { @@ -783,6 +822,54 @@ public class ViewHelper { } } + public String createHighlightText(final HighlightField highlightField) { + final Text[] fragments = highlightField.fragments(); + if (fragments != null && fragments.length != 0) { + final String[] texts = new String[fragments.length]; + for (int i = 0; i < fragments.length; i++) { + texts[i] = fragments[i].string(); + } + String value = StringUtils.join(texts, ELLIPSIS); + if (StringUtil.isNotBlank(value) && !ComponentUtil.getFessConfig().endsWithFullstop(value)) { + return value + ELLIPSIS; + } + return value; + } + return null; + } + + public TextFragment[] createTextFragmentsByHighlight(final HighlightField[] fields) { + final List<TextFragment> list = new ArrayList<>(); + for (final HighlightField field : fields) { + final Text[] fragments = field.fragments(); + if (fragments != null) { + for (final Text fragment : fragments) { + final String text = fragment.string(); + if (text.length() > textFragmentPrefixLength + textFragmentSuffixLength) { + final String target = + text.replace(originalHighlightTagPre, StringUtil.EMPTY).replace(originalHighlightTagPost, StringUtil.EMPTY); + if (target.length() > textFragmentPrefixLength + textFragmentSuffixLength) { + list.add(new TextFragment(null, target.substring(0, textFragmentPrefixLength), + target.substring(target.length() - textFragmentSuffixLength), null)); + } + } + } + } + } + return list.toArray(n -> new TextFragment[n]); + } + + public TextFragment[] createTextFragmentsByQuery() { + return LaRequestUtil.getOptionalRequest().map(req -> { + @SuppressWarnings("unchecked") + Set<String> querySet = (Set<String>) req.getAttribute(Constants.HIGHLIGHT_QUERIES); + if (querySet != null) { + return querySet.stream().map(s -> new TextFragment(null, s, null, null)).toArray(n -> new TextFragment[n]); + } + return new TextFragment[0]; + }).orElse(new TextFragment[0]); + } + public boolean isUseSession() { return useSession; } @@ -827,6 +914,30 @@ public class ViewHelper { this.actionHook = actionHook; } + public void setEncodeUrlLink(final boolean encodeUrlLink) { + this.encodeUrlLink = encodeUrlLink; + } + + public void setUrlLinkEncoding(final String urlLinkEncoding) { + this.urlLinkEncoding = urlLinkEncoding; + } + + public void setOriginalHighlightTagPre(final String originalHighlightTagPre) { + this.originalHighlightTagPre = originalHighlightTagPre; + } + + public void setOriginalHighlightTagPost(final String originalHighlightTagPost) { + this.originalHighlightTagPost = originalHighlightTagPost; + } + + public void setCacheTemplateName(final String cacheTemplateName) { + this.cacheTemplateName = cacheTemplateName; + } + + public void setFacetCacheDuration(final long facetCacheDuration) { + this.facetCacheDuration = facetCacheDuration; + } + public static class ActionHook { public ActionResponse godHandPrologue(final ActionRuntime runtime, final Function<ActionRuntime, ActionResponse> func) { @@ -850,27 +961,38 @@ public class ViewHelper { } } - public void setEncodeUrlLink(final boolean encodeUrlLink) { - this.encodeUrlLink = encodeUrlLink; - } - - public void setUrlLinkEncoding(final String urlLinkEncoding) { - this.urlLinkEncoding = urlLinkEncoding; - } - - public void setOriginalHighlightTagPre(final String originalHighlightTagPre) { - this.originalHighlightTagPre = originalHighlightTagPre; - } + // #:~:text=[prefix-,]textStart[,textEnd][,-suffix] + public static class TextFragment { + private String prefix; + private String textStart; + private String textEnd; + private String suffix; - public void setOriginalHighlightTagPost(final String originalHighlightTagPost) { - this.originalHighlightTagPost = originalHighlightTagPost; - } + TextFragment(final String prefix, final String textStart, final String textEnd, final String suffix) { + this.prefix = prefix; + this.textStart = textStart == null ? StringUtil.EMPTY : textStart; + this.textEnd = textEnd; + this.suffix = suffix; + } - public void setCacheTemplateName(final String cacheTemplateName) { - this.cacheTemplateName = cacheTemplateName; - } + public String toURLString() { + final StringBuilder buf = new StringBuilder(); + buf.append("text="); + if (StringUtil.isNotBlank(prefix)) { + buf.append(encodeToString(prefix)).append("-,"); + } + buf.append(encodeToString(textStart)); + if (StringUtil.isNotBlank(textEnd)) { + buf.append(',').append(encodeToString(textEnd)); + } + if (StringUtil.isNotBlank(suffix)) { + buf.append(",-").append(encodeToString(suffix)); + } + return buf.toString(); + } - public void setFacetCacheDuration(final long facetCacheDuration) { - this.facetCacheDuration = facetCacheDuration; + private String encodeToString(final String text) { + return URLEncoder.encode(text, Constants.CHARSET_UTF_8); + } } } diff --git a/src/main/java/org/codelibs/fess/mylasta/direction/FessConfig.java b/src/main/java/org/codelibs/fess/mylasta/direction/FessConfig.java index ea0ea9b1dd6368c63a29b56d43f9d8a4f099f9f2..5555dd6d6a8ffa7950de758f4babd09efb1657a7 100644 --- a/src/main/java/org/codelibs/fess/mylasta/direction/FessConfig.java +++ b/src/main/java/org/codelibs/fess/mylasta/direction/FessConfig.java @@ -825,6 +825,18 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction /** The key of the configuration. e.g. true */ String QUERY_HIGHLIGHT_BOUNDARY_POSITION_DETECT = "query.highlight.boundary.position.detect"; + /** The key of the configuration. e.g. query */ + String QUERY_HIGHLIGHT_TEXT_FRAGMENT_TYPE = "query.highlight.text.fragment.type"; + + /** The key of the configuration. e.g. 3 */ + String QUERY_HIGHLIGHT_TEXT_FRAGMENT_SIZE = "query.highlight.text.fragment.size"; + + /** The key of the configuration. e.g. 5 */ + String QUERY_HIGHLIGHT_TEXT_FRAGMENT_PREFIX_LENGTH = "query.highlight.text.fragment.prefix.length"; + + /** The key of the configuration. e.g. 5 */ + String QUERY_HIGHLIGHT_TEXT_FRAGMENT_SUFFIX_LENGTH = "query.highlight.text.fragment.suffix.length"; + /** The key of the configuration. e.g. 100000 */ String QUERY_MAX_SEARCH_RESULT_OFFSET = "query.max.search.result.offset"; @@ -4086,6 +4098,58 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction */ boolean isQueryHighlightBoundaryPositionDetect(); + /** + * Get the value for the key 'query.highlight.text.fragment.type'. <br> + * The value is, e.g. query <br> + * @return The value of found property. (NotNull: if not found, exception but basically no way) + */ + String getQueryHighlightTextFragmentType(); + + /** + * Get the value for the key 'query.highlight.text.fragment.size'. <br> + * The value is, e.g. 3 <br> + * @return The value of found property. (NotNull: if not found, exception but basically no way) + */ + String getQueryHighlightTextFragmentSize(); + + /** + * Get the value for the key 'query.highlight.text.fragment.size' as {@link Integer}. <br> + * The value is, e.g. 3 <br> + * @return The value of found property. (NotNull: if not found, exception but basically no way) + * @throws NumberFormatException When the property is not integer. + */ + Integer getQueryHighlightTextFragmentSizeAsInteger(); + + /** + * Get the value for the key 'query.highlight.text.fragment.prefix.length'. <br> + * The value is, e.g. 5 <br> + * @return The value of found property. (NotNull: if not found, exception but basically no way) + */ + String getQueryHighlightTextFragmentPrefixLength(); + + /** + * Get the value for the key 'query.highlight.text.fragment.prefix.length' as {@link Integer}. <br> + * The value is, e.g. 5 <br> + * @return The value of found property. (NotNull: if not found, exception but basically no way) + * @throws NumberFormatException When the property is not integer. + */ + Integer getQueryHighlightTextFragmentPrefixLengthAsInteger(); + + /** + * Get the value for the key 'query.highlight.text.fragment.suffix.length'. <br> + * The value is, e.g. 5 <br> + * @return The value of found property. (NotNull: if not found, exception but basically no way) + */ + String getQueryHighlightTextFragmentSuffixLength(); + + /** + * Get the value for the key 'query.highlight.text.fragment.suffix.length' as {@link Integer}. <br> + * The value is, e.g. 5 <br> + * @return The value of found property. (NotNull: if not found, exception but basically no way) + * @throws NumberFormatException When the property is not integer. + */ + Integer getQueryHighlightTextFragmentSuffixLengthAsInteger(); + /** * Get the value for the key 'query.max.search.result.offset'. <br> * The value is, e.g. 100000 <br> @@ -8250,6 +8314,34 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction return is(FessConfig.QUERY_HIGHLIGHT_BOUNDARY_POSITION_DETECT); } + public String getQueryHighlightTextFragmentType() { + return get(FessConfig.QUERY_HIGHLIGHT_TEXT_FRAGMENT_TYPE); + } + + public String getQueryHighlightTextFragmentSize() { + return get(FessConfig.QUERY_HIGHLIGHT_TEXT_FRAGMENT_SIZE); + } + + public Integer getQueryHighlightTextFragmentSizeAsInteger() { + return getAsInteger(FessConfig.QUERY_HIGHLIGHT_TEXT_FRAGMENT_SIZE); + } + + public String getQueryHighlightTextFragmentPrefixLength() { + return get(FessConfig.QUERY_HIGHLIGHT_TEXT_FRAGMENT_PREFIX_LENGTH); + } + + public Integer getQueryHighlightTextFragmentPrefixLengthAsInteger() { + return getAsInteger(FessConfig.QUERY_HIGHLIGHT_TEXT_FRAGMENT_PREFIX_LENGTH); + } + + public String getQueryHighlightTextFragmentSuffixLength() { + return get(FessConfig.QUERY_HIGHLIGHT_TEXT_FRAGMENT_SUFFIX_LENGTH); + } + + public Integer getQueryHighlightTextFragmentSuffixLengthAsInteger() { + return getAsInteger(FessConfig.QUERY_HIGHLIGHT_TEXT_FRAGMENT_SUFFIX_LENGTH); + } + public String getQueryMaxSearchResultOffset() { return get(FessConfig.QUERY_MAX_SEARCH_RESULT_OFFSET); } @@ -10038,6 +10130,10 @@ public interface FessConfig extends FessEnv, org.codelibs.fess.mylasta.direction defaultMap.put(FessConfig.QUERY_HIGHLIGHT_PHRASE_LIMIT, "256"); defaultMap.put(FessConfig.QUERY_HIGHLIGHT_CONTENT_DESCRIPTION_FIELDS, "hl_content,digest"); defaultMap.put(FessConfig.QUERY_HIGHLIGHT_BOUNDARY_POSITION_DETECT, "true"); + defaultMap.put(FessConfig.QUERY_HIGHLIGHT_TEXT_FRAGMENT_TYPE, "query"); + defaultMap.put(FessConfig.QUERY_HIGHLIGHT_TEXT_FRAGMENT_SIZE, "3"); + defaultMap.put(FessConfig.QUERY_HIGHLIGHT_TEXT_FRAGMENT_PREFIX_LENGTH, "5"); + defaultMap.put(FessConfig.QUERY_HIGHLIGHT_TEXT_FRAGMENT_SUFFIX_LENGTH, "5"); defaultMap.put(FessConfig.QUERY_MAX_SEARCH_RESULT_OFFSET, "100000"); defaultMap.put(FessConfig.QUERY_ADDITIONAL_DEFAULT_FIELDS, ""); defaultMap.put(FessConfig.QUERY_ADDITIONAL_RESPONSE_FIELDS, ""); diff --git a/src/main/java/org/codelibs/fess/util/QueryResponseList.java b/src/main/java/org/codelibs/fess/util/QueryResponseList.java index 3f8c649d881042f2ffb27588f08cc31a10dcbb1e..27c4c8aa6407901a9571a18eaa4e630cf816cf1e 100644 --- a/src/main/java/org/codelibs/fess/util/QueryResponseList.java +++ b/src/main/java/org/codelibs/fess/util/QueryResponseList.java @@ -23,14 +23,11 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; -import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.codelibs.core.lang.StringUtil; import org.codelibs.core.stream.StreamUtil; import org.codelibs.fesen.action.search.SearchResponse; import org.codelibs.fesen.common.document.DocumentField; -import org.codelibs.fesen.common.text.Text; import org.codelibs.fesen.search.SearchHit; import org.codelibs.fesen.search.SearchHits; import org.codelibs.fesen.search.aggregations.Aggregations; @@ -45,8 +42,6 @@ public class QueryResponseList implements List<Map<String, Object>> { private static final Logger logger = LogManager.getLogger(QueryResponseList.class); - protected static final String ELLIPSIS = "..."; - protected final List<Map<String, Object>> parent; /** The value of current page number. */ @@ -153,23 +148,20 @@ public class QueryResponseList implements List<Map<String, Object>> { docMap.putAll(searchHit.getSourceAsMap()); } + final ViewHelper viewHelper = ComponentUtil.getViewHelper(); + final Map<String, HighlightField> highlightFields = searchHit.getHighlightFields(); try { if (highlightFields != null) { - for (final Map.Entry<String, HighlightField> entry : highlightFields.entrySet()) { - final HighlightField highlightField = entry.getValue(); - final Text[] fragments = highlightField.fragments(); - if (fragments != null && fragments.length != 0) { - final String[] texts = new String[fragments.length]; - for (int i = 0; i < fragments.length; i++) { - texts[i] = fragments[i].string(); - } - String value = StringUtils.join(texts, ELLIPSIS); - if (StringUtil.isNotBlank(value) && !fessConfig.endsWithFullstop(value)) { - value = value + ELLIPSIS; - } - docMap.put(hlPrefix + highlightField.getName(), value); + highlightFields.values().stream().forEach(highlightField -> { + final String text = viewHelper.createHighlightText(highlightField); + if (text != null) { + docMap.put(hlPrefix + highlightField.getName(), text); } + }); + if (Constants.TEXT_FRAGMENT_TYPE_HIGHLIGHT.equals(fessConfig.getQueryHighlightTextFragmentType())) { + docMap.put(Constants.TEXT_FRAGMENTS, + viewHelper.createTextFragmentsByHighlight(highlightFields.values().toArray(n -> new HighlightField[n]))); } } } catch (final Exception e) { @@ -178,8 +170,11 @@ public class QueryResponseList implements List<Map<String, Object>> { } } + if (Constants.TEXT_FRAGMENT_TYPE_QUERY.equals(fessConfig.getQueryHighlightTextFragmentType())) { + docMap.put(Constants.TEXT_FRAGMENTS, viewHelper.createTextFragmentsByQuery()); + } + // ContentTitle - final ViewHelper viewHelper = ComponentUtil.getViewHelper(); if (viewHelper != null) { docMap.put(fessConfig.getResponseFieldContentTitle(), viewHelper.getContentTitle(docMap)); docMap.put(fessConfig.getResponseFieldContentDescription(), viewHelper.getContentDescription(docMap)); diff --git a/src/main/resources/fess_config.properties b/src/main/resources/fess_config.properties index 3a216cca82bb3e67c6f94bed5b7924710d7100b0..20e82bf0844de8dc3bfb160a347d2f2a5bf7dfe7 100644 --- a/src/main/resources/fess_config.properties +++ b/src/main/resources/fess_config.properties @@ -429,6 +429,10 @@ query.highlight.order=score query.highlight.phrase.limit=256 query.highlight.content.description.fields=hl_content,digest query.highlight.boundary.position.detect=true +query.highlight.text.fragment.type=query +query.highlight.text.fragment.size=3 +query.highlight.text.fragment.prefix.length=5 +query.highlight.text.fragment.suffix.length=5 query.max.search.result.offset=100000 query.additional.default.fields= query.additional.response.fields=