- Added explain api. #2184

This commit is contained in:
Martijn van Groningen 2012-08-20 18:06:36 +02:00 committed by Shay Banon
parent a8656c8a6f
commit 8365e7ba0b
20 changed files with 1122 additions and 14 deletions

View File

@ -97,6 +97,9 @@ import org.elasticsearch.action.deletebyquery.DeleteByQueryAction;
import org.elasticsearch.action.deletebyquery.TransportDeleteByQueryAction;
import org.elasticsearch.action.deletebyquery.TransportIndexDeleteByQueryAction;
import org.elasticsearch.action.deletebyquery.TransportShardDeleteByQueryAction;
import org.elasticsearch.action.explain.ExplainAction;
import org.elasticsearch.action.explain.ExplainResponse;
import org.elasticsearch.action.explain.TransportExplainAction;
import org.elasticsearch.action.get.*;
import org.elasticsearch.action.index.IndexAction;
import org.elasticsearch.action.index.TransportIndexAction;
@ -220,6 +223,7 @@ public class ActionModule extends AbstractModule {
registerAction(MultiSearchAction.INSTANCE, TransportMultiSearchAction.class);
registerAction(MoreLikeThisAction.INSTANCE, TransportMoreLikeThisAction.class);
registerAction(PercolateAction.INSTANCE, TransportPercolateAction.class);
registerAction(ExplainAction.INSTANCE, TransportExplainAction.class);
// register Name -> GenericAction Map that can be injected to instances.
MapBinder<String, GenericAction> actionsBinder

View File

@ -0,0 +1,44 @@
/*
* Licensed to ElasticSearch and Shay Banon under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. ElasticSearch 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.action.explain;
import org.elasticsearch.action.Action;
import org.elasticsearch.client.Client;
/**
* Entry point for the explain feature.
*/
public class ExplainAction extends Action<ExplainRequest, ExplainResponse, ExplainRequestBuilder> {
public static final ExplainAction INSTANCE = new ExplainAction();
public static final String NAME = "explain";
private ExplainAction() {
super(NAME);
}
public ExplainRequestBuilder newRequestBuilder(Client client) {
return new ExplainRequestBuilder(client);
}
public ExplainResponse newResponse() {
return new ExplainResponse();
}
}

View File

@ -0,0 +1,205 @@
/*
* Licensed to ElasticSearch and Shay Banon under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. ElasticSearch 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.action.explain;
import org.elasticsearch.ElasticSearchGenerationException;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.ValidateActions;
import org.elasticsearch.action.support.single.shard.SingleShardOperationRequest;
import org.elasticsearch.client.Requests;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import java.io.IOException;
import java.util.Map;
/**
* Explain request encapsulating the explain query and document identifier to get an explanation for.
*/
public class ExplainRequest extends SingleShardOperationRequest {
private static final XContentType contentType = Requests.CONTENT_TYPE;
private String type = "_all";
private String id;
private String routing;
private String preference;
private BytesReference source;
private boolean sourceUnsafe;
private String[] filteringAlias = Strings.EMPTY_ARRAY;
ExplainRequest(){
}
public ExplainRequest(String index, String type, String id) {
this.index = index;
this.type = type;
this.id = id;
}
public ExplainRequest index(String index) {
this.index = index;
return this;
}
public String type() {
return type;
}
public ExplainRequest type(String type) {
this.type = type;
return this;
}
public String id() {
return id;
}
public ExplainRequest id(String id) {
this.id = id;
return this;
}
public String routing() {
return routing;
}
public ExplainRequest routing(String routing) {
this.routing = routing;
return this;
}
/**
* Simple sets the routing. Since the parent is only used to get to the right shard.
*/
public ExplainRequest parent(String parent) {
this.routing = parent;
return this;
}
public String preference() {
return preference;
}
public ExplainRequest preference(String preference) {
this.preference = preference;
return this;
}
public BytesReference source() {
return source;
}
public boolean sourceUnsafe() {
return sourceUnsafe;
}
public ExplainRequest source(ExplainSourceBuilder sourceBuilder) {
this.source = sourceBuilder.buildAsBytes(contentType);
this.sourceUnsafe = false;
return this;
}
public ExplainRequest source(BytesReference querySource, boolean unsafe) {
this.source = querySource;
this.sourceUnsafe = unsafe;
return this;
}
public String[] filteringAlias() {
return filteringAlias;
}
public void filteringAlias(String[] filteringAlias) {
if (filteringAlias == null) {
return;
}
this.filteringAlias = filteringAlias;
}
@Override
public ExplainRequest listenerThreaded(boolean threadedListener) {
super.listenerThreaded(threadedListener);
return this;
}
@Override
public ExplainRequest operationThreaded(boolean threadedOperation) {
super.operationThreaded(threadedOperation);
return this;
}
@Override
protected void beforeLocalFork() {
if (sourceUnsafe) {
source = source.copyBytesArray();
sourceUnsafe = false;
}
}
@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = super.validate();
if (type == null) {
validationException = ValidateActions.addValidationError("type is missing", validationException);
}
if (id == null) {
validationException = ValidateActions.addValidationError("id is missing", validationException);
}
if (source == null) {
validationException = ValidateActions.addValidationError("source is missing", validationException);
}
return validationException;
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
type = in.readString();
id = in.readString();
routing = in.readOptionalString();
preference = in.readOptionalString();
source = in.readBytesReference();
sourceUnsafe = false;
filteringAlias = in.readStringArray();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(type);
out.writeString(id);
out.writeOptionalString(routing);
out.writeOptionalString(preference);
out.writeBytesReference(source);
out.writeStringArray(filteringAlias);
}
}

