Skip to content
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

Adds documentation and polish #1557

Merged
merged 12 commits into from
Dec 5, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@
import java.util.Date;

public class DateUtility {

/**
* Converts a {@link Date} to {@link LocalDateTime} using the system default timezone.
*
* @param dateToConvert the date to convert, can be null
* @return the converted {@link LocalDateTime} or null if input is null
*/
public static LocalDateTime convertToLocalDateViaInstant(Date dateToConvert) {
if (dateToConvert == null) {
return null;
}
return dateToConvert.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,21 @@ public Mono<ResponseEntity<Restaurant>> addNotesToRestaurant(
}

@PostMapping
public ResponseEntity<GenericMessage> createRestaurant(
@RequestBody RestaurantRequest restaurantRequest) {
return ResponseEntity.created(
URI.create(
String.format(
"/restaurant/%s",
this.restaurantService
.createRestaurant(restaurantRequest)
.map(Restaurant::getName))))
.body(
new GenericMessage(
"restaurant with name %s created"
.formatted(restaurantRequest.name())));
public Mono<ResponseEntity<GenericMessage>> createRestaurant(
@RequestBody @Valid RestaurantRequest restaurantRequest) {
return this.restaurantService
.createRestaurant(restaurantRequest)
.map(
restaurant ->
ResponseEntity.created(
URI.create(
"/api/restaurant/name/"
+ restaurant.getName()))
.body(
new GenericMessage(
"restaurant with name %s created"
.formatted(
restaurantRequest
.name()))));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
import com.example.mongoes.response.ResultData;
import com.example.mongoes.web.service.SearchService;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;
import java.util.List;
import org.springframework.data.elasticsearch.core.SearchPage;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -145,12 +154,43 @@ public Mono<ResponseEntity<AggregationSearchResponse>> aggregateSearch(
.map(ResponseEntity::ok);
}

@Operation(
summary = "Search restaurants within range",
description = "Find restaurants within specified distance from given coordinates",
responses = {
@ApiResponse(
responseCode = "200",
description = "Successfully retrieved restaurants",
content = @Content(schema = @Schema(implementation = ResultData.class))),
@ApiResponse(responseCode = "400", description = "Invalid parameters provided")
})
@GetMapping("/search/restaurant/withInRange")
public Flux<ResultData> searchRestaurantsWithInRange(
@RequestParam Double lat,
@RequestParam Double lon,
@RequestParam Double distance,
@RequestParam(defaultValue = "km", required = false) String unit) {
@Parameter(
description = "Latitude coordinate (between -90 and 90)",
example = "40.7128")
@RequestParam
@Min(-90)
@Max(90)
Double lat,
@Parameter(
description = "Longitude coordinate (between -180 and 180)",
example = "-74.0060")
@RequestParam
@Min(-180)
@Max(180)
Double lon,
@Parameter(description = "Distance from coordinates (must be positive)")
@RequestParam
@Positive
Double distance,
@Parameter(
description = "Unit of distance",
example = "km",
schema = @Schema(allowableValues = {"km", "mi"}))
@RequestParam(defaultValue = "km", required = false)
@Pattern(regexp = "^(km|mi)$", message = "Unit must be either 'km' or 'mi'")
String unit) {
return this.searchService.searchRestaurantsWithInRange(lat, lon, distance, unit);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.example.mongoes.web.service;

import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
import java.util.HashMap;
import java.util.Map;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchAggregation;
import org.springframework.stereotype.Service;

/**
* Processes Elasticsearch aggregations and transforms them into a structured map format. Supports
* 'terms' and 'dateRange' aggregation types.
*/
@Service
class AggregationProcessor {

/**
* Processes Elasticsearch aggregations and returns a structured map of results.
*
* @param aggregationMap Map of aggregation key to ElasticsearchAggregation
* @return Map of aggregation key to counts, where counts is a map of bucket key to document
* count
* @throws IllegalArgumentException if aggregationMap is null
*/
public Map<String, Map<String, Long>> processAggregations(
Map<String, ElasticsearchAggregation> aggregationMap) {
if (aggregationMap == null) {
throw new IllegalArgumentException("aggregationMap must not be null");
}
Map<String, Map<String, Long>> resultMap = new HashMap<>();
aggregationMap.forEach(
(String aggregateKey, ElasticsearchAggregation aggregation) -> {
Map<String, Long> countMap = new HashMap<>();
Aggregate aggregate = aggregation.aggregation().getAggregate();
processAggregate(aggregate, countMap);
resultMap.put(aggregateKey, countMap);
});
return resultMap;
}

private void processAggregate(Aggregate aggregate, Map<String, Long> countMap) {
if (aggregate.isSterms()) {
processTermsAggregate(aggregate, countMap);
} else if (aggregate.isDateRange()) {
processDateRangeAggregate(aggregate, countMap);
}
}

private void processTermsAggregate(Aggregate aggregate, Map<String, Long> countMap) {
aggregate
.sterms()
.buckets()
.array()
.forEach(bucket -> countMap.put(bucket.key().stringValue(), bucket.docCount()));
}

private void processDateRangeAggregate(Aggregate aggregate, Map<String, Long> countMap) {
aggregate.dateRange().buckets().array().stream()
.filter(bucket -> bucket.docCount() != 0)
.forEach(
bucket ->
countMap.put(
bucket.fromAsString() + " - " + bucket.toAsString(),
bucket.docCount()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,38 +67,33 @@ public Flux<Restaurant> loadData() throws IOException {
}

private Flux<Restaurant> saveAll(List<String> restaurantStringList) {
rajadilipkolli marked this conversation as resolved.
Show resolved Hide resolved
List<Restaurant> restaurantList =
restaurantStringList.stream()
.map(Document::parse)
.map(
document -> {
Restaurant restaurant = new Restaurant();
restaurant.setRestaurantId(
Long.valueOf(
document.get("restaurant_id", String.class)));
restaurant.setName(document.get("name", String.class));
restaurant.setCuisine(document.get("cuisine", String.class));
restaurant.setBorough(document.get("borough", String.class));
Address address = new Address();
Document addressDoc = (Document) document.get("address");
address.setBuilding(addressDoc.get("building", String.class));
address.setStreet(addressDoc.get("street", String.class));
address.setZipcode(
Integer.valueOf(
addressDoc.get("zipcode", String.class)));
List<Double> obj = addressDoc.getList("coord", Double.class);
Point geoJsonPoint = new Point(obj.getFirst(), obj.get(1));
address.setLocation(geoJsonPoint);
restaurant.setAddress(address);
List<Grades> gradesList =
getGradesList(
document.getList("grades", Document.class));
restaurant.setGrades(gradesList);

return restaurant;
})
.toList();
return restaurantRepository.saveAll(restaurantList);
return Flux.fromIterable(restaurantStringList)
.map(Document::parse)
.map(this::documentToRestaurant)
.flatMap(restaurantRepository::save);
}

private Restaurant documentToRestaurant(Document document) {
Restaurant restaurant = new Restaurant();
restaurant.setRestaurantId(Long.valueOf(document.get("restaurant_id", String.class)));
restaurant.setName(document.get("name", String.class));
restaurant.setCuisine(document.get("cuisine", String.class));
restaurant.setBorough(document.get("borough", String.class));

Address address = new Address();
Document addressDoc = document.get("address", Document.class);
address.setBuilding(addressDoc.get("building", String.class));
address.setStreet(addressDoc.get("street", String.class));
address.setZipcode(Integer.valueOf(addressDoc.get("zipcode", String.class)));
List<Double> coord = addressDoc.getList("coord", Double.class);
Point geoJsonPoint = new Point(coord.get(0), coord.get(1));
address.setLocation(geoJsonPoint);
restaurant.setAddress(address);

List<Grades> gradesList = getGradesList(document.getList("grades", Document.class));
restaurant.setGrades(gradesList);

return restaurant;
Comment on lines +91 to +111
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add input validation and null checks in documentToRestaurant.

The method assumes all fields exist and are of the correct type, which could lead to NullPointerException or ClassCastException.

Consider adding validation:

 private Restaurant documentToRestaurant(Document document) {
+    if (document == null) {
+        throw new IllegalArgumentException("Document cannot be null");
+    }
     Restaurant restaurant = new Restaurant();
-    restaurant.setRestaurantId(Long.valueOf(document.get("restaurant_id", String.class)));
+    String restaurantId = document.get("restaurant_id", String.class);
+    if (restaurantId == null) {
+        throw new IllegalArgumentException("restaurant_id is required");
+    }
+    restaurant.setRestaurantId(Long.valueOf(restaurantId));
     restaurant.setName(document.get("name", String.class));
     restaurant.setCuisine(document.get("cuisine", String.class));
     restaurant.setBorough(document.get("borough", String.class));

     Address address = new Address();
     Document addressDoc = document.get("address", Document.class);
+    if (addressDoc == null) {
+        throw new IllegalArgumentException("address is required");
+    }
     address.setBuilding(addressDoc.get("building", String.class));
     address.setStreet(addressDoc.get("street", String.class));
     address.setZipcode(Integer.valueOf(addressDoc.get("zipcode", String.class)));
     List<Double> coord = addressDoc.getList("coord", Double.class);
+    if (coord == null || coord.size() != 2) {
+        throw new IllegalArgumentException("Invalid coordinates");
+    }
     Point geoJsonPoint = new Point(coord.get(0), coord.get(1));
     address.setLocation(geoJsonPoint);
     restaurant.setAddress(address);

     List<Grades> gradesList = getGradesList(document.getList("grades", Document.class));
     restaurant.setGrades(gradesList);

     return restaurant;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private Restaurant documentToRestaurant(Document document) {
Restaurant restaurant = new Restaurant();
restaurant.setRestaurantId(Long.valueOf(document.get("restaurant_id", String.class)));
restaurant.setName(document.get("name", String.class));
restaurant.setCuisine(document.get("cuisine", String.class));
restaurant.setBorough(document.get("borough", String.class));
Address address = new Address();
Document addressDoc = document.get("address", Document.class);
address.setBuilding(addressDoc.get("building", String.class));
address.setStreet(addressDoc.get("street", String.class));
address.setZipcode(Integer.valueOf(addressDoc.get("zipcode", String.class)));
List<Double> coord = addressDoc.getList("coord", Double.class);
Point geoJsonPoint = new Point(coord.get(0), coord.get(1));
address.setLocation(geoJsonPoint);
restaurant.setAddress(address);
List<Grades> gradesList = getGradesList(document.getList("grades", Document.class));
restaurant.setGrades(gradesList);
return restaurant;
private Restaurant documentToRestaurant(Document document) {
if (document == null) {
throw new IllegalArgumentException("Document cannot be null");
}
Restaurant restaurant = new Restaurant();
String restaurantId = document.get("restaurant_id", String.class);
if (restaurantId == null) {
throw new IllegalArgumentException("restaurant_id is required");
}
restaurant.setRestaurantId(Long.valueOf(restaurantId));
restaurant.setName(document.get("name", String.class));
restaurant.setCuisine(document.get("cuisine", String.class));
restaurant.setBorough(document.get("borough", String.class));
Address address = new Address();
Document addressDoc = document.get("address", Document.class);
if (addressDoc == null) {
throw new IllegalArgumentException("address is required");
}
address.setBuilding(addressDoc.get("building", String.class));
address.setStreet(addressDoc.get("street", String.class));
address.setZipcode(Integer.valueOf(addressDoc.get("zipcode", String.class)));
List<Double> coord = addressDoc.getList("coord", Double.class);
if (coord == null || coord.size() != 2) {
throw new IllegalArgumentException("Invalid coordinates");
}
Point geoJsonPoint = new Point(coord.get(0), coord.get(1));
address.setLocation(geoJsonPoint);
restaurant.setAddress(address);
List<Grades> gradesList = getGradesList(document.getList("grades", Document.class));
restaurant.setGrades(gradesList);
return restaurant;

}

private List<Grades> getGradesList(List<Document> gradeDocumentList) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
package com.example.mongoes.web.service;

import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
import co.elastic.clients.elasticsearch._types.aggregations.RangeBucket;
import com.example.mongoes.document.Restaurant;
import com.example.mongoes.elasticsearch.repository.RestaurantESRepository;
import com.example.mongoes.response.AggregationSearchResponse;
import com.example.mongoes.response.ResultData;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchAggregation;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchAggregations;
import org.springframework.data.elasticsearch.core.SearchPage;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
Expand All @@ -26,9 +22,13 @@
public class SearchService {

private final RestaurantESRepository restaurantESRepository;
private final AggregationProcessor aggregationProcessor;

public SearchService(RestaurantESRepository restaurantESRepository) {
public SearchService(
RestaurantESRepository restaurantESRepository,
AggregationProcessor aggregationProcessor) {
this.restaurantESRepository = restaurantESRepository;
this.aggregationProcessor = aggregationProcessor;
}

public Mono<Flux<Restaurant>> searchMatchBorough(String query, Integer offset, Integer limit) {
Expand Down Expand Up @@ -123,7 +123,7 @@ public Mono<AggregationSearchResponse> aggregateSearch(
Map<String, Map<String, Long>> map = new HashMap<>();
if (elasticsearchAggregations != null) {
map =
aggregationFunction.apply(
aggregationProcessor.processAggregations(
elasticsearchAggregations.aggregationsAsMap());
}
return new AggregationSearchResponse(
Expand All @@ -135,47 +135,6 @@ public Mono<AggregationSearchResponse> aggregateSearch(
});
}

final Function<Map<String, ElasticsearchAggregation>, Map<String, Map<String, Long>>>
aggregationFunction =
aggregationMap -> {
Map<String, Map<String, Long>> resultMap = new HashMap<>();
aggregationMap.forEach(
(String aggregateKey, ElasticsearchAggregation aggregation) -> {
Map<String, Long> countMap = new HashMap<>();
Aggregate aggregate = aggregation.aggregation().getAggregate();
if (aggregate.isSterms()) {
aggregate
.sterms()
.buckets()
.array()
.forEach(
stringTermsBucket ->
countMap.put(
stringTermsBucket
.key()
.stringValue(),
stringTermsBucket
.docCount()));
} else if (aggregate.isDateRange()) {
List<RangeBucket> bucketList =
aggregate.dateRange().buckets().array();
bucketList.forEach(
rangeBucket -> {
if (rangeBucket.docCount() != 0) {
countMap.put(
rangeBucket.fromAsString()
+ " - "
+ rangeBucket.toAsString(),
rangeBucket.docCount());
}
});
}
resultMap.put(aggregateKey, countMap);
});

return resultMap;
};

public Flux<ResultData> searchRestaurantsWithInRange(
Double lat, Double lon, Double distance, String unit) {
GeoPoint location = new GeoPoint(lat, lon);
Expand Down
Loading