spring-data-elasticsearch searching through different enttities/indicies - elasticsearch

I have a requirement to provide functionality which will allow user to search through many different domain elements and see results as combined list. So in UI he will have to fill only one text-field and than retrive results.
To visualize lets assume i have 3 entities in domain:
#Document(indexName="car")
public class Car {
private int id;
private String type;
}
#Document(indexName="garage")
public class Garage{
private int id;
private String address;
}
#Document(indexName="shop")
public class Shop{
private int id;
private String name;
}
Now i thought i could achieve requirement like this:
...
#Inject
private ElasticsearchTemplate elasticsearchTemplate;
...
#RequestMapping(value = "/_search/all/{query}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#Timed
public List<?> search(#PathVariable String query) {
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(queryString(query))
.withIndices("car", "garage", "shop")
.build();
//THIS WORKS
elasticsearchTemplate.queryForIds(searchQuery);
//THIS THROWS ERROR ABOUT WRONG INDEXES
return elasticsearchTemplate.queryForPage(searchQuery, GlobalSearchDTO.class, new GlobalSearchResultMapper()).getContent();
}
...
class GlobalSearchDTO {
public Long id;
public String type;
public Object obj;
}
...
but when calling 2nd function - the one which is responsible for returning actual documents, the following exception is thrown:
Unable to identify index name. GlobalSearchDTO is not a Document. Make
sure the document class is annotated with #Document(indexName="foo")
I've tried with passing any domain entity as a class argument, but than i am retriving only elements from the corresponding index, not all of them. For instance calling:
return elasticsearchTemplate.queryForPage(searchQuery, Shop.class, new GlobalSearchResultMapper()).getContent();
Results in retrivng elements only from 'shop' index. It seems like for some reason dynamically provided indicies are not used.
So the question is: Is it possible to retrive data like that? Why specifying '.withIndices("car", "garage", "shop")' is not enough?
Maybe i should consider other solutions like:
search through indexes in loop(one bye one), join results and order them by score
create separate GlobalSearch entity with 'globalsearch' index
and duplicate data there
Thanks in advance!
Krzysztof

I have managed to find suitable workaround for my problem. It turned out that when using 'scroll' and 'scan' functionality dynamically provided indicies are used which means that query works as expected. Code for solution:
#RequestMapping(value = "/_search/all/{query}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#Timed
public List<?> search(#PathVariable String query) {
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(queryString(query))
.withIndices("car", "garage", "shop")
.withPageable(new PageRequest(0,1))
.build();
String scrollId = elasticsearchTemplate.scan(searchQuery, 1000, false);
List<GlobalSearchDTO> sampleEntities = new ArrayList<GlobalSearchDTO>();
boolean hasRecords = true;
while (hasRecords){
Page<GlobalSearchDTO> page = elasticsearchTemplate.scroll(scrollId, 5000L , new ResultMapper());
if(page != null) {
sampleEntities.addAll(page.getContent());
hasRecords = page.hasNext();
}
else{
hasRecords = false;
}
}
return sampleEntities;
}
}
and in the ResultMapper class:
...
for (SearchHit hit : response.getHits()) {
switch(hit.getIndex()) {
case "car": //map to DTO
case "shop": //map to DTO
case "garage": //map to DTO
}
}
...

Related

Spring boot Restful API: DTO with relationships convert to entity using ModelMapper?

