(JAVA, Elasticsearch) How can I get fields from SearchResponse? - java

I just wonder how I get fields from SearchResponse which is result of my query.
Below is my query:
{"size":99,"timeout":"10s","query":{"bool":{"filter":[{"bool":{"must":[{"range":{"LOG_GEN_TIME":{"from":"2018-11-01 12:00:01+09:00","to":"2018-11-01 23:59:59+09:00","include_lower":true,"include_upper":true,"boost":1.0}}},{"wrapper":{"query":"eyAiYm9vbCIgOiB7ICJtdXN0IiA6IFsgeyAidGVybSIgOiB7ICJBU1NFVF9JUCIgOiAiMTAuMTExLjI1Mi4xNiIgfSB9LCB7ICJ0ZXJtIiA6IHsgIkFDVElPTl9UWVBFX0NEIiA6ICIyIiB9IH0sIHsgInRlcm0iIDogeyAiRFNUX1BPUlQiIDogIjgwIiB9IH0gXSB9IH0="}}],"adjust_pure_negative":true,"boost":1.0}}],"adjust_pure_negative":true,"boost":1.0}},"_source":{"includes":["LOG_GEN_TIME","LOG_NO","ASSET_NO"],"excludes":[]},"sort":[{"LOG_GEN_TIME":{"order":"desc"}},{"LOG_NO":{"order":"desc"}}]}
and when I query this, like below:
SearchResponse searchResponse = request.get();
I got right result:
{
"took":1071,
"timed_out":false,
"_shards":{
"total":14,
"successful":14,
"skipped":0,
"failed":0
},
"_clusters":{
"total":0,
"successful":0,
"skipped":0
},
"hits":{
"total":2,
"max_score":null,
"hits":[
{
"_index":"log_20181101",
"_type":"SEC",
"_id":"1197132746951492963",
"_score":null,
"_source":{
"ASSET_NO":1,
"LOG_NO":1197132746951492963,
"LOG_GEN_TIME":"2018-11-01 09:46:28+09:00"
},
"sort":[
1541033188000,
1197132746951492963
]
},
{
"_index":"log_20181101",
"_type":"SEC",
"_id":"1197132746951492963",
"_score":null,
"_source":{
"ASSET_NO":2,
"LOG_NO":1197337264704454700,
"LOG_GEN_TIME":"2018-11-01 23:00:06+09:00"
},
"sort":[
1541080806000,
1197337264704454700
]
}
]
}
}
To use this result, I need to map this by field and value.
I think there's a way to map the field and value to the 'fields' parameter so that we could use it nicely, but I cannot find.
I hope I can use the result like this way:
SearchHit hit = ...
Map<String, SearchHitField> fields = hit.getFields();
String logNo = fields.get("LOG_NO").value();
And It seems like this is the common way to use..
Or am I misunderstanding something? Tell me other way if there's better way, please.
Any comment would be appreciated. Thanks.

I'm not clear what client you are using to query elastic. If you are using elasticsearch high level rest client then you can loop through hits and to get source you can use hit.getSourceAsMap() to get the key value of fields.
For your comment:
Firstly create a POJO class which corresponds to _source (i.e. index properties; the way data is store in elastic)
The use hit.getSourceAsString() to get _source in json format.
Use jackson ObjectMapper to map json to your pojo
Assuming you created a POJO class AssetLog
SearchHit[] searchHits = searchResponse.getHits().getHits();
for (SearchHit searchHit : searchHits) {
String hitJson = searchHit.getSourceAsString();
ObjectMapper objectMapper = new ObjectMapper();
AssetLog source = objectMapper.readValue(hitJson, AssetLog.class);
//Store source to map/array
}
Hope this helps.

Related

Java get nested value from ResponseEntity without creating a pojo

