package org.vertexium.elasticsearch7; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; import org.elasticsearch.action.search.ClearScrollResponse; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.Client; import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.geo.builders.*; import org.elasticsearch.common.unit.DistanceUnit; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.query.*; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.aggregations.AbstractAggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.geogrid.GeoGridAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; import org.elasticsearch.search.aggregations.bucket.histogram.ExtendedBounds; import org.elasticsearch.search.aggregations.bucket.histogram.HistogramAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.range.DateRangeAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.range.RangeAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.CardinalityAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.ExtendedStatsAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.PercentilesAggregationBuilder; import org.elasticsearch.search.sort.ScriptSortBuilder; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortMode; import org.elasticsearch.search.sort.SortOrder; import org.locationtech.jts.geom.Coordinate; import org.locationtech.spatial4j.context.SpatialContext; import org.locationtech.spatial4j.distance.DistanceCalculator; import org.locationtech.spatial4j.distance.DistanceUtils; import org.locationtech.spatial4j.shape.Point; import org.vertexium.*; import org.vertexium.elasticsearch7.scoring.ElasticsearchScoringStrategy; import org.vertexium.elasticsearch7.sorting.ElasticsearchSortingStrategy; import org.vertexium.elasticsearch7.utils.ElasticsearchTypes; import org.vertexium.elasticsearch7.utils.InfiniteScrollIterable; import org.vertexium.elasticsearch7.utils.PagingIterable; import org.vertexium.query.*; import org.vertexium.scoring.ScoringStrategy; import org.vertexium.sorting.SortingStrategy; import org.vertexium.type.*; import org.vertexium.util.IterableUtils; import org.vertexium.util.JoinIterable; import org.vertexium.util.VertexiumLogger; import org.vertexium.util.VertexiumLoggerFactory; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import static org.locationtech.spatial4j.distance.DistanceUtils.KM_TO_DEG; import static org.vertexium.elasticsearch7.Elasticsearch7SearchIndex.*; import static org.vertexium.elasticsearch7.utils.SearchResponseUtils.checkForFailures; import static org.vertexium.util.StreamUtils.stream; public class ElasticsearchSearchQueryBase extends QueryBase { private static final VertexiumLogger LOGGER = VertexiumLoggerFactory.getLogger(ElasticsearchSearchQueryBase.class); public static final VertexiumLogger QUERY_LOGGER = VertexiumLoggerFactory.getQueryLogger(Query.class); public static final String TOP_HITS_AGGREGATION_NAME = "__visallo_top_hits"; public static final String KEYWORD_UNMAPPED_TYPE = "keyword"; public static final String AGGREGATION_METADATA_FIELD_NAME_KEY = "fieldName"; private final Client client; private final StandardAnalyzer analyzer; private final IndexSelectionStrategy indexSelectionStrategy; private final int pageSize; private final int pagingLimit; private final TimeValue scrollKeepAlive; private final int termAggregationShardSize; private final int maxQueryStringTerms; private final String queryString; public ElasticsearchSearchQueryBase( Client client, Graph graph, String queryString, Options options, Authorizations authorizations ) { super(graph, queryString, authorizations); this.client = client; this.queryString = queryString; this.pageSize = options.pageSize; this.indexSelectionStrategy = options.indexSelectionStrategy; this.scrollKeepAlive = options.scrollKeepAlive; this.pagingLimit = options.pagingLimit; this.analyzer = options.analyzer; this.termAggregationShardSize = options.termAggregationShardSize; this.maxQueryStringTerms = options.maxQueryStringTerms; } public ElasticsearchSearchQueryBase( Client client, Graph graph, String[] similarToFields, String similarToText, Options options, Authorizations authorizations ) { super(graph, similarToFields, similarToText, authorizations); this.client = client; this.queryString = null; this.pageSize = options.pageSize; this.indexSelectionStrategy = options.indexSelectionStrategy; this.scrollKeepAlive = options.scrollKeepAlive; this.pagingLimit = options.pagingLimit; this.analyzer = options.analyzer; this.termAggregationShardSize = options.termAggregationShardSize; this.maxQueryStringTerms = options.maxQueryStringTerms; } @Override public boolean isAggregationSupported(Aggregation agg) { if (agg instanceof HistogramAggregation) { return true; } if (agg instanceof RangeAggregation) { return true; } if (agg instanceof PercentilesAggregation) { return true; } if (agg instanceof TermsAggregation) { return true; } if (agg instanceof GeohashAggregation) { return true; } if (agg instanceof StatisticsAggregation) { return true; } if (agg instanceof CalendarFieldAggregation) { return true; } if (agg instanceof CardinalityAggregation) { return true; } return false; } private SearchRequestBuilder buildQuery(EnumSet<ElasticsearchDocumentType> elementType, FetchHints fetchHints, boolean includeAggregations) { if (QUERY_LOGGER.isTraceEnabled()) { QUERY_LOGGER.trace("searching for: " + toString()); } List<QueryBuilder> filters = getFilters(elementType, fetchHints); QueryBuilder query = createQuery(getParameters()); QueryBuilder filterBuilder = getFilterBuilder(filters, fetchHints); String[] indicesToQuery = getIndexSelectionStrategy().getIndicesToQuery(this, elementType); if (QUERY_LOGGER.isTraceEnabled()) { QUERY_LOGGER.trace("indicesToQuery: %s", Joiner.on(", ").join(indicesToQuery)); } if (getSearchIndex().shouldRefreshIndexOnQuery()) { getSearchIndex().getIndexRefreshTracker().refresh(client, indicesToQuery); } SearchRequestBuilder searchRequestBuilder = getClient() .prepareSearch(indicesToQuery) .setQuery(QueryBuilders.boolQuery().must(query).filter(filterBuilder)) .storedFields( Elasticsearch7SearchIndex.ELEMENT_ID_FIELD_NAME, Elasticsearch7SearchIndex.ELEMENT_TYPE_FIELD_NAME, Elasticsearch7SearchIndex.EXTENDED_DATA_TABLE_NAME_FIELD_NAME, Elasticsearch7SearchIndex.EXTENDED_DATA_TABLE_ROW_ID_FIELD_NAME ); if (fetchHints.equals(FetchHints.NONE)) { searchRequestBuilder.storedFields( Elasticsearch7SearchIndex.OUT_VERTEX_ID_FIELD_NAME, Elasticsearch7SearchIndex.IN_VERTEX_ID_FIELD_NAME, Elasticsearch7SearchIndex.EDGE_LABEL_FIELD_NAME ); } if (getParameters().getMinScore() != null) { searchRequestBuilder.setMinScore(getParameters().getMinScore().floatValue()); } if (includeAggregations) { List<AggregationBuilder> aggs = getElasticsearchAggregations(getAggregations()); for (AggregationBuilder aggregationBuilder : aggs) { searchRequestBuilder.addAggregation(aggregationBuilder); } } applySort(searchRequestBuilder); return searchRequestBuilder; } protected QueryBuilder createQueryStringQuery(QueryStringQueryParameters queryParameters) { String queryString = queryParameters.getQueryString(); if (queryString == null || queryString.equals("*")) { return QueryBuilders.matchAllQuery(); } queryString = getSearchIndex().getQueryStringTransformer().transform(queryString, getParameters().getAuthorizations()); Collection<String> fields = getSearchIndex().getQueryablePropertyNames(getGraph(), getParameters().getAuthorizations()); QueryStringQueryBuilder qs = QueryBuilders.queryStringQuery(queryString); for (String field : fields) { qs = qs.field(getSearchIndex().replaceFieldnameDots(field)); } qs.allowLeadingWildcard(false); return qs; } protected List<QueryBuilder> getFilters(EnumSet<ElasticsearchDocumentType> elementTypes, FetchHints fetchHints) { List<QueryBuilder> filters = new ArrayList<>(); if (elementTypes != null) { addElementTypeFilter(filters, elementTypes); } if (!fetchHints.isIncludeHidden()) { String[] hiddenVertexPropertyNames = getPropertyNames(HIDDEN_VERTEX_FIELD_NAME); if (hiddenVertexPropertyNames != null && hiddenVertexPropertyNames.length > 0) { BoolQueryBuilder elementIsNotHiddenQuery = QueryBuilders.boolQuery(); for (String hiddenVertexPropertyName : hiddenVertexPropertyNames) { elementIsNotHiddenQuery.mustNot(QueryBuilders.existsQuery(hiddenVertexPropertyName)); } filters.add(elementIsNotHiddenQuery); } } for (HasContainer has : getParameters().getHasContainers()) { if (has instanceof HasValueContainer) { filters.add(getFiltersForHasValueContainer((HasValueContainer) has)); } else if (has instanceof HasPropertyContainer) { filters.add(getFilterForHasPropertyContainer((HasPropertyContainer) has)); } else if (has instanceof HasNotPropertyContainer) { filters.add(getFilterForHasNotPropertyContainer((HasNotPropertyContainer) has)); } else if (has instanceof HasExtendedData) { filters.add(getFilterForHasExtendedData((HasExtendedData) has)); } else if (has instanceof HasAuthorizationContainer) { filters.add(getFilterForHasAuthorizationContainer((HasAuthorizationContainer) has)); } else { throw new VertexiumException("Unexpected type " + has.getClass().getName()); } } if ((elementTypes == null || elementTypes.contains(ElasticsearchDocumentType.EDGE)) && getParameters().getEdgeLabels().size() > 0) { String[] edgeLabelsArray = getParameters().getEdgeLabels().toArray(new String[0]); filters.add(QueryBuilders.termsQuery(Elasticsearch7SearchIndex.EDGE_LABEL_FIELD_NAME, edgeLabelsArray)); } if (elementTypes == null || elementTypes.contains(ElasticsearchDocumentType.EDGE_EXTENDED_DATA) || elementTypes.contains(ElasticsearchDocumentType.VERTEX_EXTENDED_DATA) ) { Elasticsearch7SearchIndex es = (Elasticsearch7SearchIndex) ((GraphWithSearchIndex) getGraph()).getSearchIndex(); Collection<String> queryableVisibilities = es.getQueryableExtendedDataVisibilities(getGraph(), getParameters().getAuthorizations()); TermsQueryBuilder extendedDataVisibilitiesTerms = QueryBuilders.termsQuery(EXTENDED_DATA_TABLE_COLUMN_VISIBILITIES_FIELD_NAME, queryableVisibilities); if (elementTypes == null || elementTypes.contains(ElasticsearchDocumentType.EDGE) || elementTypes.contains(ElasticsearchDocumentType.VERTEX) ) { TermsQueryBuilder extendedDataTerms = QueryBuilders.termsQuery(ELEMENT_TYPE_FIELD_NAME, Arrays.asList(ElasticsearchDocumentType.EDGE_EXTENDED_DATA.getKey(), ElasticsearchDocumentType.VERTEX_EXTENDED_DATA.getKey())); BoolQueryBuilder elementOrExtendedDataFilter = QueryBuilders.boolQuery(); elementOrExtendedDataFilter.should(QueryBuilders.boolQuery().mustNot(extendedDataTerms)); elementOrExtendedDataFilter.should(extendedDataVisibilitiesTerms); elementOrExtendedDataFilter.minimumShouldMatch(1); filters.add(elementOrExtendedDataFilter); } else { filters.add(extendedDataVisibilitiesTerms); } } if (getParameters().getIds() != null) { String[] idsArray = getParameters().getIds().toArray(new String[0]); filters.add(QueryBuilders.termsQuery(ELEMENT_ID_FIELD_NAME, idsArray)); } Elasticsearch7SearchIndex es = (Elasticsearch7SearchIndex) ((GraphWithSearchIndex) getGraph()).getSearchIndex(); Collection<String> fields = es.getQueryableElementTypeVisibilityPropertyNames(getGraph(), getParameters().getAuthorizations()); BoolQueryBuilder atLeastOneFieldExistsFilter = QueryBuilders.boolQuery(); for (String field : fields) { atLeastOneFieldExistsFilter.should(new ExistsQueryBuilder(field)); } atLeastOneFieldExistsFilter.minimumShouldMatch(1); filters.add(atLeastOneFieldExistsFilter); return filters; } protected void applySort(SearchRequestBuilder q) { AtomicBoolean sortedById = new AtomicBoolean(false); for (SortContainer sortContainer : getParameters().getSortContainers()) { if (sortContainer instanceof PropertySortContainer) { applySortProperty(q, (PropertySortContainer) sortContainer, sortedById); } else if (sortContainer instanceof SortingStrategySortContainer) { applySortStrategy(q, (SortingStrategySortContainer) sortContainer); } else { throw new VertexiumException("Unexpected sorting type: " + sortContainer.getClass().getName()); } } q.addSort("_score", SortOrder.DESC); if (!sortedById.get()) { // If an id sort isn't specified, default is to sort by score and then sort id by ascending order after specified sorts q.addSort(Elasticsearch7SearchIndex.ELEMENT_ID_FIELD_NAME, SortOrder.ASC); } } private void applySortStrategy(SearchRequestBuilder q, SortingStrategySortContainer sortContainer) { SortingStrategy sortingStrategy = sortContainer.sortingStrategy; if (!(sortingStrategy instanceof ElasticsearchSortingStrategy)) { throw new VertexiumException(String.format( "sorting strategies must implement %s to work with Elasticsearch", ElasticsearchSortingStrategy.class.getName() )); } ((ElasticsearchSortingStrategy) sortingStrategy).updateElasticsearchQuery( getGraph(), getSearchIndex(), q, getParameters(), sortContainer.direction ); } protected void applySortProperty(SearchRequestBuilder q, PropertySortContainer sortContainer, AtomicBoolean sortedById) { SortOrder esOrder = sortContainer.direction == SortDirection.ASCENDING ? SortOrder.ASC : SortOrder.DESC; if (Element.ID_PROPERTY_NAME.equals(sortContainer.propertyName)) { q.addSort(Elasticsearch7SearchIndex.ELEMENT_ID_FIELD_NAME, esOrder); sortedById.set(true); } else if (Edge.LABEL_PROPERTY_NAME.equals(sortContainer.propertyName)) { q.addSort( SortBuilders.fieldSort(Elasticsearch7SearchIndex.EDGE_LABEL_FIELD_NAME) .unmappedType(KEYWORD_UNMAPPED_TYPE) .order(esOrder) ); } else if (Edge.OUT_VERTEX_ID_PROPERTY_NAME.equals(sortContainer.propertyName)) { q.addSort( SortBuilders.fieldSort(Elasticsearch7SearchIndex.OUT_VERTEX_ID_FIELD_NAME) .unmappedType(KEYWORD_UNMAPPED_TYPE) .order(esOrder) ); } else if (Edge.IN_VERTEX_ID_PROPERTY_NAME.equals(sortContainer.propertyName)) { q.addSort( SortBuilders.fieldSort(Elasticsearch7SearchIndex.IN_VERTEX_ID_FIELD_NAME) .unmappedType(KEYWORD_UNMAPPED_TYPE) .order(esOrder) ); } else if (Edge.IN_OR_OUT_VERTEX_ID_PROPERTY_NAME.equals(sortContainer.propertyName)) { throw new VertexiumException("Cannot sort by " + Edge.IN_OR_OUT_VERTEX_ID_PROPERTY_NAME); } else if (ExtendedDataRow.TABLE_NAME.equals(sortContainer.propertyName)) { q.addSort( SortBuilders.fieldSort(Elasticsearch7SearchIndex.EXTENDED_DATA_TABLE_NAME_FIELD_NAME) .unmappedType(KEYWORD_UNMAPPED_TYPE) .order(esOrder) ); } else if (ExtendedDataRow.ROW_ID.equals(sortContainer.propertyName)) { q.addSort( SortBuilders.fieldSort(Elasticsearch7SearchIndex.EXTENDED_DATA_TABLE_ROW_ID_FIELD_NAME) .unmappedType(KEYWORD_UNMAPPED_TYPE) .order(esOrder) ); } else if (ExtendedDataRow.ELEMENT_ID.equals(sortContainer.propertyName)) { q.addSort( SortBuilders.fieldSort(Elasticsearch7SearchIndex.ELEMENT_ID_FIELD_NAME) .unmappedType(KEYWORD_UNMAPPED_TYPE) .order(esOrder) ); } else if (ExtendedDataRow.ELEMENT_TYPE.equals(sortContainer.propertyName)) { q.addSort( SortBuilders.fieldSort(Elasticsearch7SearchIndex.ELEMENT_TYPE_FIELD_NAME) .unmappedType(KEYWORD_UNMAPPED_TYPE) .order(esOrder) ); } else { PropertyDefinition propertyDefinition = getGraph().getPropertyDefinition(sortContainer.propertyName); if (propertyDefinition == null) { return; } if (!getSearchIndex().isPropertyInIndex(getGraph(), sortContainer.propertyName)) { return; } if (!propertyDefinition.isSortable()) { throw new VertexiumException("Cannot sort on non-sortable fields"); } String[] propertyNames = getPropertyNames(propertyDefinition.getPropertyName()); if (propertyNames.length > 1) { String scriptSrc = "def fieldValues = []; for (def fieldName : params.fieldNames) { if(doc[fieldName].size() !=0) { fieldValues.addAll(doc[fieldName]); }} " + "if (params.esOrder == 'asc') { Collections.sort(fieldValues); } else { Collections.sort(fieldValues, Collections.reverseOrder()); }"; if (propertyDefinition.getDataType() == String.class) { scriptSrc += "return fieldValues.length > 0 ? fieldValues[0] : (params.esOrder == 'asc' ? Character.toString(Character.MAX_VALUE) : '');"; } else { scriptSrc += "return fieldValues.length > 0 ? (fieldValues[0] instanceof JodaCompatibleZonedDateTime ? fieldValues[0].getMillis() : fieldValues[0]) : (params.esOrder == 'asc' ? Long.MAX_VALUE : Long.MIN_VALUE);"; } List<String> fieldNames = Arrays.stream(propertyNames).map(propertyName -> propertyName + (propertyDefinition.getDataType() == String.class ? Elasticsearch7SearchIndex.EXACT_MATCH_PROPERTY_NAME_SUFFIX : "") ).collect(Collectors.toList()); HashMap<String, Object> scriptParams = new HashMap<>(); scriptParams.put("fieldNames", fieldNames); scriptParams.put("esOrder", esOrder == SortOrder.DESC ? "desc" : "asc"); scriptParams.put("dataType", propertyDefinition.getDataType().getSimpleName()); Script script = new Script(ScriptType.INLINE, "painless", scriptSrc, scriptParams); ScriptSortBuilder.ScriptSortType sortType = propertyDefinition.getDataType() == String.class ? ScriptSortBuilder.ScriptSortType.STRING : ScriptSortBuilder.ScriptSortType.NUMBER; q.addSort(SortBuilders.scriptSort(script, sortType) .order(esOrder) .sortMode(esOrder == SortOrder.DESC ? SortMode.MAX : SortMode.MIN)); } else if (propertyNames.length == 1) { String sortField = propertyNames[0]; String unmappedType = ElasticsearchTypes.fromJavaClass(propertyDefinition.getDataType()); if (propertyDefinition.getDataType() == String.class) { sortField += Elasticsearch7SearchIndex.EXACT_MATCH_PROPERTY_NAME_SUFFIX; unmappedType = KEYWORD_UNMAPPED_TYPE; } q.addSort( SortBuilders.fieldSort(sortField) .unmappedType(unmappedType) .order(esOrder) ); } } } @Override public QueryResultsIterable<? extends VertexiumObject> search(EnumSet<VertexiumObjectType> objectTypes, FetchHints fetchHints) { validateQueryString(); if (shouldUseScrollApi()) { return searchScroll(objectTypes, fetchHints); } return searchPaged(objectTypes, fetchHints); } private void validateQueryString() { if (queryString == null || queryString.length() <= maxQueryStringTerms) { return; } try { try (TokenStream tokens = analyzer.tokenStream("", queryString)) { tokens.reset(); int tokenCount = 0; while (tokens.incrementToken()) { if (++tokenCount > maxQueryStringTerms) { tokens.end(); throw new VertexiumException("Exceeded maximum query string terms of " + maxQueryStringTerms); } } tokens.end(); } } catch (IOException e) { throw new VertexiumException("Failed to count number of query string terms", e); } } private QueryResultsIterable<? extends VertexiumObject> searchScroll(EnumSet<VertexiumObjectType> objectTypes, FetchHints fetchHints) { return new QueryInfiniteScrollIterable<VertexiumObject>(objectTypes, fetchHints, getParameters().getLimit()) { @Override protected ElasticsearchGraphQueryIterable<VertexiumObject> searchResponseToIterable(SearchResponse searchResponse) { return ElasticsearchSearchQueryBase.this.searchResponseToVertexiumObjectIterable(searchResponse, fetchHints); } @Override protected IdStrategy getIdStrategy() { return getSearchIndex().getIdStrategy(); } }; } private void closeScroll(String scrollId) { try { ClearScrollResponse clearScrollResponse = client.prepareClearScroll() .addScrollId(scrollId) .execute().actionGet(); if (!clearScrollResponse.isSucceeded()) { LOGGER.warn("Unable to clear scroll " + scrollId); } } catch (Exception ex) { throw new VertexiumException("Could not close iterator " + scrollId, ex); } } private QueryResultsIterable<? extends VertexiumObject> searchPaged(EnumSet<VertexiumObjectType> objectTypes, FetchHints fetchHints) { return new PagingIterable<VertexiumObject>(getParameters().getSkip(), getParameters().getLimit(), pageSize) { @Override protected ElasticsearchGraphQueryIterable<VertexiumObject> getPageIterable(int skip, int limit, boolean includeAggregations) { SearchResponse response; try { response = getSearchResponse(ElasticsearchDocumentType.fromVertexiumObjectTypes(objectTypes), fetchHints, skip, limit, includeAggregations); } catch (IndexNotFoundException ex) { LOGGER.debug("Index missing: %s (returning empty iterable)", ex.getMessage()); return createEmptyIterable(); } catch (VertexiumNoMatchingPropertiesException ex) { LOGGER.debug("Could not find property: %s (returning empty iterable)", ex.getPropertyName()); return createEmptyIterable(); } return searchResponseToVertexiumObjectIterable(response, fetchHints); } }; } private ElasticsearchGraphQueryIterable<VertexiumObject> searchResponseToVertexiumObjectIterable(SearchResponse response, FetchHints fetchHints) { final SearchHits hits = response.getHits(); Ids ids = new Ids(getIdStrategy(), hits); if (LOGGER.isDebugEnabled()) { LOGGER.debug( "elasticsearch results (vertices: %d + edges: %d + extended data: %d = %d)", ids.getVertexIds().size(), ids.getEdgeIds().size(), ids.getExtendedDataIds().size(), ids.getVertexIds().size() + ids.getEdgeIds().size() + ids.getExtendedDataIds().size() ); } // since ES doesn't support security we will rely on the graph to provide edge filtering // and rely on the DefaultGraphQueryIterable to provide property filtering QueryParameters filterParameters = getParameters().clone(); filterParameters.setSkip(0); // ES already did a skip List<Iterable<? extends VertexiumObject>> items = new ArrayList<>(); Authorizations authorizations = filterParameters.getAuthorizations(); if (ids.getVertexIds().size() > 0) { if (fetchHints.equals(FetchHints.NONE)) { items.add(getElasticsearchVertices(hits, fetchHints, authorizations)); } else { Iterable<? extends VertexiumObject> vertices = getGraph().getVertices(ids.getVertexIds(), fetchHints, authorizations); items.add(vertices); } } if (ids.getEdgeIds().size() > 0) { if (fetchHints.equals(FetchHints.NONE)) { items.add(getElasticsearchEdges(hits, fetchHints, authorizations)); } else { Iterable<? extends VertexiumObject> edges = getGraph().getEdges(ids.getEdgeIds(), fetchHints, authorizations); items.add(edges); } } if (ids.getExtendedDataIds().size() > 0) { Iterable<? extends VertexiumObject> extendedDataRows = getGraph().getExtendedData(ids.getExtendedDataIds(), fetchHints, authorizations); items.add(extendedDataRows); } Iterable<VertexiumObject> vertexiumObjects = new JoinIterable<>(items); List<VertexiumObject> sortedVertexiumObjects = sortVertexiumObjectsByResultOrder(vertexiumObjects, ids.getIds()); // TODO instead of passing false here to not evaluate the query string it would be better to support the Lucene query return createIterable(response, filterParameters, sortedVertexiumObjects, response.getTook().millis(), hits); } private QueryResultsIterable<SearchHit> searchHits(EnumSet<VertexiumObjectType> objectTypes, FetchHints fetchHints) { if (shouldUseScrollApi()) { return searchScrollHits(objectTypes, fetchHints); } return searchPagedHits(objectTypes, fetchHints); } private QueryInfiniteScrollIterable<SearchHit> searchScrollHits(EnumSet<VertexiumObjectType> objectTypes, FetchHints fetchHints) { return new QueryInfiniteScrollIterable<SearchHit>(objectTypes, fetchHints, getParameters().getLimit()) { @Override protected ElasticsearchGraphQueryIterable<SearchHit> searchResponseToIterable(SearchResponse searchResponse) { return ElasticsearchSearchQueryBase.this.searchResponseToSearchHitsIterable(searchResponse); } @Override protected IdStrategy getIdStrategy() { return getSearchIndex().getIdStrategy(); } }; } private PagingIterable<SearchHit> searchPagedHits(EnumSet<VertexiumObjectType> objectTypes, FetchHints fetchHints) { return new PagingIterable<SearchHit>(getParameters().getSkip(), getParameters().getLimit(), pageSize) { @Override protected ElasticsearchGraphQueryIterable<SearchHit> getPageIterable(int skip, int limit, boolean includeAggregations) { SearchResponse response; try { response = getSearchResponse(ElasticsearchDocumentType.fromVertexiumObjectTypes(objectTypes), fetchHints, skip, limit, includeAggregations); } catch (IndexNotFoundException ex) { LOGGER.debug("Index missing: %s (returning empty iterable)", ex.getMessage()); return createEmptyIterable(); } catch (VertexiumNoMatchingPropertiesException ex) { LOGGER.debug("Could not find property: %s (returning empty iterable)", ex.getPropertyName()); return createEmptyIterable(); } return searchResponseToSearchHitsIterable(response); } }; } private ElasticsearchGraphQueryIterable<SearchHit> searchResponseToSearchHitsIterable(SearchResponse response) { SearchHits hits = response.getHits(); QueryParameters filterParameters = getParameters().clone(); Iterable<SearchHit> hitsIterable = IterableUtils.toIterable(hits.getHits()); return createIterable(response, filterParameters, hitsIterable, response.getTook().millis(), hits); } private List<ElasticsearchVertex> getElasticsearchVertices(SearchHits hits, FetchHints fetchHints, Authorizations authorizations) { return stream(hits) .map(hit -> { String elementId = hit.getFields().get(Elasticsearch7SearchIndex.ELEMENT_ID_FIELD_NAME).getValue(); return new ElasticsearchVertex( getGraph(), elementId, fetchHints, authorizations ); }).collect(Collectors.toList()); } private List<ElasticsearchEdge> getElasticsearchEdges(SearchHits hits, FetchHints fetchHints, Authorizations authorizations) { return stream(hits) .map(hit -> { String inVertexId = hit.getFields().get(Elasticsearch7SearchIndex.IN_VERTEX_ID_FIELD_NAME).getValue(); String outVertexId = hit.getFields().get(Elasticsearch7SearchIndex.OUT_VERTEX_ID_FIELD_NAME).getValue(); String label = hit.getFields().get(Elasticsearch7SearchIndex.EDGE_LABEL_FIELD_NAME).getValue(); String elementId = hit.getFields().get(Elasticsearch7SearchIndex.ELEMENT_ID_FIELD_NAME).getValue(); return new ElasticsearchEdge( getGraph(), elementId, label, inVertexId, outVertexId, fetchHints, authorizations ); }).collect(Collectors.toList()); } @Override public QueryResultsIterable<String> vertexIds(EnumSet<IdFetchHint> idFetchHints) { FetchHints fetchHints = idFetchHintsToElementFetchHints(idFetchHints); return new ElasticsearchGraphQueryIdIterable<>(getIdStrategy(), searchHits(EnumSet.of(VertexiumObjectType.VERTEX), fetchHints)); } @Override public QueryResultsIterable<String> edgeIds(EnumSet<IdFetchHint> idFetchHints) { FetchHints fetchHints = idFetchHintsToElementFetchHints(idFetchHints); return new ElasticsearchGraphQueryIdIterable<>(getIdStrategy(), searchHits(EnumSet.of(VertexiumObjectType.EDGE), fetchHints)); } @Override public QueryResultsIterable<ExtendedDataRowId> extendedDataRowIds(EnumSet<IdFetchHint> idFetchHints) { FetchHints fetchHints = idFetchHintsToElementFetchHints(idFetchHints); return new ElasticsearchGraphQueryIdIterable<>(getIdStrategy(), searchHits(EnumSet.of(VertexiumObjectType.EXTENDED_DATA), fetchHints)); } @Override public QueryResultsIterable<String> elementIds(EnumSet<IdFetchHint> idFetchHints) { FetchHints fetchHints = idFetchHintsToElementFetchHints(idFetchHints); return new ElasticsearchGraphQueryIdIterable<>(getIdStrategy(), searchHits(VertexiumObjectType.ELEMENTS, fetchHints)); } private <T extends VertexiumObject> List<T> sortVertexiumObjectsByResultOrder(Iterable<T> vertexiumObjects, List<String> ids) { ImmutableMap<String, T> itemMap = Maps.uniqueIndex(vertexiumObjects, vertexiumObject -> { if (vertexiumObject instanceof Element) { return ((Element) vertexiumObject).getId(); } else if (vertexiumObject instanceof ExtendedDataRow) { return ((ExtendedDataRow) vertexiumObject).getId().toString(); } else { throw new VertexiumException("Unhandled searchable item type: " + vertexiumObject.getClass().getName()); } }); List<T> results = new ArrayList<>(); for (String id : ids) { T item = itemMap.get(id); if (item != null) { results.add(item); } } return results; } private <T> EmptyElasticsearchGraphQueryIterable<T> createEmptyIterable() { return new EmptyElasticsearchGraphQueryIterable<>(ElasticsearchSearchQueryBase.this, getParameters()); } protected <T> ElasticsearchGraphQueryIterable<T> createIterable( SearchResponse response, QueryParameters filterParameters, Iterable<T> vertexiumObjects, long searchTimeInMillis, SearchHits hits ) { return new ElasticsearchGraphQueryIterable<>( this, response, filterParameters, vertexiumObjects, hits.getTotalHits().value, searchTimeInMillis * 1000000, hits ); } private SearchResponse getSearchResponse(EnumSet<ElasticsearchDocumentType> elementType, FetchHints fetchHints, int skip, int limit, boolean includeAggregations) { SearchRequestBuilder q = buildQuery(elementType, fetchHints, includeAggregations) .setFrom(skip) .setSize(limit) .setTrackTotalHits(true); if (QUERY_LOGGER.isTraceEnabled()) { QUERY_LOGGER.trace("query: %s", q); } SearchResponse searchResponse = checkForFailures(q.execute().actionGet()); if (LOGGER.isDebugEnabled()) { SearchHits hits = searchResponse.getHits(); LOGGER.debug( "elasticsearch results %d of %d (time: %dms)", hits.getHits().length, hits.getTotalHits().value, searchResponse.getTook().millis() ); } return searchResponse; } protected QueryBuilder getFilterForHasNotPropertyContainer(HasNotPropertyContainer hasNotProperty) { PropertyDefinition[] propertyDefinitions = StreamSupport.stream(hasNotProperty.getKeys().spliterator(), false) .map(this::getPropertyDefinition) .filter(Objects::nonNull) .toArray(PropertyDefinition[]::new); if (propertyDefinitions.length == 0) { // If we can't find a property this means none of them are defined on the graph return QueryBuilders.matchAllQuery(); } List<QueryBuilder> filters = new ArrayList<>(); for (PropertyDefinition propDef : propertyDefinitions) { String[] propertyNames = getPropertyNames(propDef.getPropertyName()); for (String propertyName : propertyNames) { filters.add(QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery(propertyName))); if (GeoShape.class.isAssignableFrom(propDef.getDataType())) { filters.add(QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery(propertyName + Elasticsearch7SearchIndex.GEO_PROPERTY_NAME_SUFFIX))); } else if (isExactMatchPropertyDefinition(propDef)) { filters.add(QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery(propertyName + Elasticsearch7SearchIndex.EXACT_MATCH_PROPERTY_NAME_SUFFIX))); } } } if (filters.isEmpty()) { // If we didn't add any filters, this means it doesn't exist on any elements so the hasNot query should match all records. return QueryBuilders.matchAllQuery(); } return getSingleFilterOrAndTheFilters(filters, hasNotProperty); } private QueryBuilder getFilterForHasExtendedData(HasExtendedData has) { BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); for (HasExtendedDataFilter hasExtendedDataFilter : has.getFilters()) { boolQuery.should(getFilterForHasExtendedDataFilter(hasExtendedDataFilter)); } boolQuery.minimumShouldMatch(1); return boolQuery; } private QueryBuilder getFilterForHasExtendedDataFilter(HasExtendedDataFilter has) { BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); boolean hasQuery = false; if (has.getElementType() != null) { boolQuery.must( QueryBuilders.termQuery( Elasticsearch7SearchIndex.ELEMENT_TYPE_FIELD_NAME, ElasticsearchDocumentType.getExtendedDataDocumentTypeFromElementType(has.getElementType()).getKey() ) ); hasQuery = true; } if (has.getElementId() != null) { boolQuery.must(QueryBuilders.termQuery(Elasticsearch7SearchIndex.ELEMENT_ID_FIELD_NAME, has.getElementId())); hasQuery = true; } if (has.getTableName() != null) { boolQuery.must(QueryBuilders.termQuery(Elasticsearch7SearchIndex.EXTENDED_DATA_TABLE_NAME_FIELD_NAME, has.getTableName())); hasQuery = true; } if (!hasQuery) { throw new VertexiumException("Cannot include a hasExtendedData clause with all nulls"); } return boolQuery; } protected QueryBuilder getFilterForHasAuthorizationContainer(HasAuthorizationContainer hasAuthorization) { PropertyNameVisibilitiesStore visibilitiesStore = getSearchIndex().getPropertyNameVisibilitiesStore(); Authorizations auths = getParameters().getAuthorizations(); Graph graph = getGraph(); Set<String> hashes = stream(hasAuthorization.getAuthorizations()) .flatMap(authorization -> visibilitiesStore.getHashesWithAuthorization(graph, authorization, auths).stream()) .collect(Collectors.toSet()); List<QueryBuilder> filters = new ArrayList<>(); for (PropertyDefinition propertyDefinition : graph.getPropertyDefinitions()) { String propertyName = propertyDefinition.getPropertyName(); Set<String> matchingPropertyHashes = visibilitiesStore.getHashes(graph, propertyName, auths).stream() .filter(hashes::contains) .collect(Collectors.toSet()); for (String fieldName : getSearchIndex().addHashesToPropertyName(propertyName, matchingPropertyHashes)) { filters.add(QueryBuilders.existsQuery(getSearchIndex().replaceFieldnameDots(fieldName))); } } List<String> internalFields = Arrays.asList( Elasticsearch7SearchIndex.ELEMENT_TYPE_FIELD_NAME, Elasticsearch7SearchIndex.HIDDEN_VERTEX_FIELD_NAME, Elasticsearch7SearchIndex.HIDDEN_PROPERTY_FIELD_NAME ); internalFields.forEach(fieldName -> { Collection<String> fieldHashes = visibilitiesStore.getHashes(graph, fieldName, auths); Collection<String> matchingFieldHashes = fieldHashes.stream().filter(hashes::contains).collect(Collectors.toSet()); for (String fieldNameWithHash : getSearchIndex().addHashesToPropertyName(fieldName, matchingFieldHashes)) { filters.add(QueryBuilders.existsQuery(fieldNameWithHash)); } }); if (filters.isEmpty()) { throw new VertexiumNoMatchingPropertiesException(Joiner.on(", ").join(hasAuthorization.getAuthorizations())); } return getSingleFilterOrOrTheFilters(filters, hasAuthorization); } protected QueryBuilder getFilterForHasPropertyContainer(HasPropertyContainer hasProperty) { PropertyDefinition[] propertyDefinitions = StreamSupport.stream(hasProperty.getKeys().spliterator(), false) .map(this::getPropertyDefinition) .filter(Objects::nonNull) .toArray(PropertyDefinition[]::new); if (propertyDefinitions.length == 0) { // If we didn't find any property definitions, this means none of them are defined on the graph throw new VertexiumNoMatchingPropertiesException(Joiner.on(", ").join(hasProperty.getKeys())); } List<QueryBuilder> filters = new ArrayList<>(); for (PropertyDefinition propDef : propertyDefinitions) { String[] propertyNames = getPropertyNames(propDef.getPropertyName()); for (String propertyName : propertyNames) { filters.add(QueryBuilders.existsQuery(propertyName)); if (GeoShape.class.isAssignableFrom(propDef.getDataType())) { filters.add(QueryBuilders.existsQuery(propertyName + Elasticsearch7SearchIndex.GEO_PROPERTY_NAME_SUFFIX)); } else if (isExactMatchPropertyDefinition(propDef)) { filters.add(QueryBuilders.existsQuery(propertyName + Elasticsearch7SearchIndex.EXACT_MATCH_PROPERTY_NAME_SUFFIX)); } } } if (filters.isEmpty()) { // If we didn't add any filters, this means it doesn't exist on any elements so raise an error throw new VertexiumNoMatchingPropertiesException(Joiner.on(", ").join(hasProperty.getKeys())); } return getSingleFilterOrOrTheFilters(filters, hasProperty); } protected QueryBuilder getFiltersForHasValueContainer(HasValueContainer has) { if (has.predicate instanceof Compare) { return getFilterForComparePredicate((Compare) has.predicate, has); } else if (has.predicate instanceof Contains) { return getFilterForContainsPredicate((Contains) has.predicate, has); } else if (has.predicate instanceof TextPredicate) { return getFilterForTextPredicate((TextPredicate) has.predicate, has); } else if (has.predicate instanceof GeoCompare) { return getFilterForGeoComparePredicate((GeoCompare) has.predicate, has); } else { throw new VertexiumException("Unexpected predicate type " + has.predicate.getClass().getName()); } } protected QueryBuilder getFilterForGeoComparePredicate(GeoCompare compare, HasValueContainer has) { PropertyDefinition[] propertyDefinitions = StreamSupport.stream(has.getKeys().spliterator(), false) .map(this::getPropertyDefinition) .filter(Objects::nonNull) .toArray(PropertyDefinition[]::new); if (propertyDefinitions.length == 0) { // If we didn't find any property definitions, this means none of them are defined on the graph throw new VertexiumNoMatchingPropertiesException(Joiner.on(", ").join(has.getKeys())); } if (!(has.value instanceof GeoShape)) { throw new VertexiumNotSupportedException("GeoCompare searches only accept values of type GeoShape"); } GeoShape value = (GeoShape) has.value; if (value instanceof GeoHash) { value = ((GeoHash) value).toGeoRect(); } List<QueryBuilder> filters = new ArrayList<>(); for (PropertyDefinition propertyDefinition : propertyDefinitions) { if (propertyDefinition != null && !GeoShape.class.isAssignableFrom(propertyDefinition.getDataType())) { throw new VertexiumNotSupportedException("Unable to perform geo query on field of type: " + propertyDefinition.getDataType().getName()); } String[] propertyNames = getPropertyNames(propertyDefinition.getPropertyName()); for (String propertyName : propertyNames) { ShapeRelation relation = ShapeRelation.getRelationByName(compare.getCompareName()); if (propertyDefinition.getDataType() == GeoPoint.class && value instanceof GeoCircle) { GeoCircle geoCircle = (GeoCircle) value; GeoDistanceQueryBuilder geoDistanceQueryBuilder = new GeoDistanceQueryBuilder(propertyName + GEO_POINT_PROPERTY_NAME_SUFFIX) .point(geoCircle.getLatitude(), geoCircle.getLongitude()) .distance(geoCircle.getRadius(), DistanceUnit.KILOMETERS) .ignoreUnmapped(true); if (relation == ShapeRelation.DISJOINT) { BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); filters.add(boolQueryBuilder.mustNot(geoDistanceQueryBuilder)); } else { filters.add(geoDistanceQueryBuilder); } } else { ShapeBuilder shapeBuilder = getShapeBuilder(value); filters.add(new GeoShapeQueryBuilder(propertyName + Elasticsearch7SearchIndex.GEO_PROPERTY_NAME_SUFFIX, shapeBuilder.buildGeometry()) .ignoreUnmapped(true) .relation(relation) ); } } } if (filters.isEmpty()) { // If we didn't add any filters, this means it doesn't exist on any elements so raise an error throw new VertexiumNoMatchingPropertiesException(Joiner.on(", ").join(has.getKeys())); } return getSingleFilterOrOrTheFilters(filters, has); } private ShapeBuilder getShapeBuilder(GeoShape geoShape) { if (geoShape instanceof GeoCircle) { return getCircleBuilder((GeoCircle) geoShape); } else if (geoShape instanceof GeoRect) { return getEnvelopeBuilder((GeoRect) geoShape); } else if (geoShape instanceof GeoCollection) { return getGeometryCollectionBuilder((GeoCollection) geoShape); } else if (geoShape instanceof GeoLine) { return getLineStringBuilder((GeoLine) geoShape); } else if (geoShape instanceof GeoPoint) { return getPointBuilder((GeoPoint) geoShape); } else if (geoShape instanceof GeoPolygon) { return getPolygonBuilder((GeoPolygon) geoShape); } else { throw new VertexiumException("Unexpected has value type " + geoShape.getClass().getName()); } } private GeometryCollectionBuilder getGeometryCollectionBuilder(GeoCollection geoCollection) { GeometryCollectionBuilder shapeBuilder = new GeometryCollectionBuilder(); geoCollection.getGeoShapes().forEach(shape -> shapeBuilder.shape(getShapeBuilder(shape))); return shapeBuilder; } private PointBuilder getPointBuilder(GeoPoint geoPoint) { return new PointBuilder(geoPoint.getLongitude(), geoPoint.getLatitude()); } private ShapeBuilder getCircleBuilder(GeoCircle geoCircle) { // NOTE: as of ES7, storing circles is no longer supported so we need approximate the circle with a polygon double radius = geoCircle.getRadius(); double maxSideLengthKm = getSearchIndex().getConfig().getGeocircleToPolygonSideLength(); maxSideLengthKm = Math.min(radius, maxSideLengthKm); // calculate how many points we need to use given the length of a polygon side int numberOfPoints = (int) Math.ceil(Math.PI / Math.asin((maxSideLengthKm / (2 * radius)))); numberOfPoints = Math.min(numberOfPoints, getSearchIndex().getConfig().getGeocircleToPolygonMaxNumSides()); // Given the number of sides, loop through slices of 360 degrees and calculate the lat/lon at that radius and heading SpatialContext spatialContext = SpatialContext.GEO; DistanceCalculator distanceCalculator = spatialContext.getDistCalc(); Point centerPoint = spatialContext.getShapeFactory().pointXY(DistanceUtils.normLonDEG(geoCircle.getLongitude()), DistanceUtils.normLatDEG(geoCircle.getLatitude())); ArrayList<GeoPoint> points = new ArrayList<>(); for (float angle = 360; angle > 0; angle -= 360.0 / numberOfPoints) { Point point = distanceCalculator.pointOnBearing(centerPoint, geoCircle.getRadius() * KM_TO_DEG, angle, spatialContext, null); points.add(new GeoPoint(point.getY(), point.getX())); } // Polygons must start/end at the same point, so add the first point onto the end points.add(points.get(0)); return getPolygonBuilder(new GeoPolygon(points, geoCircle.getDescription())); } private EnvelopeBuilder getEnvelopeBuilder(GeoRect geoRect) { Coordinate topLeft = new Coordinate(geoRect.getNorthWest().getLongitude(), geoRect.getNorthWest().getLatitude()); Coordinate bottomRight = new Coordinate(geoRect.getSouthEast().getLongitude(), geoRect.getSouthEast().getLatitude()); return new EnvelopeBuilder(topLeft, bottomRight); } private LineStringBuilder getLineStringBuilder(GeoLine geoLine) { List<Coordinate> coordinates = geoLine.getGeoPoints().stream() .map(geoPoint -> new Coordinate(geoPoint.getLongitude(), geoPoint.getLatitude())) .collect(Collectors.toList()); return new LineStringBuilder(coordinates); } private PolygonBuilder getPolygonBuilder(GeoPolygon geoPolygon) { CoordinatesBuilder coordinatesBuilder = new CoordinatesBuilder(); geoPolygon.getOuterBoundary().stream() .map(geoPoint -> new Coordinate(geoPoint.getLongitude(), geoPoint.getLatitude())) .forEach(coordinatesBuilder::coordinate); PolygonBuilder polygonBuilder = new PolygonBuilder(coordinatesBuilder); geoPolygon.getHoles().forEach(hole -> { List<Coordinate> coordinates = hole.stream() .map(geoPoint -> new Coordinate(geoPoint.getLongitude(), geoPoint.getLatitude())) .collect(Collectors.toList()); polygonBuilder.hole(new LineStringBuilder(coordinates)); }); return polygonBuilder; } private QueryBuilder getSingleFilterOrOrTheFilters(List<QueryBuilder> filters, HasContainer has) { if (filters.size() > 1) { BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); for (QueryBuilder filter : filters) { boolQuery.should(filter); } boolQuery.minimumShouldMatch(1); return boolQuery; } else if (filters.size() == 1) { return filters.get(0); } else { throw new VertexiumException("Unexpected filter count, expected at least 1 filter for: " + has); } } private QueryBuilder getSingleFilterOrAndTheFilters(List<QueryBuilder> filters, HasContainer has) { if (filters.size() > 1) { BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); for (QueryBuilder filter : filters) { boolQuery.must(filter); } return boolQuery; } else if (filters.size() == 1) { return filters.get(0); } else { throw new VertexiumException("Unexpected filter count, expected at least 1 filter for: " + has); } } protected QueryBuilder getFilterForTextPredicate(TextPredicate compare, HasValueContainer has) { String[] propertyNames = StreamSupport.stream(has.getKeys().spliterator(), false) .flatMap(key -> Arrays.stream(getPropertyNames(key))) .toArray(String[]::new); if (propertyNames.length == 0) { throw new VertexiumNoMatchingPropertiesException(Joiner.on(", ").join(has.getKeys())); } Object value = has.value; if (value instanceof String) { value = ((String) value).toLowerCase(); // using the standard analyzer all strings are lower-cased. } List<QueryBuilder> filters = new ArrayList<>(); for (String propertyName : propertyNames) { switch (compare) { case CONTAINS: if (value instanceof String) { String[] terms = splitStringIntoTerms((String) value); BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); for (String term : terms) { boolQueryBuilder.must(QueryBuilders.termQuery(propertyName, term)); } filters.add(boolQueryBuilder); } else { filters.add(QueryBuilders.termQuery(propertyName, value)); } break; case DOES_NOT_CONTAIN: BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); if (value instanceof String) { String[] terms = splitStringIntoTerms((String) value); filters.add(boolQueryBuilder.mustNot(QueryBuilders.termsQuery(propertyName, terms))); } else { filters.add(boolQueryBuilder.mustNot(QueryBuilders.termQuery(propertyName, value))); } break; default: throw new VertexiumException("Unexpected text predicate " + has.predicate); } } if (compare.equals(TextPredicate.DOES_NOT_CONTAIN)) { return getSingleFilterOrAndTheFilters(filters, has); } return getSingleFilterOrOrTheFilters(filters, has); } protected QueryBuilder getFilterForContainsPredicate(Contains contains, HasValueContainer has) { String[] propertyNames = StreamSupport.stream(has.getKeys().spliterator(), false) .flatMap(key -> Arrays.stream(getPropertyNames(key))) .toArray(String[]::new); if (propertyNames.length == 0) { if (contains.equals(Contains.NOT_IN)) { return QueryBuilders.matchAllQuery(); } throw new VertexiumNoMatchingPropertiesException(Joiner.on(", ").join(has.getKeys())); } Object value = has.value; if (value instanceof Iterable) { value = IterableUtils.toArray((Iterable<?>) value, Object.class); } List<QueryBuilder> filters = new ArrayList<>(); for (String propertyName : propertyNames) { filters.add(getFilterForProperty(contains, has, propertyName, value)); } if (contains == Contains.NOT_IN) { return getSingleFilterOrAndTheFilters(filters, has); } return getSingleFilterOrOrTheFilters(filters, has); } private QueryBuilder getFilterForProperty(Contains contains, HasValueContainer has, String propertyName, Object value) { if (Element.ID_PROPERTY_NAME.equals(propertyName)) { propertyName = Elasticsearch7SearchIndex.ELEMENT_ID_FIELD_NAME; } else if (Edge.LABEL_PROPERTY_NAME.equals(propertyName)) { propertyName = Elasticsearch7SearchIndex.EDGE_LABEL_FIELD_NAME; } else if (Edge.OUT_VERTEX_ID_PROPERTY_NAME.equals(propertyName)) { propertyName = Elasticsearch7SearchIndex.OUT_VERTEX_ID_FIELD_NAME; } else if (Edge.IN_VERTEX_ID_PROPERTY_NAME.equals(propertyName)) { propertyName = Elasticsearch7SearchIndex.IN_VERTEX_ID_FIELD_NAME; } else if (Edge.IN_OR_OUT_VERTEX_ID_PROPERTY_NAME.equals(propertyName)) { return QueryBuilders.boolQuery() .should(getFilterForProperty(contains, has, Edge.OUT_VERTEX_ID_PROPERTY_NAME, value)) .should(getFilterForProperty(contains, has, Edge.IN_VERTEX_ID_PROPERTY_NAME, value)) .minimumShouldMatch(1); } else if (ExtendedDataRow.TABLE_NAME.equals(propertyName)) { propertyName = Elasticsearch7SearchIndex.EXTENDED_DATA_TABLE_NAME_FIELD_NAME; } else if (ExtendedDataRow.ROW_ID.equals(propertyName)) { propertyName = Elasticsearch7SearchIndex.EXTENDED_DATA_TABLE_ROW_ID_FIELD_NAME; } else if (ExtendedDataRow.ELEMENT_ID.equals(propertyName)) { propertyName = Elasticsearch7SearchIndex.ELEMENT_ID_FIELD_NAME; } else if (ExtendedDataRow.ELEMENT_TYPE.equals(propertyName)) { propertyName = Elasticsearch7SearchIndex.ELEMENT_TYPE_FIELD_NAME; value = ElasticsearchDocumentType.getExtendedDataDocumentTypeFromElementType(ElementType.parse(value)).getKey(); } else if (value instanceof String || value instanceof String[] || (value instanceof Object[] && ((Object[]) value).length > 0 && ((Object[]) value)[0] instanceof String) ) { propertyName = propertyName + Elasticsearch7SearchIndex.EXACT_MATCH_PROPERTY_NAME_SUFFIX; } switch (contains) { case IN: return QueryBuilders.termsQuery(propertyName, (Object[]) value); case NOT_IN: return QueryBuilders.boolQuery().mustNot(QueryBuilders.termsQuery(propertyName, (Object[]) value)); default: throw new VertexiumException("Unexpected Contains predicate " + has.predicate); } } protected QueryBuilder getFilterForComparePredicate(Compare compare, HasValueContainer has) { String[] propertyNames = StreamSupport.stream(has.getKeys().spliterator(), false) .flatMap(key -> Arrays.stream(getPropertyNames(key))) .toArray(String[]::new); if (propertyNames.length == 0) { if (compare.equals(Compare.NOT_EQUAL)) { return QueryBuilders.matchAllQuery(); } throw new VertexiumNoMatchingPropertiesException(Joiner.on(", ").join(has.getKeys())); } Object value = convertQueryValue(has.value); List<QueryBuilder> filters = new ArrayList<>(); for (String propertyName : propertyNames) { filters.add(getFilterForProperty(compare, has, propertyName, value)); } if (compare == Compare.NOT_EQUAL) { return getSingleFilterOrAndTheFilters(filters, has); } return getSingleFilterOrOrTheFilters(filters, has); } private QueryBuilder getFilterForProperty(Compare compare, HasValueContainer has, String propertyName, Object value) { if (Element.ID_PROPERTY_NAME.equals(propertyName)) { propertyName = Elasticsearch7SearchIndex.ELEMENT_ID_FIELD_NAME; } else if (Edge.LABEL_PROPERTY_NAME.equals(propertyName)) { propertyName = Elasticsearch7SearchIndex.EDGE_LABEL_FIELD_NAME; } else if (Edge.OUT_VERTEX_ID_PROPERTY_NAME.equals(propertyName)) { propertyName = Elasticsearch7SearchIndex.OUT_VERTEX_ID_FIELD_NAME; } else if (Edge.IN_VERTEX_ID_PROPERTY_NAME.equals(propertyName)) { propertyName = Elasticsearch7SearchIndex.IN_VERTEX_ID_FIELD_NAME; } else if (Edge.IN_OR_OUT_VERTEX_ID_PROPERTY_NAME.equals(propertyName)) { return QueryBuilders.boolQuery() .should(getFilterForProperty(compare, has, Edge.OUT_VERTEX_ID_PROPERTY_NAME, value)) .should(getFilterForProperty(compare, has, Edge.IN_VERTEX_ID_PROPERTY_NAME, value)) .minimumShouldMatch(1); } else if (ExtendedDataRow.TABLE_NAME.equals(propertyName)) { propertyName = Elasticsearch7SearchIndex.EXTENDED_DATA_TABLE_NAME_FIELD_NAME; } else if (ExtendedDataRow.ROW_ID.equals(propertyName)) { propertyName = Elasticsearch7SearchIndex.EXTENDED_DATA_TABLE_ROW_ID_FIELD_NAME; } else if (ExtendedDataRow.ELEMENT_ID.equals(propertyName)) { propertyName = Elasticsearch7SearchIndex.ELEMENT_ID_FIELD_NAME; } else if (ExtendedDataRow.ELEMENT_TYPE.equals(propertyName)) { propertyName = Elasticsearch7SearchIndex.ELEMENT_TYPE_FIELD_NAME; value = ElasticsearchDocumentType.getExtendedDataDocumentTypeFromElementType(ElementType.parse(value)).getKey(); } else if (has.value instanceof IpV4Address) { // this value is converted to a string and should not use the exact match field } else if (value instanceof String || value instanceof String[]) { propertyName = propertyName + Elasticsearch7SearchIndex.EXACT_MATCH_PROPERTY_NAME_SUFFIX; } switch (compare) { case EQUAL: if (has.value instanceof DateOnly) { ZonedDateTime lower = (ZonedDateTime) value; ZonedDateTime upper = lower.plusDays(1).minus(1, ChronoUnit.MILLIS); return QueryBuilders.rangeQuery(propertyName).gte(lower).lte(upper); } else { return QueryBuilders.termQuery(propertyName, value); } case GREATER_THAN_EQUAL: return QueryBuilders.rangeQuery(propertyName).gte(value); case GREATER_THAN: if (has.value instanceof DateOnly) { value = ((ZonedDateTime) value).plusDays(1).minus(1, ChronoUnit.MILLIS); } return QueryBuilders.rangeQuery(propertyName).gt(value); case LESS_THAN_EQUAL: if (has.value instanceof DateOnly) { value = ((ZonedDateTime) value).plusDays(1).minus(1, ChronoUnit.MILLIS); } return QueryBuilders.rangeQuery(propertyName).lte(value); case LESS_THAN: return QueryBuilders.rangeQuery(propertyName).lt(value); case NOT_EQUAL: return QueryBuilders.boolQuery().mustNot(QueryBuilders.termQuery(propertyName, value)); case STARTS_WITH: if (!(value instanceof String)) { throw new VertexiumException("STARTS_WITH may only be used to query String values"); } return QueryBuilders.prefixQuery(propertyName, (String) value); case RANGE: if (!(value instanceof Range)) { throw new VertexiumException("RANGE may only be used to query Range values"); } Range range = (Range) value; Object startValue = convertQueryValue(range.getStart()); Object endValue = convertQueryValue(range.getEnd()); if (range.getEnd() instanceof DateOnly) { endValue = ((ZonedDateTime) endValue).plusDays(1).minus(1, ChronoUnit.MILLIS); } if (startValue instanceof String || endValue instanceof String) { propertyName = propertyName + Elasticsearch7SearchIndex.EXACT_MATCH_PROPERTY_NAME_SUFFIX; } RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery(propertyName); if (startValue != null) { if (range.isInclusiveStart()) { rangeQueryBuilder = rangeQueryBuilder.gte(startValue); } else { rangeQueryBuilder = rangeQueryBuilder.gt(startValue); } } if (endValue != null) { if (range.isInclusiveEnd()) { rangeQueryBuilder = rangeQueryBuilder.lte(endValue); } else { rangeQueryBuilder = rangeQueryBuilder.lt(endValue); } } return rangeQueryBuilder; default: throw new VertexiumException("Unexpected Compare predicate " + has.predicate); } } private Object convertQueryValue(Object value) { if (value instanceof DateOnly) { value = ((DateOnly) value).getUtcDate(); } if (value instanceof Date) { return ZonedDateTime.ofInstant(((Date) value).toInstant(), ZoneOffset.UTC); } if (value instanceof BigInteger) { return ((BigInteger) value).intValue(); } if (value instanceof BigDecimal) { return ((BigDecimal) value).doubleValue(); } if (value instanceof IpV4Address) { return value.toString(); } return value; } protected String[] getPropertyNames(String propertyName) { return getSearchIndex().getPropertyNames(getGraph(), propertyName, getParameters().getAuthorizations()); } public Elasticsearch7SearchIndex getSearchIndex() { return (Elasticsearch7SearchIndex) ((GraphWithSearchIndex) getGraph()).getSearchIndex(); } protected void addElementTypeFilter(List<QueryBuilder> filters, EnumSet<ElasticsearchDocumentType> elementType) { if (elementType != null) { filters.add(createElementTypeFilter(elementType)); } } protected TermsQueryBuilder createElementTypeFilter(EnumSet<ElasticsearchDocumentType> elementType) { List<String> values = new ArrayList<>(); for (ElasticsearchDocumentType et : elementType) { values.add(et.getKey()); } return QueryBuilders.termsQuery( Elasticsearch7SearchIndex.ELEMENT_TYPE_FIELD_NAME, values.toArray(new String[values.size()]) ); } protected QueryBuilder getFilterBuilder(List<QueryBuilder> filters, FetchHints fetchHints) { BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); for (QueryBuilder filter : filters) { boolQuery.must(filter); } if (!fetchHints.isIgnoreAdditionalVisibilities()) { boolQuery.must(getAdditionalVisibilitiesFilter()); } return boolQuery; } private QueryBuilder getAdditionalVisibilitiesFilter() { return getSearchIndex().getAdditionalVisibilitiesFilter(getParameters().getAuthorizations()); } private String[] splitStringIntoTerms(String value) { try { List<String> results = new ArrayList<>(); try (TokenStream tokens = analyzer.tokenStream("", value)) { CharTermAttribute term = tokens.getAttribute(CharTermAttribute.class); tokens.reset(); while (tokens.incrementToken()) { String t = term.toString().trim(); if (t.length() > 0) { results.add(t); } } } return results.toArray(new String[results.size()]); } catch (IOException e) { throw new VertexiumException("Could not tokenize string: " + value, e); } } protected QueryBuilder createQuery(QueryParameters queryParameters) { QueryBuilder query; if (queryParameters instanceof QueryStringQueryParameters) { query = createQueryStringQuery((QueryStringQueryParameters) queryParameters); } else if (queryParameters instanceof SimilarToTextQueryParameters) { query = createSimilarToTextQuery((SimilarToTextQueryParameters) queryParameters); } else { throw new VertexiumException("Query parameters not supported of type: " + queryParameters.getClass().getName()); } ScoringStrategy scoringStrategy = queryParameters.getScoringStrategy(); if (scoringStrategy != null) { if (!(scoringStrategy instanceof ElasticsearchScoringStrategy)) { throw new VertexiumException("scoring strategies must implement " + ElasticsearchScoringStrategy.class.getName() + " to work with Elasticsearch"); } query = ((ElasticsearchScoringStrategy) scoringStrategy).updateElasticsearchQuery( getGraph(), getSearchIndex(), query, queryParameters ); } return query; } protected QueryBuilder createSimilarToTextQuery(SimilarToTextQueryParameters similarTo) { List<String> allFields = new ArrayList<>(); String[] fields = similarTo.getFields(); for (String field : fields) { Collections.addAll(allFields, getPropertyNames(field)); } MoreLikeThisQueryBuilder q = QueryBuilders.moreLikeThisQuery( allFields.toArray(new String[allFields.size()]), new String[]{similarTo.getText()}, null ); if (similarTo.getMinTermFrequency() != null) { q.minTermFreq(similarTo.getMinTermFrequency()); } if (similarTo.getMaxQueryTerms() != null) { q.maxQueryTerms(similarTo.getMaxQueryTerms()); } if (similarTo.getMinDocFrequency() != null) { q.minDocFreq(similarTo.getMinDocFrequency()); } if (similarTo.getMaxDocFrequency() != null) { q.maxDocFreq(similarTo.getMaxDocFrequency()); } if (similarTo.getBoost() != null) { q.boost(similarTo.getBoost()); } return q; } public Client getClient() { return client; } protected List<AggregationBuilder> getElasticsearchAggregations(Iterable<Aggregation> aggregations) { List<AggregationBuilder> aggs = new ArrayList<>(); for (Aggregation agg : aggregations) { if (agg instanceof HistogramAggregation) { aggs.addAll(getElasticsearchHistogramAggregations((HistogramAggregation) agg)); } else if (agg instanceof RangeAggregation) { aggs.addAll(getElasticsearchRangeAggregations((RangeAggregation) agg)); } else if (agg instanceof PercentilesAggregation) { aggs.addAll(getElasticsearchPercentilesAggregations((PercentilesAggregation) agg)); } else if (agg instanceof TermsAggregation) { aggs.addAll(getElasticsearchTermsAggregations((TermsAggregation) agg)); } else if (agg instanceof GeohashAggregation) { aggs.addAll(getElasticsearchGeohashAggregations((GeohashAggregation) agg)); } else if (agg instanceof StatisticsAggregation) { aggs.addAll(getElasticsearchStatisticsAggregations((StatisticsAggregation) agg)); } else if (agg instanceof CalendarFieldAggregation) { aggs.addAll(getElasticsearchCalendarFieldAggregation((CalendarFieldAggregation) agg)); } else if (agg instanceof CardinalityAggregation) { aggs.addAll(getElasticsearchCardinalityAggregations((CardinalityAggregation) agg)); } else { throw new VertexiumException("Could not add aggregation of type: " + agg.getClass().getName()); } } return aggs; } protected List<AggregationBuilder> getElasticsearchGeohashAggregations(GeohashAggregation agg) { List<AggregationBuilder> aggs = new ArrayList<>(); PropertyDefinition propertyDefinition = getPropertyDefinition(agg.getFieldName()); if (propertyDefinition == null) { throw new VertexiumException("Unknown property " + agg.getFieldName() + " for geohash aggregation."); } if (propertyDefinition.getDataType() != GeoPoint.class) { throw new VertexiumNotSupportedException("Only GeoPoint properties are valid for Geohash aggregation. Invalid property " + agg.getFieldName()); } for (String propertyName : getPropertyNames(agg.getFieldName())) { String visibilityHash = getSearchIndex().getPropertyVisibilityHashFromPropertyName(propertyName); String aggName = createAggregationName(agg.getAggregationName(), visibilityHash); GeoGridAggregationBuilder geoHashAgg = AggregationBuilders.geohashGrid(aggName); geoHashAgg.field(propertyName + Elasticsearch7SearchIndex.GEO_POINT_PROPERTY_NAME_SUFFIX); geoHashAgg.precision(agg.getPrecision()); aggs.add(geoHashAgg); } return aggs; } protected List<AbstractAggregationBuilder> getElasticsearchStatisticsAggregations(StatisticsAggregation agg) { List<AbstractAggregationBuilder> aggs = new ArrayList<>(); for (String propertyName : getPropertyNames(agg.getFieldName())) { String visibilityHash = getSearchIndex().getPropertyVisibilityHashFromPropertyName(propertyName); String aggName = createAggregationName(agg.getAggregationName(), visibilityHash); ExtendedStatsAggregationBuilder statsAgg = AggregationBuilders.extendedStats(aggName); statsAgg.field(propertyName); aggs.add(statsAgg); } return aggs; } protected List<AbstractAggregationBuilder> getElasticsearchPercentilesAggregations(PercentilesAggregation agg) { String propertyName = getSearchIndex().addVisibilityToPropertyName(getGraph(), agg.getFieldName(), agg.getVisibility()); String visibilityHash = getSearchIndex().getPropertyVisibilityHashFromPropertyName(propertyName); String aggName = createAggregationName(agg.getAggregationName(), visibilityHash); PercentilesAggregationBuilder percentilesAgg = AggregationBuilders.percentiles(aggName); percentilesAgg.field(propertyName); if (agg.getPercents() != null && agg.getPercents().length > 0) { percentilesAgg.percentiles(agg.getPercents()); } return Collections.singletonList(percentilesAgg); } private String createAggregationName(String aggName, String visibilityHash) { if (visibilityHash != null && visibilityHash.length() > 0) { return aggName + "_" + visibilityHash; } return aggName; } protected List<AggregationBuilder> getElasticsearchCardinalityAggregations(CardinalityAggregation agg) { List<AggregationBuilder> cardinalityAggs = new ArrayList<>(); String fieldName = agg.getPropertyName(); if (Element.ID_PROPERTY_NAME.equals(fieldName) || Edge.LABEL_PROPERTY_NAME.equals(fieldName) || Edge.OUT_VERTEX_ID_PROPERTY_NAME.equals(fieldName) || Edge.IN_VERTEX_ID_PROPERTY_NAME.equals(fieldName) || ExtendedDataRow.TABLE_NAME.equals(fieldName) || ExtendedDataRow.ROW_ID.equals(fieldName) || ExtendedDataRow.ELEMENT_ID.equals(fieldName) || ExtendedDataRow.ELEMENT_TYPE.equals(fieldName)) { Map<String, Object> metadata = new HashMap<>(); metadata.put(AGGREGATION_METADATA_FIELD_NAME_KEY, fieldName); if (Element.ID_PROPERTY_NAME.equals(fieldName) || ExtendedDataRow.ELEMENT_ID.equals(fieldName)) { fieldName = ELEMENT_ID_FIELD_NAME; } else if (ExtendedDataRow.ELEMENT_TYPE.equals(fieldName)) { fieldName = ELEMENT_TYPE_FIELD_NAME; } String aggregationName = createAggregationName(agg.getAggregationName(), "0"); CardinalityAggregationBuilder cardinalityAgg = AggregationBuilders.cardinality(aggregationName); cardinalityAgg.setMetaData(metadata); cardinalityAgg.field(fieldName); cardinalityAggs.add(cardinalityAgg); } else { throw new VertexiumException("Cannot use cardinality aggregation on properties with visibility: " + fieldName); } return cardinalityAggs; } protected List<AggregationBuilder> getElasticsearchTermsAggregations(TermsAggregation agg) { List<AggregationBuilder> termsAggs = new ArrayList<>(); String fieldName = agg.getPropertyName(); if (Edge.IN_OR_OUT_VERTEX_ID_PROPERTY_NAME.equals(fieldName)) { throw new VertexiumException("Cannot aggregate by: " + fieldName); } if (Element.ID_PROPERTY_NAME.equals(fieldName) || Edge.LABEL_PROPERTY_NAME.equals(fieldName) || Edge.OUT_VERTEX_ID_PROPERTY_NAME.equals(fieldName) || Edge.IN_VERTEX_ID_PROPERTY_NAME.equals(fieldName) || ExtendedDataRow.TABLE_NAME.equals(fieldName) || ExtendedDataRow.ROW_ID.equals(fieldName) || ExtendedDataRow.ELEMENT_ID.equals(fieldName) || ExtendedDataRow.ELEMENT_TYPE.equals(fieldName)) { Map<String, Object> metadata = new HashMap<>(); metadata.put(AGGREGATION_METADATA_FIELD_NAME_KEY, fieldName); if (Element.ID_PROPERTY_NAME.equals(fieldName) || ExtendedDataRow.ELEMENT_ID.equals(fieldName)) { fieldName = ELEMENT_ID_FIELD_NAME; } else if (ExtendedDataRow.ELEMENT_TYPE.equals(fieldName)) { fieldName = ELEMENT_TYPE_FIELD_NAME; } TermsAggregationBuilder termsAgg = AggregationBuilders.terms(createAggregationName(agg.getAggregationName(), "0")); termsAgg.setMetaData(metadata); termsAgg.field(fieldName); if (agg.getSize() != null) { termsAgg.size(agg.getSize()); } termsAgg.shardSize(termAggregationShardSize); for (AggregationBuilder subAgg : getElasticsearchAggregations(agg.getNestedAggregations())) { termsAgg.subAggregation(subAgg); } termsAggs.add(termsAgg); } else { PropertyDefinition propertyDefinition = getPropertyDefinition(fieldName); for (String propertyName : getPropertyNames(fieldName)) { boolean exactMatchProperty = isExactMatchPropertyDefinition(propertyDefinition); String propertyNameWithSuffix; if (exactMatchProperty) { propertyNameWithSuffix = propertyName + Elasticsearch7SearchIndex.EXACT_MATCH_PROPERTY_NAME_SUFFIX; } else { propertyNameWithSuffix = propertyName; } String visibilityHash = getSearchIndex().getPropertyVisibilityHashFromPropertyName(propertyNameWithSuffix); String aggregationName = createAggregationName(agg.getAggregationName(), visibilityHash); TermsAggregationBuilder termsAgg = AggregationBuilders.terms(aggregationName); termsAgg.field(propertyNameWithSuffix); if (agg.getSize() != null) { termsAgg.size(agg.getSize()); } termsAgg.shardSize(termAggregationShardSize); if (exactMatchProperty && propertyDefinition.getTextIndexHints().contains(TextIndexHint.FULL_TEXT)) { termsAgg.subAggregation( AggregationBuilders.topHits(TOP_HITS_AGGREGATION_NAME) .fetchSource(new String[]{propertyName}, new String[0]) .size(1) ); } for (AggregationBuilder subAgg : getElasticsearchAggregations(agg.getNestedAggregations())) { termsAgg.subAggregation(subAgg); } termsAggs.add(termsAgg); } if (agg.isIncludeHasNotCount()) { QueryBuilder hasNotQuery = getFilterForHasNotPropertyContainer(new HasNotPropertyContainer(fieldName)); String aggregationName = createAggregationName(agg.getAggregationName(), AGGREGATION_HAS_NOT_SUFFIX); FilterAggregationBuilder filterAgg = AggregationBuilders.filter(aggregationName, hasNotQuery); termsAggs.add(filterAgg); } } return termsAggs; } private boolean isExactMatchPropertyDefinition(PropertyDefinition propertyDefinition) { return propertyDefinition != null && propertyDefinition.getDataType().equals(String.class) && propertyDefinition.getTextIndexHints().contains(TextIndexHint.EXACT_MATCH); } private Collection<? extends AggregationBuilder> getElasticsearchCalendarFieldAggregation(CalendarFieldAggregation agg) { List<AggregationBuilder> aggs = new ArrayList<>(); PropertyDefinition propertyDefinition = getPropertyDefinition(agg.getPropertyName()); if (propertyDefinition == null) { throw new VertexiumException("Could not find mapping for property: " + agg.getPropertyName()); } Class propertyDataType = propertyDefinition.getDataType(); for (String propertyName : getPropertyNames(agg.getPropertyName())) { String visibilityHash = getSearchIndex().getPropertyVisibilityHashFromPropertyName(propertyName); String aggName = createAggregationName(agg.getAggregationName(), visibilityHash); if (propertyDataType == Date.class) { HistogramAggregationBuilder histAgg = AggregationBuilders.histogram(aggName); histAgg.interval(1); if (agg.getMinDocumentCount() != null) { histAgg.minDocCount(agg.getMinDocumentCount()); } else { histAgg.minDocCount(1L); } Script script = new Script( ScriptType.INLINE, "painless", getCalendarFieldAggregationScript(agg, propertyName), ImmutableMap.of( "tzId", agg.getTimeZone().getID(), "fieldName", propertyName, "calendarField", agg.getCalendarField()) ); histAgg.script(script); for (AggregationBuilder subAgg : getElasticsearchAggregations(agg.getNestedAggregations())) { histAgg.subAggregation(subAgg); } aggs.add(histAgg); } else { throw new VertexiumException("Only dates are supported for hour of day aggregations"); } } return aggs; } private String getCalendarFieldAggregationScript(CalendarFieldAggregation agg, String propertyName) { String prefix = "def d = doc[params.fieldName]; ZoneId zone = ZoneId.of(params.tzId); " + "if (d == null || d.size() == 0) { return -1; } " + "d = Instant.ofEpochMilli(d.value.millis).atZone(zone);"; switch (agg.getCalendarField()) { case Calendar.DAY_OF_MONTH: return prefix + "return d.getDayOfMonth();"; case Calendar.DAY_OF_WEEK: return prefix + "d = d.getDayOfWeek().getValue() + 1; return d > 7 ? d - 7 : d;"; case Calendar.HOUR_OF_DAY: return prefix + "return d.getHour();"; case Calendar.MONTH: return prefix + "return d.getMonthValue() - 1;"; case Calendar.YEAR: return prefix + "d.getYear();"; default: LOGGER.warn("Slow operation toGregorianCalendar() for calendar field: %d", agg.getCalendarField()); return prefix + "return GregorianCalendar.from(d).get(params.calendarField);"; } } protected List<AggregationBuilder> getElasticsearchHistogramAggregations(HistogramAggregation agg) { List<AggregationBuilder> aggs = new ArrayList<>(); PropertyDefinition propertyDefinition = getPropertyDefinition(agg.getFieldName()); if (propertyDefinition == null) { throw new VertexiumException("Could not find mapping for property: " + agg.getFieldName()); } Class propertyDataType = propertyDefinition.getDataType(); for (String propertyName : getPropertyNames(agg.getFieldName())) { String visibilityHash = getSearchIndex().getPropertyVisibilityHashFromPropertyName(propertyName); String aggName = createAggregationName(agg.getAggregationName(), visibilityHash); if (propertyDataType == Date.class) { DateHistogramAggregationBuilder dateAgg = AggregationBuilders.dateHistogram(aggName); dateAgg.field(propertyName); String interval = agg.getInterval(); if (Pattern.matches("^[0-9\\.]+$", interval)) { interval += "ms"; } DateHistogramInterval histogramInterval = new DateHistogramInterval(interval); try { TimeValue.parseTimeValue(histogramInterval.toString(), null, getClass().getSimpleName() + ".interval"); dateAgg.fixedInterval(histogramInterval); } catch (IllegalArgumentException e) { dateAgg.calendarInterval(histogramInterval); } dateAgg.minDocCount(1L); if (agg.getMinDocumentCount() != null) { dateAgg.minDocCount(agg.getMinDocumentCount()); } if (agg.getExtendedBounds() != null) { HistogramAggregation.ExtendedBounds<?> bounds = agg.getExtendedBounds(); if (bounds.getMinMaxType().isAssignableFrom(Long.class)) { dateAgg.extendedBounds(new ExtendedBounds((Long) bounds.getMin(), (Long) bounds.getMax())); } else if (bounds.getMinMaxType().isAssignableFrom(Date.class)) { dateAgg.extendedBounds(new ExtendedBounds( ((Date) bounds.getMin()).getTime(), ((Date) bounds.getMax()).getTime() )); } else if (bounds.getMinMaxType().isAssignableFrom(String.class)) { dateAgg.extendedBounds(new ExtendedBounds((String) bounds.getMin(), (String) bounds.getMax())); } else { throw new VertexiumException("Unhandled extended bounds type. Expected Long, String, or Date. Found: " + bounds.getMinMaxType().getName()); } } for (AggregationBuilder subAgg : getElasticsearchAggregations(agg.getNestedAggregations())) { dateAgg.subAggregation(subAgg); } aggs.add(dateAgg); } else { HistogramAggregationBuilder histogramAgg = AggregationBuilders.histogram(aggName); histogramAgg.field(propertyName); histogramAgg.interval(Double.parseDouble(agg.getInterval())); histogramAgg.minDocCount(1L); if (agg.getMinDocumentCount() != null) { histogramAgg.minDocCount(agg.getMinDocumentCount()); } if (agg.getExtendedBounds() != null) { HistogramAggregation.ExtendedBounds<?> bounds = agg.getExtendedBounds(); if (bounds.getMinMaxType().isAssignableFrom(Long.class)) { histogramAgg.extendedBounds((Long) bounds.getMin(), (Long) bounds.getMax()); } else if (bounds.getMinMaxType().isAssignableFrom(Double.class)) { histogramAgg.extendedBounds((Double) bounds.getMin(), (Double) bounds.getMax()); } else { throw new VertexiumException("Unhandled extended bounds type. Expected Double or Long. Found: " + bounds.getMinMaxType().getName()); } } for (AggregationBuilder subAgg : getElasticsearchAggregations(agg.getNestedAggregations())) { histogramAgg.subAggregation(subAgg); } aggs.add(histogramAgg); } } return aggs; } protected List<AggregationBuilder> getElasticsearchRangeAggregations(RangeAggregation agg) { List<AggregationBuilder> aggs = new ArrayList<>(); PropertyDefinition propertyDefinition = getPropertyDefinition(agg.getFieldName()); if (propertyDefinition == null) { throw new VertexiumException("Could not find mapping for property: " + agg.getFieldName()); } Class propertyDataType = propertyDefinition.getDataType(); for (String propertyName : getPropertyNames(agg.getFieldName())) { String visibilityHash = getSearchIndex().getPropertyVisibilityHashFromPropertyName(propertyName); String aggName = createAggregationName(agg.getAggregationName(), visibilityHash); if (propertyDataType == Date.class) { DateRangeAggregationBuilder dateRangeBuilder = AggregationBuilders.dateRange(aggName); dateRangeBuilder.field(propertyName); if (!Strings.isNullOrEmpty(agg.getFormat())) { dateRangeBuilder.format(agg.getFormat()); } for (RangeAggregation.Range range : agg.getRanges()) { applyRange(dateRangeBuilder, range); } for (AggregationBuilder subAgg : getElasticsearchAggregations(agg.getNestedAggregations())) { dateRangeBuilder.subAggregation(subAgg); } aggs.add(dateRangeBuilder); } else { RangeAggregationBuilder rangeBuilder = AggregationBuilders.range(aggName); rangeBuilder.field(propertyName); if (!Strings.isNullOrEmpty(agg.getFormat())) { throw new VertexiumException("Invalid use of format for property: " + agg.getFieldName() + ". Format is only valid for date properties"); } for (RangeAggregation.Range range : agg.getRanges()) { Object from = range.getFrom(); Object to = range.getTo(); if ((from != null && !(from instanceof Number)) || (to != null && !(to instanceof Number))) { throw new VertexiumException("Invalid range for property: " + agg.getFieldName() + ". Both to and from must be Numeric."); } rangeBuilder.addRange( range.getKey(), from == null ? Double.MIN_VALUE : ((Number) from).doubleValue(), to == null ? Double.MAX_VALUE : ((Number) to).doubleValue() ); } for (AggregationBuilder subAgg : getElasticsearchAggregations(agg.getNestedAggregations())) { rangeBuilder.subAggregation(subAgg); } aggs.add(rangeBuilder); } } return aggs; } private void applyRange(DateRangeAggregationBuilder dateRangeBuilder, RangeAggregation.Range range) { Object from = range.getFrom(); Object to = range.getTo(); if ((from == null || from instanceof String) && (to == null || to instanceof String)) { String fromString = (String) from; String toString = (String) to; dateRangeBuilder.addRange(range.getKey(), fromString, toString); } else if ((from == null || from instanceof Number) && (to == null || to instanceof Number)) { double fromDouble = from == null ? null : ((Number) from).doubleValue(); double toDouble = to == null ? null : ((Number) to).doubleValue(); dateRangeBuilder.addRange(range.getKey(), fromDouble, toDouble); } else if ((from == null || from instanceof ZonedDateTime) && (to == null || to instanceof ZonedDateTime)) { ZonedDateTime fromDateTime = (ZonedDateTime) from; ZonedDateTime toDateTime = (ZonedDateTime) to; dateRangeBuilder.addRange(range.getKey(), fromDateTime, toDateTime); } else if ((from == null || from instanceof Date) && (to == null || to instanceof Date)) { ZonedDateTime fromDateTime = from == null ? null : ZonedDateTime.ofInstant(((Date) from).toInstant(), ZoneOffset.UTC); ZonedDateTime toDateTime = to == null ? null : ZonedDateTime.ofInstant(((Date) to).toInstant(), ZoneOffset.UTC); dateRangeBuilder.addRange(range.getKey(), fromDateTime, toDateTime); } else { String fromClassName = from == null ? null : from.getClass().getName(); String toClassName = to == null ? null : to.getClass().getName(); throw new VertexiumException("unhandled range types " + fromClassName + ", " + toClassName); } } protected PropertyDefinition getPropertyDefinition(String propertyName) { return getGraph().getPropertyDefinition(propertyName); } private boolean shouldUseScrollApi() { return getParameters().getSkip() == 0 && (getParameters().getLimit() == null || getParameters().getLimit() > pagingLimit); } protected IndexSelectionStrategy getIndexSelectionStrategy() { return indexSelectionStrategy; } public String getAggregationName(String name) { return getSearchIndex().getAggregationName(name); } public IdStrategy getIdStrategy() { return getSearchIndex().getIdStrategy(); } protected FetchHints idFetchHintsToElementFetchHints(EnumSet<IdFetchHint> idFetchHints) { return idFetchHints.contains(IdFetchHint.INCLUDE_HIDDEN) ? FetchHints.builder().setIncludeHidden(true).build() : FetchHints.NONE; } @Override public String toString() { return this.getClass().getName() + "{" + "parameters=" + getParameters() + ", pageSize=" + pageSize + '}'; } private abstract class QueryInfiniteScrollIterable<T> extends InfiniteScrollIterable<T> { private final EnumSet<VertexiumObjectType> objectTypes; private final FetchHints fetchHints; public QueryInfiniteScrollIterable(EnumSet<VertexiumObjectType> objectTypes, FetchHints fetchHints, Long limit) { super(limit); this.objectTypes = objectTypes; this.fetchHints = fetchHints; } @Override protected SearchResponse getInitialSearchResponse() { try { SearchRequestBuilder q = buildQuery(ElasticsearchDocumentType.fromVertexiumObjectTypes(objectTypes), fetchHints, true) .setSize(pageSize) .setScroll(scrollKeepAlive) .setTrackTotalHits(true); if (QUERY_LOGGER.isTraceEnabled()) { QUERY_LOGGER.trace("query: %s", q); } return checkForFailures(q.execute().actionGet()); } catch (IndexNotFoundException ex) { LOGGER.debug("Index missing: %s (returning empty iterable)", ex.getMessage()); return null; } catch (VertexiumNoMatchingPropertiesException ex) { LOGGER.debug("Could not find property: %s (returning empty iterable)", ex.getPropertyName()); return null; } } @Override protected SearchResponse getNextSearchResponse(String scrollId) { try { return client.prepareSearchScroll(scrollId) .setScroll(scrollKeepAlive) .execute().actionGet(); } catch (Exception ex) { throw new VertexiumException("Failed to request more items from scroll " + scrollId, ex); } } @Override protected void closeScroll(String scrollId) { ElasticsearchSearchQueryBase.this.closeScroll(scrollId); } } private static class Ids { private final List<String> vertexIds; private final List<String> edgeIds; private final List<String> ids; private final List<ExtendedDataRowId> extendedDataIds; public Ids(IdStrategy idStrategy, SearchHits hits) { vertexIds = new ArrayList<>(); edgeIds = new ArrayList<>(); extendedDataIds = new ArrayList<>(); ids = new ArrayList<>(); for (SearchHit hit : hits) { ElasticsearchDocumentType dt = ElasticsearchDocumentType.fromSearchHit(hit); if (dt == null) { continue; } switch (dt) { case VERTEX: String vertexId = idStrategy.vertexIdFromSearchHit(hit); ids.add(vertexId); vertexIds.add(vertexId); break; case EDGE: String edgeId = idStrategy.edgeIdFromSearchHit(hit); ids.add(edgeId); edgeIds.add(edgeId); break; case VERTEX_EXTENDED_DATA: case EDGE_EXTENDED_DATA: ExtendedDataRowId extendedDataRowId = idStrategy.extendedDataRowIdFromSearchHit(hit); ids.add(extendedDataRowId.toString()); extendedDataIds.add(extendedDataRowId); break; default: LOGGER.warn("Unhandled document type: %s", dt); break; } } } public List<String> getVertexIds() { return vertexIds; } public List<String> getEdgeIds() { return edgeIds; } public List<String> getIds() { return ids; } public List<ExtendedDataRowId> getExtendedDataIds() { return extendedDataIds; } } @SuppressWarnings("unused") public static class Options { public int pageSize; public IndexSelectionStrategy indexSelectionStrategy; public TimeValue scrollKeepAlive; public StandardAnalyzer analyzer = new StandardAnalyzer(); public int pagingLimit; public int termAggregationShardSize; public int maxQueryStringTerms; public int getPageSize() { return pageSize; } public Options setPageSize(int pageSize) { this.pageSize = pageSize; return this; } public IndexSelectionStrategy getIndexSelectionStrategy() { return indexSelectionStrategy; } public Options setIndexSelectionStrategy(IndexSelectionStrategy indexSelectionStrategy) { this.indexSelectionStrategy = indexSelectionStrategy; return this; } public TimeValue getScrollKeepAlive() { return scrollKeepAlive; } public Options setScrollKeepAlive(TimeValue scrollKeepAlive) { this.scrollKeepAlive = scrollKeepAlive; return this; } public StandardAnalyzer getAnalyzer() { return analyzer; } public Options setAnalyzer(StandardAnalyzer analyzer) { this.analyzer = analyzer; return this; } public int getPagingLimit() { return pagingLimit; } public Options setPagingLimit(int pagingLimit) { this.pagingLimit = pagingLimit; return this; } public int getTermAggregationShardSize() { return termAggregationShardSize; } public Options setTermAggregationShardSize(int termAggregationShardSize) { this.termAggregationShardSize = termAggregationShardSize; return this; } public int getMaxQueryStringTerms() { return maxQueryStringTerms; } public Options setMaxQueryStringTerms(int maxQueryStringTerms) { this.maxQueryStringTerms = maxQueryStringTerms; return this; } } }