-
Notifications
You must be signed in to change notification settings - Fork 25.6k
Add parsing from xContent to SearchResponse #22533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
In preparation to be able to parse SearchResponse from its rest representation for the java rest client, this adds fromXContent to SearchResponse. Most of the information in the original object is preserved when parsing it back. However, the exceptions in the "failure" section won't be identical to the original ones on the server side since they are parsed back to a generic ElasticsearchException on the receiving side. Also the "aggregations", "suggest" and "profile" section parsing is currently skipped and will be added by subsequent PRs.
javanna
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
left some comments, looks good though. I would start a new feature branch based on this given that the parsing will be completed only once we have also done profile, suggest and aggs sections which will take a while.
| if (Fields._SCROLL_ID.equals(currentFieldName)) { | ||
| scrollId = parser.text(); | ||
| } else if (Fields.TOOK.equals(currentFieldName)) { | ||
| tookInMillis = parser.intValue(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isn't took a long?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right, changes that
| timedOut = parser.booleanValue(); | ||
| } else if (Fields.TERMINATED_EARLY.equals(currentFieldName)) { | ||
| terminatedEarly = parser.booleanValue(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move the else to this line?
| return Strings.toString(this); | ||
| } | ||
|
|
||
| public static class InternalSearchResponse { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just wondering, do we even need this class? Shall we rather collapse all of its fields to SearchResponse ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is still needed as its own class because it is used as an "incomplete" search response in e.g. SeachPhaseController#merge() and in some other places where some information we need for the SearchResponse ctor is not available yet. I also wanted to keep the amount of changes small, so I'd prefer to leave it for now. This would be another refactoring that is not necessary for parsing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's fine.
| return Strings.toString(this); | ||
| } | ||
|
|
||
| public static class InternalSearchResponse { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this still implement Streamable if we keep it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could, it still has the read/write methods. However, they should not be used anywhere else than in SearchResponse, so I'd go one step in the other direction and even reduce the visibility of write/read.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok because although the class can be created separately, it should always be used as part of the SearchResponse when serializing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just checked where InternalSearchResponse is used, all places that I found where this is instantiated it is later wrapped in a SearchResponse in the same method, so I think it's never sent across the wire independently.
| response.getShardFailures()); | ||
| } | ||
|
|
||
| public static final class ShardsFields { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need this inner class? I think we tend to be removing those when we find them rather than adding new ones
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure, I will remove it
|
|
||
| // the "_shard/total/failures" section makes if impossible to directly compare xContent, because | ||
| // the failures in the parsed SearchResponse are wrapped in an extra ElasticSearchException on the client side. | ||
| // Because of this we compare the "top level" fields for equality and the subsections xContent equivalence independently |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just an idea, but maybe we could instead generate random response without exceptions and check that like we usually do, and have specific tests for exceptions? Otherwise exceptions affect the way we test the rest of the response which doesn't sound optimal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I will change this. However, I'd like the createTestItem() method to still be able to output an object with set "failures", so I will make this controllable from the outside. In our "normal" fromXContent() test we can use xContent comparison again then, In another test with "failures" at the moment the only thing we can really check is that we can parse the response without throwing an error I think. Other more detailed tests are covered by ShardSearchFailureTests already I think.
| SearchResponse response = new SearchResponse( | ||
| new InternalSearchResponse(new InternalSearchHits(hits, 100, 1.5f), null, null, null, false, null), null, 0, 0, 0, | ||
| new ShardSearchFailure[0]); | ||
| BytesReference xContent = toXContent(response, XContentType.JSON); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you could call Strings.toString instead I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to know.
|
|
||
| public void testFromXContent() throws IOException { | ||
| ShardSearchFailure response = createTestItem(); | ||
| XContentType xcontentType = XContentType.JSON; //randomFrom(XContentType.values()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leftover?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep
| assertEquals(response.shardId(), parsed.shardId()); | ||
|
|
||
| // we cannot compare the cause, because it will be wrapped in an outer ElasticSearchException | ||
| // best effort: try to check that the original message appears somewhere in the renderes xContent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/renderes/rendered . Instead, we could build what we expect given the exception? Isn't it just wrapping what we have into an ElasticsearchException with the same message?
| assertMapEquals((Map<String, Object>) expected, (Map<String, Object>) actual, path); | ||
| } else if (expected instanceof List) { | ||
| assertListEquals((List<Object>) expected, (List<Object>) actual); | ||
| assertListEquals((List<Object>) expected, (List<Object>) actual, path); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we still need these changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added these path arguments to be able to better debug errors in map-comparisons. When we compare xContent and some nested map has different size, we currently only get the expected and actual size as output. This is very useful to print in the test error message, so I'd like to leave it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't follow, there is a new argument to these methods that always gets passed in as "" ? or what am I missing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got it, ok!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In assertToXContentEquivalent() we start with an empty path. Whenever assertMapEquals() finds an expected key, we append that to the path. Whenever an expected value or a map size differs, we additionally print the collected path information up until then. Its basically a recursion, "path" being the collector. the assertListEquals() method also needs the argument because we need to pass it on for other nested objects inside.
tlrx
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like it, thanks for doing it @cbuescher. I think it makes sense to have a dedicated branch until every pieces are merged in, though you could already add ShardSearchFailure changes in its own pull request.
| } else { | ||
| throwUnknownField(currentFieldName, parser.getTokenLocation()); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it needs another else block with throwUnknownField(currentFieldName, parser.getTokenLocation()) here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, on that level its more like an unexpected token, we have another helper for that which I will use.
| } | ||
| } | ||
| return new SearchResponse(new InternalSearchResponse(hits, null, null, null, timedOut, terminatedEarly), | ||
| scrollId, totalShards, successfulShards, tookInMillis, failures.toArray(new ShardSearchFailure[0])); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use failures.toArray(new ShardSearchFailure[failures.size()]) ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, good point.
| private static final String REASON_FIELD = "reason"; | ||
| private static final String NODE_FIELD = "node"; | ||
| private static final String INDEX_FIELD = "index"; | ||
| private static final String SHARD_FIELD = "shard"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think in other classes we didn't add a _FIELD suffix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just added the _FIELD suffix to other classes in this PR, I think it better communicates that this is a field name.
| throwUnknownField(currentFieldName, parser.getTokenLocation()); | ||
| } | ||
| } else { | ||
| throw new ParsingException(parser.getTokenLocation(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
XContentParserUtils.throwUnknownToken() instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, forgot about that for a second
| builder.field("total", total); | ||
| builder.field("successful", successful); | ||
| builder.field("failed", failed); | ||
| builder.startObject(ShardsFields._SHARDS); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would make sense, yes.
| import com.carrotsearch.hppc.cursors.ObjectCursor; | ||
| import com.carrotsearch.hppc.cursors.ObjectObjectCursor; | ||
|
|
||
| import org.apache.logging.log4j.message.ParameterizedMessage; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be nice to revert this change (it "pollutes" a bit the Git history)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense, thanks for catching, didn't see that
| + "\"max_score\":1.5," | ||
| + "\"hits\":[{\"_type\":\"type\",\"_id\":\"id1\",\"_score\":2.0}]" | ||
| + "}" | ||
| + "}", xContent.utf8ToString()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the indentation ;)
| try (XContentParser expectedParser = xContentType.xContent().createParser(NamedXContentRegistry.EMPTY, expected)) { | ||
| Map<String, Object> expectedMap = expectedParser.map(); | ||
| assertMapEquals(expectedMap, actualMap); | ||
| assertMapEquals(expectedMap, actualMap, ""); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this empty path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned in another comment, I added these path arguments to be able to better debug errors in map-comparisons when we compare xContent. This empty path is used as initial value, assertMapEquals() will be called recursively later.
893ed52 to
6bbbd2e
Compare
javanna
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
left a couple of comments, LGTM otherwise, no need for another review round on my end
| Boolean terminatedEarly = null; | ||
| long tookInMillis = 0; | ||
| int successfulShards = 0; | ||
| int totalShards = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shall we use -1 as default values, just to make sure not to confuse them with actual values that get returned?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
| String currentFieldName = null; | ||
| InternalSearchHits hits = null; | ||
| boolean timedOut = false; | ||
| Boolean terminatedEarly = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shall timedOut also be a Boolean? maybe not because it's always returned while terminatedEarly is not? trying to understand the difference between the two
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, thats it. "timedOut" is always set, always rendered out, so it should always be parsed. "terminatedEarly" can also be "null" in the original object, it only rendered when its present, so its okay if we don't parse it.
| // we cannot compare the cause, because it will be wrapped in an outer ElasticSearchException | ||
| // best effort: try to check that the original message appears somewhere in the rendered xContent | ||
| String originalMsg = response.getCause().getMessage(); | ||
| assertTrue(parsed.getCause().getMessage().contains(originalMsg)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead, can we build what we expect given the exception? Isn't it just wrapping what we have into an ElasticsearchException with the same message? Or does it get too complicated? I think Tanguy did something similar in some other test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will look into it but won't spend too much time on it at this point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Building the "expected" value (either the object or its xContent rendering) is really difficult and will break easiliy without using ElasticsearchException#fromXContent(), which unfortunately is what we want to test here. The way this wraps the original exception and rewrites the message in it is hard to mock without actually repeating it, which defies the usefulness of a test IMHO. Still looking but I'm a bit clueless at this point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can be complicated to rebuild the object, how about doing something like:
assertEquals(parsed.getCause().getMessage(), "Elasticsearch exception [type=parse_exception, reason=" + originalMsg +"]");
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, thats another slightly different option, that works when we don't test for nested causes which I think we don't have to here. Will change this, although it doesn't add much to the current version.
|
Closing this since it is quiet stale now. |
In preparation to be able to parse SearchResponse from its rest representation
for the java rest client, this adds fromXContent to SearchResponse. Most of the
information in the original object is preserved when parsing it back. However,
the exceptions in the "failure" section won't be identical to the original ones
on the server side since they are parsed back to a generic
ElasticsearchException on the receiving side. Also the "aggregations", "suggest"
and "profile" section parsing is currently skipped and will be added by
subsequent PRs.