I'm now confused about how to do a CRUD in a Rest API with Spring.
Let me explain, I have two routes to POST and PUT an entity. I created two DTOs createPostRequest and updatePostRequest for this. Because when adding, the properties cannot be null, while when updating they can (nulled properties are ignored).
Problem 1:
On my frontend, the user is asked to choose a list of tags from the database (multi select html). This is why createPostRequest has a tags property typed TagDTO. But, how can I use modelMapper to map, for example, the createPostRequest to the Post entity making sure that the tags exist in the database?
if for example a user try to insert a tag that does not exist, I was thinking of doing something like this:
postEntity.setTags(tagService.findAllByIds(postEntity.getTagsId()));
This makes a lot of repetition in the code, because between create and update method of my entity in service, there is a lot of identical code.
Problem 2:
Based on my problem 1, how can I easily map my two DTOs to the same entity without repeating the code 2x?
Code example - PostService (see comment)
This is an example for the update, but there will be almost identical code for the create, so how do I proceed?
#Transactional
public Post update(Integer postId, UpdatePostRequest request) {
return Optional.ofNullable(this.getById(postId)).map(post -> {
// here how to map non-null properties of my request
// into my post taking in consideration my comment above?
postDAO.save(post);
return post;
}).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}
================================
UPDATE:
As requested, found the code bellow.
The controller:
#RestController
#RequestMapping("/v1/posts")
public class PostController {
RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json; charset=UTF-8")
public ResponseEntity<Object> update(#Valid #RequestBody CreatePostRequest createPostRequest) {
Post post = postService.create(createPostRequest);
return new ApiResponseHandler(new PostDTO(post), HttpStatus.OK).response();
}
RequestMapping(value = "/{postId}", method = RequestMethod.PUT, consumes = "application/json", produces = "application/json; charset=UTF-8")
public ResponseEntity<Object> update(#Valid #RequestBody UpdatePostRequest updatePostRequest, #PathVariable Integer postId) {
Post post = postService.update(postId, updatePostRequest);
return new ApiResponseHandler(new PostDTO(post), HttpStatus.OK).response();
}
}
CreatePostRequest :
#Data
public class CreatePostRequest {
#NotNull
#Size(min = 10, max = 30)
private Sting title;
#NotNull
#Size(min = 50, max = 600)
private String description
#NotNull
#ValidDateString
private String expirationDate;
#NotNull
private List<TagDTO> tags;
public List<Integer> getTagIds() {
return this.getTags().stream().map(TagDTO::getId).collect(Collectors.toList());
}
}
UpdatePostRequest :
#Data
public class UpdatePostRequest {
#Size(min = 10, max = 30)
private Sting title;
#Size(min = 50, max = 600)
private String description
#ValidDateString
private String expirationDate;
private List<TagDTO> tags;
public List<Integer> getTagIds() {
return this.getTags().stream().map(TagDTO::getId).collect(Collectors.toList());
}
}
The service :
#Service
#Transactional
public class PostService {
#Transactional
public Post create(CreatePostRequest request) {
ModelMapper modelMapper = new ModelMapper();
Post post = modelMapper.map(request, Post.class);
// map will not work for tags : how to check that tags exists in database ?
return postDAO.save(post);
}
#Transactional
public Post update(Integer postId, UpdatePostRequest request) {
return Optional.ofNullable(this.getById(postId)).map(post -> {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().setSkipNullEnabled(true);
modelMapper.map(request, post);
// map will not work for tags : how to check that tags exists in database ?
postDAO.save(post);
return post;
}).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}
}
To avoid duplication of two similar DTOs you could use #Validated group validations. This allows you to actively set which validations are to be done on each property. You can read more about this in the following online resource https://www.baeldung.com/spring-valid-vs-validated. You would begin with the creation of two market interfaces:
interface OnCreate {}
interface OnUpdate {}
You can then use these marker interfaces with any constraint annotation in your common DTO:
#Data
public class CreateOrUpdatePostRequest {
#NotNull(groups = OnCreate.class)
#Size(min = 10, max = 30, groups = {OnCreate.class, OnUpdate.class})
private Sting title;
#NotNull(groups = OnCreate.class)
#Size(min = 50, max = 600, groups = {OnCreate.class, OnUpdate.class})
private String description
#NotNull(groups = OnCreate.class)
#ValidDateString(groups = {OnCreate.class, OnUpdate.class})
private String expirationDate;
#NotNull(groups = OnCreate.class)
private List<TagDTO> tags;
public List<Integer> getTagIds() {
return this.getTags().stream().map(TagDTO::getId).collect(Collectors.toList());
}
}
Finally, you just need to annotate your methods in the Controller accordingly:
#RestController
#RequestMapping("/v1/posts")
#Validated
public class PostController {
#RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json; charset=UTF-8")
public ResponseEntity<Object> update(#Validated(OnCreate.class) #RequestBody CreateOrUpdatePostRequest createPostRequest) {
Post post = postService.create(createPostRequest);
return new ApiResponseHandler(new PostDTO(post), HttpStatus.OK).response();
}
#RequestMapping(value = "/{postId}", method = RequestMethod.PUT, consumes = "application/json", produces = "application/json; charset=UTF-8")
public ResponseEntity<Object> update(#Validated(OnUpdate.class) #RequestBody CreateOrUpdatePostRequest updatePostRequest, #PathVariable Integer postId) {
Post post = postService.update(postId, updatePostRequest);
return new ApiResponseHandler(new PostDTO(post), HttpStatus.OK).response();
}
}
With this, you can have a single mapping function.
Still, keep in mind that using validation groups can easily become an anti-pattern given that we are mixing different concerns. With validation groups, the validated entity has to know the validation rules for all the use cases it is used in. Having said that, I usually avoid using validation groups unless it is really necessary.
Regarding tags I guess your only option is to query the database. The ones that do not exist you should create them (I guess), so something along the following lines:
List<Integer> tagsId = createOrUpdatePostRequest.getTagsId();
List<Tag> tags = tagService.findAllByIds(tagsId);
List<Integer> nonExistentTagsId = tagsId.stream().filter(id -> tags.stream().noneMatch(tag -> tag.getId().equals(id)));
if (!nonExistentTagsId.isEmpty()) {
// create Tags and add them to tags List
}

