/* * Copyright 2012 Paul Merlin. * * Licensed 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. */ package org.qi4j.index.elasticsearch; import java.util.Map; import org.elasticsearch.action.count.CountRequestBuilder; import org.elasticsearch.action.count.CountResponse; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.index.query.AndFilterBuilder; import org.elasticsearch.index.query.FilterBuilder; import org.elasticsearch.index.query.OrFilterBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.sort.SortOrder; import org.qi4j.api.Qi4j; import org.qi4j.api.composite.Composite; import org.qi4j.api.entity.EntityReference; import org.qi4j.api.injection.scope.This; import org.qi4j.api.mixin.Mixins; import org.qi4j.api.query.grammar.*; import org.qi4j.api.value.ValueComposite; import org.qi4j.api.value.ValueDescriptor; import org.qi4j.functional.Function; import org.qi4j.functional.Iterables; import org.qi4j.functional.Specification; import org.qi4j.spi.query.EntityFinder; import org.qi4j.spi.query.EntityFinderException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.elasticsearch.index.query.FilterBuilders.*; import static org.elasticsearch.index.query.QueryBuilders.*; @Mixins( ElasticSearchFinder.Mixin.class ) public interface ElasticSearchFinder extends EntityFinder { class Mixin implements EntityFinder { private static final Logger LOGGER = LoggerFactory.getLogger( ElasticSearchFinder.class ); @This private ElasticSearchSupport support; @Override public Iterable<EntityReference> findEntities( Class<?> resultType, Specification<Composite> whereClause, OrderBy[] orderBySegments, Integer firstResult, Integer maxResults, Map<String, Object> variables ) throws EntityFinderException { // Prepare request SearchRequestBuilder request = support.client().prepareSearch( support.index() ); AndFilterBuilder filterBuilder = baseFilters( resultType ); QueryBuilder queryBuilder = processWhereSpecification( filterBuilder, whereClause, variables ); request.setQuery( filteredQuery( queryBuilder, filterBuilder ) ); if ( firstResult != null ) { request.setFrom( firstResult ); } if ( maxResults != null ) { request.setSize( maxResults ); } else { //request.setSize( Integer.MAX_VALUE ); // TODO Use scrolls? } if ( orderBySegments != null ) { for ( OrderBy order : orderBySegments ) { request.addSort( order.property().toString(), order.order() == OrderBy.Order.ASCENDING ? SortOrder.ASC : SortOrder.DESC ); } } // Log LOGGER.debug( "Will search Entities: {}", request ); // Execute SearchResponse response = request.execute().actionGet(); return Iterables.map( new Function<SearchHit, EntityReference>() { @Override public EntityReference map( SearchHit from ) { return EntityReference.parseEntityReference( from.id() ); } }, response.getHits() ); } @Override public EntityReference findEntity( Class<?> resultType, Specification<Composite> whereClause, Map<String, Object> variables ) throws EntityFinderException { // Prepare request SearchRequestBuilder request = support.client().prepareSearch( support.index() ); AndFilterBuilder filterBuilder = baseFilters( resultType ); QueryBuilder queryBuilder = processWhereSpecification( filterBuilder, whereClause, variables ); request.setQuery( filteredQuery( queryBuilder, filterBuilder ) ); request.setSize( 1 ); // Log LOGGER.debug( "Will search Entity: {}", request ); // Execute SearchResponse response = request.execute().actionGet(); if ( response.hits().totalHits() == 1 ) { return EntityReference.parseEntityReference( response.hits().getAt( 0 ).id() ); } return null; } @Override public long countEntities( Class<?> resultType, Specification<Composite> whereClause, Map<String, Object> variables ) throws EntityFinderException { // Prepare request CountRequestBuilder request = support.client().prepareCount( support.index() ); AndFilterBuilder filterBuilder = baseFilters( resultType ); QueryBuilder queryBuilder = processWhereSpecification( filterBuilder, whereClause, variables ); request.setQuery( filteredQuery( queryBuilder, filterBuilder ) ); // Log LOGGER.debug( "Will count Entities: {}", request ); // Execute CountResponse count = request.execute().actionGet(); return count.count(); } private static AndFilterBuilder baseFilters( Class<?> resultType ) { return andFilter( termFilter( "_types", resultType.getName() ) ); } private QueryBuilder processWhereSpecification( AndFilterBuilder filterBuilder, Specification<Composite> spec, Map<String, Object> variables ) throws EntityFinderException { if ( spec == null ) { return matchAllQuery(); } if ( spec instanceof QuerySpecification ) { return wrapperQuery( ( ( QuerySpecification ) spec ).query() ); } processSpecification( filterBuilder, spec, variables ); return matchAllQuery(); } private void processSpecification( FilterBuilder filterBuilder, Specification<Composite> spec, Map<String, Object> variables ) throws EntityFinderException { if ( spec instanceof BinarySpecification ) { BinarySpecification binSpec = ( BinarySpecification ) spec; processBinarySpecification( filterBuilder, binSpec, variables ); } else if ( spec instanceof NotSpecification ) { NotSpecification notSpec = ( NotSpecification ) spec; processNotSpecification( filterBuilder, notSpec, variables ); } else if ( spec instanceof EqSpecification || spec instanceof NeSpecification ) { ComparisonSpecification<?> compSpec = ( ComparisonSpecification<?> ) spec; processEqualitySpecification( filterBuilder, compSpec, variables ); } else if ( spec instanceof ComparisonSpecification ) { ComparisonSpecification<?> compSpec = ( ComparisonSpecification<?> ) spec; processComparisonSpecification( filterBuilder, compSpec, variables ); } else if ( spec instanceof ContainsAllSpecification ) { ContainsAllSpecification<?> contAllSpec = ( ContainsAllSpecification ) spec; processContainsAllSpecification( filterBuilder, contAllSpec, variables ); } else if ( spec instanceof ContainsSpecification ) { ContainsSpecification<?> contSpec = ( ContainsSpecification ) spec; processContainsSpecification( filterBuilder, contSpec, variables ); } else if ( spec instanceof MatchesSpecification ) { MatchesSpecification matchSpec = ( MatchesSpecification ) spec; processMatchesSpecification( filterBuilder, matchSpec, variables ); } else if ( spec instanceof PropertyNotNullSpecification ) { PropertyNotNullSpecification<?> propNotNullSpec = ( PropertyNotNullSpecification ) spec; processPropertyNotNullSpecification( filterBuilder, propNotNullSpec ); } else if ( spec instanceof PropertyNullSpecification ) { PropertyNullSpecification<?> propNullSpec = ( PropertyNullSpecification ) spec; processPropertyNullSpecification( filterBuilder, propNullSpec ); } else if ( spec instanceof AssociationNotNullSpecification ) { AssociationNotNullSpecification<?> assNotNullSpec = ( AssociationNotNullSpecification ) spec; processAssociationNotNullSpecification( filterBuilder, assNotNullSpec ); } else if ( spec instanceof AssociationNullSpecification ) { AssociationNullSpecification<?> assNullSpec = ( AssociationNullSpecification ) spec; processAssociationNullSpecification( filterBuilder, assNullSpec ); } else if ( spec instanceof ManyAssociationContainsSpecification ) { ManyAssociationContainsSpecification<?> manyAssContSpec = ( ManyAssociationContainsSpecification ) spec; processManyAssociationContainsSpecification( filterBuilder, manyAssContSpec, variables ); } else { throw new UnsupportedOperationException( "Query specification unsupported by Elastic Search: " + spec.getClass() + ": " + spec ); } } private void addFilter( FilterBuilder filter, FilterBuilder into ) { if ( into instanceof AndFilterBuilder ) { ( ( AndFilterBuilder ) into ).add( filter ); } else if ( into instanceof OrFilterBuilder ) { ( ( OrFilterBuilder ) into ).add( filter ); } else { throw new UnsupportedOperationException( "FilterBuilder is nor an AndFB nor an OrFB, cannot continue." ); } } private String toString( Object value, Map<String, Object> variables ) { if ( value == null ) { return null; } if ( value instanceof Variable ) { Variable var = ( Variable ) value; Object realValue = variables.get( var.variableName() ); if ( realValue == null ) { throw new IllegalArgumentException( "Variable " + var.variableName() + " not bound" ); } return realValue.toString(); } return value.toString(); } private void processBinarySpecification( FilterBuilder filterBuilder, BinarySpecification spec, Map<String, Object> variables ) throws EntityFinderException { LOGGER.trace( "Processing BinarySpecification {}", spec ); Iterable<Specification<Composite>> operands = spec.operands(); if ( spec instanceof AndSpecification ) { AndFilterBuilder andFilterBuilder = new AndFilterBuilder(); for ( Specification<Composite> operand : operands ) { processSpecification( andFilterBuilder, operand, variables ); } addFilter( andFilterBuilder, filterBuilder ); } else if ( spec instanceof OrSpecification ) { OrFilterBuilder orFilterBuilder = new OrFilterBuilder(); for ( Specification<Composite> operand : operands ) { processSpecification( orFilterBuilder, operand, variables ); } addFilter( orFilterBuilder, filterBuilder ); } else { throw new UnsupportedOperationException( "Query specification unsupported by Elastic Search: " + spec.getClass() + ": " + spec ); } } private void processNotSpecification( FilterBuilder filterBuilder, NotSpecification spec, Map<String, Object> variables ) throws EntityFinderException { LOGGER.trace( "Processing NotSpecification {}", spec ); AndFilterBuilder operandFilter = new AndFilterBuilder(); processSpecification( operandFilter, spec.operand(), variables ); addFilter( notFilter( operandFilter ), filterBuilder ); } private void processEqualitySpecification( FilterBuilder filterBuilder, ComparisonSpecification<?> spec, Map<String, Object> variables ) throws EntityFinderException { LOGGER.trace( "Processing EqualitySpecification {}", spec ); String name = spec.property().toString(); if ( spec.value() instanceof ValueComposite ) { // Query by complex property "example value" ValueComposite value = ( ValueComposite ) spec.value(); ValueDescriptor valueDescriptor = ( ValueDescriptor ) Qi4j.FUNCTION_DESCRIPTOR_FOR.map( value ); throw new UnsupportedOperationException( "ElasticSearch Index/Query does not support complex " + "queries, ie. queries by 'example value'." ); } else { // Query by simple property value String value = toString( spec.value(), variables ); if ( spec instanceof EqSpecification ) { addFilter( termFilter( name, value ), filterBuilder ); } else if ( spec instanceof NeSpecification ) { addFilter( notFilter( termFilter( name, value ) ), filterBuilder ); } } } private void processComparisonSpecification( FilterBuilder filterBuilder, ComparisonSpecification<?> spec, Map<String, Object> variables ) { LOGGER.trace( "Processing ComparisonSpecification {}", spec ); String name = spec.property().toString(); String value = toString( spec.value(), variables ); if ( spec instanceof GeSpecification ) { addFilter( rangeFilter( name ).from( value ).includeLower( true ), filterBuilder ); } else if ( spec instanceof GtSpecification ) { addFilter( rangeFilter( name ).from( value ).includeLower( false ), filterBuilder ); } else if ( spec instanceof LeSpecification ) { addFilter( rangeFilter( name ).to( value ).includeUpper( true ), filterBuilder ); } else if ( spec instanceof LtSpecification ) { addFilter( rangeFilter( name ).to( value ).includeUpper( false ), filterBuilder ); } else { throw new UnsupportedOperationException( "Query specification unsupported by Elastic Search: " + spec.getClass() + ": " + spec ); } } private void processContainsAllSpecification( FilterBuilder filterBuilder, ContainsAllSpecification<?> spec, Map<String, Object> variables ) { LOGGER.trace( "Processing ContainsAllSpecification {}", spec ); String name = spec.collectionProperty().toString(); AndFilterBuilder contAllFilter = new AndFilterBuilder(); for ( Object value : spec.containedValues() ) { if ( value instanceof ValueComposite ) { // Query by complex property "example value" ValueComposite valueComposite = ( ValueComposite ) value; ValueDescriptor valueDescriptor = ( ValueDescriptor ) Qi4j.FUNCTION_DESCRIPTOR_FOR.map( valueComposite ); throw new UnsupportedOperationException( "ElasticSearch Index/Query does not support complex " + "queries, ie. queries by 'example value'." ); } else { contAllFilter.add( termFilter( name, toString( value, variables ) ) ); } } addFilter( contAllFilter, filterBuilder ); } private void processContainsSpecification( FilterBuilder filterBuilder, ContainsSpecification<?> spec, Map<String, Object> variables ) { LOGGER.trace( "Processing ContainsSpecification {}", spec ); String name = spec.collectionProperty().toString(); if ( spec.value() instanceof ValueComposite ) { // Query by complex property "example value" ValueComposite value = ( ValueComposite ) spec.value(); ValueDescriptor valueDescriptor = ( ValueDescriptor ) Qi4j.FUNCTION_DESCRIPTOR_FOR.map( value ); throw new UnsupportedOperationException( "ElasticSearch Index/Query does not support complex " + "queries, ie. queries by 'example value'." ); } else { String value = toString( spec.value(), variables ); addFilter( termFilter( name, value ), filterBuilder ); } } private void processMatchesSpecification( FilterBuilder filterBuilder, MatchesSpecification spec, Map<String, Object> variables ) { LOGGER.trace( "Processing MatchesSpecification {}", spec ); // https://github.com/elasticsearch/elasticsearch/issues/988 // http://elasticsearch-users.115913.n3.nabble.com/Regex-Query-td3301347.html throw new UnsupportedOperationException( "Query specification unsupported by Elastic Search: " + spec.getClass() + ": " + spec ); } private void processPropertyNotNullSpecification( FilterBuilder filterBuilder, PropertyNotNullSpecification<?> spec ) { LOGGER.trace( "Processing PropertyNotNullSpecification {}", spec ); addFilter( existsFilter( spec.property().toString() ), filterBuilder ); } private void processPropertyNullSpecification( FilterBuilder filterBuilder, PropertyNullSpecification<?> spec ) { LOGGER.trace( "Processing PropertyNullSpecification {}", spec ); addFilter( missingFilter( spec.property().toString() ), filterBuilder ); } private void processAssociationNotNullSpecification( FilterBuilder filterBuilder, AssociationNotNullSpecification<?> spec ) { LOGGER.trace( "Processing AssociationNotNullSpecification {}", spec ); addFilter( existsFilter( spec.association().toString() + ".identity" ), filterBuilder ); } private void processAssociationNullSpecification( FilterBuilder filterBuilder, AssociationNullSpecification<?> spec ) { LOGGER.trace( "Processing AssociationNullSpecification {}", spec ); addFilter( missingFilter( spec.association().toString() + ".identity" ), filterBuilder ); } private void processManyAssociationContainsSpecification( FilterBuilder filterBuilder, ManyAssociationContainsSpecification<?> spec, Map<String, Object> variables ) { LOGGER.trace( "Processing ManyAssociationContainsSpecification {}", spec ); String name = spec.manyAssociation().toString() + ".identity"; String value = toString( spec.value(), variables ); addFilter( termFilter( name, value ), filterBuilder ); } } }