View File

@ -0,0 +1,133 @@
/*
* Licensed to ElasticSearch and Shay Banon under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. ElasticSearch 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.action.explain;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.BaseRequestBuilder;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import java.util.Map;
/**
* A builder for {@link ExplainRequest}.
*/
public class ExplainRequestBuilder extends BaseRequestBuilder<ExplainRequest, ExplainResponse> {
private ExplainSourceBuilder sourceBuilder;
ExplainRequestBuilder(Client client) {
super(client, new ExplainRequest());
}
public ExplainRequestBuilder(Client client, String index, String type, String id) {
super(client, new ExplainRequest());
request().index(index);
request().type(type);
request().id(id);
}
/**
* Sets the index to get a score explanation for.
*/
public ExplainRequestBuilder setIndex(String index) {
request().index(index);
return this;
}
/**
* Sets the type to get a score explanation for.
*/
public ExplainRequestBuilder setType(String type) {
request().type(type);
return this;
}
/**
* Sets the id to get a score explanation for.
*/
public ExplainRequestBuilder setId(String id) {
request().id(id);
return this;
}
/**
* Sets the routing for sharding.
*/
public ExplainRequestBuilder setRouting(String routing) {
request().routing(routing);
return this;
}
/**
* Simple sets the routing. Since the parent is only used to get to the right shard.
*/
public ExplainRequestBuilder setParent(String parent) {
request().parent(parent);
return this;
}
/**
* Sets the shard preference.
*/
public ExplainRequestBuilder setPreference(String preference) {
request().preference(preference);
return this;
}
/**
* Sets the query to get a score explanation for.
*/
public ExplainRequestBuilder setQuery(QueryBuilder queryBuilder) {
sourceBuilder().query(queryBuilder);
return this;
}
public ExplainRequestBuilder setSource(BytesReference querySource, boolean unsafe) {
request().source(querySource, unsafe);
return this;
}
/**
* Sets whether the actual explain action should occur in a different thread if executed locally.
*/
public ExplainRequestBuilder operationThreaded(boolean threadedOperation) {
request().operationThreaded(threadedOperation);
return this;
}
protected void doExecute(ActionListener<ExplainResponse> listener) {
if (sourceBuilder != null) {
request.source(sourceBuilder);
}
client.explain(request, listener);
}
private ExplainSourceBuilder sourceBuilder() {
if (sourceBuilder == null) {
sourceBuilder = new ExplainSourceBuilder();
}
return sourceBuilder;
}
}

View File

@ -0,0 +1,96 @@
/*
* Licensed to ElasticSearch and Shay Banon under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. ElasticSearch 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.action.explain;
import org.apache.lucene.search.Explanation;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import java.io.IOException;
import static org.elasticsearch.common.lucene.Lucene.readExplanation;
import static org.elasticsearch.common.lucene.Lucene.writeExplanation;
/**
* Response containing the score explanation.
*/
public class ExplainResponse implements ActionResponse {
private Explanation explanation;
private boolean exists;
ExplainResponse() {
}
public ExplainResponse(boolean exists) {
this.exists = exists;
}
public ExplainResponse(boolean exists, Explanation explanation) {
this.exists = exists;
this.explanation = explanation;
}
public Explanation getExplanation() {
return explanation();
}
public Explanation explanation() {
return explanation;
}
public boolean isMatch() {
return match();
}
public boolean match() {
return explanation != null && explanation.isMatch();
}
public boolean hasExplanation() {
return explanation != null;
}
public boolean exists() {
return exists;
}
public boolean isExists() {
return exists();
}
public void readFrom(StreamInput in) throws IOException {
exists = in.readBoolean();
if (in.readBoolean()) {
explanation = readExplanation(in);
}
}
public void writeTo(StreamOutput out) throws IOException {
out.writeBoolean(exists);
if (explanation == null) {
out.writeBoolean(false);
} else {
out.writeBoolean(true);
writeExplanation(out, explanation);
}
}
}

