ElasticSearch in Spring with #Query - elasticsearch

I have successfully created a query using ElasticSearch's _plugin/head interface. The query is meant to return the latest timestamp for a specific device at a specific location. The query looks as follows:
{
"query":{
"bool":{
"must":[
{
"term":{
"deviceevent.location.id":"1"
}
},
{
"term":{
"deviceevent.deviceId":"AHE1LDD01"
}
}
]
}
},
"from":0,
"size":1,
"sort":{
"timestamp":{
"order":"desc"
}
}
}
The above query works as intended.
Now using Spring-Boot and Spring-Data-ElasticSearch, I defined my own ElasticSearchRepository which looks as follows:
package com.repository.elasticsearch;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import com.domain.DeviceEvent;
public interface DeviceEventRepository extends ElasticsearchRepository<DeviceEvent, String>
{
#Query("{\"bool\":{\"must\":[{\"term\":{\"deviceevent.location.id\": \"?0\"}},{\"term\":{\"deviceevent.deviceId\": \"?1\"}}]}},\"from\": 0,\"size\": 1,\"sort\":{\"timestamp\":{\"order\":\"desc\"}}")
DeviceEvent findLatestCheckInAtLocation(Long locationId, String deviceId);
}
The above code is breaking mainly because I would expect it to return one DeviceEvent, but it's actually returning a device events with count = 10 (The default Page size). It seems also that the results are not being ordered by the timestamp in a descending order. It's as if the size and order parts of the query are not being picked up.
What am I doing wrong here?

Instead of controlling the results size in the query annotation.
Use the Pageable interface, the following is taken from the documentation.
public interface BookRepository extends ElasticsearchRepository<Book, String> {
#Query("{"bool" : {"must" : {"field" : {"name" : "?0"}}}}")
Page<Book> findByName(String name,Pageable pageable);
}
This would allow you to:
findByName("foo-name", new PageRequest(0,1));
If you want to sort also:
findByName("foo-name", new PageRequest(0,1, new Sort(new Sort.Order(Sort.Direction.ASC,"name")))).getContent().get(0);

Related

Spring data elasticsearch how to create repository method for keyword field

Let's say I have mapping like this, and I want to search by the "requestId.keyword" field to fetch the exact match requests. How can I implement it with the Spring Data Elasticsearch repository without using #Query annotation?
"requestId": {
"type": "text",
"analyzer": "1_to_15_analyzer_without_space",
"search_analyzer": "all_symbols_and_fold_analyzer",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
This is not possible with the mechanism to build queries by introspecting the method name. The first idea is to have something like (I am using a Foo entity here):
SearchHits<Foo> searchByRequestId_Keyword(String keyword);
The analysis of the method name is done in the spring-data-common module which only uses the property names of the Java properties of an entity (might be nested). But the keyword subfield only exists in Elasticsearch and - if not autocreated - in the #MultiField annotation. But the code to parse the methodname does not use store-specific information and so an approach like this will not work and fail with the error that keyword is not a property of text - which is right for the Java object.
What you can do is to first add a custom repository fragment interface:
public interface FooKeywordRepository {
SearchHits<Foo> searchByRequestIdKeyword(String keyword);
}
and provide an implementation that must be named like the interface with Impl as suffix:
import org.elasticsearch.index.query.QueryBuilders;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.Query;
public class FooKeywordRepositoryImpl implements FooKeywordRepository {
private final ElasticsearchOperations operations;
public FooKeywordRepositoryImpl(ElasticsearchOperations operations) {
this.operations = operations;
}
#Override
public SearchHits<Foo> searchByRequestIdKeyword(String keyword) {
Query query1 = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.termQuery("requestId.keyword", keyword))
.build();
Query query2 = new CriteriaQuery(Criteria.where("requestId.keyword").is(keyword));
return operations.search(query1, Foo.class); // could be query2 as well
}
}
You have an ElasticsearchOperations injected and use that to execute a query that you build. I have put in two ways to build the query, both work.
Your repository definition to use would then be:
public interface FooRepository extends ElasticsearchRepository<Foo, String>, FooKeywordRepository {
// other custom methods if needed
}

Making Date queries on MongoDB using JSON on SpringDataMongoDB