Spring WebFlux - Convert Flux to List<Object>

I am learning Spring WebFlux.
My Entity goes like this:
#Table("users")
public class User {
#Id
private Integer id;
private String name;
private int age;
private double salary;
}
I have a Repository (R2 using H2 Database) like below:
public interface UserRepository extends ReactiveCrudRepository<User,Integer> {
}
And my controller is:
#Autowired
private UserRepository userRepository;
private static List<User> userList = new ArrayList<>();
#PostConstruct
public void initializeStockObjects() {
User stock1 = new User(11, "aaaa", 123, 123);
User stock2 = new User(12, "bbb", 123, 123);
User stock3 = new User(13, "ccc", 123, 123);
userList.add(stock1);
userList.add(stock2);
userList.add(stock3);
}
#RequestMapping(value = "/livelistofusers", method = RequestMethod.GET, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<List<User>> getUsers() {
return getUserData(userList);
}
public Flux<List<User>> getUserData(List<User> userList) {
Flux<Long> interval = Flux.interval(Duration.ofSeconds(3));
interval.subscribe((i) -> userList.forEach(user -> addNewUser(user)));
Flux<List<User>> transactionFlux = Flux.fromStream(Stream.generate(() -> userList));
return Flux.zip(interval, transactionFlux).map(Tuple2::getT2);
}
All good till this point. I am able to return the the entire list of users every 3 seconds to the view. No issues at all here.
Now, I want to send the Flue i.e. Flux flux2 = userRepository.findAll() to the view. That means, instead of return getUserData(userList); how can I do return getUserData(flux2(...what should I do here ???... I tried couple of things but I end up making the Blocking list instead of Non-Blocking ...)); ?
Question: How can I achieve this? i.e. How can I send the entire Flux every 3 seconds to my view. I am feeling lost here and clueless. Any relevant help links or solution will be greatly appreciated.
Edit:
As per Nipuna's comments I tried this:
#RequestMapping(value = "/livelistofusersall", method = RequestMethod.GET, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<List<User>> getUsersall() {
Flux<Long> interval = Flux.interval(Duration.ofSeconds(3));
interval.subscribe((i) -> userRepository.findAll());
Flux<List<User>> transactionFlux = userRepository.findAll().collectList().flatMapMany(Flux::just);
return Flux.zip(interval, transactionFlux).map(Tuple2::getT2);
}
But now at my context path, the list loads but "only once" after a wait of 3 seconds. What I am missing here?
You can use collectList() operator in Flux for this which gives a Mono of List.
userRepository.findAll().collectList().flatMapMany(Flux::just);