View File

@ -0,0 +1,76 @@
/*
* Licensed to ElasticSearch and Shay Banon under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. ElasticSearch 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.action.explain;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilderException;
import java.io.IOException;
public class ExplainSourceBuilder implements ToXContent {
private QueryBuilder queryBuilder;
private BytesReference queryBinary;
public ExplainSourceBuilder query(QueryBuilder query) {
this.queryBuilder = query;
return this;
}
public ExplainSourceBuilder query(BytesReference queryBinary) {
this.queryBinary = queryBinary;
return this;
}
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
if (queryBuilder != null) {
builder.field("query");
queryBuilder.toXContent(builder, params);
}
if (queryBinary != null) {
if (XContentFactory.xContentType(queryBinary) == builder.contentType()) {
builder.rawField("query", queryBinary);
} else {
builder.field("query_binary", queryBinary);
}
}
builder.endObject();
return builder;
}
public BytesReference buildAsBytes(XContentType contentType) throws SearchSourceBuilderException {
try {
XContentBuilder builder = XContentFactory.contentBuilder(contentType);
toXContent(builder, ToXContent.EMPTY_PARAMS);
return builder.bytes();
} catch (Exception e) {
throw new SearchSourceBuilderException("Failed to build search source", e);
}
}
}

View File

@ -0,0 +1,165 @@
/*
* Licensed to ElasticSearch and Shay Banon under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. ElasticSearch 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.action.explain;
import com.google.common.collect.ImmutableMap;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.*;
import org.elasticsearch.ElasticSearchException;
import org.elasticsearch.action.support.single.shard.TransportShardSingleOperationAction;
import org.elasticsearch.cluster.ClusterService;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.block.ClusterBlockException;
import org.elasticsearch.cluster.block.ClusterBlockLevel;
import org.elasticsearch.cluster.routing.ShardIterator;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.engine.Engine;
import org.elasticsearch.index.mapper.ParsedDocument;
import org.elasticsearch.index.mapper.Uid;
import org.elasticsearch.index.mapper.internal.UidFieldMapper;
import org.elasticsearch.index.query.ParsedQuery;
import org.elasticsearch.index.service.IndexService;
import org.elasticsearch.index.shard.service.IndexShard;
import org.elasticsearch.indices.IndicesService;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.search.SearchParseElement;
import org.elasticsearch.search.internal.InternalSearchRequest;
import org.elasticsearch.search.internal.SearchContext;
import org.elasticsearch.search.query.QueryParseElement;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
import java.io.IOException;
import java.util.Map;
/**
* Explain transport action. Computes the explain on the targeted shard.
*/
// TODO: AggregatedDfs. Currently the idf can be different then when executing a normal search with explain.
public class TransportExplainAction extends TransportShardSingleOperationAction<ExplainRequest, ExplainResponse> {
private final IndicesService indicesService;
private final ScriptService scriptService;
@Inject
public TransportExplainAction(Settings settings, ThreadPool threadPool, ClusterService clusterService,
TransportService transportService, IndicesService indicesService,
ScriptService scriptService) {
super(settings, threadPool, clusterService, transportService);
this.indicesService = indicesService;
this.scriptService = scriptService;
}
protected String transportAction() {
return ExplainAction.NAME;
}
protected String executor() {
return ThreadPool.Names.GET; // Or use Names.SEARCH?
}
@Override
protected void resolveRequest(ClusterState state, ExplainRequest request) {
String concreteIndex = state.metaData().concreteIndex(request.index());
request.filteringAlias(state.metaData().filteringAliases(concreteIndex, request.index()));
request.index(state.metaData().concreteIndex(request.index()));
}
protected ExplainResponse shardOperation(ExplainRequest request, int shardId) throws ElasticSearchException {
IndexService indexService = indicesService.indexService(request.index());
IndexShard indexShard = indexService.shardSafe(shardId);
Term uidTerm = UidFieldMapper.TERM_FACTORY.createTerm(Uid.createUid(request.type(), request.id()));
Engine.GetResult result = indexShard.get(new Engine.Get(false, uidTerm));
if (!result.exists()) {
return new ExplainResponse(false);
}
SearchContext context = new SearchContext(
0,
new InternalSearchRequest().types(new String[]{request.type()})
.filteringAliases(request.filteringAlias()),
null, indexShard.searcher(), indexService, indexShard,
scriptService
);
SearchContext.setCurrent(context);
context.parsedQuery(retrieveParsedQuery(request, indexService));
context.preProcess();
int topLevelDocId = result.docIdAndVersion().docId + result.docIdAndVersion().docStart;
try {
Explanation explanation = context.searcher().explain(context.query(), topLevelDocId);
return new ExplainResponse(true, explanation);
} catch (IOException e) {
throw new ElasticSearchException("Could not explain", e);
} finally {
context.release();
SearchContext.removeCurrent();
}
}
private ParsedQuery retrieveParsedQuery(ExplainRequest request, IndexService indexService) {
try {
XContentParser parser = XContentHelper.createParser(request.source());
for (XContentParser.Token token = parser.nextToken(); token != XContentParser.Token.END_OBJECT; token = parser.nextToken()) {
if (token == XContentParser.Token.FIELD_NAME) {
String fieldName = parser.currentName();
if ("query".equals(fieldName)) {
return indexService.queryParserService().parse(parser);
} else if ("query_binary".equals(fieldName)) {
byte[] querySource = parser.binaryValue();
XContentParser qSourceParser = XContentFactory.xContent(querySource).createParser(querySource);
return indexService.queryParserService().parse(qSourceParser);
}
}
}
} catch (Exception e) {
throw new ElasticSearchException("Couldn't parse query from source.", e);
}
throw new ElasticSearchException("No query specified");
}
protected ExplainRequest newRequest() {
return new ExplainRequest();
}
protected ExplainResponse newResponse() {
return new ExplainResponse();
}
protected ClusterBlockException checkGlobalBlock(ClusterState state, ExplainRequest request) {
return state.blocks().globalBlockedException(ClusterBlockLevel.READ);
}
protected ClusterBlockException checkRequestBlock(ClusterState state, ExplainRequest request) {
return state.blocks().indexBlockedException(ClusterBlockLevel.READ, request.index());
}
protected ShardIterator shards(ClusterState state, ExplainRequest request) throws ElasticSearchException {
return clusterService.operationRouting()
.getShards(clusterService.state(), request.index(), request.type(), request.id(), request.routing(), request.preference());
}
}

