blob: 0c551d50951cf6a27a67ad1253288b8508720db2 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2014, 2019 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.internal.rhelp.core.index;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.queryparser.flexible.core.QueryNodeException;
import org.apache.lucene.queryparser.flexible.core.QueryNodeParseException;
import org.apache.lucene.queryparser.flexible.standard.StandardQueryParser;
import org.apache.lucene.queryparser.flexible.standard.config.StandardQueryConfigHandler.Operator;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermInSetQuery;
import org.eclipse.statet.jcommons.collections.ImCollections;
import org.eclipse.statet.jcommons.collections.ImList;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.jcommons.status.ErrorStatus;
import org.eclipse.statet.jcommons.status.StatusException;
import org.eclipse.statet.internal.rhelp.core.RHelpCoreInternals;
import org.eclipse.statet.rhelp.core.RHelpCore;
import org.eclipse.statet.rhelp.core.RHelpSearchQuery;
@NonNullByDefault
public final class REnvIndexSearchQuery implements REnvIndexSchema {
private static final ImList<String> TOPIC_SEARCH_FIELDS= ImCollections.newList(
ALIAS_FIELD_NAME );
private static final ImList<String> DOC_SEARCH_FIELDS= ImCollections.newList(
TITLE_TXT_FIELD_NAME,
DESCRIPTION_TXT_FIELD_NAME,
DOC_TXT_FIELD_NAME,
EXAMPLES_TXT_FIELD_NAME );
private static final Map<String, Float> FIELD_BOOSTS= new HashMap<>();
static { // see BoostQuery
FIELD_BOOSTS.put(ALIAS_TXT_FIELD_NAME, 2.0f);
FIELD_BOOSTS.put(TITLE_TXT_FIELD_NAME, 2.0f);
FIELD_BOOSTS.put(DESCRIPTION_TXT_FIELD_NAME, 1.5f);
FIELD_BOOSTS.put(EXAMPLES_TXT_FIELD_NAME, 0.5f);
}
private final static Analyzer QUERY_ANALYZER= new DefaultAnalyzer();
public static REnvIndexSearchQuery compile(final RHelpSearchQuery searchQuery) throws StatusException {
try {
Query query= null;
ImList<String> fields= ImCollections.emptyList();
if (searchQuery.getSearchString().length() > 0) {
switch (searchQuery.getSearchType()) {
case RHelpSearchQuery.TOPIC_SEARCH:
fields= TOPIC_SEARCH_FIELDS;
query= createMainQuery(searchQuery.getSearchString(), fields, null);
break;
case RHelpSearchQuery.FIELD_SEARCH:
fields= sortFields(searchQuery.getEnabledFields());
if (fields.isEmpty()) {
break;
}
query= createMainQuery(searchQuery.getSearchString(), fields, FIELD_BOOSTS);
break;
case RHelpSearchQuery.DOC_SEARCH:
fields= DOC_SEARCH_FIELDS;
query= createMainQuery(searchQuery.getSearchString(), fields, FIELD_BOOSTS);
break;
default:
break;
}
}
final List<Query> filters= new ArrayList<>(4);
{ final List<String> packages= searchQuery.getPackages();
if (!packages.isEmpty()) {
filters.add(new TermInSetQuery(PACKAGE_FIELD_NAME,
REnvIndexUtils.toByteRefTerms(packages) ));
}
}
{ final List<String> keywords= searchQuery.getKeywords();
if (!keywords.isEmpty()) {
filters.add(new TermInSetQuery(KEYWORD_FIELD_NAME,
REnvIndexUtils.toByteRefTerms(keywords) ));
}
}
filters.add(REnvIndexReader.DOCTYPE_PAGE_FILTER);
return new REnvIndexSearchQuery(query, fields, filters);
}
catch (final QueryNodeParseException e) {
RHelpCoreInternals.log(new ErrorStatus(RHelpCore.BUNDLE_ID,
String.format("An error occurred when creating the Lucene query for: %1$s.", //$NON-NLS-1$
searchQuery.toString() ),
e ));
throw new StatusException(new ErrorStatus(RHelpCore.BUNDLE_ID,
"The search string is invalid: " + e.getLocalizedMessage() ));
}
catch (final Exception e) {
RHelpCoreInternals.log(new ErrorStatus(RHelpCore.BUNDLE_ID,
String.format("An error occurred when creating the Lucene query for %1$s.", //$NON-NLS-1$
searchQuery.toString() ),
e ));
throw new StatusException(new ErrorStatus(RHelpCore.BUNDLE_ID,
"An error occurred when preparing the R help query." ));
}
}
private static ImList<String> sortFields(final List<String> fields) {
return ImCollections.toList(fields, new Comparator<String>() {
@Override
public int compare(final String o1, final String o2) {
return getRank(o1) - getRank(o2);
}
});
}
static Query createMainQuery(final String queryText,
final ImList<String> fields, final @Nullable Map<String, Float> fieldBoosts) throws QueryNodeException {
final StandardQueryParser p= new StandardQueryParser(QUERY_ANALYZER);
p.setDefaultOperator(Operator.AND);
p.setAllowLeadingWildcard(true);
p.setMultiFields(fields.toArray(new String[fields.size()]));
if (fieldBoosts != null) {
p.setFieldsBoost(fieldBoosts);
}
return p.parse(queryText, null);
}
private static int getRank(final String o) {
if (o == PAGE_FIELD_NAME) {
return 1;
}
if (o == ALIAS_FIELD_NAME || o == ALIAS_TXT_FIELD_NAME) {
return 2;
}
if (o == TITLE_TXT_FIELD_NAME) {
return 3;
}
if (o == CONCEPT_TXT_FIELD_NAME) {
return 4;
}
if (o == DESCRIPTION_TXT_FIELD_NAME) {
return 5;
}
if (o == DOC_TXT_FIELD_NAME || o == DOC_HTML_FIELD_NAME) {
return 6;
}
if (o == EXAMPLES_TXT_FIELD_NAME) {
return 15;
}
return 10;
}
public final ImList<String> fieldNames;
private final @Nullable Query query;
private final ImList<Query> filters;
private REnvIndexSearchQuery(final @Nullable Query mainQuery,
final List<String> fieldNames, final List<Query> filters) {
this.fieldNames= ImCollections.toList(fieldNames);
this.query= mainQuery;
this.filters= ImCollections.toList(filters);
}
public @Nullable Query getQuery() {
return this.query;
}
public ImList<Query> getFilters() {
return this.filters;
}
}