Redis GEO實現(xiàn)搜索附近用戶的項目實踐
前言
做完 homie 匹配系統(tǒng)后就在學習其他東西了,因此擱置了好久。最近呢因為不想學新技術了就打算利用 Redis GEO 實現(xiàn)搜索附近用戶的功能,算是一個拓展吧!整體的實現(xiàn)并不困難,學完 Redis 再看會更輕松(未學過也沒事)。話不多說直接開始擼代碼吧。
設計思路和流程
在 User(用戶)表中添加兩個字段 longitude(經度)和 dimension(維度),用以存儲用戶的經緯度坐標。因為Redis GEO 通過每個用戶的經緯度坐標計算用戶間的距離,同時其 Redis 數(shù)據類型為ZSET,ZSET 是一個有序的 List 類似 Java 的 SortedSet。在此場景 value 就是用戶id,score 是經緯度信息( ZSET 根據 score值升序排序)。
create table hjj.user ( username varchar(256) null comment '用戶昵稱', id bigint auto_increment comment 'id' primary key, userAccount varchar(256) null comment '賬戶', avatarUrl varchar(1024) null comment '用戶頭像', gender tinyint null comment '用戶性別', profile varchar(512) null comment '個人簡介', userPassword varchar(512) not null comment '用戶密碼', phone varchar(128) null comment '電話', email varchar(512) null comment '郵箱', userStatus int default 0 not null comment '狀態(tài) 0 - 正常', createTime datetime default CURRENT_TIMESTAMP null comment '創(chuàng)建時間', updateTime datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新時間', isDelete tinyint default 0 not null comment '是否刪除', userRole int default 0 not null comment '用戶角色 0 - 普通用戶 1 - 管理員', planetCode varchar(512) null comment '星球編號', tags varchar(1024) null comment '標簽列表(json)', longitude decimal(10, 6) null comment '經度', dimension decimal(10, 6) null comment '緯度' ) comment '用戶';
2. 在 UserVO 類中添加distance字段,用以向前端返回每個用戶與自己之間的距離,類型為Double。
/** * 用戶信息封裝類 */ @Data public class UserVO { /** * id */ private long id; /** * 用戶昵稱 */ private String username; /** * 賬戶 */ private String userAccount; /** * 用戶頭像 */ private String avatarUrl; /** * 用戶性別 */ private Integer gender; /** * 用戶簡介 */ private String profile; /** * 電話 */ private String phone; /** * 郵箱 */ private String email; /** * 狀態(tài) 0 - 正常 */ private Integer userStatus; /** * 創(chuàng)建時間 */ private Date createTime; /** * 更新時間 */ private Date updateTime; /** * 用戶角色 0 - 普通用戶 1 - 管理員 */ private Integer userRole; /** * 星球編號 */ private String planetCode; /** * 標簽列表 json */ private String tags; /** * 用戶距離 */ private Double distance; private static final long serialVersionUID = 1L; }
基本業(yè)務實現(xiàn)
導入各個用戶經緯度數(shù)據
編寫測試類導入各個用戶的經緯度信息并且寫入Redis中,Redis GEO會根據它計算出一個 score值。進行 Redis GEO 相關操作時可以使用 Spring Data Redis 提供現(xiàn)成的操作 Redis 的模板——StringRedisTemplate,注意其 Key/Value 都是String類型。
stringRedisTemplate.opsForGeo().add() 支持一次一次地傳入經緯度信息,可以通過List和Map集合類型傳入用戶經緯度信息,這里我們用List集合。第一個參數(shù)為Redis的key,這不用過多介紹。第二個參數(shù)為List類型,泛型為RedisGeoCommands.GeoLocation<String>,其參數(shù)為用戶id和Point(Point可以理解為是一個圓的一個點吧,經緯度就是x/y坐標)。
stringRedisTemplate.opsForGeo().add()傳入的參數(shù):
@Test public void importUserGEOByRedis() { List<User> userList = userService.list(); // 查詢所有用戶 String key = RedisConstant.USER_GEO_KEY; // Redis的key List<RedisGeoCommands.GeoLocation<String>> locationList = new ArrayList<>(userList.size()); // 初始化地址(經緯度)List for (User user : userList) { locationList.add(new RedisGeoCommands.GeoLocation<>(String.valueOf(user.getId()), new Point(user.getLongitude(), user.getDimension()))); // 往locationList添加每個用戶的經緯度數(shù)據 } stringRedisTemplate.opsForGeo().add(key, locationList); // 將每個用戶的經緯度信息寫入Redis中 }
結果:
獲取用戶 id = 1 與其他用戶的距離
編寫一個測試類計算用戶 id = 1 與其他用戶之間的距離。利用stringRedisTemplate.opsForGeo().distance()方法,其主要參數(shù)為member1和member2,Metric是計算距離的單位類型。從名稱就可以知道m(xù)ember1和member2其實就是用戶1和用戶2的信息,因為我們在上面用 locationList.add() 添加用戶id和用戶的經度坐標,所以這兩個member就是用戶id咯。
?所以寫個循環(huán)就可以算出用戶 id = 1 與其他用戶的距離
@Test public void getUserGeo() { String key = RedisConstant.USER_GEO_KEY; List<User> userList = userService.list(); // 計算每個用戶與登錄用戶的距離 for (User user : userList) { Distance distance = stringRedisTemplate.opsForGeo().distance(key, "1", String.valueOf(user.getId()), RedisGeoCommands.DistanceUnit.KILOMETERS); System.out.println("User: " + user.getId() + ", Distance: " + distance.getValue() + " " + distance.getUnit()); } }
結果:
搜索附近用戶
利用現(xiàn)成的 stringRedisTemplate.opsForGeo().radius 方法,第一個參數(shù)依然是Redis的key,第二個參數(shù)是Circle,看代碼和名稱就知道其是一個圓(傳入Point即圓心和圓的半徑)。想象搜索附近的用戶就是搜索以你為圓心,半徑為搜索距離的圓內的用戶。理解這些代碼就能順理成章的擼出來了,是不是不算難。
@Test public void searchUserByGeo() { User loginUser = userService.getById(1); Distance geoRadius = new Distance(1500, RedisGeoCommands.DistanceUnit.KILOMETERS); Circle circle = new Circle(new Point(loginUser.getLongitude(), loginUser.getDimension()), geoRadius); RedisGeoCommands.GeoRadiusCommandArgs geoRadiusCommandArgs = RedisGeoCommands.GeoRadiusCommandArgs .newGeoRadiusArgs().includeCoordinates(); GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo().radius(RedisConstant.USER_GEO_KEY, circle, geoRadiusCommandArgs); for (GeoResult<RedisGeoCommands.GeoLocation<String>> result : results) { if (!result.getContent().getName().equals("1")) { System.out.println(result.getContent().getName()); // 打印1500km內的用戶id } } }
注意:搜索附近的用戶會搜索到自己,所以可以加一個判斷以排除自己。
結果:
?應用至項目中
改寫用戶推薦接口
注意返回類型是UserVO不是User,因為我的前端展示了推薦用戶和自己之間的距離。
UserController.recommendUsers:
@GetMapping("/recommend") public BaseResponse<List<UserVO>> recommendUsers(long pageSize, long pageNum, HttpServletRequest request){ User loginUser = userService.getLoginUser(request); QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.ne("id", loginUser.getId()); IPage<User> page = new Page<>(pageNum, pageSize); IPage<User> userIPage = userService.page(page, queryWrapper); String redisUserGeoKey = RedisConstant.USER_GEO_KEY; // 將User轉換為UserVO List<UserVO> userVOList = userIPage.getRecords().stream() .map(user -> { // 查詢距離 Distance distance = stringRedisTemplate.opsForGeo().distance(redisUserGeoKey, String.valueOf(loginUser.getId()), String.valueOf(user.getId()), RedisGeoCommands.DistanceUnit.KILOMETERS); double value = distance.getValue(); // 創(chuàng)建UserVO對象并設置屬性 UserVO userVO = new UserVO(); // 這里可以用BeanUtils.copyProperties(),就沒必要重復set了 userVO.setId(user.getId()); userVO.setUsername(user.getUsername()); userVO.setUserAccount(user.getUserAccount()); userVO.setAvatarUrl(user.getAvatarUrl()); userVO.setGender(user.getGender()); userVO.setProfile(user.getProfile()); userVO.setPhone(user.getPhone()); userVO.setEmail(user.getEmail()); userVO.setUserStatus(user.getUserStatus()); userVO.setCreateTime(user.getCreateTime()); userVO.setUpdateTime(user.getUpdateTime()); userVO.setUserRole(user.getUserRole()); userVO.setPlanetCode(user.getPlanetCode()); userVO.setTags(user.getTags()); userVO.setDistance(value); // 設置距離值 return userVO; }) .collect(Collectors.toList()); System.out.println(userVOList); return ResultUtils.success(userVOList); }
改寫匹配用戶接口
UserController.matchUsers:
/** * 推薦最匹配的用戶 * @return */ @GetMapping("/match") public BaseResponse<List<UserVO>> matchUsers(long num, HttpServletRequest request){ if (num <=0 || num > 20) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User loginUser = userService.getLoginUser(request); return ResultUtils.success(userService.matchUsers(num ,loginUser)); }
UserServiceImpl.matchUsers:
@Override public List<UserVO> matchUsers(long num, User loginUser) { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.isNotNull("tags"); queryWrapper.ne("id", loginUser.getId()); queryWrapper.select("id","tags"); List<User> userList = this.list(queryWrapper); String tags = loginUser.getTags(); Gson gson = new Gson(); List<String> tagList = gson.fromJson(tags, new TypeToken<List<String>>() { }.getType()); // 用戶列表的下表 => 相似度' List<Pair<User,Long>> list = new ArrayList<>(); // 依次計算當前用戶和所有用戶的相似度 for (int i = 0; i <userList.size(); i++) { User user = userList.get(i); String userTags = user.getTags(); //無標簽的 或當前用戶為自己 if (StringUtils.isBlank(userTags) || user.getId() == loginUser.getId()){ continue; } List<String> userTagList = gson.fromJson(userTags, new TypeToken<List<String>>() { }.getType()); //計算分數(shù) long distance = AlgorithmUtils.minDistance(tagList, userTagList); list.add(new Pair<>(user,distance)); } //按編輯距離有小到大排序 List<Pair<User, Long>> topUserPairList = list.stream() .sorted((a, b) -> (int) (a.getValue() - b.getValue())) .limit(num) .collect(Collectors.toList()); //有順序的userID列表 List<Long> userListVo = topUserPairList.stream().map(pari -> pari.getKey().getId()).collect(Collectors.toList()); //根據id查詢user完整信息 QueryWrapper<User> userQueryWrapper = new QueryWrapper<>(); userQueryWrapper.in("id",userListVo); Map<Long, List<User>> userIdUserListMap = this.list(userQueryWrapper).stream() .map(user -> getSafetyUser(user)) .collect(Collectors.groupingBy(User::getId)); List<User> finalUserList = new ArrayList<>(); for (Long userId : userListVo){ finalUserList.add(userIdUserListMap.get(userId).get(0)); } String redisUserGeoKey = RedisConstant.USER_GEO_KEY; List<UserVO> finalUserVOList = finalUserList.stream().map(user -> { Distance distance = stringRedisTemplate.opsForGeo().distance(redisUserGeoKey, String.valueOf(loginUser.getId()), String.valueOf(user.getId()), RedisGeoCommands.DistanceUnit.KILOMETERS); UserVO userVO = new UserVO(); userVO.setId(user.getId()); // 這里可以用BeanUtils.copyProperties(),就沒必要重復set了 userVO.setUsername(user.getUsername()); userVO.setUserAccount(user.getUserAccount()); userVO.setAvatarUrl(user.getAvatarUrl()); userVO.setGender(user.getGender()); userVO.setProfile(user.getProfile()); userVO.setPhone(user.getPhone()); userVO.setEmail(user.getEmail()); userVO.setUserStatus(user.getUserStatus()); userVO.setCreateTime(user.getCreateTime()); userVO.setUpdateTime(user.getUpdateTime()); userVO.setUserRole(user.getUserRole()); userVO.setPlanetCode(user.getPlanetCode()); userVO.setTags(user.getTags()); userVO.setDistance(distance.getValue()); return userVO; }).collect(Collectors.toList()); return finalUserVOList; }
添加搜索附近用戶接口
UserController.searchNearby:
/** * 搜索附近用戶 */ @GetMapping("/searchNearby") public BaseResponse<List<UserVO>> searchNearby(int radius, HttpServletRequest request) { if (radius <= 0 || radius > 10000) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User user = userService.getLoginUser(request); User loginUser = userService.getById(user.getId()); List<UserVO> userVOList = userService.searchNearby(radius, loginUser); return ResultUtils.success(userVOList); }
UserServiceImpl.searchNearby:
@Override public List<UserVO> searchNearby(int radius, User loginUser) { String geoKey = RedisConstant.USER_GEO_KEY; String userId = String.valueOf(loginUser.getId()); Double longitude = loginUser.getLongitude(); Double dimension = loginUser.getDimension(); if (longitude == null || dimension == null) { throw new BusinessException(ErrorCode.NULL_ERROR, "登錄用戶經緯度參數(shù)為空"); } Distance geoRadius = new Distance(radius, RedisGeoCommands.DistanceUnit.KILOMETERS); Circle circle = new Circle(new Point(longitude, dimension), geoRadius); GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo() .radius(geoKey, circle); List<Long> userIdList = new ArrayList<>(); for (GeoResult<RedisGeoCommands.GeoLocation<String>> result : results) { String id = result.getContent().getName(); if (!userId.equals(id)) { userIdList.add(Long.parseLong(id)); } } List<UserVO> userVOList = userIdList.stream().map( id -> { UserVO userVO = new UserVO(); User user = this.getById(id); BeanUtils.copyProperties(user, userVO); Distance distance = stringRedisTemplate.opsForGeo().distance(geoKey, userId, String.valueOf(id), RedisGeoCommands.DistanceUnit.KILOMETERS); userVO.setDistance(distance.getValue()); return userVO; } ).collect(Collectors.toList()); return userVOList; }
到此這篇關于Redis GEO實現(xiàn)搜索附近用戶的項目實踐的文章就介紹到這了,更多相關Redis GEO內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Redis報錯:Could not create server TCP 
這篇文章主要介紹了Redis報錯:Could not create server TCP listening socket 127.0.0.1:6379: bind:解決方法,是安裝與啟動Redis過程中比較常見的問題,需要的朋友可以參考下2023-06-06