package de.komoot.photon.query;
import com.google.common.collect.ImmutableSet;
import com.vividsolutions.jts.geom.Point;
import org.elasticsearch.common.unit.Fuzziness;
import org.elasticsearch.index.query.*;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* There are four {@link de.komoot.photon.query.PhotonQueryBuilder.State states} that this query builder goes through before a query can be executed on elastic search. Of these,
* three are of importance. <ul> <li>{@link de.komoot.photon.query.PhotonQueryBuilder.State#PLAIN PLAIN} The query builder is being used to build a query without any tag filters.
* </li> <li>{@link de.komoot.photon.query.PhotonQueryBuilder.State#FILTERED FILTERED} The query builder is being used to build a query that has tag filters and can no longer be
* used to build a PLAIN filter. </li> <li>{@link de.komoot.photon.query.PhotonQueryBuilder.State#FINISHED FINISHED} The query builder has been built and the query has been placed
* inside a {@link FilteredQueryBuilder filtered query}. Further calls to any methods will have no effect on this query builder.</li> </ul>
* <p/>
* Created by Sachin Dole on 2/12/2015.
*
* @see de.komoot.photon.query.TagFilterQueryBuilder
*/
public class PhotonQueryBuilder implements TagFilterQueryBuilder {
private FunctionScoreQueryBuilder queryBuilder;
private Integer limit = 50;
private FilterBuilder filterBuilderForTopLevelFilter;
private State state;
private OrFilterBuilder orFilterBuilderForIncludeTagFiltering = null;
private AndFilterBuilder andFilterBuilderForExcludeTagFiltering = null;
private MatchQueryBuilder defaultMatchQueryBuilder;
private MatchQueryBuilder languageMatchQueryBuilder;
private FilteredQueryBuilder finalFilteredQueryBuilder;
private PhotonQueryBuilder(String query, String language) {
defaultMatchQueryBuilder = QueryBuilders.
matchQuery("collector.default", query).
fuzziness(Fuzziness.ONE).
prefixLength(2).
analyzer("search_ngram").
minimumShouldMatch("100%");
languageMatchQueryBuilder = QueryBuilders.
matchQuery(String.format("collector.%s.ngrams", language), query).
fuzziness(Fuzziness.ONE).
prefixLength(2).
analyzer("search_ngram").
minimumShouldMatch("100%");
queryBuilder = QueryBuilders.functionScoreQuery(
QueryBuilders.boolQuery().must(
QueryBuilders.boolQuery().should(
defaultMatchQueryBuilder
).should(
languageMatchQueryBuilder
).minimumShouldMatch("1")
).should(
QueryBuilders.matchQuery(String.format("name.%s.raw", language), query).boost(200).analyzer("search_raw")
).should(
QueryBuilders.matchQuery(String.format("collector.%s.raw", language), query).boost(100).analyzer("search_raw")
),
ScoreFunctionBuilders.scriptFunction("general-score", "groovy")
).boostMode("multiply").scoreMode("multiply");
filterBuilderForTopLevelFilter = FilterBuilders.orFilter(
FilterBuilders.missingFilter("housenumber"),
FilterBuilders.queryFilter(
QueryBuilders.matchQuery("housenumber", query).analyzer("standard")
),
FilterBuilders.existsFilter(String.format("name.%s.raw", language))
);
state = State.PLAIN;
}
/**
* Create an instance of this builder which can then be embellished as needed.
*
* @param query the value for photon query parameter "q"
* @param language
* @return An initialized {@link TagFilterQueryBuilder photon query builder}.
*/
public static TagFilterQueryBuilder builder(String query, String language) {
return new PhotonQueryBuilder(query, language);
}
@Override
public TagFilterQueryBuilder withLimit(Integer limit) {
this.limit = limit == null || limit < 1 ? 15 : limit;
this.limit = this.limit > 50 ? 50 : this.limit;
return this;
}
@Override
public TagFilterQueryBuilder withLocationBias(Point point) {
if(point == null) return this;
queryBuilder.add(ScoreFunctionBuilders.scriptFunction("location-biased-score", "groovy").param("lon", point.getX()).param("lat", point.getY()));
return this;
}
@Override
public TagFilterQueryBuilder withTags(Map<String, Set<String>> tags) {
if(!checkTags(tags)) return this;
ensureFiltered();
List<AndFilterBuilder> termFilters = new ArrayList<AndFilterBuilder>(tags.size());
for(String tagKey : tags.keySet()) {
Set<String> valuesToInclude = tags.get(tagKey);
TermFilterBuilder keyFilter = FilterBuilders.termFilter("osm_key", tagKey);
TermsFilterBuilder valueFilter = FilterBuilders.termsFilter("osm_value", valuesToInclude.toArray(new String[valuesToInclude.size()]));
AndFilterBuilder includeAndFilter = FilterBuilders.andFilter(keyFilter, valueFilter);
termFilters.add(includeAndFilter);
}
this.appendIncludeTermFilters(termFilters);
return this;
}
@Override
public TagFilterQueryBuilder withKeys(Set<String> keys) {
if(!checkTags(keys)) return this;
ensureFiltered();
List<TermsFilterBuilder> termFilters = new ArrayList<TermsFilterBuilder>(keys.size());
termFilters.add(FilterBuilders.termsFilter("osm_key", keys.toArray()));
this.appendIncludeTermFilters(termFilters);
return this;
}
@Override
public TagFilterQueryBuilder withValues(Set<String> values) {
if(!checkTags(values)) return this;
ensureFiltered();
List<TermsFilterBuilder> termFilters = new ArrayList<TermsFilterBuilder>(values.size());
termFilters.add(FilterBuilders.termsFilter("osm_value", values.toArray(new String[values.size()])));
this.appendIncludeTermFilters(termFilters);
return this;
}
@Override
public TagFilterQueryBuilder withTagsNotValues(Map<String, Set<String>> tags) {
if(!checkTags(tags)) return this;
ensureFiltered();
List<AndFilterBuilder> termFilters = new ArrayList<AndFilterBuilder>(tags.size());
for(String tagKey : tags.keySet()) {
Set<String> valuesToInclude = tags.get(tagKey);
TermFilterBuilder keyFilter = FilterBuilders.termFilter("osm_key", tagKey);
TermsFilterBuilder valueFilter = FilterBuilders.termsFilter("osm_value", valuesToInclude.toArray(new String[valuesToInclude.size()]));
NotFilterBuilder negatedValueFilter = FilterBuilders.notFilter(valueFilter);
AndFilterBuilder includeAndFilter = FilterBuilders.andFilter(keyFilter, negatedValueFilter);
termFilters.add(includeAndFilter);
}
this.appendIncludeTermFilters(termFilters);
return this;
}
@Override
public TagFilterQueryBuilder withoutTags(Map<String, Set<String>> tagsToExclude) {
if(!checkTags(tagsToExclude)) return this;
ensureFiltered();
List<NotFilterBuilder> termFilters = new ArrayList<NotFilterBuilder>(tagsToExclude.size());
for(String tagKey : tagsToExclude.keySet()) {
Set<String> valuesToExclude = tagsToExclude.get(tagKey);
TermFilterBuilder keyFilter = FilterBuilders.termFilter("osm_key", tagKey);
TermsFilterBuilder valueFilter = FilterBuilders.termsFilter("osm_value", valuesToExclude.toArray(new String[valuesToExclude.size()]));
AndFilterBuilder andFilterForExclusions = FilterBuilders.andFilter(keyFilter, valueFilter);
termFilters.add(FilterBuilders.notFilter(andFilterForExclusions));
}
this.appendExcludeTermFilters(termFilters);
return this;
}
@Override
public TagFilterQueryBuilder withoutKeys(Set<String> keysToExclude) {
if(!checkTags(keysToExclude)) return this;
ensureFiltered();
List<NotFilterBuilder> termFilters = new ArrayList<NotFilterBuilder>(keysToExclude.size());
termFilters.add(FilterBuilders.notFilter(FilterBuilders.termsFilter("osm_key", keysToExclude.toArray())));
this.appendExcludeTermFilters(termFilters);
return this;
}
@Override
public TagFilterQueryBuilder withoutValues(Set<String> valuesToExclude) {
if(!checkTags(valuesToExclude)) return this;
ensureFiltered();
List<NotFilterBuilder> termFilters = new ArrayList<NotFilterBuilder>(valuesToExclude.size());
termFilters.add(FilterBuilders.notFilter(FilterBuilders.termsFilter("osm_value", valuesToExclude.toArray())));
this.appendExcludeTermFilters(termFilters);
return this;
}
@Override
public TagFilterQueryBuilder withKeys(String... keys) {
return this.withKeys(ImmutableSet.<String>builder().add(keys).build());
}
@Override
public TagFilterQueryBuilder withValues(String... values) {
return this.withValues(ImmutableSet.<String>builder().add(values).build());
}
@Override
public TagFilterQueryBuilder withoutKeys(String... keysToExclude) {
return this.withoutKeys(ImmutableSet.<String>builder().add(keysToExclude).build());
}
@Override
public TagFilterQueryBuilder withoutValues(String... valuesToExclude) {
return this.withoutValues(ImmutableSet.<String>builder().add(valuesToExclude).build());
}
@Override
public TagFilterQueryBuilder withStrictMatch() {
defaultMatchQueryBuilder.minimumShouldMatch("100%");
languageMatchQueryBuilder.minimumShouldMatch("100%");
return this;
}
@Override
public TagFilterQueryBuilder withLenientMatch() {
defaultMatchQueryBuilder.minimumShouldMatch("-1");
languageMatchQueryBuilder.minimumShouldMatch("-1");
return this;
}
/**
* When this method is called, all filters are placed inside their {@link OrFilterBuilder OR} or {@link AndFilterBuilder AND} containers and the top level filter builder is
* built. Subsequent invocations of this method have no additional effect. Note that after this method is called, calling other methods on this class also have no effect.
*
* @see TagFilterQueryBuilder#buildQuery()
*/
@Override
public QueryBuilder buildQuery() {
if(state.equals(State.FINISHED)) {
return finalFilteredQueryBuilder;
}
if(state.equals(State.FILTERED)) {
if(orFilterBuilderForIncludeTagFiltering != null)
((AndFilterBuilder) filterBuilderForTopLevelFilter).add(orFilterBuilderForIncludeTagFiltering);
if(andFilterBuilderForExcludeTagFiltering != null)
((AndFilterBuilder) filterBuilderForTopLevelFilter).add(andFilterBuilderForExcludeTagFiltering);
}
state = State.FINISHED;
finalFilteredQueryBuilder = QueryBuilders.filteredQuery(queryBuilder, filterBuilderForTopLevelFilter);
return finalFilteredQueryBuilder;
}
@Override
public Integer getLimit() {
return limit;
}
private Boolean checkTags(Set<String> keys) {
return !(keys == null || keys.isEmpty());
}
private Boolean checkTags(Map<String, Set<String>> tags) {
return !(tags == null || tags.isEmpty());
}
private void appendIncludeTermFilters(List<? extends FilterBuilder> termFilters) {
if(orFilterBuilderForIncludeTagFiltering == null) {
orFilterBuilderForIncludeTagFiltering = FilterBuilders.orFilter(termFilters.toArray(new FilterBuilder[termFilters.size()]));
} else {
for(FilterBuilder eachTagFilter : termFilters) {
orFilterBuilderForIncludeTagFiltering.add(eachTagFilter);
}
}
}
private void appendExcludeTermFilters(List<NotFilterBuilder> termFilters) {
if(andFilterBuilderForExcludeTagFiltering == null) {
andFilterBuilderForExcludeTagFiltering = FilterBuilders.andFilter(termFilters.toArray(new FilterBuilder[termFilters.size()]));
} else {
for(FilterBuilder eachTagFilter : termFilters) {
andFilterBuilderForExcludeTagFiltering.add(eachTagFilter);
}
}
}
private void ensureFiltered() {
if(state.equals(State.PLAIN)) {
filterBuilderForTopLevelFilter = FilterBuilders.andFilter(filterBuilderForTopLevelFilter);
} else if(filterBuilderForTopLevelFilter instanceof AndFilterBuilder) {
//good! nothing to do because query builder is already filtered.
} else {
throw new RuntimeException("This code is not in valid state. It is expected that the filter builder field should either be AndFilterBuilder or OrFilterBuilder. Found" +
" " + filterBuilderForTopLevelFilter.getClass() + " instead.");
}
state = State.FILTERED;
}
private enum State {
PLAIN, FILTERED, QUERY_ALREADY_BUILT, FINISHED,
}
}