I'm having some trouble making MongoDB Date queries using #Query annotation on SpringDataMongoDB on a project created using JHipster.
Since JHipster was used to create the project most of the queries were created using Spring Data query builder mechanism and for more refined queries, instead of using Type-safe Query methods I decided to stick with JHipster's standard configuration and make personalized queries using #Query annotation that allows the creation of MongoDBJSON queries.
However, I can't reference in my Json queries any entity field of type Date or LocalDate.
I tried to adopt as a solution the answer from this thread without success.
Query attempts
#Repository
public interface CourseClassRepository extends MongoRepository<CourseClass, String> {
// WORKS - query with `endDate` directly constructed by Spring Data
// This sollution however isn't enought, since 'experience_enrollments.device_id' cannot be used as a parameter
List<CourseClass> findAllByInstitutionIdAndEndDateIsGreaterThanEqual(Long institutionId, LocalDate dateLimit);
// Using #Query to create a JSON query doesn't work.
// apparently data parameter cannot be found. This is weird, considering that in any other #Query created the parameter is found just fine.
// ERROR: org.bson.json.JsonParseException: Invalid JSON input. Position: 124. Character: '?'
#Query(" { 'experience_enrollments.device_id' : ?0, 'institution_id': ?1, 'end_date': { $gte: { $date: ?2 } } } ")
List<CourseClass> findAllByExperienceDeviceAndInstitutionIdAndEndDate(String deviceId, Long institutionId, Date dateLimit);
// Adopting the stackoverflow answer mentioned above also throws an error. I belive that this error is related to the fact that '?2' is being interpreted as a String value and not as reference to a parameter
// ERROR: org.bson.json.JsonParseException: Failed to parse string as a date
#Query(" { 'experience_enrollments.device_id' : ?0, 'institution_id': ?1, 'end_date': { $gte: { $date: '?2' } } } ")
List<CourseClass> findAllByExperienceDeviceAndInstitutionIdAndEndDate(String deviceId, Long institutionId, Date dateLimit);
// Even hardcoding the date parameter, the query throws an error
// ERROR: org.bson.codecs.configuration.CodecConfigurationException: Can't find a codec for class java.time.ZonedDateTime.
#Query(" { 'experience_enrollments.device_id' : ?0, 'institution_id': ?1, 'end_date': { '$gte': { '$date': '2015-05-16T07:55:23.257Z' } } }")
List<CourseClass> findAllByExperienceDeviceAndInstitutionIdAndEndDate(String deviceId, Long institutionId);
}
Database Configurations
#Configuration
#EnableMongoRepositories("br.com.pixinside.lms.course.repository")
#Profile("!" + JHipsterConstants.SPRING_PROFILE_CLOUD)
#Import(value = MongoAutoConfiguration.class)
#EnableMongoAuditing(auditorAwareRef = "springSecurityAuditorAware")
public class DatabaseConfiguration {
#Bean
public MongoCustomConversions customConversions() {
List<Converter<?, ?>> converters = new ArrayList<>();
converters.add(DateToZonedDateTimeConverter.INSTANCE);
converters.add(ZonedDateTimeToDateConverter.INSTANCE);
return new MongoCustomConversions(converters);
}
}
Date converters
public static class DateToZonedDateTimeConverter implements Converter<Date, ZonedDateTime> {
public static final DateToZonedDateTimeConverter INSTANCE = new DateToZonedDateTimeConverter();
private DateToZonedDateTimeConverter() {
}
#Override
public ZonedDateTime convert(Date source) {
return source == null ? null : ZonedDateTime.ofInstant(source.toInstant(), ZoneId.systemDefault());
}
}
public static class ZonedDateTimeToDateConverter implements Converter<ZonedDateTime, Date> {
public static final ZonedDateTimeToDateConverter INSTANCE = new ZonedDateTimeToDateConverter();
private ZonedDateTimeToDateConverter() {
}
#Override
public Date convert(ZonedDateTime source) {
return source == null ? null : Date.from(source.toInstant());
}
}
Turns out that, as mentioned by Christoph Strobl, the behavior was, in fact, a bug. So it won't be necessary to worry about that in a future version of Spring Data MongoDB. Until there, I'm sharing my solution.
Since I was unable to use MongoDBJSon to create the query, I used the MongoTemplate and everything was just fine.
import org.springframework.data.mongodb.core.MongoTemplate;
import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Query.query;
#Autowired
public MongoTemplate mongoTemplate;
public List<CourseClass> findEnrolledOnExperienceDeviceWithMaxEndDateAndInstitutionId(String deviceId, LocalDate endDate, Long institutionId) {
return mongoTemplate.find(query(
where("experience_enrollments.device_id").is(deviceId)
.and("institution_id").is(institutionId)
.and("end_date").gte(endDate)), CourseClass.class);
}

Spring REST repository shows wrong URL

