/**
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community 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://opensource.org/licenses/ecl2.txt
*
* 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.opencastproject.matterhorn.search.impl;
import static org.opencastproject.matterhorn.search.impl.IndexSchema.TEXT;
import static org.opencastproject.matterhorn.search.impl.IndexSchema.TEXT_FUZZY;
import org.opencastproject.matterhorn.search.SearchQuery;
import org.opencastproject.util.DateTimeSupport;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.FilterBuilder;
import org.elasticsearch.index.query.FilterBuilders;
import org.elasticsearch.index.query.FuzzyLikeThisQueryBuilder;
import org.elasticsearch.index.query.MatchAllQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.QueryStringQueryBuilder;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.index.query.TermsQueryBuilder;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Matterhorn implementation of the elastic search query builder.
*/
public abstract class AbstractElasticsearchQueryBuilder<T extends SearchQuery> implements QueryBuilder {
/** Term queries on fields */
private Map<String, Set<Object>> searchTerms = null;
/** Negative term queries on fields */
private Map<String, Set<Object>> negativeSearchTerms = null;
/** Fields that must be empty */
private Set<String> emptyFields = null;
/** Fields that need to match all values */
protected List<ValueGroup> groups = null;
/** Fields that must not be empty */
private Set<String> nonEmptyFields = null;
/** Fields that query a date range */
private Set<DateRange> dateRanges = null;
/** Filter expression */
protected String filter = null;
/** Text query */
protected String text = null;
/** Fuzzy text query */
protected String fuzzyText = null;
/** The original search query */
private T query = null;
/** The boolean query */
private QueryBuilder queryBuilder = null;
/**
* Creates a new elastic search query based on the raw query.
*
* @param query
* the search query
*/
public AbstractElasticsearchQueryBuilder(T query) {
this.query = query;
buildQuery(query);
createQuery();
}
/**
* Returns the original search query.
*
* @return the search query
*/
public T getQuery() {
return query;
}
/**
* {@inheritDoc}
*
* @see org.elasticsearch.index.query.BaseQueryBuilder#doXContent(org.elasticsearch.common.xcontent.XContentBuilder,
* org.elasticsearch.common.xcontent.ToXContent.Params)
*/
public abstract void buildQuery(T query);
/**
* Create the actual query. We start with a query that matches everything, then move to the boolean conditions,
* finally add filter queries.
*/
private void createQuery() {
queryBuilder = new MatchAllQueryBuilder();
// The boolean query builder
BoolQueryBuilder booleanQuery = new BoolQueryBuilder();
// Terms
if (searchTerms != null) {
for (Map.Entry<String, Set<Object>> entry : searchTerms.entrySet()) {
Set<Object> values = entry.getValue();
if (values.size() == 1)
booleanQuery.must(new TermsQueryBuilder(entry.getKey(), values.iterator().next()));
else
booleanQuery.must(new TermsQueryBuilder(entry.getKey(), values.toArray(new String[values.size()])));
}
this.queryBuilder = booleanQuery;
}
// Negative terms
if (negativeSearchTerms != null) {
for (Map.Entry<String, Set<Object>> entry : negativeSearchTerms.entrySet()) {
Set<Object> values = entry.getValue();
if (values.size() == 1)
booleanQuery.mustNot(new TermsQueryBuilder(entry.getKey(), values.iterator().next()));
else
booleanQuery.mustNot(new TermsQueryBuilder(entry.getKey(), values.toArray(new String[values.size()])));
}
this.queryBuilder = booleanQuery;
}
// Date ranges
if (dateRanges != null) {
for (DateRange dr : dateRanges) {
booleanQuery.must(dr.getQueryBuilder());
}
this.queryBuilder = booleanQuery;
}
// Text
if (text != null) {
QueryStringQueryBuilder queryBuilder = QueryBuilders.queryString(text).field(TEXT);
booleanQuery.must(queryBuilder);
this.queryBuilder = booleanQuery;
}
// Fuzzy text
if (fuzzyText != null) {
FuzzyLikeThisQueryBuilder fuzzyQueryBuilder = QueryBuilders.fuzzyLikeThisQuery(TEXT_FUZZY).likeText(fuzzyText);
booleanQuery.must(fuzzyQueryBuilder);
this.queryBuilder = booleanQuery;
}
QueryBuilder unfilteredQuery = queryBuilder;
List<FilterBuilder> filters = new ArrayList<FilterBuilder>();
// Add filtering for AND terms
if (groups != null) {
for (ValueGroup group : groups) {
filters.addAll(group.getFilterBuilders());
}
}
// Non-Empty fields
if (nonEmptyFields != null) {
for (String field : nonEmptyFields) {
filters.add(FilterBuilders.existsFilter(field));
}
}
// Empty fields
if (emptyFields != null) {
for (String field : emptyFields) {
filters.add(FilterBuilders.missingFilter(field));
}
}
// Filter expressions
if (filter != null) {
filters.add(FilterBuilders.termFilter(IndexSchema.TEXT, filter));
}
// Apply the filters
if (filters.size() == 1) {
this.queryBuilder = QueryBuilders.filteredQuery(unfilteredQuery, filters.get(0));
} else if (filters.size() > 1) {
FilterBuilder andFilter = FilterBuilders.andFilter(filters.toArray(new FilterBuilder[filters.size()]));
this.queryBuilder = QueryBuilders.filteredQuery(unfilteredQuery, andFilter);
}
}
/**
* Stores <code>fieldValue</code> as a search term on the <code>fieldName</code> field.
*
* @param fieldName
* the field name
* @param fieldValue
* the field value
* @param clean
* <code>true</code> to escape solr special characters in the field value
*/
protected void and(String fieldName, Object fieldValue, boolean clean) {
// Fix the field name, just in case
fieldName = StringUtils.trim(fieldName);
// Make sure the data structures are set up accordingly
if (searchTerms == null)
searchTerms = new HashMap<String, Set<Object>>();
Set<Object> termValues = searchTerms.get(fieldName);
if (termValues == null) {
termValues = new HashSet<Object>();
searchTerms.put(fieldName, termValues);
}
// Add the term
termValues.add(fieldValue);
}
/**
* Stores <code>fieldValues</code> as search terms on the <code>fieldName</code> field.
*
* @param fieldName
* the field name
* @param fieldValues
* the field value
* @param clean
* <code>true</code> to escape solr special characters in the field value
*/
protected void and(String fieldName, Object[] fieldValues, boolean clean) {
for (Object v : fieldValues) {
and(fieldName, v, clean);
}
}
/**
* Stores <code>fieldValue</code> as a search term on the <code>fieldName</code> field.
*
* @param fieldName
* the field name
* @param startDate
* the start date
* @param endDate
* the end date
*/
protected void and(String fieldName, Date startDate, Date endDate) {
// Fix the field name, just in case
fieldName = StringUtils.trim(fieldName);
// Make sure the data structures are set up accordingly
if (dateRanges == null)
dateRanges = new HashSet<DateRange>();
// Add the term
DateRange dateRange = new DateRange(fieldName, startDate, endDate);
dateRanges.add(dateRange);
}
/**
* Stores <code>fieldValue</code> as a negative search term on the <code>fieldName</code> field.
*
* @param fieldName
* the field name
* @param fieldValue
* the field value
* @param clean
* <code>true</code> to escape solr special characters in the field value
*/
protected void andNot(String fieldName, Object fieldValue, boolean clean) {
// Fix the field name, just in case
fieldName = StringUtils.trim(fieldName);
// Make sure the data structures are set up accordingly
if (negativeSearchTerms == null)
negativeSearchTerms = new HashMap<String, Set<Object>>();
Set<Object> termValues = negativeSearchTerms.get(fieldName);
if (termValues == null) {
termValues = new HashSet<Object>();
negativeSearchTerms.put(fieldName, termValues);
}
// Add the term
termValues.add(fieldValue);
}
/**
* Stores <code>fieldValues</code> as negative search terms on the <code>fieldName</code> field.
*
* @param fieldName
* the field name
* @param fieldValues
* the field value
* @param clean
* <code>true</code> to escape solr special characters in the field value
*/
protected void andNot(String fieldName, Object[] fieldValues, boolean clean) {
for (Object v : fieldValues) {
andNot(fieldName, v, clean);
}
}
/**
* Encodes the field name as part of the AND clause of a solr query: <tt>AND -fieldName : [* TO *]</tt>.
*
* @param fieldName
* the field name
*/
protected void andEmpty(String fieldName) {
if (emptyFields == null)
emptyFields = new HashSet<String>();
emptyFields.add(StringUtils.trim(fieldName));
}
/**
* Encodes the field name as part of the AND clause of a solr query: <tt>AND fieldName : ["" TO *]</tt>.
*
* @param fieldName
* the field name
*/
protected void andNotEmpty(String fieldName) {
if (nonEmptyFields == null)
nonEmptyFields = new HashSet<String>();
nonEmptyFields.add(StringUtils.trim(fieldName));
}
/**
* {@inheritDoc}
*
* @see org.elasticsearch.common.xcontent.ToXContent#toXContent(org.elasticsearch.common.xcontent.XContentBuilder,
* org.elasticsearch.common.xcontent.ToXContent.Params)
*/
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return queryBuilder.toXContent(builder, params);
}
/**
* {@inheritDoc}
*
* @see org.elasticsearch.index.query.QueryBuilder#buildAsBytes()
*/
@Override
public BytesReference buildAsBytes() throws ElasticsearchException {
return queryBuilder.buildAsBytes();
}
/**
* {@inheritDoc}
*
* @see org.elasticsearch.index.query.QueryBuilder#buildAsBytes(org.elasticsearch.common.xcontent.XContentType)
*/
@Override
public BytesReference buildAsBytes(XContentType contentType) {
return queryBuilder.buildAsBytes(contentType);
}
/**
* Utility class to hold date range specifications and turn them into elastic search queries.
*/
public static final class DateRange {
/** The field name */
private String field = null;
/** The start date */
private Date startDate = null;
/** The end date */
private Date endDate = null;
/**
* Creates a new date range specification with the given field name, start and end dates. <code>null</code> may be
* passed in for start or end dates that should remain unspecified.
*
* @param field
* the field name
* @param start
* the start date
* @param end
* the end date
*/
public DateRange(String field, Date start, Date end) {
this.field = field;
this.startDate = start;
this.endDate = end;
}
/**
* Returns the range query that is represented by this date range.
*
* @return the range query builder
*/
QueryBuilder getQueryBuilder() {
RangeQueryBuilder rqb = new RangeQueryBuilder(field);
if (startDate != null)
rqb.from(DateTimeSupport.toUTC(startDate.getTime()));
if (endDate != null)
rqb.to(DateTimeSupport.toUTC(endDate.getTime()));
return rqb;
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof DateRange) {
return ((DateRange) obj).field.equals(field);
}
return false;
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return field.hashCode();
}
}
/**
* Stores a group of values which will later be added to the query using AND.
*/
public static final class ValueGroup {
/** The field name */
private String field = null;
/** The values to store */
private Object[] values = null;
/**
* Creates a new value group for the given field and values.
*
* @param field
* the field name
* @param values
* the values
*/
public ValueGroup(String field, Object... values) {
this.field = field;
this.values = values;
}
/**
* Returns the filter that will make sure only documents are returned that match all of the values at once.
*
* @return the filter builder
*/
List<FilterBuilder> getFilterBuilders() {
List<FilterBuilder> filters = new ArrayList<FilterBuilder>(values.length);
for (Object v : values) {
filters.add(FilterBuilders.termFilter(field, v.toString()));
}
return filters;
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof DateRange) {
return ((DateRange) obj).field.equals(field);
}
return false;
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return field.hashCode();
}
}
}