View File

@ -0,0 +1,23 @@
/*
* Licensed to ElasticSearch and Shay Banon under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. ElasticSearch 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.
*/
/**
* Explain action.
*/
package org.elasticsearch.action.explain;

View File

@ -91,6 +91,9 @@ public abstract class SingleShardOperationRequest implements ActionRequest {
return this;
}
protected void beforeLocalFork() {
}
@Override
public void readFrom(StreamInput in) throws IOException {
index = in.readUTF();

View File

@ -149,6 +149,7 @@ public abstract class TransportShardSingleOperationAction<Request extends Single
if (shardRouting.currentNodeId().equals(nodes.localNodeId())) {
if (request.operationThreaded()) {
request.beforeLocalFork();
threadPool.executor(executor).execute(new Runnable() {
@Override
public void run() {

View File

@ -32,6 +32,9 @@ import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.deletebyquery.DeleteByQueryRequest;
import org.elasticsearch.action.deletebyquery.DeleteByQueryRequestBuilder;
import org.elasticsearch.action.deletebyquery.DeleteByQueryResponse;
import org.elasticsearch.action.explain.ExplainRequest;
import org.elasticsearch.action.explain.ExplainRequestBuilder;
import org.elasticsearch.action.explain.ExplainResponse;
import org.elasticsearch.action.get.*;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexRequestBuilder;
@ -431,4 +434,29 @@ public interface Client {
* @param type The type of the doc
*/
PercolateRequestBuilder preparePercolate(String index, String type);
/**
* Computes a score explanation for the specified request.
*
* @param index The index this explain is targeted for
* @param type The type this explain is targeted for
* @param id The document identifier this explain is targeted for
*/
ExplainRequestBuilder prepareExplain(String index, String type, String id);
/**
* Computes a score explanation for the specified request.
*
* @param request The request encapsulating the query and document identifier to compute a score explanation for
*/
ActionFuture<ExplainResponse> explain(ExplainRequest request);
/**
* Computes a score explanation for the specified request.
*
* @param request The request encapsulating the query and document identifier to compute a score explanation for
* @param listener A listener to be notified of the result
*/
void explain(ExplainRequest request, ActionListener<ExplainResponse> listener);
}

View File

@ -36,6 +36,10 @@ import org.elasticsearch.action.deletebyquery.DeleteByQueryAction;
import org.elasticsearch.action.deletebyquery.DeleteByQueryRequest;
import org.elasticsearch.action.deletebyquery.DeleteByQueryRequestBuilder;
import org.elasticsearch.action.deletebyquery.DeleteByQueryResponse;
import org.elasticsearch.action.explain.ExplainAction;
import org.elasticsearch.action.explain.ExplainRequest;
import org.elasticsearch.action.explain.ExplainRequestBuilder;
import org.elasticsearch.action.explain.ExplainResponse;
import org.elasticsearch.action.get.*;
import org.elasticsearch.action.index.IndexAction;
import org.elasticsearch.action.index.IndexRequest;
@ -285,4 +289,19 @@ public abstract class AbstractClient implements InternalClient {
public PercolateRequestBuilder preparePercolate(String index, String type) {
return new PercolateRequestBuilder(this, index, type);
}
@Override
public ExplainRequestBuilder prepareExplain(String index, String type, String id) {
return new ExplainRequestBuilder(this, index, type, id);
}
@Override
public ActionFuture<ExplainResponse> explain(ExplainRequest request) {
return execute(ExplainAction.INSTANCE, request);
}
@Override
public void explain(ExplainRequest request, ActionListener<ExplainResponse> listener) {
execute(ExplainAction.INSTANCE, request, listener);
}
}

View File

@ -30,6 +30,8 @@ import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.deletebyquery.DeleteByQueryRequest;
import org.elasticsearch.action.deletebyquery.DeleteByQueryResponse;
import org.elasticsearch.action.explain.ExplainRequest;
import org.elasticsearch.action.explain.ExplainResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.get.MultiGetRequest;
@ -425,4 +427,14 @@ public class TransportClient extends AbstractClient {
public void percolate(PercolateRequest request, ActionListener<PercolateResponse> listener) {
internalClient.percolate(request, listener);
}
@Override
public ActionFuture<ExplainResponse> explain(ExplainRequest request) {
return internalClient.explain(request);
}
@Override
public void explain(ExplainRequest request, ActionListener<ExplainResponse> listener) {
internalClient.explain(request, listener);
}
}

View File

@ -38,23 +38,25 @@ public class UidField extends AbstractField {
public static class DocIdAndVersion {
public final int docId;
public final int docStart;
public final long version;
public final IndexReader reader;
public DocIdAndVersion(int docId, long version, IndexReader reader) {
public DocIdAndVersion(int docId, long version, IndexReader reader, int docStart) {
this.docId = docId;
this.version = version;
this.reader = reader;
this.docStart = docStart;
}
}
// this works fine for nested docs since they don't have the payload which has the version
// so we iterate till we find the one with the payload
public static DocIdAndVersion loadDocIdAndVersion(IndexReader reader, Term term) {
public static DocIdAndVersion loadDocIdAndVersion(IndexReader subReader, int docStart, Term term) {
int docId = Lucene.NO_DOC;
TermPositions uid = null;
try {
uid = reader.termPositions(term);
uid = subReader.termPositions(term);
if (!uid.next()) {
return null; // no doc
}
@ -70,11 +72,11 @@ public class UidField extends AbstractField {
continue;
}
byte[] payload = uid.getPayload(new byte[8], 0);
return new DocIdAndVersion(docId, Numbers.bytesToLong(payload), reader);
return new DocIdAndVersion(docId, Numbers.bytesToLong(payload), subReader, docStart);
} while (uid.next());
return new DocIdAndVersion(docId, -2, reader);
return new DocIdAndVersion(docId, -2, subReader, docStart);
} catch (Exception e) {
return new DocIdAndVersion(docId, -2, reader);
return new DocIdAndVersion(docId, -2, subReader, docStart);
} finally {
if (uid != null) {
try {

View File

@ -341,13 +341,15 @@ public class RobinEngine extends AbstractIndexShardComponent implements Engine {
Searcher searcher = searcher();
try {
UnicodeUtil.UTF8Result utf8 = Unicode.fromStringAsUtf8(get.uid().text());
for (IndexReader reader : searcher.searcher().subReaders()) {
BloomFilter filter = bloomCache.filter(reader, UidFieldMapper.NAME, asyncLoadBloomFilter);
for (int i = 0; i < searcher.searcher().subReaders().length; i++) {
IndexReader subReader = searcher.searcher().subReaders()[i];
int docStart = searcher.searcher().docStarts()[i];
BloomFilter filter = bloomCache.filter(subReader, UidFieldMapper.NAME, asyncLoadBloomFilter);
// we know that its not there...
if (!filter.isPresent(utf8.result, 0, utf8.length)) {
continue;
}
UidField.DocIdAndVersion docIdAndVersion = UidField.loadDocIdAndVersion(reader, get.uid());
UidField.DocIdAndVersion docIdAndVersion = UidField.loadDocIdAndVersion(subReader, docStart, get.uid());
if (docIdAndVersion != null && docIdAndVersion.docId != Lucene.NO_DOC) {
return new GetResult(searcher, docIdAndVersion);
}

View File

@ -64,6 +64,7 @@ import org.elasticsearch.rest.action.bulk.RestBulkAction;
import org.elasticsearch.rest.action.count.RestCountAction;
import org.elasticsearch.rest.action.delete.RestDeleteAction;
import org.elasticsearch.rest.action.deletebyquery.RestDeleteByQueryAction;
import org.elasticsearch.rest.action.explain.RestExplainAction;
import org.elasticsearch.rest.action.get.RestGetAction;
import org.elasticsearch.rest.action.get.RestHeadAction;
import org.elasticsearch.rest.action.get.RestMultiGetAction;
@ -159,5 +160,7 @@ public class RestActionModule extends AbstractModule {
bind(RestValidateQueryAction.class).asEagerSingleton();
bind(RestMoreLikeThisAction.class).asEagerSingleton();
bind(RestExplainAction.class).asEagerSingleton();
}
}

View File

@ -0,0 +1,146 @@
/*
* Licensed to ElasticSearch and Shay Banon under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. ElasticSearch 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.rest.action.explain;
import org.apache.lucene.search.Explanation;
import org.elasticsearch.ElasticSearchIllegalArgumentException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.explain.ExplainRequest;
import org.elasticsearch.action.explain.ExplainResponse;
import org.elasticsearch.action.explain.ExplainSourceBuilder;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentBuilderString;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.QueryStringQueryBuilder;
import org.elasticsearch.rest.*;
import java.io.IOException;
import static org.elasticsearch.rest.RestRequest.Method.GET;
import static org.elasticsearch.rest.RestStatus.NOT_FOUND;
import static org.elasticsearch.rest.RestStatus.OK;
import static org.elasticsearch.rest.action.support.RestXContentBuilder.restContentBuilder;
/**
* Rest action for computing a score explanation for specific documents.
*/
public class RestExplainAction extends BaseRestHandler {
@Inject
public RestExplainAction(Settings settings, Client client, RestController controller) {
super(settings, client);
controller.registerHandler(GET, "/{index}/{type}/{id}/_explain", this);
}
@Override
public void handleRequest(final RestRequest request, final RestChannel channel) {
final ExplainRequest explainRequest = new ExplainRequest(request.param("index"), request.param("type"), request.param("id"));
explainRequest.parent(request.param("parent"));
explainRequest.routing(request.param("routing"));
explainRequest.preference(request.param("preference"));
String sourceString = request.param("source");
String queryString = request.param("q");
if (request.hasContent()) {
explainRequest.source(request.content(), request.contentUnsafe());
} else if (sourceString != null) {
explainRequest.source(new BytesArray(request.param("source")), false);
} else if (queryString != null) {
QueryStringQueryBuilder queryStringBuilder = QueryBuilders.queryString(queryString);
queryStringBuilder.defaultField(request.param("df"));
queryStringBuilder.analyzer(request.param("analyzer"));
queryStringBuilder.analyzeWildcard(request.paramAsBoolean("analyze_wildcard", false));
queryStringBuilder.lowercaseExpandedTerms(request.paramAsBoolean("lowercase_expanded_terms", true));
queryStringBuilder.lenient(request.paramAsBooleanOptional("lenient", null));
String defaultOperator = request.param("default_operator");
if (defaultOperator != null) {
if ("OR".equals(defaultOperator)) {
queryStringBuilder.defaultOperator(QueryStringQueryBuilder.Operator.OR);
} else if ("AND".equals(defaultOperator)) {
queryStringBuilder.defaultOperator(QueryStringQueryBuilder.Operator.AND);
} else {
throw new ElasticSearchIllegalArgumentException("Unsupported defaultOperator [" + defaultOperator + "], can either be [OR] or [AND]");
}
}
ExplainSourceBuilder explainSourceBuilder = new ExplainSourceBuilder();
explainSourceBuilder.query(queryStringBuilder);
explainRequest.source(explainSourceBuilder);
}
client.explain(explainRequest, new ActionListener<ExplainResponse>() {
@Override
public void onResponse(ExplainResponse response) {
try {
XContentBuilder builder = restContentBuilder(request);
builder.startObject();
builder.field(Fields.OK, response.exists());
builder.field(Fields.MATCHES, response.match());
if (response.hasExplanation()) {
builder.startObject(Fields.EXPLANATION);
buildExplanation(builder, response.explanation());
builder.endObject();
}
builder.endObject();
channel.sendResponse(new XContentRestResponse(request, response.exists() ? OK : NOT_FOUND, builder));
} catch (Exception e) {
onFailure(e);
}
}
private void buildExplanation(XContentBuilder builder, Explanation explanation) throws IOException {
builder.field(Fields.VALUE, explanation.getValue());
builder.field(Fields.DESCRIPTION, explanation.getDescription());
Explanation[] innerExps = explanation.getDetails();
if (innerExps != null) {
builder.startArray(Fields.DETAILS);
for (Explanation exp : innerExps) {
builder.startObject();
buildExplanation(builder, exp);
builder.endObject();
}
builder.endArray();
}
}
@Override
public void onFailure(Throwable e) {
try {
channel.sendResponse(new XContentThrowableRestResponse(request, e));
} catch (IOException e1) {
logger.error("Failed to send failure response", e1);
}
}
});
}
static class Fields {
static final XContentBuilderString OK = new XContentBuilderString("ok");
static final XContentBuilderString MATCHES = new XContentBuilderString("matches");
static final XContentBuilderString EXPLANATION = new XContentBuilderString("explanation");
static final XContentBuilderString VALUE = new XContentBuilderString("value");
static final XContentBuilderString DESCRIPTION = new XContentBuilderString("description");
static final XContentBuilderString DETAILS = new XContentBuilderString("details");
}
}

View File

@ -195,4 +195,14 @@ public class ContextIndexSearcher extends ExtendedIndexSearcher {
super.search(weight, combinedFilter, collector);
}
}
@Override
public Explanation explain(Query query, int doc) throws IOException {
if (searchContext.aliasFilter() == null) {
return super.explain(query, doc);
}
FilteredQuery filteredQuery = new FilteredQuery(query, searchContext.aliasFilter());
return super.explain(filteredQuery, doc);
}
}

View File

@ -0,0 +1,136 @@
/*
* Licensed to ElasticSearch and Shay Banon under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. ElasticSearch 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.test.integration.explain;
import org.elasticsearch.action.explain.ExplainResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.index.query.FilterBuilders;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.test.integration.AbstractNodesTests;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
/**
*/
public class ExplainActionTests extends AbstractNodesTests {
protected Client client;
@BeforeClass
public void startNodes() {
startNode("node1");
startNode("node2");
client = client("node1");
}
@AfterClass
public void closeNodes() {
client.close();
closeAllNodes();
}
@Test
public void testSimple() throws Exception {
client.admin().indices().prepareDelete("test").execute().actionGet();
client.admin().indices().prepareCreate("test").setSettings(
ImmutableSettings.settingsBuilder().put("index.refresh_interval", -1)
).execute().actionGet();
client.prepareIndex("test", "test", "1")
.setSource("field", "value1")
.execute().actionGet();
ExplainResponse response = client.prepareExplain("test", "test", "1")
.setQuery(QueryBuilders.matchAllQuery())
.execute().actionGet();
assertNotNull(response);
assertFalse(response.exists()); // not a match b/c not realtime
assertFalse(response.match()); // not a match b/c not realtime
client.admin().indices().prepareRefresh("test").execute().actionGet();
response = client.prepareExplain("test", "test", "1")
.setQuery(QueryBuilders.matchAllQuery())
.execute().actionGet();
assertNotNull(response);
assertTrue(response.match());
assertNotNull(response.explanation());
assertTrue(response.explanation().isMatch());
assertThat(response.explanation().getValue(), equalTo(1.0f));
client.admin().indices().prepareRefresh("test").execute().actionGet();
response = client.prepareExplain("test", "test", "1")
.setQuery(QueryBuilders.termQuery("field", "value2"))
.execute().actionGet();
assertNotNull(response);
assertTrue(response.exists());
assertFalse(response.match());
assertNotNull(response.explanation());
assertFalse(response.explanation().isMatch());
client.admin().indices().prepareRefresh("test").execute().actionGet();
response = client.prepareExplain("test", "test", "1")
.setQuery(QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("field", "value1"))
.must(QueryBuilders.termQuery("field", "value2"))
)
.execute().actionGet();
assertNotNull(response);
assertTrue(response.exists());
assertFalse(response.match());
assertNotNull(response.explanation());
assertFalse(response.explanation().isMatch());
assertThat(response.explanation().getDetails().length, equalTo(2));
response = client.prepareExplain("test", "test", "2")
.setQuery(QueryBuilders.matchAllQuery())
.execute().actionGet();
assertNotNull(response);
assertFalse(response.exists());
assertFalse(response.match());
}
@Test
public void testExplainWithAlias() throws Exception {
client.admin().indices().prepareDelete().execute().actionGet();
client.admin().indices().prepareCreate("test")
.execute().actionGet();
client.admin().indices().prepareAliases().addAlias("test", "alias1", FilterBuilders.termFilter("field2", "value2"))
.execute().actionGet();
client.prepareIndex("test", "test", "1").setSource("field1", "value1", "field2", "value1").execute().actionGet();
client.admin().indices().prepareRefresh("test").execute().actionGet();
ExplainResponse response = client.prepareExplain("alias1", "test", "1")
.setQuery(QueryBuilders.matchAllQuery())
.execute().actionGet();
assertNotNull(response);
assertTrue(response.exists());
assertFalse(response.match());
}
}

View File

@ -52,14 +52,14 @@ public class UidFieldTests {
writer.addDocument(doc);
reader = reader.reopen();
assertThat(UidField.loadVersion(reader, new Term("_uid", "1")), equalTo(-2l));
assertThat(UidField.loadDocIdAndVersion(reader, new Term("_uid", "1")).version, equalTo(-2l));
assertThat(UidField.loadDocIdAndVersion(reader, 0, new Term("_uid", "1")).version, equalTo(-2l));
doc = new Document();
doc.add(new UidField("_uid", "1", 1));
writer.updateDocument(new Term("_uid", "1"), doc);
reader = reader.reopen();
assertThat(UidField.loadVersion(reader, new Term("_uid", "1")), equalTo(1l));
assertThat(UidField.loadDocIdAndVersion(reader, new Term("_uid", "1")).version, equalTo(1l));
assertThat(UidField.loadDocIdAndVersion(reader, 0, new Term("_uid", "1")).version, equalTo(1l));
doc = new Document();
UidField uid = new UidField("_uid", "1", 2);
@ -67,7 +67,7 @@ public class UidFieldTests {
writer.updateDocument(new Term("_uid", "1"), doc);
reader = reader.reopen();
assertThat(UidField.loadVersion(reader, new Term("_uid", "1")), equalTo(2l));
assertThat(UidField.loadDocIdAndVersion(reader, new Term("_uid", "1")).version, equalTo(2l));
assertThat(UidField.loadDocIdAndVersion(reader, 0, new Term("_uid", "1")).version, equalTo(2l));
// test reuse of uid field
doc = new Document();
@ -76,11 +76,11 @@ public class UidFieldTests {
writer.updateDocument(new Term("_uid", "1"), doc);
reader = reader.reopen();
assertThat(UidField.loadVersion(reader, new Term("_uid", "1")), equalTo(3l));
assertThat(UidField.loadDocIdAndVersion(reader, new Term("_uid", "1")).version, equalTo(3l));
assertThat(UidField.loadDocIdAndVersion(reader, 0, new Term("_uid", "1")).version, equalTo(3l));
writer.deleteDocuments(new Term("_uid", "1"));
reader = reader.reopen();
assertThat(UidField.loadVersion(reader, new Term("_uid", "1")), equalTo(-1l));
assertThat(UidField.loadDocIdAndVersion(reader, new Term("_uid", "1")), nullValue());
assertThat(UidField.loadDocIdAndVersion(reader, 0, new Term("_uid", "1")), nullValue());
}
}