Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added support for OR and NOT queries #356

Merged
merged 5 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
import static no.unit.nva.search2.constant.ApplicationConstants.COLON;
import static no.unit.nva.search2.constant.ApplicationConstants.COMMA;
import static no.unit.nva.search2.constant.ApplicationConstants.ZERO;
import static no.unit.nva.search2.model.ResourceParameterKey.CONTRIBUTOR;
import static no.unit.nva.search2.model.ParameterKey.escapeSearchString;
import static no.unit.nva.search2.model.ResourceParameterKey.CONTRIBUTOR_ID;
import static no.unit.nva.search2.model.ResourceParameterKey.FIELDS;
import static no.unit.nva.search2.model.ResourceParameterKey.FROM;
import static no.unit.nva.search2.model.ResourceParameterKey.SEARCH_AFTER;
Expand All @@ -35,7 +35,6 @@
import no.unit.nva.search.CachedJwtProvider;
import no.unit.nva.search2.model.OpenSearchClient;
import no.unit.nva.search2.model.OpenSearchSwsResponse;
import no.unit.nva.search2.model.ParameterKey.FieldOperator;
import no.unit.nva.search2.model.QueryBuilderSourceWrapper;
import no.unit.nva.search2.model.QueryBuilderWrapper;
import no.unit.nva.search2.model.ResourceParameterKey;
Expand All @@ -44,11 +43,11 @@
import nva.commons.secrets.SecretsReader;
import org.jetbrains.annotations.NotNull;
import org.opensearch.common.collect.Tuple;
import org.opensearch.index.query.AbstractQueryBuilder;
import org.opensearch.index.query.BoolQueryBuilder;
import org.opensearch.index.query.MultiMatchQueryBuilder;
import org.opensearch.index.query.MultiMatchQueryBuilder.Type;
import org.opensearch.index.query.Operator;
import org.opensearch.index.query.QueryBuilder;
import org.opensearch.index.query.QueryBuilders;
import org.opensearch.index.query.RangeQueryBuilder;
import org.opensearch.search.builder.SearchSourceBuilder;
Expand Down Expand Up @@ -94,15 +93,9 @@ public OpenSearchSwsResponse doSearch(ResourceAwsQuery query) {
}

private Stream<QueryBuilderWrapper> createQueryBuilderStream(ResourceAwsQuery query) {
AbstractQueryBuilder<?> queryBuilder;
if (query.isPresent(SEARCH_ALL)) {
queryBuilder = multiMatchQuery(query);
} else if (query.hasNoSearchValue()) {
queryBuilder = QueryBuilders.matchAllQuery();
} else {
queryBuilder = boolQuery(query);
}

var queryBuilder = query.hasNoSearchValue()
? QueryBuilders.matchAllQuery()
: boolQuery(query);
return Stream.of(new QueryBuilderWrapper(queryBuilder, query));
}

Expand All @@ -122,9 +115,9 @@ private QueryBuilderSourceWrapper populateSearchRequest(QueryBuilderWrapper quer

builder.size(query.getValue(SIZE).as());
builder.from(query.getValue(FROM).as());
if (query.isPresent(SORT)) {
getSortStream(query).forEach(orderTuple -> builder.sort(orderTuple.v1(), orderTuple.v2()));
}
getSortStream(query)
.forEach(orderTuple -> builder.sort(orderTuple.v1(), orderTuple.v2()));

return new QueryBuilderSourceWrapper(builder, query.openSearchUri());
}

Expand Down Expand Up @@ -152,61 +145,53 @@ private OpenSearchSwsResponse handleResponse(HttpResponse<String> response) {
.orElseThrow();
}

/**
* Creates a multi match query, all words needs to be present, within a document.
* @param query ResourceAwsQuery
* @return a MultiMatchQueryBuilder
*/
private MultiMatchQueryBuilder multiMatchQuery(ResourceAwsQuery query) {
var fields = extractFields(query.getValue(FIELDS).toString());
var value = escapeSearchString(query.getValue(SEARCH_ALL).toString());
return QueryBuilders
.multiMatchQuery(value, fields)
.type(Type.CROSS_FIELDS)
.operator(Operator.AND);
}