Values in the response of api are empty

ResponseInJsonFormat is a class,
#Builder
#Getter
public class ResponseInJsonFormat {
// Id field in Eloqua
private String id;
// List of field values
private List<FieldValues> fieldValues;
}
And FieldValues is also a class defined as,
#Builder
#Setter
#JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class FieldValues {
// id of the parameter
private String ID;
// parameters
private String type;
// Value :- value of the parameter
private String value;
}
I'm writing an api ,
#RequestMapping(value = "/service/XYZService", method = RequestMethod.POST)
public ResponseInJsonFormat sendData(#RequestParam String a) {
FieldValues parameter = FieldValues.builder()
.ID(FIELD_ID)
.type(FILED_TYPE)
.value(a)
.build();
List<FieldValues> values = Arrays.asList(parameter);
ResponseInJsonFormat response = ResponseInJsonFormat .builder()
.fieldValues(values)
.id(ID)
.build();
// need to return ResponseInJsonFormat
return response
}
But when I call to a service it returns something like,
{
"id": "110",
"fieldValues": [
{}
]
}
Could somebody assist about this empty json object of fieldValues.
I havnt understand why to put #JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
if I dont use this annotation I get an error as,
no properties discovered to create BeanSerializer
So I used the annotation, the error has gone but getting empty value for the same, even though the value is explicitly assigned.
I do not understand why to do all this it just being a list.
Is there any document I should read about,please advice.
If I'm not wrong, spring return the response in JSON format. Then why to do all this.

Caching with Spring and ehcache doesnt work as expected

I have a Product model object like this -
class ProductDTO {
int id;
String code;
String description;
//getters and setters go here
}
I am writing a service (code below) that looks up products by id or code and returns their description. I am using Spring 4 and ehcache to cache the results.
I have 2 methods - one for lookup by id and one for lookup by code - they are getProductByCode and getProductById. Both return the description as a string. They do so by calling getAllProducts() which returns a list of all products. The callers then search the list for a product matching the id or code and return the description.
getAllProducts() also calls 2 methods with #CachePut for each product - to save the description Strings in cache - by key code and id.
Caching works properly if the same arguments are passed for code or id to to the getProductByCode and getProductById methods. But if I pass a different argument, getAllProducts() is called again.
How do I achieve the desired behavior - where every time a call is made to getAllProducts(), all descriptions get cached and a subsequent call looks up the cache rather than going to the repository?
public class ProductServiceImpl implements ProductService {
#Autowired
ProductsRepository ProductRepo;
#Override
public List<ProductDTO> getAllProducts() {
List<ProductDTO> products = ProductRepo.getAllProducts();
for(ProductDTO prodDTO : products) {
String desc = prodDTO.getDescription();
String code = prodDTO.getCode();
int id = prodDTO.getId();
putDescriptionInCache(desc, code);
putDescriptionInCache(desc, id);
}
return products;
}
#CachePut(value = "products", key = "#id")
public String putDescriptionInCache(String description, int id){
return description;
}
#CachePut(value = "products", key = "#code")
public String putDescriptionInCache(String description, String code){
return description;
}
#Override
#Cacheable(value="products", key="#id")
public String getProductById(Integer id) throws NullPointerException {
String dtoDesc = null;
List<ProductDTO> products = getAllProducts();
for(ProductDTO currDTO : products) {
int currId = currDTO.getId();
if(id.equals(new Integer(currId))) {
dtoDesc = currDTO.getDescription();
}
}
return dtoDesc;
}
#Override
#Cacheable(value="products", key="#code")
public String getProductByCode(String code) throws NullPointerException {
String dtoDesc = null;
List<ProductDTO> products = getAllProducts();
for(ProductDTO currDTO : products) {
String currCode = currDTO.getCode();
if(currCode.equals(code)) {
dtoDesc = currDTO.getDescription();
}
}
return dtoDesc;
}
}
As it was commented by M. Deinum, the problem comes from the annotations, like CachePut or Cacheable, being transformed into an aspect at runtime. And the main limitation with that approach is that calls from the same class are not properly captured.
As you replied yourself in the comments section, moving the annotated methods to another type that is injected in the current one solves the problem.

