From 8365e7ba0b86c76efbcb615db0f7bbaf2a89b6b0 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Mon, 20 Aug 2012 18:06:36 +0200 Subject: [PATCH] - Added explain api. #2184 --- .../elasticsearch/action/ActionModule.java | 4 + .../action/explain/ExplainAction.java | 44 ++++ .../action/explain/ExplainRequest.java | 205 ++++++++++++++++++ .../action/explain/ExplainRequestBuilder.java | 133 ++++++++++++ .../action/explain/ExplainResponse.java | 96 ++++++++ .../action/explain/ExplainSourceBuilder.java | 76 +++++++ .../explain/TransportExplainAction.java | 165 ++++++++++++++ .../action/explain/package-info.java | 23 ++ .../shard/SingleShardOperationRequest.java | 3 + .../TransportShardSingleOperationAction.java | 1 + .../java/org/elasticsearch/client/Client.java | 28 +++ .../client/support/AbstractClient.java | 19 ++ .../client/transport/TransportClient.java | 12 + .../common/lucene/uid/UidField.java | 14 +- .../index/engine/robin/RobinEngine.java | 8 +- .../rest/action/RestActionModule.java | 3 + .../action/explain/RestExplainAction.java | 146 +++++++++++++ .../search/internal/ContextIndexSearcher.java | 10 + .../explain/ExplainActionTests.java | 136 ++++++++++++ .../unit/common/lucene/uid/UidFieldTests.java | 10 +- 20 files changed, 1122 insertions(+), 14 deletions(-) create mode 100644 src/main/java/org/elasticsearch/action/explain/ExplainAction.java create mode 100644 src/main/java/org/elasticsearch/action/explain/ExplainRequest.java create mode 100644 src/main/java/org/elasticsearch/action/explain/ExplainRequestBuilder.java create mode 100644 src/main/java/org/elasticsearch/action/explain/ExplainResponse.java create mode 100644 src/main/java/org/elasticsearch/action/explain/ExplainSourceBuilder.java create mode 100644 src/main/java/org/elasticsearch/action/explain/TransportExplainAction.java create mode 100644 src/main/java/org/elasticsearch/action/explain/package-info.java create mode 100644 src/main/java/org/elasticsearch/rest/action/explain/RestExplainAction.java create mode 100644 src/test/java/org/elasticsearch/test/integration/explain/ExplainActionTests.java diff --git a/src/main/java/org/elasticsearch/action/ActionModule.java b/src/main/java/org/elasticsearch/action/ActionModule.java index 16539186e89..76016008f25 100644 --- a/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/src/main/java/org/elasticsearch/action/ActionModule.java @@ -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 actionsBinder diff --git a/src/main/java/org/elasticsearch/action/explain/ExplainAction.java b/src/main/java/org/elasticsearch/action/explain/ExplainAction.java new file mode 100644 index 00000000000..fbf355dc41f --- /dev/null +++ b/src/main/java/org/elasticsearch/action/explain/ExplainAction.java @@ -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 { + + 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(); + } +} diff --git a/src/main/java/org/elasticsearch/action/explain/ExplainRequest.java b/src/main/java/org/elasticsearch/action/explain/ExplainRequest.java new file mode 100644 index 00000000000..0cb0bdd0b0f --- /dev/null +++ b/src/main/java/org/elasticsearch/action/explain/ExplainRequest.java @@ -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); + } +} diff --git a/src/main/java/org/elasticsearch/action/explain/ExplainRequestBuilder.java b/src/main/java/org/elasticsearch/action/explain/ExplainRequestBuilder.java new file mode 100644 index 00000000000..e895eec69d7 --- /dev/null +++ b/src/main/java/org/elasticsearch/action/explain/ExplainRequestBuilder.java @@ -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 { + + 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 listener) { + if (sourceBuilder != null) { + request.source(sourceBuilder); + } + + client.explain(request, listener); + } + + private ExplainSourceBuilder sourceBuilder() { + if (sourceBuilder == null) { + sourceBuilder = new ExplainSourceBuilder(); + } + return sourceBuilder; + } + +} diff --git a/src/main/java/org/elasticsearch/action/explain/ExplainResponse.java b/src/main/java/org/elasticsearch/action/explain/ExplainResponse.java new file mode 100644 index 00000000000..e782bd40130 --- /dev/null +++ b/src/main/java/org/elasticsearch/action/explain/ExplainResponse.java @@ -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); + } + } +} diff --git a/src/main/java/org/elasticsearch/action/explain/ExplainSourceBuilder.java b/src/main/java/org/elasticsearch/action/explain/ExplainSourceBuilder.java new file mode 100644 index 00000000000..77d1ce9de3f --- /dev/null +++ b/src/main/java/org/elasticsearch/action/explain/ExplainSourceBuilder.java @@ -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); + } + } +} diff --git a/src/main/java/org/elasticsearch/action/explain/TransportExplainAction.java b/src/main/java/org/elasticsearch/action/explain/TransportExplainAction.java new file mode 100644 index 00000000000..57f25dae0b6 --- /dev/null +++ b/src/main/java/org/elasticsearch/action/explain/TransportExplainAction.java @@ -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 { + + 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()); + } +} diff --git a/src/main/java/org/elasticsearch/action/explain/package-info.java b/src/main/java/org/elasticsearch/action/explain/package-info.java new file mode 100644 index 00000000000..33b74e03c72 --- /dev/null +++ b/src/main/java/org/elasticsearch/action/explain/package-info.java @@ -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; diff --git a/src/main/java/org/elasticsearch/action/support/single/shard/SingleShardOperationRequest.java b/src/main/java/org/elasticsearch/action/support/single/shard/SingleShardOperationRequest.java index 0d6aaa5daff..6a3258c6546 100644 --- a/src/main/java/org/elasticsearch/action/support/single/shard/SingleShardOperationRequest.java +++ b/src/main/java/org/elasticsearch/action/support/single/shard/SingleShardOperationRequest.java @@ -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(); diff --git a/src/main/java/org/elasticsearch/action/support/single/shard/TransportShardSingleOperationAction.java b/src/main/java/org/elasticsearch/action/support/single/shard/TransportShardSingleOperationAction.java index 010d34094a4..ce87bb1d6f4 100644 --- a/src/main/java/org/elasticsearch/action/support/single/shard/TransportShardSingleOperationAction.java +++ b/src/main/java/org/elasticsearch/action/support/single/shard/TransportShardSingleOperationAction.java @@ -149,6 +149,7 @@ public abstract class TransportShardSingleOperationAction 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 listener); + } \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/client/support/AbstractClient.java b/src/main/java/org/elasticsearch/client/support/AbstractClient.java index 46a6e5653e9..0c80799a43f 100644 --- a/src/main/java/org/elasticsearch/client/support/AbstractClient.java +++ b/src/main/java/org/elasticsearch/client/support/AbstractClient.java @@ -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 explain(ExplainRequest request) { + return execute(ExplainAction.INSTANCE, request); + } + + @Override + public void explain(ExplainRequest request, ActionListener listener) { + execute(ExplainAction.INSTANCE, request, listener); + } } diff --git a/src/main/java/org/elasticsearch/client/transport/TransportClient.java b/src/main/java/org/elasticsearch/client/transport/TransportClient.java index bcad0511799..24e107e8424 100644 --- a/src/main/java/org/elasticsearch/client/transport/TransportClient.java +++ b/src/main/java/org/elasticsearch/client/transport/TransportClient.java @@ -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 listener) { internalClient.percolate(request, listener); } + + @Override + public ActionFuture explain(ExplainRequest request) { + return internalClient.explain(request); + } + + @Override + public void explain(ExplainRequest request, ActionListener listener) { + internalClient.explain(request, listener); + } } diff --git a/src/main/java/org/elasticsearch/common/lucene/uid/UidField.java b/src/main/java/org/elasticsearch/common/lucene/uid/UidField.java index 885328f068b..2fd24860281 100644 --- a/src/main/java/org/elasticsearch/common/lucene/uid/UidField.java +++ b/src/main/java/org/elasticsearch/common/lucene/uid/UidField.java @@ -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 { diff --git a/src/main/java/org/elasticsearch/index/engine/robin/RobinEngine.java b/src/main/java/org/elasticsearch/index/engine/robin/RobinEngine.java index 6edc75baec2..c500bd21392 100644 --- a/src/main/java/org/elasticsearch/index/engine/robin/RobinEngine.java +++ b/src/main/java/org/elasticsearch/index/engine/robin/RobinEngine.java @@ -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); } diff --git a/src/main/java/org/elasticsearch/rest/action/RestActionModule.java b/src/main/java/org/elasticsearch/rest/action/RestActionModule.java index 7b37770f645..3fa46e2c8fd 100644 --- a/src/main/java/org/elasticsearch/rest/action/RestActionModule.java +++ b/src/main/java/org/elasticsearch/rest/action/RestActionModule.java @@ -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(); } } diff --git a/src/main/java/org/elasticsearch/rest/action/explain/RestExplainAction.java b/src/main/java/org/elasticsearch/rest/action/explain/RestExplainAction.java new file mode 100644 index 00000000000..5b67fc458f9 --- /dev/null +++ b/src/main/java/org/elasticsearch/rest/action/explain/RestExplainAction.java @@ -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() { + + @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"); + } +} \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/search/internal/ContextIndexSearcher.java b/src/main/java/org/elasticsearch/search/internal/ContextIndexSearcher.java index 91e6bc4a858..3056befe761 100644 --- a/src/main/java/org/elasticsearch/search/internal/ContextIndexSearcher.java +++ b/src/main/java/org/elasticsearch/search/internal/ContextIndexSearcher.java @@ -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); + } } \ No newline at end of file diff --git a/src/test/java/org/elasticsearch/test/integration/explain/ExplainActionTests.java b/src/test/java/org/elasticsearch/test/integration/explain/ExplainActionTests.java new file mode 100644 index 00000000000..95f26e22d64 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/integration/explain/ExplainActionTests.java @@ -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()); + } + +} diff --git a/src/test/java/org/elasticsearch/test/unit/common/lucene/uid/UidFieldTests.java b/src/test/java/org/elasticsearch/test/unit/common/lucene/uid/UidFieldTests.java index e406abfaf3f..eccb3d38960 100644 --- a/src/test/java/org/elasticsearch/test/unit/common/lucene/uid/UidFieldTests.java +++ b/src/test/java/org/elasticsearch/test/unit/common/lucene/uid/UidFieldTests.java @@ -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()); } }