/**
* Creates a boolean query, with all the search parameters.
* @param query ResourceAwsQuery
* @return a BoolQueryBuilder
*/
@SuppressWarnings({"PMD.SwitchStmtsShouldHaveDefault"})
private BoolQueryBuilder boolQuery(ResourceAwsQuery query) {
var bq = QueryBuilders.boolQuery();
query.getOpenSearchParameters()
.forEach((key, value) -> {
var searchFields = key.searchFields().toArray(String[]::new);
if (hasMultipleFields(searchFields)) {
bq.must(QueryBuilders
.multiMatchQuery(escapeSearchString(value), searchFields)
.operator(Operator.AND));
if (key.equals(CONTRIBUTOR)) {
addPromotedQuery(query, bq);
}
if (key.equals(SEARCH_ALL)) {
bq.must(multiMatchQuery(query));
} else {
var searchField = searchFields[0];
if (isRangeQuery(key)) {
bq.must(rangeQuery(key.searchOperator(), searchField, value));
} else {
bq.must(QueryBuilders
.matchQuery(searchField, escapeSearchString(value))
.boost(key.fieldBoost())
.operator(Operator.AND));
if (key.equals(CONTRIBUTOR)) {
addPromotedQuery(query, bq);
}
switch (key.searchOperator()) {
case MUST -> bq.must(buildQuery(key, value));
case MUST_NOT -> bq.mustNot(buildQuery(key, value));
case SHOULD -> bq.should(buildQuery(key, value));
case GREATER_THAN_OR_EQUAL_TO, LESS_THAN -> bq.must(rangeQuery(key, value));
}
if (key.equals(CONTRIBUTOR_ID)) {
addPromotedQuery(query, bq);
}
}
});
return bq;
}

/**
* Creates a multi match query, all words needs to be present, within a document.
* @param query ResourceAwsQuery
* @return a MultiMatchQueryBuilder
*/
private MultiMatchQueryBuilder multiMatchQuery(ResourceAwsQuery query) {
var fields = extractFields(query.getValue(FIELDS).toString());
var value = escapeSearchString(query.getValue(SEARCH_ALL).toString());
return QueryBuilders
.multiMatchQuery(value, fields)
.type(Type.CROSS_FIELDS)
.operator(Operator.AND);
}

private void addPromotedQuery(ResourceAwsQuery query, BoolQueryBuilder bq) {
var promotedPublications = userSettingsClient
.doSearch(query)
.promotedPublications();
if (hasPromotedPublications(promotedPublications)) {
query.removeKey(SORT); // remove sort to avoid sorting by score
query.removeKey(SORT); // remove sort to avoid messing up "sorting by score"
for (int i = 0; i < promotedPublications.size(); i++) {
bq.should(
QueryBuilders
Expand All @@ -216,13 +201,27 @@ private void addPromotedQuery(ResourceAwsQuery query, BoolQueryBuilder bq) {
}
}

private RangeQueryBuilder rangeQuery(FieldOperator operator, String fieldName, String value) {
return switch (operator) {
case NONE, EQUALS -> throw new IllegalArgumentException("Operator not supported");
case GREATER_THAN -> QueryBuilders.rangeQuery(fieldName).gt(value);
case GREATER_THAN_OR_EQUAL_TO -> QueryBuilders.rangeQuery(fieldName).gte(value);
case LESS_THAN -> QueryBuilders.rangeQuery(fieldName).lt(value);
case LESS_THAN_OR_EQUAL_TO -> QueryBuilders.rangeQuery(fieldName).lte(value);
private QueryBuilder buildQuery(ResourceParameterKey key, String value) {
final var searchFields = key.searchFields().toArray(String[]::new);
if (hasMultipleFields()) {
return QueryBuilders
.multiMatchQuery(escapeSearchString(value), searchFields)
.operator(Operator.AND);
}
var searchField = searchFields[0];
return QueryBuilders
.matchQuery(searchField, escapeSearchString(value))
.boost(key.fieldBoost())
.operator(Operator.AND);
}

private RangeQueryBuilder rangeQuery(ResourceParameterKey key, String value) {
final var searchField = key.searchFields().toArray()[0].toString();

return switch (key.searchOperator()) {
case MUST, MUST_NOT, SHOULD -> throw new IllegalArgumentException("Operator not supported");
case GREATER_THAN_OR_EQUAL_TO -> QueryBuilders.rangeQuery(searchField).gte(value);
case LESS_THAN -> QueryBuilders.rangeQuery(searchField).lt(value);
};
}

Expand Down Expand Up @@ -266,7 +265,4 @@ private boolean hasPromotedPublications(List<String> promotedPublications) {
return nonNull(promotedPublications) && !promotedPublications.isEmpty();
}

private static boolean isRangeQuery(ResourceParameterKey key) {
return key.searchOperator() != FieldOperator.EQUALS;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,27 @@ protected void assignDefaultValues() {
protected void setValue(String key, String value) {
var qpKey = keyFromString(key);
switch (qpKey) {
case SEARCH_AFTER, FROM,
SIZE, PAGE -> query.setQueryValue(qpKey, value);
case SEARCH_AFTER, FROM, SIZE, PAGE -> query.setQueryValue(qpKey, value);
case FIELDS -> query.setQueryValue(qpKey, expandFields(value));
case SORT -> addSortQuery(value);
case SORT_ORDER -> addSortOrderQuery(value);
case CREATED_BEFORE, CREATED_SINCE,
MODIFIED_BEFORE, MODIFIED_SINCE,
PUBLISHED_BEFORE, PUBLISHED_SINCE -> query.setSearchFieldValue(qpKey, expandDate(value));
case CATEGORY, CONTRIBUTOR,
DOI, FUNDING, FUNDING_SOURCE, ID,
INSTITUTION, ISSN, ISBN, ORCID,
PROJECT, SEARCH_ALL, TITLE,
UNIT, USER, PUBLICATION_YEAR -> query.setSearchFieldValue(qpKey, value);
case CATEGORY, CATEGORY_NOT, CATEGORY_SHOULD,
CONTRIBUTOR_ID, CONTRIBUTOR, CONTRIBUTOR_NOT, CONTRIBUTOR_SHOULD,
DOI, DOI_NOT, DOI_SHOULD,
FUNDING, FUNDING_SOURCE, FUNDING_SOURCE_NOT, FUNDING_SOURCE_SHOULD,
ID, ID_NOT, ID_SHOULD,
INSTITUTION, INSTITUTION_NOT, INSTITUTION_SHOULD,
ISBN, ISBN_NOT, ISBN_SHOULD, ISSN, ISSN_NOT, ISSN_SHOULD,
ORCID, ORCID_NOT, ORCID_SHOULD,
PROJECT, PROJECT_NOT, PROJECT_SHOULD,
PUBLICATION_YEAR, PUBLICATION_YEAR_SHOULD,
SEARCH_ALL,
TITLE, TITLE_NOT, TITLE_SHOULD,
UNIT, UNIT_NOT, UNIT_SHOULD,
USER, USER_NOT, USER_SHOULD -> query.setSearchFieldValue(qpKey, value);
case LANG -> {
// ignore and continue
}
Expand Down Expand Up @@ -213,8 +221,8 @@ private String expandFields(String value) {
return ALL.equals(value) || isNull(value)
? ALL
: Arrays.stream(value.split(COMMA))
.filter(this::keyIsValid)
.collect(Collectors.joining(COMMA));
.filter(this::keyIsValid)
.collect(Collectors.joining(COMMA));
}

private boolean keyIsValid(String key) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public final class ApplicationConstants {
public static final String COLON = ":";
public static final String COMMA = ",";
public static final String EQUAL = "=";
public static final String PIPE = "|";
public static final String PLUS = "+";
public static final String PREFIX = "(";
public static final String QUOTE = "'";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,10 @@ public class Patterns {

/**
* Pattern for matching a date string.
* yyyy-MM-dd | yyyy-MM-ddTHH:mm:ssZ | yyyy-MM-ddTHH:mm:ss.SSSZ
* yyyy | yyyy-MM-dd | yyyy-MM-ddTHH:mm:ssZ | yyyy-MM-ddTHH:mm:ss.SSSZ
*/
public static final String PATTERN_IS_DATE = "(\\d){4}-(\\d){2}-(\\d){2}[T]*[(\\d){2}:(\\d){2}:(\\d){2,6}Z]*";
public static final String PATTERN_IS_DATE = "\\d{4}(-\\d{2}(-\\d{2}(T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?Z?)?)?)?";

/**
* Pattern for matching a date string.
* yyyy | yyyy-MM-dd
*/
public static final String PATTERN_IS_DATE_STRING = "\\b\\d{4}(?:-\\d{2}-\\d{2})?\\b";
public static final String PATTERN_IS_ADD_SLASH = "\\\\$1";
public static final String PATTERN_IS_IGNORE_CASE = "(?i)";
public static final String PATTERN_IS_NONE_OR_ONE = ".?";
Expand All @@ -33,6 +28,6 @@ public class Patterns {
* - + & | ! ( ) { } [ ] ^ " ~ * ? : \ /
* with the intention of escaping them.
*/
public static final String PATTERN_IS_SPECIAL_CHARACTERS = "([-+&|!\\(\\){}\\[\\]^\"~*?:\\\\/])";
public static final String PATTERN_IS_SPECIAL_CHARACTERS = "([-+&|!\\(\\){}\\[\\]^\"\\\\~*?:\\/])";

}
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ public <T> T as() {
return null;
}
return (T) switch (key.fieldType()) {
case DATE, DATE_STRING -> castDateTime();
case DATE -> castDateTime();
case NUMBER -> castNumber();
default -> value;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package no.unit.nva.search2.model;

import static no.unit.nva.search2.constant.Patterns.PATTERN_IS_ADD_SLASH;
import static no.unit.nva.search2.constant.Patterns.PATTERN_IS_IGNORE_CASE;
import static no.unit.nva.search2.constant.Patterns.PATTERN_IS_NONE_OR_ONE;
import static no.unit.nva.search2.constant.Patterns.PATTERN_IS_SPECIAL_CHARACTERS;
import java.util.Collection;
import java.util.function.Predicate;
Expand Down Expand Up @@ -38,10 +40,24 @@ enum ValueEncoding {
}

enum ParamKind {
DATE, DATE_STRING, NUMBER, STRING, SORT_STRING, CUSTOM
DATE, NUMBER, STRING, SORT_STRING, CUSTOM
}

enum FieldOperator {
NONE, EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL_TO, LESS_THAN, LESS_THAN_OR_EQUAL_TO
MUST(""),
MUST_NOT("NOT"),
SHOULD("SHOULD"),
GREATER_THAN_OR_EQUAL_TO("SINCE"),
LESS_THAN("BEFORE");

private final String keyPattern;

FieldOperator(String pattern) {
this.keyPattern = PATTERN_IS_IGNORE_CASE + PATTERN_IS_NONE_OR_ONE + pattern;
}

public String pattern() {
return keyPattern;
}
}
}
Loading
Loading