Java: GroupSequenceProvider for Validation, object is null in getValidationGroups method

This is what I am trying to achieve:
I have an update request object and user is allowed to do Partial Updates. But I want to validate the field only if it is in the request body. Otherwise, it is OK to be null. To achieve this, I am using GroupSequenceProvider to let the Validator know what groups to validate. What am I doing wrong here? If there is a blunder, how do I fix it?
Documentation: https://docs.jboss.org/hibernate/validator/5.1/reference/en-US/html/chapter-groups.html#example-implementing-using-default-group-sequence-provider
#GroupSequenceProvider(UpdateUserRegistrationGroupSequenceProvider.class)
public class UpdateUserRegistrationRequestV1 {
#NotBlank(groups = {EmailExistsInRequest.class})
#Email(groups = {EmailExistsInRequest.class})
#SafeHtml(whitelistType = SafeHtml.WhiteListType.NONE, groups = {EmailExistsInRequest.class})
private String email;
#NotNull(groups = {PasswordExistsInRequest.class})
#Size(min = 8, max = 255, groups = {PasswordExistsInRequest.class})
private String password;
#NotNull(groups = {FirstNameExistsInRequest.class})
#Size(max = 255, groups = {FirstNameExistsInRequest.class})
#SafeHtml(whitelistType = SafeHtml.WhiteListType.NONE, groups = {FirstNameExistsInRequest.class})
private String firstName;
// THERE ARE GETTERS AND SETTERS BELOW
}
Group Sequence Provider Code:
public class UpdateUserRegistrationGroupSequenceProvider implements DefaultGroupSequenceProvider<UpdateUserRegistrationRequestV1> {
public interface EmailExistsInRequest {}
public interface PasswordExistsInRequest {}
public interface FirstNameExistsInRequest {}
#Override
public List<Class<?>> getValidationGroups(UpdateUserRegistrationRequestV1 updateUserRegistrationRequestV1) {
List<Class<?>> defaultGroupSequence = new ArrayList<Class<?>>();
defaultGroupSequence.add(Default.class);
defaultGroupSequence.add(UpdateUserRegistrationRequestV1.class);
if(StringUtils.hasText(updateUserRegistrationRequestV1.getEmail())) {
defaultGroupSequence.add(EmailExistsInRequest.class);
}
if(StringUtils.hasText(updateUserRegistrationRequestV1.getPassword())) {
defaultGroupSequence.add(PasswordExistsInRequest.class);
}
if(StringUtils.hasText(updateUserRegistrationRequestV1.getFirstName())) {
defaultGroupSequence.add(FirstNameExistsInRequest.class);
}
return defaultGroupSequence;
}
}
I am using Spring MVC, so this is how my controller method looks,
#RequestMapping(value = "/{userId}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE)
#ResponseStatus(HttpStatus.NO_CONTENT)
public void updateUser(#PathVariable("userId") Long userId,
#RequestBody #Valid UpdateUserRegistrationRequestV1 request) {
logger.info("Received update request = " + request + " for userId = " + userId);
registrationService.updateUser(userId, conversionService.convert(request, User.class));
}
Now the problem is, the parameter "updateUserRegistrationRequestV1" in the UpdateUserRegistrationGroupSequenceProvider.getValidationGroups method is null. This is the request object that I am sending in the request body and I am sending email field with it.
What am I doing wrong?
I too went through the same issue ,and hopefully solved it
You just have to check the object is null and put all your conditions inside it.
public List<Class<?>> getValidationGroups(Employee object) {
List<Class<?>> sequence = new ArrayList<>();
//first check if the object is null
if(object != null ){
if (!object.isDraft()) {
sequence.add(Second.class);
}
}
// Apply all validation rules from default group
sequence.add(Employee.class);
return sequence;
}

Resources