I developed sample application with Spring Boot. I've one abstract class (Employee) and two concrete subclasss for example full time and part time employee.
I preferred a joined type of inheritance and 3 table created by JPA provider.
Also I created REST repository for Employee. Looks like below:
package com.caysever.repository;
import com.caysever.model.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
/**
* Created by alican on 04.05.2017.
*/
#RepositoryRestResource(path = "employee")
public interface EmployeeRepository extends JpaRepository<Employee, Long>{
}
When I invoke the **/employee** URL in browser, I'm getting content as below:
{
"fullTimeEmployees" : [ {
"name" : "Alican",
"surname" : "Akkuş",
"birthDay" : "2017-05-04T12:37:20.189+0000",
"gender" : "MALE",
"totalWorkingHoursOfWeek" : 40,
"_links" : {
"self" : {
"href" : "http://localhost:8080/fullTimeEmployee/1"
},
"fullTimeEmployee" : {
"href" : "http://localhost:8080/fullTimeEmployee/1"
}
}
}
When I invoked this URL for first employee localhost:8080/fullTimeEmployee/1 , I getting the 404 status code, not found. But I will getting the first employee with this URL localhost:8080/employee/1.
You can see all codes at GitHub -> https://github.com/AlicanAkkus/jpa-inheritance-strategy
Why Spring REST generates the fullTimeEmployee URL?
I think that with #RepositoryRestResource you are modifying the export details, such as using /empoyee instead of the default value of /fullTimeEmployee
Try with
#RepositoryRestResource(collectionResourceRel = "fullTimeEmployees", path = "fullTimeEmployees")
Or if you want to use /employee
#RepositoryRestResource(collectionResourceRel = "employee", path = "employee")
The path is sets the segment under which this resource is to be exported and the collectionResourceRel sets the value to use when generating links to the collection resource.
Hope this helps
A workaround for this is to add repository interfaces for the concrete classes, sharing the path of the superclass repository.
#RepositoryRestResource(collectionResourceRel = "employee", path = "employee")
public interface FullTimeEmployeeRepository extends JpaRepository<FullTimeEmployee, Long> {
}
#RepositoryRestResource(collectionResourceRel = "employee", path = "employee")
public interface PartTimeEmployeeRepository extends JpaRepository<PartTimeEmployee, Long> {
}
This will generate the links with the "employee" path regardless of the subclass type.
"_links" : {
"self" : {
"href" : "http://localhost:8080/employee/1"
},
"fullTimeEmployee" : {
"href" : "http://localhost:8080/employee/1"
}
}
I don't know if there is another way to work around the issue.

Spring Data Rest, SpringFox and JpaRepository custom finders

NB: using Spring Boot 1.4.2 + SpringFox 2.6.0
Hi, I'm having an issue with Swagger 2 forms on my API documentation over a #RepositoryRestResource. The code below works fine (REST access OK):
#RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends JpaRepository<Person, Long> {
Person findByLastName(#Param("name") String name);
}
And the HATEOAS links are right too: calling URL /api/people/search
ends up with this (notice parameter "name"):
{
"_links": {
"findByLastName": {
"href": "http://localhost:8080/api/people/search/findByLastName{?name}",
"templated": true
},
"self": {
"href": "http://localhost:8080/api/people/search"
}
}
}
The REST API is ok: URL /api/people/search/findByLastName?name=foobar returns data when executed with a browser
BUT in Swagger the GET parameter type is interpreted as "body" instead of "query" and the form submission (curl ... -d 'foobar'...) fails in 404, attempting to submit "name" as request body.
So I tried to set Swagger explicitly, like this:
#RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends JpaRepository<Person, Long> {
#ApiOperation("Find somebody by it's last name")
#ApiImplicitParams({
#ApiImplicitParam(name = "name", paramType = "query")
})
Person findByLastName(#Param("name") #ApiParam(name = "name") String name);
}
without any success, despite the fact that "name" is well retained in the form as the parameter name in this example :-(
body parameter type on GET query
Does anyone know what could be done to make that Swagger form to work? Thx for your help
This is it : #Param configures Spring Data REST, while #RequestParam fits Swagger
#RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends JpaRepository<Person, Long> {
// #Param Spring Data REST : Use #Param or compile with -parameters on JDK 8
// #RequestParam Swagger : paramType=query cf. $Api*Param
Person findByLastName(#Param("name") #RequestParam("name") String name);
}
Me happy!

spring hateoas generates different responses for collection or pojo

I have two classes
import org.springframework.hateoas.ResourceSupport;
public class A{}
public class B{}
public class AResource extends ResourceSupport {
private final A a;
}
public class BResource extends ResourceSupport {
private final B b;
}
#Controller
public class Controller {
#RequestMapping
#ResponseBody
public Set<AResource> giveMeColl() {
}
#RequestMapping
#ResponseBody
public BResource giveMeSingle() {
}
}
both responses add links object but for resource A is "links" and for resource B is "_link" and also structure changes
//RESPONSE FOR A
[
{
"content":{
//my fancy object
},
"links":[
{
"rel": "self",
"href": "http://localhost:8080/myid/22"
}
]
}
]
{
"content":{
//my fancy object
},
"_links":[
{
"self": "http://localhost:8080/myid/22/someelse/33"
}]
}
both resources are constructed with assemblers and both are adding the link from the ids
AResource aresource = new AResource(a);
resource.add(linkTo(methodOn(Controller.class).giveMeColl()).withSelfRel());
BResource bresource = new BResource(b);
resource.add(linkTo(methodOn(Controller.class).giveMeSingle()).withSelfRel());
Response headers for a is
"content-type": "application/json;charset=UTF-8"
and for b
"content-type": "application/hal+json;charset=UTF-8"
Could it be because returning an array is not really Restful? as Some post suggest
p.s. I have added and removed #EnableHypermediaSupport but doesn't seem to affect the problem.
"_links" follows the HAL specification. Spring HATEOAS includes a dedicated serializer for that, but it is only used for classes that extend ResourceSupport.
Returning a simple array is not exactly "unRESTful", but it doesn't meet the REST maturity level 3 (Hypermedia controls). In order to achieve that you can wrap the collection into a Resources instance, which extends ResourceSupport. You should get the same link serialization for both types then.

Resources