Search: Allow to pass a search filter, applying only on the query (and not on facets for example), closes #650.
This commit is contained in:
parent
10ff150bb8
commit
5a4686aee5
|
@ -28,6 +28,7 @@ import org.elasticsearch.action.search.SearchType;
|
||||||
import org.elasticsearch.client.Client;
|
import org.elasticsearch.client.Client;
|
||||||
import org.elasticsearch.client.action.support.BaseRequestBuilder;
|
import org.elasticsearch.client.action.support.BaseRequestBuilder;
|
||||||
import org.elasticsearch.common.unit.TimeValue;
|
import org.elasticsearch.common.unit.TimeValue;
|
||||||
|
import org.elasticsearch.index.query.xcontent.XContentFilterBuilder;
|
||||||
import org.elasticsearch.index.query.xcontent.XContentQueryBuilder;
|
import org.elasticsearch.index.query.xcontent.XContentQueryBuilder;
|
||||||
import org.elasticsearch.search.Scroll;
|
import org.elasticsearch.search.Scroll;
|
||||||
import org.elasticsearch.search.builder.SearchSourceBuilder;
|
import org.elasticsearch.search.builder.SearchSourceBuilder;
|
||||||
|
@ -201,6 +202,33 @@ public class SearchRequestBuilder extends BaseRequestBuilder<SearchRequest, Sear
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a filter on the query executed that only applies to the search query
|
||||||
|
* (and not facets for example).
|
||||||
|
*/
|
||||||
|
public SearchRequestBuilder setFilter(XContentFilterBuilder filter) {
|
||||||
|
sourceBuilder().filter(filter);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a filter on the query executed that only applies to the search query
|
||||||
|
* (and not facets for example).
|
||||||
|
*/
|
||||||
|
public SearchRequestBuilder setFilter(String filter) {
|
||||||
|
sourceBuilder().filter(filter);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a filter on the query executed that only applies to the search query
|
||||||
|
* (and not facets for example).
|
||||||
|
*/
|
||||||
|
public SearchRequestBuilder setFilter(byte[] filter) {
|
||||||
|
sourceBuilder().filter(filter);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* From index to start the search from. Defaults to <tt>0</tt>.
|
* From index to start the search from. Defaults to <tt>0</tt>.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elastic Search and Shay Banon under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. Elastic Search licenses this
|
||||||
|
* file to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.elasticsearch.common.lucene.search;
|
||||||
|
|
||||||
|
import org.apache.lucene.index.IndexReader;
|
||||||
|
import org.apache.lucene.search.Collector;
|
||||||
|
import org.apache.lucene.search.Filter;
|
||||||
|
import org.apache.lucene.search.Scorer;
|
||||||
|
import org.elasticsearch.common.lucene.docset.DocSet;
|
||||||
|
import org.elasticsearch.common.lucene.docset.DocSets;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author kimchy (shay.banon)
|
||||||
|
*/
|
||||||
|
public class FilteredCollector extends Collector {
|
||||||
|
|
||||||
|
private final Collector collector;
|
||||||
|
|
||||||
|
private final Filter filter;
|
||||||
|
|
||||||
|
private DocSet docSet;
|
||||||
|
|
||||||
|
public FilteredCollector(Collector collector, Filter filter) {
|
||||||
|
this.collector = collector;
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void setScorer(Scorer scorer) throws IOException {
|
||||||
|
collector.setScorer(scorer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void collect(int doc) throws IOException {
|
||||||
|
if (docSet.get(doc)) {
|
||||||
|
collector.collect(doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void setNextReader(IndexReader reader, int docBase) throws IOException {
|
||||||
|
docSet = DocSets.convert(reader, filter.getDocIdSet(reader));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public boolean acceptsDocsOutOfOrder() {
|
||||||
|
return collector.acceptsDocsOutOfOrder();
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,6 +30,7 @@ import org.elasticsearch.common.xcontent.ToXContent;
|
||||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
import org.elasticsearch.common.xcontent.XContentFactory;
|
import org.elasticsearch.common.xcontent.XContentFactory;
|
||||||
import org.elasticsearch.common.xcontent.XContentType;
|
import org.elasticsearch.common.xcontent.XContentType;
|
||||||
|
import org.elasticsearch.index.query.xcontent.XContentFilterBuilder;
|
||||||
import org.elasticsearch.index.query.xcontent.XContentQueryBuilder;
|
import org.elasticsearch.index.query.xcontent.XContentQueryBuilder;
|
||||||
import org.elasticsearch.search.facet.AbstractFacetBuilder;
|
import org.elasticsearch.search.facet.AbstractFacetBuilder;
|
||||||
import org.elasticsearch.search.highlight.HighlightBuilder;
|
import org.elasticsearch.search.highlight.HighlightBuilder;
|
||||||
|
@ -69,6 +70,10 @@ public class SearchSourceBuilder implements ToXContent {
|
||||||
|
|
||||||
private byte[] queryBinary;
|
private byte[] queryBinary;
|
||||||
|
|
||||||
|
private XContentFilterBuilder filterBuilder;
|
||||||
|
|
||||||
|
private byte[] filterBinary;
|
||||||
|
|
||||||
private int from = -1;
|
private int from = -1;
|
||||||
|
|
||||||
private int size = -1;
|
private int size = -1;
|
||||||
|
@ -122,6 +127,33 @@ public class SearchSourceBuilder implements ToXContent {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a filter on the query executed that only applies to the search query
|
||||||
|
* (and not facets for example).
|
||||||
|
*/
|
||||||
|
public SearchSourceBuilder filter(XContentFilterBuilder filter) {
|
||||||
|
this.filterBuilder = filter;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a filter on the query executed that only applies to the search query
|
||||||
|
* (and not facets for example).
|
||||||
|
*/
|
||||||
|
public SearchSourceBuilder filter(String filterString) {
|
||||||
|
this.filterBinary = Unicode.fromStringAsBytes(filterString);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a filter on the query executed that only applies to the search query
|
||||||
|
* (and not facets for example).
|
||||||
|
*/
|
||||||
|
public SearchSourceBuilder filter(byte[] filter) {
|
||||||
|
this.filterBinary = filter;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* From index to start the search from. Defaults to <tt>0</tt>.
|
* From index to start the search from. Defaults to <tt>0</tt>.
|
||||||
*/
|
*/
|
||||||
|
@ -357,6 +389,19 @@ public class SearchSourceBuilder implements ToXContent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filterBuilder != null) {
|
||||||
|
builder.field("filter");
|
||||||
|
filterBuilder.toXContent(builder, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterBinary != null) {
|
||||||
|
if (XContentFactory.xContentType(queryBinary) == builder.contentType()) {
|
||||||
|
builder.rawField("filter", filterBinary);
|
||||||
|
} else {
|
||||||
|
builder.field("filter_binary", queryBinary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (explain != null) {
|
if (explain != null) {
|
||||||
builder.field("explain", explain);
|
builder.field("explain", explain);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.elasticsearch.common.collect.Lists;
|
||||||
import org.elasticsearch.common.collect.Maps;
|
import org.elasticsearch.common.collect.Maps;
|
||||||
import org.elasticsearch.common.lucene.MultiCollector;
|
import org.elasticsearch.common.lucene.MultiCollector;
|
||||||
import org.elasticsearch.common.lucene.search.ExtendedIndexSearcher;
|
import org.elasticsearch.common.lucene.search.ExtendedIndexSearcher;
|
||||||
|
import org.elasticsearch.common.lucene.search.FilteredCollector;
|
||||||
import org.elasticsearch.index.engine.Engine;
|
import org.elasticsearch.index.engine.Engine;
|
||||||
import org.elasticsearch.search.dfs.CachedDfSource;
|
import org.elasticsearch.search.dfs.CachedDfSource;
|
||||||
|
|
||||||
|
@ -116,6 +117,11 @@ public class ContextIndexSearcher extends ExtendedIndexSearcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void search(Weight weight, Filter filter, Collector collector) throws IOException {
|
@Override public void search(Weight weight, Filter filter, Collector collector) throws IOException {
|
||||||
|
if (searchContext.parsedFilter() != null) {
|
||||||
|
// this will only get applied to the actual search collector and not
|
||||||
|
// to any scoped collectors
|
||||||
|
collector = new FilteredCollector(collector, searchContext.parsedFilter());
|
||||||
|
}
|
||||||
if (searchContext.timeout() != null) {
|
if (searchContext.timeout() != null) {
|
||||||
collector = new TimeLimitingCollector(collector, searchContext.timeout().millis());
|
collector = new TimeLimitingCollector(collector, searchContext.timeout().millis());
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
package org.elasticsearch.search.internal;
|
package org.elasticsearch.search.internal;
|
||||||
|
|
||||||
|
import org.apache.lucene.search.Filter;
|
||||||
import org.apache.lucene.search.Query;
|
import org.apache.lucene.search.Query;
|
||||||
import org.apache.lucene.search.Sort;
|
import org.apache.lucene.search.Sort;
|
||||||
import org.elasticsearch.ElasticSearchException;
|
import org.elasticsearch.ElasticSearchException;
|
||||||
|
@ -115,6 +116,8 @@ public class SearchContext implements Releasable {
|
||||||
|
|
||||||
private Query query;
|
private Query query;
|
||||||
|
|
||||||
|
private Filter filter;
|
||||||
|
|
||||||
private int[] docIdsToLoad;
|
private int[] docIdsToLoad;
|
||||||
|
|
||||||
private int docsIdsToLoadFrom;
|
private int docsIdsToLoadFrom;
|
||||||
|
@ -291,6 +294,15 @@ public class SearchContext implements Releasable {
|
||||||
return this.sort;
|
return this.sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SearchContext parsedFilter(Filter filter) {
|
||||||
|
this.filter = filter;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Filter parsedFilter() {
|
||||||
|
return this.filter;
|
||||||
|
}
|
||||||
|
|
||||||
public String queryParserName() {
|
public String queryParserName() {
|
||||||
return queryParserName;
|
return queryParserName;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elastic Search and Shay Banon under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. Elastic Search licenses this
|
||||||
|
* file to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.elasticsearch.search.query;
|
||||||
|
|
||||||
|
import org.elasticsearch.common.xcontent.XContentFactory;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentParser;
|
||||||
|
import org.elasticsearch.index.query.xcontent.XContentIndexQueryParser;
|
||||||
|
import org.elasticsearch.search.SearchParseElement;
|
||||||
|
import org.elasticsearch.search.internal.SearchContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author kimchy (shay.banon)
|
||||||
|
*/
|
||||||
|
public class FilterBinaryParseElement implements SearchParseElement {
|
||||||
|
|
||||||
|
@Override public void parse(XContentParser parser, SearchContext context) throws Exception {
|
||||||
|
XContentIndexQueryParser indexQueryParser = (XContentIndexQueryParser) context.queryParser();
|
||||||
|
byte[] filterSource = parser.binaryValue();
|
||||||
|
XContentParser fSourceParser = XContentFactory.xContent(filterSource).createParser(filterSource);
|
||||||
|
try {
|
||||||
|
context.parsedFilter(indexQueryParser.parseInnerFilter(fSourceParser));
|
||||||
|
} finally {
|
||||||
|
fSourceParser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elastic Search and Shay Banon under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. Elastic Search licenses this
|
||||||
|
* file to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.elasticsearch.search.query;
|
||||||
|
|
||||||
|
import org.elasticsearch.common.xcontent.XContentParser;
|
||||||
|
import org.elasticsearch.index.query.xcontent.XContentIndexQueryParser;
|
||||||
|
import org.elasticsearch.search.SearchParseElement;
|
||||||
|
import org.elasticsearch.search.internal.SearchContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author kimchy (shay.banon)
|
||||||
|
*/
|
||||||
|
public class FilterParseElement implements SearchParseElement {
|
||||||
|
|
||||||
|
@Override public void parse(XContentParser parser, SearchContext context) throws Exception {
|
||||||
|
XContentIndexQueryParser indexQueryParser = (XContentIndexQueryParser) context.queryParser();
|
||||||
|
context.parsedFilter(indexQueryParser.parseInnerFilter(parser));
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,6 +34,10 @@ public class QueryBinaryParseElement implements SearchParseElement {
|
||||||
XContentIndexQueryParser indexQueryParser = (XContentIndexQueryParser) context.queryParser();
|
XContentIndexQueryParser indexQueryParser = (XContentIndexQueryParser) context.queryParser();
|
||||||
byte[] querySource = parser.binaryValue();
|
byte[] querySource = parser.binaryValue();
|
||||||
XContentParser qSourceParser = XContentFactory.xContent(querySource).createParser(querySource);
|
XContentParser qSourceParser = XContentFactory.xContent(querySource).createParser(querySource);
|
||||||
|
try {
|
||||||
context.parsedQuery(indexQueryParser.parse(qSourceParser));
|
context.parsedQuery(indexQueryParser.parse(qSourceParser));
|
||||||
|
} finally {
|
||||||
|
qSourceParser.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -57,6 +57,9 @@ public class QueryPhase implements SearchPhase {
|
||||||
.put("query", new QueryParseElement())
|
.put("query", new QueryParseElement())
|
||||||
.put("queryBinary", new QueryBinaryParseElement())
|
.put("queryBinary", new QueryBinaryParseElement())
|
||||||
.put("query_binary", new QueryBinaryParseElement())
|
.put("query_binary", new QueryBinaryParseElement())
|
||||||
|
.put("filter", new FilterParseElement())
|
||||||
|
.put("filterBinary", new FilterBinaryParseElement())
|
||||||
|
.put("filter_binary", new FilterBinaryParseElement())
|
||||||
.put("sort", new SortParseElement())
|
.put("sort", new SortParseElement())
|
||||||
.putAll(facetPhase.parseElements());
|
.putAll(facetPhase.parseElements());
|
||||||
return parseElements.build();
|
return parseElements.build();
|
||||||
|
@ -73,6 +76,7 @@ public class QueryPhase implements SearchPhase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void execute(SearchContext searchContext) throws QueryPhaseExecutionException {
|
public void execute(SearchContext searchContext) throws QueryPhaseExecutionException {
|
||||||
|
// set the filter on the searcher
|
||||||
if (searchContext.parsedQuery().scopePhases().length > 0) {
|
if (searchContext.parsedQuery().scopePhases().length > 0) {
|
||||||
// we have scoped queries, refresh the id cache
|
// we have scoped queries, refresh the id cache
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -73,6 +73,58 @@ public class SimpleFacetsTests extends AbstractNodesTests {
|
||||||
return client("server1");
|
return client("server1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test public void testSearchFilter() throws Exception {
|
||||||
|
try {
|
||||||
|
client.admin().indices().prepareDelete("test").execute().actionGet();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
client.admin().indices().prepareCreate("test").execute().actionGet();
|
||||||
|
client.admin().cluster().prepareHealth().setWaitForGreenStatus().execute().actionGet();
|
||||||
|
|
||||||
|
client.admin().cluster().prepareHealth().setWaitForGreenStatus().execute().actionGet();
|
||||||
|
|
||||||
|
client.prepareIndex("test", "type1").setSource(jsonBuilder().startObject()
|
||||||
|
.field("tag", "green")
|
||||||
|
.endObject()).execute().actionGet();
|
||||||
|
client.admin().indices().prepareFlush().setRefresh(true).execute().actionGet();
|
||||||
|
|
||||||
|
client.prepareIndex("test", "type1").setSource(jsonBuilder().startObject()
|
||||||
|
.field("tag", "blue")
|
||||||
|
.endObject()).execute().actionGet();
|
||||||
|
|
||||||
|
client.admin().indices().prepareRefresh().execute().actionGet();
|
||||||
|
|
||||||
|
SearchResponse searchResponse = client.prepareSearch()
|
||||||
|
.setQuery(matchAllQuery())
|
||||||
|
.addFacet(termsFacet("facet1").field("tag").size(10))
|
||||||
|
.execute().actionGet();
|
||||||
|
|
||||||
|
assertThat(searchResponse.hits().hits().length, equalTo(2));
|
||||||
|
TermsFacet facet = searchResponse.facets().facet("facet1");
|
||||||
|
assertThat(facet.name(), equalTo("facet1"));
|
||||||
|
assertThat(facet.entries().size(), equalTo(2));
|
||||||
|
assertThat(facet.entries().get(0).term(), anyOf(equalTo("green"), equalTo("blue")));
|
||||||
|
assertThat(facet.entries().get(0).count(), equalTo(1));
|
||||||
|
assertThat(facet.entries().get(1).term(), anyOf(equalTo("green"), equalTo("blue")));
|
||||||
|
assertThat(facet.entries().get(1).count(), equalTo(1));
|
||||||
|
|
||||||
|
searchResponse = client.prepareSearch()
|
||||||
|
.setQuery(matchAllQuery())
|
||||||
|
.setFilter(termFilter("tag", "blue"))
|
||||||
|
.addFacet(termsFacet("facet1").field("tag").size(10))
|
||||||
|
.execute().actionGet();
|
||||||
|
|
||||||
|
assertThat(searchResponse.hits().hits().length, equalTo(1));
|
||||||
|
facet = searchResponse.facets().facet("facet1");
|
||||||
|
assertThat(facet.name(), equalTo("facet1"));
|
||||||
|
assertThat(facet.entries().size(), equalTo(2));
|
||||||
|
assertThat(facet.entries().get(0).term(), anyOf(equalTo("green"), equalTo("blue")));
|
||||||
|
assertThat(facet.entries().get(0).count(), equalTo(1));
|
||||||
|
assertThat(facet.entries().get(1).term(), anyOf(equalTo("green"), equalTo("blue")));
|
||||||
|
assertThat(facet.entries().get(1).count(), equalTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
@Test public void testFacetsWithSize0() throws Exception {
|
@Test public void testFacetsWithSize0() throws Exception {
|
||||||
try {
|
try {
|
||||||
client.admin().indices().prepareDelete("test").execute().actionGet();
|
client.admin().indices().prepareDelete("test").execute().actionGet();
|
||||||
|
|
Loading…
Reference in New Issue