| /* |
| * |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. The ASF licenses this file to You |
| * under the Apache 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://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * 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. For additional information regarding |
| * copyright in this work, please see the NOTICE file in the top level |
| * directory of this distribution. |
| * |
| */ |
| package org.apache.usergrid.persistence.index.impl; |
| |
| |
| import org.apache.usergrid.persistence.index.IndexAlias; |
| import org.elasticsearch.action.search.SearchRequestBuilder; |
| import org.elasticsearch.action.search.SearchType; |
| import org.elasticsearch.index.query.BoolFilterBuilder; |
| import org.elasticsearch.index.query.FilterBuilder; |
| import org.elasticsearch.index.query.FilterBuilders; |
| import org.elasticsearch.index.query.QueryBuilder; |
| import org.elasticsearch.index.query.TermFilterBuilder; |
| import org.elasticsearch.search.sort.FieldSortBuilder; |
| import org.elasticsearch.search.sort.GeoDistanceSortBuilder; |
| import org.elasticsearch.search.sort.SortBuilders; |
| import org.elasticsearch.search.sort.SortOrder; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import org.apache.usergrid.persistence.core.scope.ApplicationScope; |
| import org.apache.usergrid.persistence.index.EntityIndex; |
| import org.apache.usergrid.persistence.index.SearchEdge; |
| import org.apache.usergrid.persistence.index.SearchTypes; |
| import org.apache.usergrid.persistence.index.exceptions.IndexException; |
| import org.apache.usergrid.persistence.index.query.ParsedQuery; |
| import org.apache.usergrid.persistence.index.query.SortPredicate; |
| import org.apache.usergrid.persistence.index.query.tree.QueryVisitor; |
| |
| import com.google.common.base.Optional; |
| import com.google.common.base.Preconditions; |
| |
| import java.util.List; |
| import java.util.Map; |
| |
| import static org.apache.usergrid.persistence.index.impl.IndexingUtils.createContextName; |
| import static org.apache.usergrid.persistence.index.impl.SortBuilder.sortPropertyTermFilter; |
| |
| |
| /** |
| * The strategy for creating a search request from a parsed query |
| */ |
| |
| public class SearchRequestBuilderStrategy { |
| |
| private static final Logger logger = LoggerFactory.getLogger( SearchRequestBuilderStrategy.class ); |
| |
| private final EsProvider esProvider; |
| private final ApplicationScope applicationScope; |
| private final IndexAlias alias; |
| private final int cursorTimeout; |
| |
| |
| public SearchRequestBuilderStrategy( final EsProvider esProvider, final ApplicationScope applicationScope, |
| final IndexAlias alias, int cursorTimeout ) { |
| |
| this.esProvider = esProvider; |
| this.applicationScope = applicationScope; |
| this.alias = alias; |
| this.cursorTimeout = cursorTimeout; |
| } |
| |
| |
| /** |
| * Get the search request builder |
| */ |
| public SearchRequestBuilder getBuilder( final SearchEdge searchEdge, final SearchTypes searchTypes, |
| final QueryVisitor visitor, final int limit, final int from, |
| final List<SortPredicate> sortPredicates, |
| final Map<String, Class> fieldsWithType ) { |
| |
| Preconditions |
| .checkArgument( limit <= EntityIndex.MAX_LIMIT, "limit is greater than max " + EntityIndex.MAX_LIMIT ); |
| |
| Preconditions.checkNotNull( visitor, "query visitor cannot be null"); |
| |
| SearchRequestBuilder srb = |
| esProvider.getClient().prepareSearch( alias.getReadAlias() ).setTypes( IndexingUtils.ES_ENTITY_TYPE ) |
| .setSearchType( SearchType.QUERY_THEN_FETCH ); |
| |
| |
| final Optional<QueryBuilder> queryBuilder = visitor.getQueryBuilder(); |
| |
| if ( queryBuilder.isPresent() ) { |
| srb.setQuery( queryBuilder.get() ); |
| } |
| |
| srb.setPostFilter( createFilterBuilder( searchEdge, visitor, searchTypes ) ); |
| |
| |
| srb = srb.setFrom( from ).setSize( limit ); |
| |
| |
| //if we have a geo field, sort by closest to farthest by default |
| final GeoSortFields geoFields = visitor.getGeoSorts(); |
| |
| |
| //no sort predicates, sort by edge time descending, entity id second |
| if ( sortPredicates.size() == 0 ) { |
| applyDefaultSortPredicates( srb, geoFields ); |
| } |
| else { |
| applySortPredicates( srb, sortPredicates, geoFields, fieldsWithType ); |
| } |
| |
| |
| return srb; |
| } |
| |
| |
| /** |
| * Apply our default sort predicate logic |
| */ |
| private void applyDefaultSortPredicates( final SearchRequestBuilder srb, final GeoSortFields geoFields ) { |
| //we have geo fields, sort through them in visit order |
| for ( String geoField : geoFields.fields() ) { |
| |
| final GeoDistanceSortBuilder geoSort = geoFields.applyOrder( geoField, SortOrder.ASC ); |
| |
| srb.addSort( geoSort ); |
| } |
| |
| //now sort by edge timestamp, then entity id |
| //sort by the edge timestamp |
| srb.addSort( SortBuilders.fieldSort( IndexingUtils.EDGE_TIMESTAMP_FIELDNAME ).order( SortOrder.DESC ) ); |
| |
| // removing secondary sort by entity ID -- takes ES resources and provides no benefit |
| //sort by the entity id if our times are equal |
| //srb.addSort( SortBuilders.fieldSort( IndexingUtils.ENTITY_ID_FIELDNAME ).order( SortOrder.ASC ) ); |
| |
| return; |
| } |
| |
| |
| /** |
| * Invoked when there are sort predicates |
| */ |
| private void applySortPredicates( final SearchRequestBuilder srb, final List<SortPredicate> sortPredicates, |
| final GeoSortFields geoFields, final Map<String, Class> knownFieldsWithType ) { |
| |
| Preconditions.checkNotNull(sortPredicates, "sort predicates list cannot be null"); |
| |
| for ( SortPredicate sp : sortPredicates ) { |
| |
| final SortOrder order = sp.getDirection().toEsSort(); |
| final String propertyName = sp.getPropertyName(); |
| |
| // if the user specified a geo field in their sort, then honor their sort order and use the field they |
| // specified. this is added first so it's known on the response hit when fetching the geo distance later |
| // see org.apache.usergrid.persistence.index.impl.IndexingUtils.parseIndexDocId(org.elasticsearch.search.SearchHit, boolean) |
| if ( geoFields.contains( propertyName ) ) { |
| final GeoDistanceSortBuilder geoSort = geoFields.applyOrder( propertyName, SortOrder.ASC ); |
| srb.addSort( geoSort ); |
| } |
| // fieldsWithType gives the caller an option to provide any schema related details on properties that |
| // might appear in a sort predicate. loop through these and set a specific sort, rather than adding a sort |
| // for all possible types |
| else if ( knownFieldsWithType != null && knownFieldsWithType.size() > 0 && knownFieldsWithType.containsKey(propertyName)) { |
| |
| String esFieldName = EsQueryVistor.getFieldNameForClass(knownFieldsWithType.get(propertyName)); |
| // always make sure string sorts use the unanalyzed field |
| if ( esFieldName.equals(IndexingUtils.FIELD_STRING_NESTED)){ |
| esFieldName = IndexingUtils.FIELD_STRING_NESTED_UNANALYZED; |
| } |
| |
| srb.addSort( createSort( order, esFieldName, propertyName ) ); |
| |
| } |
| //apply regular sort logic which check all possible data types, since this is not a known property name |
| else { |
| |
| //sort order is arbitrary if the user changes data types. Double, long, string, boolean are supported |
| //default sort types |
| srb.addSort( createSort( order, IndexingUtils.FIELD_DOUBLE_NESTED, propertyName ) ); |
| srb.addSort( createSort( order, IndexingUtils.FIELD_LONG_NESTED, propertyName ) ); |
| |
| /** |
| * We always want to sort by the unanalyzed string field to ensure correct ordering |
| */ |
| srb.addSort( createSort( order, IndexingUtils.FIELD_STRING_NESTED_UNANALYZED, propertyName ) ); |
| srb.addSort( createSort( order, IndexingUtils.FIELD_BOOLEAN_NESTED, propertyName ) ); |
| } |
| } |
| } |
| |
| |
| /** |
| * Create our filter builder. We need to restrict our results on edge search, as well as on types, and any filters |
| * that came from the grammar. |
| */ |
| private FilterBuilder createFilterBuilder( final SearchEdge searchEdge, final QueryVisitor visitor, |
| final SearchTypes searchTypes ) { |
| String context = createContextName( applicationScope, searchEdge ); |
| |
| |
| // Add our filter for context to our query for fast execution. |
| // Fast because it utilizes bitsets internally. See this post for more detail. |
| // http://www.elasticsearch.org/blog/all-about-elasticsearch-filter-bitsets/ |
| |
| // TODO evaluate performance when it's an all query. |
| // Do we need to put the context term first for performance? |
| |
| //make sure we have entity in the context |
| BoolFilterBuilder boolQueryFilter = FilterBuilders.boolFilter(); |
| |
| //add our edge search |
| boolQueryFilter.must( FilterBuilders.termFilter( IndexingUtils.EDGE_SEARCH_FIELDNAME, context ) ); |
| |
| |
| /** |
| * For the types the user specified, add them to an OR so 1 of them must match |
| */ |
| final String[] sourceTypes = searchTypes.getTypeNames( applicationScope ); |
| |
| |
| if ( sourceTypes.length > 0 ) { |
| final FilterBuilder[] typeTerms = new FilterBuilder[sourceTypes.length]; |
| |
| for ( int i = 0; i < sourceTypes.length; i++ ) { |
| typeTerms[i] = FilterBuilders.termFilter( IndexingUtils.ENTITY_TYPE_FIELDNAME, sourceTypes[i] ); |
| } |
| |
| //add all our types, 1 type must match per query |
| boolQueryFilter.must( FilterBuilders.orFilter( typeTerms ) ); |
| } |
| |
| //if we have a filter from our visitor, add it |
| |
| Optional<FilterBuilder> queryBuilder = visitor.getFilterBuilder(); |
| |
| if ( queryBuilder.isPresent() ) { |
| boolQueryFilter.must( queryBuilder.get() ); |
| } |
| |
| return boolQueryFilter; |
| } |
| |
| |
| /** |
| * Create a sort for the property name and field name specified |
| * |
| * @param sortOrder The sort order |
| * @param fieldName The name of the field for the type |
| * @param propertyName The property name the user specified for the sort |
| */ |
| private FieldSortBuilder createSort( final SortOrder sortOrder, final String fieldName, |
| final String propertyName ) { |
| |
| final TermFilterBuilder propertyFilter = sortPropertyTermFilter( propertyName ); |
| |
| |
| return SortBuilders.fieldSort( fieldName ).order( sortOrder ).setNestedFilter( propertyFilter ); |
| } |
| } |