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:
kimchy 2011-01-26 12:55:30 +02:00
parent 10ff150bb8
commit 5a4686aee5
12 changed files with 297 additions and 3 deletions

View File

@ -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>.
*/ */

View File

@ -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();
}
}

View File

@ -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);
} }

View File

@ -7,7 +7,7 @@
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an

View File

@ -7,7 +7,7 @@
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an

View File

@ -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());
} }

View File

@ -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;
} }

View File

@ -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();
}
}
}

View File

@ -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));
}
}

View File

@ -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);
context.parsedQuery(indexQueryParser.parse(qSourceParser)); try {
context.parsedQuery(indexQueryParser.parse(qSourceParser));
} finally {
qSourceParser.close();
}
} }
} }

View File

@ -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 {

View File

@ -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();