forked from folio-org/mod-search
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
MSEARCH-14 Implement CQL query parser (folio-org#4)
* MSEARCH-14 Implement CQL query parser
- Loading branch information
1 parent
3c157b4
commit 207a75d
Showing
25 changed files
with
669 additions
and
130 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,4 +16,3 @@ buildMvn { | |
} | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
146 changes: 146 additions & 0 deletions
146
src/main/java/org/folio/search/cql/CqlSearchQueryConverter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
package org.folio.search.cql; | ||
|
||
import static org.elasticsearch.index.query.QueryBuilders.boolQuery; | ||
import static org.elasticsearch.index.query.QueryBuilders.matchQuery; | ||
import static org.elasticsearch.index.query.QueryBuilders.rangeQuery; | ||
import static org.elasticsearch.index.query.QueryBuilders.termQuery; | ||
import static org.elasticsearch.index.query.QueryBuilders.wildcardQuery; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.apache.commons.lang3.StringUtils; | ||
import org.elasticsearch.index.query.BoolQueryBuilder; | ||
import org.elasticsearch.index.query.QueryBuilder; | ||
import org.elasticsearch.search.builder.SearchSourceBuilder; | ||
import org.elasticsearch.search.sort.SortOrder; | ||
import org.folio.cql2pgjson.exception.CQLFeatureUnsupportedException; | ||
import org.folio.cql2pgjson.model.CqlModifiers; | ||
import org.folio.cql2pgjson.model.CqlSort; | ||
import org.folio.search.exception.SearchServiceException; | ||
import org.folio.search.model.service.CqlSearchRequest; | ||
import org.springframework.stereotype.Component; | ||
import org.z3950.zing.cql.CQLBooleanNode; | ||
import org.z3950.zing.cql.CQLNode; | ||
import org.z3950.zing.cql.CQLParser; | ||
import org.z3950.zing.cql.CQLSortNode; | ||
import org.z3950.zing.cql.CQLTermNode; | ||
|
||
/** | ||
* Convert a CQL query into a elasticsearch query. | ||
* | ||
* <p> Contextual Query Language (CQL) Specification: | ||
* <a href="https://www.loc.gov/standards/sru/cql/spec.html">https://www.loc.gov/standards/sru/cql/spec.html</a> | ||
* </p> | ||
*/ | ||
@Slf4j | ||
@Component | ||
@RequiredArgsConstructor | ||
public class CqlSearchQueryConverter { | ||
|
||
private static final String ASTERISKS_SIGN = "*"; | ||
|
||
/** | ||
* Parses {@link CqlSearchRequest} object to the elasticsearch. | ||
*/ | ||
public SearchSourceBuilder convert(CqlSearchRequest request) { | ||
var cqlQuery = request.getCqlQuery(); | ||
try { | ||
var cqlNode = new CQLParser().parse(cqlQuery); | ||
return toCriteria(request, cqlNode); | ||
} catch (Exception e) { | ||
throw new SearchServiceException(String.format( | ||
"Failed to parse cql query [cql: '%s', resource: %s]", cqlQuery, request.getResource()), e); | ||
} | ||
} | ||
|
||
private SearchSourceBuilder toCriteria(CqlSearchRequest searchRequest, CQLNode node) | ||
throws CQLFeatureUnsupportedException { | ||
var queryBuilder = new SearchSourceBuilder(); | ||
queryBuilder.trackTotalHits(true); | ||
|
||
if (node instanceof CQLSortNode) { | ||
for (var sortIndex : ((CQLSortNode) node).getSortIndexes()) { | ||
var modifiers = new CqlModifiers(sortIndex); | ||
queryBuilder.sort(sortIndex.getBase(), getSortOrder(modifiers.getCqlSort())); | ||
} | ||
} | ||
|
||
return queryBuilder | ||
.query(convertToQuery(node)) | ||
.from(searchRequest.getOffset()) | ||
.size(searchRequest.getLimit()); | ||
} | ||
|
||
private QueryBuilder convertToQuery(CQLNode node) { | ||
var cqlNode = node; | ||
if (node instanceof CQLSortNode) { | ||
cqlNode = ((CQLSortNode) node).getSubtree(); | ||
} | ||
if (cqlNode instanceof CQLTermNode) { | ||
return convertToTermQuery((CQLTermNode) cqlNode); | ||
} | ||
if (cqlNode instanceof CQLBooleanNode) { | ||
return convertToBoolQuery((CQLBooleanNode) cqlNode); | ||
} | ||
throw new UnsupportedOperationException("Unsupported node: " + node.getClass().getSimpleName()); | ||
} | ||
|
||
private QueryBuilder convertToTermQuery(CQLTermNode node) { | ||
var fieldName = node.getIndex(); | ||
var term = node.getTerm(); | ||
if (term.contains(ASTERISKS_SIGN)) { | ||
return wildcardQuery(fieldName, term); | ||
} | ||
var comparator = StringUtils.lowerCase(node.getRelation().getBase()); | ||
switch (comparator) { | ||
case "=": | ||
return termQuery(fieldName, term); | ||
case "adj": | ||
case "all": | ||
case "any": | ||
case "==": | ||
return matchQuery(fieldName, term); | ||
case "<>": | ||
return boolQuery().mustNot(termQuery(fieldName, term)); | ||
case "<": | ||
return rangeQuery(fieldName).lt(term); | ||
case ">": | ||
return rangeQuery(fieldName).gt(term); | ||
case "<=": | ||
return rangeQuery(fieldName).lte(term); | ||
case ">=": | ||
return rangeQuery(fieldName).gte(term); | ||
default: | ||
throw unsupportedException(comparator); | ||
} | ||
} | ||
|
||
private BoolQueryBuilder convertToBoolQuery(CQLBooleanNode node) { | ||
var operator = node.getOperator(); | ||
var boolQuery = boolQuery(); | ||
switch (operator) { | ||
case OR: | ||
return boolQuery | ||
.should(convertToQuery(node.getLeftOperand())) | ||
.should(convertToQuery(node.getRightOperand())); | ||
case AND: | ||
return boolQuery | ||
.must(convertToQuery(node.getLeftOperand())) | ||
.must(convertToQuery(node.getRightOperand())); | ||
case NOT: | ||
return boolQuery | ||
.must(convertToQuery(node.getLeftOperand())) | ||
.mustNot(convertToQuery(node.getRightOperand())); | ||
default: | ||
throw unsupportedException(operator.name()); | ||
} | ||
} | ||
|
||
private static SortOrder getSortOrder(CqlSort cqlSort) { | ||
return cqlSort == CqlSort.DESCENDING ? SortOrder.DESC : SortOrder.ASC; | ||
} | ||
|
||
private static UnsupportedOperationException unsupportedException(String operator) { | ||
return new UnsupportedOperationException(String.format("Not implemented yet [operator: %s]", operator)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 3 additions & 11 deletions
14
src/main/java/org/folio/search/model/rest/response/PagedResult.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,10 @@ | ||
package org.folio.search.model.rest.response; | ||
|
||
import java.util.List; | ||
import lombok.Data; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Data | ||
@NoArgsConstructor | ||
public class PagedResult<T> { | ||
public interface PagedResult<T> { | ||
|
||
private long total; | ||
private long totalPages; | ||
long getTotalRecords(); | ||
|
||
private int pageSize; | ||
private int pageNumber; | ||
|
||
private List<T> content; | ||
List<T> getInstances(); | ||
} |
13 changes: 8 additions & 5 deletions
13
src/main/java/org/folio/search/model/rest/response/SearchResult.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,14 @@ | ||
package org.folio.search.model.rest.response; | ||
|
||
import java.util.List; | ||
import java.util.Map; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Data; | ||
import lombok.EqualsAndHashCode; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Data | ||
@NoArgsConstructor | ||
@EqualsAndHashCode(callSuper = true) | ||
public class SearchResult extends PagedResult<Object> { | ||
@AllArgsConstructor(staticName = "of") | ||
public class SearchResult implements PagedResult<Map<String, Object>> { | ||
|
||
private final long totalRecords; | ||
private final List<Map<String, Object>> instances; | ||
} |
43 changes: 43 additions & 0 deletions
43
src/main/java/org/folio/search/model/service/CqlSearchRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package org.folio.search.model.service; | ||
|
||
import lombok.AllArgsConstructor; | ||
import lombok.Builder; | ||
import lombok.Data; | ||
import lombok.NoArgsConstructor; | ||
|
||
/** | ||
* CQL based search request model. | ||
*/ | ||
@Data | ||
@Builder | ||
@NoArgsConstructor | ||
@AllArgsConstructor(staticName = "of") | ||
public class CqlSearchRequest { | ||
|
||
/** | ||
* Resource name. | ||
*/ | ||
private String resource; | ||
|
||
/** | ||
* CQL query. | ||
*/ | ||
private String cqlQuery; | ||
|
||
/** | ||
* Request tenant id. | ||
*/ | ||
private String tenantId; | ||
|
||
/** | ||
* Page size. | ||
*/ | ||
@Builder.Default | ||
private Integer limit = 100; | ||
|
||
/** | ||
* Page number. | ||
*/ | ||
@Builder.Default | ||
private Integer offset = 0; | ||
} |
Oops, something went wrong.