I am trying to get a single nested value from a ResponseEntity but I am trying to do so without having to create a pojo for every possible item as this is a third party api response.
Example response.getBody() as it appears in Postman:
{
"message": "2 records found",
"records": [
{
"Account": {
"Id": "1",
"Name": "Foo Inc"
},
"CaseNumber": "200",
"Contact": {
"FirstName": "Foo",
"LastName": "Bar"
},
"Status": "In Progress",
"StatusMessage": "We are working on this."
},
{
"Account": {
"Id": "1",
"Name": "Foo Inc"
},
"CaseNumber": "100",
"Contact": {
"FirstName": "Foo",
"LastName": "Bar"
},
"Status": "Closed"
}
]
}
Basically, if I were in JS, I am looking for:
for(let record of res.body.records){
if(record && record.CaseNumber === "200"){
console.log(record.Status)
}
res.body.records[0].Status
Currently, they are are doing this to check if the response is empty:
ResponseEntity<Object> response = restTemplate.exchange(sfdcURL, HttpMethod.POST, entity, Object.class);
LinkedHashMap<Object, Object> resMap = (LinkedHashMap<Object, Object>) response.getBody();
List<Object> recordsList = (List<Object>) resMap.get("records");
if (recordsList.size() <= 0) { return error }
But I need to get the value of of "Status" and I need to do so without creating a pojo.
I appreciate any guidance on how I can do this in Java
UPDATE
So the response.getBody() is returned and when it is displayed in Postman, it looks like the pretty JSON shown above. However, when I do:
System.out.println(response.getBody().toString())
it looks like:
{message=2 Records Found, records=[{Account={Id=1, Name=Foo Inc}, CaseNumber=200, Contact={FirstName=Foo, LastName=Bar}, //etc
To make it worse, one of the fields appears in the console as follows (including linebreaks):
[...], Status=In Progress, LastEmail=From: noreply#blah.com
Sent: 2022-08-08 10:14:54
To: foo#bar.com
Subject: apropos case #200
Hello Foo,
We are working on your case and stuff
Thank you,
us, StatusMessage=We are working on this., OtherFields=blah, [...]
text.replaceAll("=", ":") would help some, but won't add quotations marks nor would it help separate that email block.
How can I so that the responses here like ObjectMapper and JSONObject can work?
You can either convert the string to valid json (not that trivial) and deserialise into a Map<String, Object>, or just pluck the value out of the raw string using regex:
String statusOfCaseNumber200 = response.getBody().toString()
.replaceAll(".*CaseNumber=200\\b.*?\\bStatus=([^,}]*).*", "$1");
This matches the whole string, captures the desired status value then replaces with the status, effectively "extracting" it.
The regex:
.*CaseNumber=200\b everything up to and including CaseNumber=200 (not matching longer numbers like 2001)
.*? as few chars as possible
\\bStatus= "Status=" without any preceding word chars
([^,}]*) non comma/curly brace characters
.* the rest
It's not bulletproof, but it will probably work for your use case so it doesn't need to be bulletproof.
Some test code:
String body = "{message=2 Records Found, records=[{Account={Id=1, Name=Foo Inc}, CaseNumber=200, Contact={FirstName=Foo, LastName=Bar}, Status=In Progress, StatusMessage=We are working on this.}, {Account={Id=1, Name=Foo Inc}, CaseNumber=100, Contact={FirstName=Foo, LastName=Bar}, Status=Closed}]";
String statusOfCaseNumber200 = body.replaceAll(".*CaseNumber=200\\b.*?\\bStatus=([^,}]*).*", "$1");
System.out.println(statusOfCaseNumber200); // "In Progress"
PLEASE DO NOT use Genson as Hiran showed in his example. The library hasn't been updated since 2019 and has many vulnerable dependencies!
Use Jackson or Gson.
Here how you can serialize a string into a Jackson JsonNode:
ObjectMapper mapper = new ObjectMapper();
String json = ...;
JsonNode node = mapper.readTree(json);
If you want to serialize a JSON object string into a Map:
ObjectMapper mapper = new ObjectMapper();
String json = ...;
Map<String, Object> map = mapper.readValue(json, HashMap.class);
You can read more about JsonNode here and a tutorial here.
You can use JSON-Java library and your code will look like this:
JSONObject jsonObject = new JSONObject(JSON_STRING);
String status = jsonObject.getJSONArray("records")
.getJSONObject(0)
.getString("Status");
System.out.println(status);
Or in a loop
JSONArray jsonArray = new JSONObject(jsonString).getJSONArray("records");
for(int i =0; i < jsonArray.length(); i++) {
String status = jsonArray
.getJSONObject(i)
.getString("Status");
System.out.println(status);
}
So the response.getBody() is returned and when it is displayed in Postman, it looks like the pretty JSON shown above. However, when I do:
...
text.replaceAll("=", ":") would help some, but won't add quotations
marks nor would it help separate that email block.
How can I so that the responses here like ObjectMapper and JSONObject
can work?
Firstly, Jackson is the default message converter which Spring Web uses under the hood to serialize and deserialize JSON. You don't need to introduce any dependencies.
Secondly, the process serialization/deserialization is handled by the framework automatically, so that in many cases you don't need to deal with the ObjectMapper yourself.
To emphasize, I'll repeat: in most of the cases in Spring you don't need to handle raw JSON yourself. And in the body of ResponseEntiry<Object> produced by the method RestTemplate.exchange() you have a LinkedHashMap in the guise of Object, it's not a raw JSON (if you want to know why it is a LinkedHashMap, well because that's how Jackson stores information, and it's a subclass of Object like any other class in Java). And sure, when you're invoking toString() on any implementation of the Map you'll get = between a Key and a Value.
So, the problem you've mentioned in the updated question is artificial.
If you want to deal with a Map instead of an object with properly typed properties and here's how you can do that:
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<LinkedHashMap<String, Object>> response = restTemplate.exchange(
sfdcURL, HttpMethod.POST, entity, new ParameterizedTypeReference<>() {}
);
Map<String, Object> resMap = response.getBody();
List<Object> recordsList = (List<Object>) resMap.get("records");
if (recordsList.isEmpty()) { ... }
If there are redundant lines in the Values which you want to trim, then as a remedy you can introduce a custom Jackson-module declaring a Deserializer which would handle leading/trailing white-space and new lines, described in this answer. Deserialize in the module would be applied by default, other options would require creating classes representing domain objects which you for some reasons want to avoid.
As Oliver suggested JsonNode seems to be the best approach. But, if I receive the ResponseEntity<Object>, I still cannot figure out a way to convert it to readable Json (and thus convert it to JsonNode), so I am still open to responses for that part.
I was able to get it to work by changing the ResponseEntity<Object> to ResponseEntity<JsonNode> so this is what I will be submitting for now:
ResponseEntity<JsonNode> response = restTemplate.exchange(sfdcURL,
HttpMethod.POST, entity, JsonNode.class);
JsonNode records = response.getBody().get("records");
String status = null;
String statusMessage = null;
for (JsonNode rec : records) {
if(rec.get("CaseNumber").asText().equals(caseNumber)) {
status = rec.get("Status").asText();
if(rec.has("StatusMessage")) {
statusMessage = rec.get("StatusMessage").asText();
}
} else {
statusMessage = "Invalid CaseNumber";
}
}
Because the overall method returns a ResponseEntity<Object> I then converted my strings to a HashMap and returned that:
HashMap<String, String> resMap = new HashMap<String, String>();
resMap.put("Status", status);
resMap.put("StatusMessage", statusMessage);
return new ResponseEntity<>(resMap, HttpStatus.OK);
This is not a perfect solution, but it works for now. Would still be better for exception handling if I could receive a ResponseEntity<Object> and then convert it to a JsonNode though. Thanks everyone for the responses!

Map json dynamically

I have a requirement to build a JSON dynamically and need to call an external API.
For instance,
Input : "FIRST_NAME": "XXX"
Based on the above input I need to build a JSON dynamically like below
{
"Req":{
"user":{
"CreatedTime":"2017-03-02T07:52:58Z",
"UpdatedTime":"2017-03-02T07:52:58Z",
"Details":{
"Names":[
{
"Name":{
"First":"kirtq"
}
}
]
}
}
}
}
If I get contact number as input : CONTACT_NUMBER:889999999
Then I have to build a JSON like below
{
"UpdateMemberReq": {
"Customer": {
"CreatedTime": "2017-03-02T07:52:58Z",
"UpdatedTime": "2017-03-02T07:52:58Z",
"CustomerDetails": {
"Contacts": {
"MobilePhone": {
"value": "07888728687"
}
}
}
}
}
}
Like this I have around 30 fields for each request I will get one filed based on that I have to build a JSON dynamically and once I prepared the JSON dynamically I have to call an external API (POST) by passing this JSON as raw type in the body.
I have implemented like below .
List list = new ArrayList();
Name user = mapper.readValue(json2, Name.class);
System.out.println(user);
Map<String, Object> name1 = new HashMap<>();
name1.put("Name", user);
list.add(name1);
Map<String, Object> map1 = new HashMap<>();
map1.put("Names", list);
Map<String, Object> map2 = new HashMap<>();
map2.put("CustomerDetails",map1);
Map<String,Object> map = new HashMap();
map.put("Customer",map2);
Can anyone suggest to me the best way to handle this in java/spring boot?
Thanks!!
Can anyone suggest to me the best way to handle this in java/spring boot?
Given that you don't have a fixed schema for which you want to create JSON, you'll have to do exactly like you do.
This means assembling a map dynamically and then mapping it to a json string.
What you can do to improve is try to extract common and reusable components, for building certain parts of the request.
I'd recommend you create a class structure to keep things manageable with some classes like ...
JsonGenerationService ( the main service the rest of the code uses )
UserJsonGenerator -> generates JSON for user entities
CustomerJsonGenerator -> generates JSON for customers
JsonGeneratorCommon -> contains all the common methods

Elasticsearch - Terms Aggregation nested field

I have following problem. I have a nested field ("list") with 2 properties (fieldB & fieldC).
This is how a document looks like:
"fieldA: "1",
"list": [
{"fieldB": "ABC",
"fieldC": "DEF"},
{"fieldB": "ABC",
"fieldC": "GHI"},
{"fieldB": "UVW",
"fieldC": "XYZ"},...]
},
I want to get a distinct list of all possible fieldC values for "ABC" (fieldB) over all documents. So far I've tried this in Java (Java REST Client):
SearchRequest searchRequest = new SearchRequest("abc*");
QueryBuilder matchQueryBuilder = QueryBuilders.boolQuery()
.must(QueryBuilders.nestedQuery("aList",
QueryBuilders.matchQuery("list.fieldB.keyword", "ABC"), ScoreMode.None));
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(matchQueryBuilder)
.aggregation(AggregationBuilders.nested("listAgg","list")
.subAggregation(AggregationBuilders.terms("fieldBAgg")
.field("list.fieldB.keyword")));
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = null;
try {
searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
e.printStackTrace();
}
Nested list = searchResponse.getAggregations().get("listAgg");
Terms fieldBs = list.getAggregations().get("fieldBAgg");
With that query I get all documents which include "ABC" in fieldB and I get all fieldC values. But I just want the fieldC values where fieldB is "ABC".
So in that example I get DEF, GHI and XYZ. But i just want DEF and GHI. Does anybody have an idea how to solve this?
The nested constraint in the query part will only select all documents that do have a nested field satisfying the constraint. You also need to add that same constraint in the aggregation part, otherwise you're going to aggregate all nested fields of all the selected documents, which is what you're seeing. Proceed like this instead:
// 1. terms aggregation on the desired nested field
nestedField = AggregationBuilders.terms("fieldBAgg").field("list.fieldC.keyword");
// 2. filter aggregation on the desired nested field value
onlyBQuery = QueryBuilders.termQuery("list.fieldB.keyword", "ABC");
onlyBFilter = AggregationBuilders.filter("onlyFieldB", onlyBQuery).subAggregation(nestedField);
// 3. parent nested aggregation
nested = AggregationBuilders.nested("listAgg", "list").subAggregation(onlyBFilter);
// 4. main query/aggregation
sourceBuilder.query(matchQueryBuilder).aggregation(nested);

How to extract values from a String that cannot be converted to Json

While processing the DialogFlow Response object, I get the below given string as textPayload. If this is a Json string, I can easily convert it to a JSONObject and then extract the values. However, could not convert this to a Json Object. How do I get the values for the keys in this string? What is a good way to parse this string in Java?
String to be processed
Dialogflow Response : id: "XXXXXXXXXXXX"
lang: "en"
session_id: "XXXXX"
timestamp: "2020-04-26T16:38:26.162Z"
result {
source: "agent"
resolved_query: "Yes"
score: 1.0
parameters {
}
contexts {
name: "enaccaccountblocked-followup"
lifespan: 1
parameters {
}
}
metadata {
intent_id: "XXXXXXXXXXXX"
intent_name: "EN : ACC : Freezing Process - Yes"
end_conversation: true
webhook_used: "false"
webhook_for_slot_filling_used: "false"
is_fallback_intent: "false"
}
fulfillment {
speech: "Since you have been permanently blocked, please request to unblock your account"
messages {
lang: "en"
type {
number_value: 0.0
}
speech {
string_value: "Since you have been permanently blocked, please request to unblock your account."
}
}
}
}
status {
code: 200
error_type: "success"
}
Convert it to valid json, then map using one of the many libraries out there.
You'll only need to:
replace "Dialogflow Response :" with {
add } to the end
add commas between attributes, ie
at the end of every line with a ":"
after "}", except when the next non-whitespace is also "}"
Jackson (at least) can be configured to allow quotes around attribute names as optional.
Deserializing to a Map<String, Object> works for all valid json (except an array, which this isn't).
If I understand you correctly the issue here is that the keys do not have quotations marks, hence, a JSON parser will reject this.
Since the keys all start on a new line with some white-space and all end with a colon : you can fix this easily with a regular expression.
See How to Fix JSON Key Values without double-quotes?
You can then parse it to a Map via
Map<String, Object> map
= objectMapper.readValue(json, new TypeReference<Map<String,Object>>(){});
(but I assume you are aware of this).
Create a class for TextPayload object like this.
public class TextPayload {
private int session_id;
private String lang;
private String timestamp;
private String[] metadata ;
//Other attributes
//getters setters
}
Then using an ObjectMapper extract the values from textpayload like this:
ObjectMapper mapper = new ObjectMapper();
TextPayload textPayload = mapper.readValue(output, User.class);
To utilize ObjectMapper and hands on with it follow this
you can use the nodejs package parse-dialogflow-log to parse the textResponse string.
replace "Dialogflow Response :" with "{"
add "}" to the end
run the package on the result and you'll get a nice json.

Aggregation in Elasticsearch

I want to apply group by clause on date field for elasticsearch query. This is my code.
SearchRequestBuilder srb = client
.prepareSearch(ConstantsValue.indexName)
.setTypes(ConstantsValue._Type)
.addAggregation(
AggregationBuilders
.dateHistogram("aggs")
.field("DTCREATED")
.interval(Interval.MONTH)
.format("yyyy-MM-dd")
.preZone("+05:30")
.preZoneAdjustLargeInterval(true)
.minDocCount(1)
)
.setSize(Integer.MAX_VALUE)
.setQuery(query);
SearchResponse response = srb
.setSearchType(SearchType.QUERY_AND_FETCH)
.setFetchSource(ConstantsValue.fieldList, null)
.execute()
.actionGet();
But query does not return expected result.
Result displayed is as follows
Value :{"DTCREATED":"2016-09-29T18:30:00.000Z"}
Key :AVfdaeSC3n3Bn-RaoFgg
Value :{"DTCREATED":"2016-09-29T18:30:00.000Z"}
Key :AVfdaeSC3n3Bn-RaoFgl
Value :{"DTCREATED":"2016-09-29T18:30:00.000Z"}
Key :AVfdaeSC3n3Bn-RaoFgq
Value :{"DTCREATED":"2016-08-31T18:30:00.000Z"}
Key :AVfdaeSC3n3Bn-RaoFgv
Value :{"DTCREATED":"2016-09-06T18:30:00.000Z"}
Key :AVfdaeSC3n3Bn-RaoFg0
Value :{"DTCREATED":"2016-09-22T18:30:00.000Z"}
Key :AVfdaeSC3n3Bn-RaoFg5
Value :{"DTCREATED":"2016-09-22T18:30:00.000Z"}
Key :AVfdaeSC3n3Bn-RaoFhA
Value :{"DTCREATED":"2016-09-12T18:30:00.000Z"}
Key :AVfdaeSC3n3Bn-RaoFhF
I am new in elasticsearch and don't know what I am missing.
Any help is greatly appreciated!
There's no groupby like clause in ES but then you could use the Aggregations in order to group by the field you want. For example I'm using the post http request below in order to group using userid and get the count for each userid.
The search query would look like this:
http://localhost:9200/response_summary/_search
In the above, response_summary is the index. i'm trying do the search.
The body of the request can be something like this:
{
"query":{
"query_string":{
"query":"api:\"smsmessaging\" AND operatorid:\"ROBI\""
}
},
"aggs":{
"total":{
"terms":{
"field":"userid"
},
"aggs":{
"grades_count":{
"value_count":{
"script":"doc['userid'].value"
}
}
}
}
}
}
So you could mention the field you wanted to groupby within the aggs tag and get the count as a sample in the above. You could modify as you wish. Could have a look at this thread as well.

Categories