Requirements:
Search for people nearby through the specified points. It is required to filter the age, sort the results according to the distance, and show how far she / he is from you
Design:
ES provides many ways to search geographic locations:
- geo_bounding_box : find the point in the specified rectangle.
- geo_distance : find the point within a given distance from the specified position.
- geo_distance_range : find the point whose distance from the specified point is between the given minimum distance and maximum distance.
The first two are commonly used.
1. Geographic coordinate box model filter
This is by far the most effective geographic coordinate filter because it is very simple to calculate. Specify a rectangle, and then the filter only needs to judge whether the longitude of the coordinate is between the left and right boundaries and whether the latitude is between the upper and lower boundaries: generally, it only needs to set the upper left coordinates And the lower right coordinates.
GET /attractions/restaurant/_search { "query": { "filtered": { "filter": { "geo_bounding_box": { "type": "indexed", "location": { "top_left": { "lat": 40.8, "lon": -74.0 }, "bottom_right": { "lat": 40.7, "lon": -73.0 } } } } } } }
2. Geographic distance filter
Geographic distance filter( geo_distance ) Draw a circle with the location as the center of the circle to find the documents in which the geographical coordinates fall.
GET /attractions/restaurant/_search { "query": { "filtered": { "filter": { "geo_distance": { "distance": "1km", "location": { "lat": 40.715, "lon": -73.988 } } } } } }
There are many kinds of distance units available to us: https://www.elastic.co/guide/en/elasticsearch/reference/5.6/common-options.html#distance-units . the commonly used m, km is enough.
3. Geographical location sorting
The search results can be sorted by the distance from the specified point. When sorting by distance is possible, Score by distance It is usually a better solution. However, this sort is still used to calculate the current distance. Search example:
GET /attractions/restaurant/_search { "query": { "filtered": { "filter": { "geo_bounding_box": { "type": "indexed", "location": { "top_left": { "lat": 40.8, "lon": -74.0 }, "bottom_right": { "lat": 40.4, "lon": -73.0 } } } } } }, "sort": [ { "_geo_distance": { "location": { "lat": 40.715, "lon": -73.998 }, "order": "asc", "unit": "km", "distance_type": "plane" } } ] }
Interpret the following: (pay attention to the sort object)
- Calculate the distance between the location field and the specified lat/lon point in each document.
- Write the distance in km to the sort key of each returned result.
- Use the fast but slightly less accurate plane calculation method.
Environmental preparation
Use elasticsearch version 7.8.0 to introduce dependencies.
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.0-SNAPSHOT</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> <version>2.4.0-SNAPSHOT</version> </dependency>
1. Design database field and ES field mapping
Database field design: add two fields longitude and latitude
CREATE TABLE `es_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'Primary key', `name` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL, `age` int(5) DEFAULT NULL, `tags` varchar(255) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT 'Multi label ''|'' division', `user_desc` varchar(255) COLLATE utf8mb4_bin DEFAULT '' COMMENT 'User profile', `is_deleted` varchar(1) COLLATE utf8mb4_bin NOT NULL DEFAULT 'N', `gmt_create` datetime DEFAULT CURRENT_TIMESTAMP, `gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `lat` decimal(10,6) DEFAULT '0.000000' COMMENT 'dimension', `lon` decimal(10,6) DEFAULT '0.000000' COMMENT 'longitude', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=657 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
Entity class:
/** * (EsUser)Entity class */ @Data public class EsUserEntity implements Serializable { private static final long serialVersionUID = 578800011612714754L; /** * Primary key */ private Long id; private String name; private Integer age; /** * Multiple labels are divided by '|' */ private String tags; /** * User profile */ private String userDesc; private String isDeleted = "0"; private Date gmtCreate; private Date gmtModified; // longitude private Double lat; // dimension private Double lon; }
Object mapping in Java:
@Data @Document(indexName = "es_user") public class ESUser { @Id private Long id; @Field(type = FieldType.Text) private String name; @Field(type = FieldType.Integer) private Integer age; @Field(type = FieldType.Keyword) private List<String> tags; @Field(type = FieldType.Text, analyzer = "ik_max_word") private String desc; @GeoPointField private GeoPoint location; }
2. Prepare massive mock data
Here, longitude: 120.24, dimension: 30.3 are used as the center of the circle, and some nearby points of mock are used as the coordinate data of mock user
@ApiOperation("Input test") @PostMapping("/content/test-insert") public Long importEsUser(Long num) { for (int i = 0; i < num; i++) { ThreadPoolUtil.execute(() -> { EsUserEntity esUser = generateRandomMockerUser(); esUserService.importEsUser(esUser); }); } return num; } // mock Random user data private EsUserEntity generateRandomMockerUser() { // 120.247589,30.306362 EsUserEntity esUserEntity = new EsUserEntity(); int age = new Random().nextInt(20) + 5; esUserEntity.setAge(age); boolean flag = age % 2 > 0; esUserEntity.setName(flag ? RandomCodeUtil.getRandomChinese("0") : RandomCodeUtil.getRandomChinese("1")); esUserEntity.setTags(flag ? "good|Java|handsome" : "lovely|steady|React"); esUserEntity.setUserDesc(flag ? "Havoc in heaven,Nantianmen guard, Good at programming, cooking" : "Sky guard,Good at programming,sleep"); String latRandNumber = RandomCodeUtil.getRandNumberCode(4); String lonRandNumber = RandomCodeUtil.getRandNumberCode(4); esUserEntity.setLon(Double.valueOf("120.24" + latRandNumber)); esUserEntity.setLat(Double.valueOf("30.30" + lonRandNumber)); return esUserEntity; }
Design the object and search criteria class returned to the foreground
/** * Function Description: ES user search results */ @Data public class PeopleNearByVo { private ESUserVo esUserVo; private Double distance; }
/** * Function Description: ES user search results */ @Data public class ESUserVo { private Long id; private String name; private Integer age; private List<String> tags; // Highlight part private List<String> highLightTags; private String desc; // Highlight part private List<String> highLightDesc; // coordinate private GeoPoint location; }
Search class
/** * Function Description: search for people nearby */ @Data public class ESUserLocationSearch { // latitude [3.86, 53.55] private Double lat; // longitude [73.66, 135.05] private Double lon; // Search scope(Unit meter) private Integer distance; // Age greater than or equal to private Integer ageGte; // Younger than private Integer ageLt; }
Core search method:
/** * Search for people nearby * @param locationSearch * @return */ public Page<PeopleNearByVo> queryNearBy(ESUserLocationSearch locationSearch) { Integer distance = locationSearch.getDistance(); Double lat = locationSearch.getLat(); Double lon = locationSearch.getLon(); Integer ageGte = locationSearch.getAgeGte(); Integer ageLt = locationSearch.getAgeLt(); // First build query criteria BoolQueryBuilder defaultQueryBuilder = QueryBuilders.boolQuery(); // Distance search criteria if (distance != null && lat != null && lon != null) { defaultQueryBuilder.filter(QueryBuilders.geoDistanceQuery("location") .distance(distance, DistanceUnit.METERS) .point(lat, lon) ); } // Filter age conditions if (ageGte != null && ageLt != null) { defaultQueryBuilder.filter(QueryBuilders.rangeQuery("age").gte(ageGte).lt(ageLt)); } // Paging condition PageRequest pageRequest = PageRequest.of(0, 10); // Geographic location sorting GeoDistanceSortBuilder sortBuilder = SortBuilders.geoDistanceSort("location", lat, lon); //Assembly conditions NativeSearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(defaultQueryBuilder) .withPageable(pageRequest) .withSort(sortBuilder) .build(); SearchHits<ESUser> searchHits = elasticsearchRestTemplate.search(searchQuery, ESUser.class); List<PeopleNearByVo> peopleNearByVos = Lists.newArrayList(); for (SearchHit<ESUser> searchHit : searchHits) { ESUser content = searchHit.getContent(); ESUserVo esUserVo = new ESUserVo(); BeanUtils.copyProperties(content, esUserVo); PeopleNearByVo peopleNearByVo = new PeopleNearByVo(); peopleNearByVo.setEsUserVo(esUserVo); peopleNearByVo.setDistance((Double) searchHit.getSortValues().get(0)); peopleNearByVos.add(peopleNearByVo); } // Assemble paging objects Page<PeopleNearByVo> peopleNearByVoPage = new PageImpl<>(peopleNearByVos, pageRequest, searchHits.getTotalHits()); return peopleNearByVoPage; }
controller layer
@RequestMapping(value = "/query-doc/nearBy", method = RequestMethod.POST) @ApiOperation("Search nearby people according to coordinate points") public Page<PeopleNearByVo> queryNearBy(@RequestBody ESUserLocationSearch locationSearch) { return esUserService.queryNearBy(locationSearch); }
4. swagger test
The search conditions in the figure below are: take 30.30 north latitude and 120.24 east longitude as the coordinate points to search for people who are over 18 years old and less than 25 years old within 100 meters nearby
Two were found and sorted by distance. The age range is correct. The first is 39 meters and the second is 85 meters. The result is correct.
{ "content": [ { "esUserVo": { "id": 601, "name": "Ji Fulin", "age": 22, "tags": [ "lovely", "steady", "React" ], "highLightTags": null, "desc": "Sky guard,Good at programming,sleep", "highLightDesc": null, "location": { "lat": 30.300214, "lon": 120.240329, "geohash": "wtms25urd9r8", "fragment": true } }, "distance": 39.53764107382481 }, { "esUserVo": { "id": 338, "name": "Lu Jun", "age": 20, "tags": [ "lovely", "steady", "React" ], "highLightTags": null, "desc": "Sky guard,Good at programming,sleep", "highLightDesc": null, "location": { "lat": 30.300242, "lon": 120.240846, "geohash": "wtms25uxwy3p", "fragment": true } }, "distance": 85.56052789780142 } ], "pageable": { "sort": { "sorted": false, "unsorted": true, "empty": true }, "offset": 0, "pageNumber": 0, "pageSize": 10, "paged": true, "unpaged": false }, "last": true, "totalPages": 1, "totalElements": 2, "size": 10, "number": 0, "sort": { "sorted": false, "unsorted": true, "empty": true }, "numberOfElements": 2, "first": true, "empty": false }