在我们熟悉了 es 的基本rest 的操作之后,我们将使用SpringBoot进行整合,进一步熟悉Java API的相关操作。
1.创建一个标准的Springboot项目,引入Boot相关依赖之后,还需要导入依赖(与es服务端版本需要保持一致):
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>transport</artifactId> <version>6.5.1</version> </dependency>
2.注入比要的配置类,这里注意要新建一个 classpath:es-config.properties:
@Component @PropertySource("classpath:es-config.properties") @Data public class EsConfig { //集群名称 @Value("${es.cluster.name}") private String clusterName; // master ip @Value("${es.host.ip}") private String ip; //master port @Value("${es.host.port}") private int port; } @Configuration public class SearchConfig { @Autowired EsConfig esConfig; @Bean public TransportClient client() throws UnknownHostException { InetSocketTransportAddress node = new InetSocketTransportAddress( InetAddress.getByName(esConfig.getIp()), esConfig.getPort() ); Settings settings = Settings.builder() .put("cluster.name", esConfig.getClusterName()) .build(); TransportClient client = new PreBuiltTransportClient(settings); client.addTransportAddress(node); return client; } }
3.创建针对es的增删改服务类,我们将其想象为一个数据库,这样子能优化我们的理解:
@Service public class BookService { private String indexName = "book"; //相当于数据库名称 private String indexType = "technology"; //相当于数据表名称 @Autowired private TransportClient client; public GetResponse getById(String id) { return this.client.prepareGet(indexName, indexType, id).get(); } // 增加 public IndexResponse add(String name, Double price, String publicationDate) throws Exception { XContentBuilder content = XContentFactory.jsonBuilder() .startObject() .field("name", name) .field("price", price) .field("publication_date", publicationDate) .endObject(); IndexResponse response = this.client.prepareIndex(indexName, indexType) .setSource(content) .get(); return response; } // 删除 public DeleteResponse remove(String id) { return this.client.prepareDelete(indexName, indexType, id).get(); } //修改 public UpdateResponse modify(String id, String name, Double price) throws Exception { UpdateRequest request = new UpdateRequest(indexName, indexType, id); XContentBuilder builder = XContentFactory.jsonBuilder() .startObject(); if (name != null) { builder.field("name", name); } if (price != null) { builder.field("price", price); } builder.endObject(); request.doc(builder); return this.client.update(request).get(); } }
这样子就完成最基本的增删改的操作服务,那么还有个查询呢?那么接下去我们采用一个简单的搜索附近的人的例子来理解一下查询的流程:
1.定义实体:
@Data public class People { private Double lat; //纬度 private Double lon; //经度 private String wxNo; //微信号 private String nickName;//昵称 private String sex; //性别 public People(String wxNo, String nickName, String sex, Double lat, Double lon) { this.wxNo = wxNo; this.nickName = nickName; this.sex = sex; this.lat = lat; this.lon = lon; } }
2.由于属性均为随机生成,这里提供一个随机数工具类:
public class RandomUtil { private static Random random = new Random(); private static final char[] sexs = "男女".toCharArray(); private static final char[] wxNo = "abcdefghijklmnopqrstuvwxyz0123456789".toLowerCase().toCharArray(); private static final char[] firstName = "赵钱孙李周吴郑王冯陈卫蒋沈韩杨朱秦许何吕施张孔曹严金魏陶姜谢邹窦章苏潘葛范彭谭夏胡".toCharArray(); //确保车牌号不重复,声明一个缓存区 private static Set<String> wxNoCache; /** * 随机生成性别 * * @return */ public static String randomSex() { int i = random.nextInt(sexs.length); return ("" + sexs[i]); } /** * 随机生成微信号 * * @return */ public static String randomWxNo() { //初始化缓冲区 openCache(); //微信号自动生成规则,wx_开头加上10位数字字组合 StringBuffer sb = new StringBuffer(); for (int c = 0; c < 10; c++) { int i = random.nextInt(wxNo.length); sb.append(wxNo[i]); } String carNum = ("wx_" + sb.toString()); //为了防止微信号重复,生成以后检查一下 //如果重复,递归重新生成,直到不重复为止 if (wxNoCache.contains(carNum)) { return randomWxNo(); } wxNoCache.add(carNum); return carNum; } /** * 随机生成坐标 */ public static double[] randomPoint(double myLat, double myLon) { //随机生成一组附近的坐标 double s = random.nextDouble(); //格式化保留6位小数 DecimalFormat df = new DecimalFormat("######0.000000"); String slon = df.format(s + myLon); String slat = df.format(s + myLat); Double dlon = Double.valueOf(slon); Double dlat = Double.valueOf(slat); return new double[]{dlat, dlon}; } /** * 随机生成微信名称 * * @return */ public static String randomNickName(String sex) { int i = random.nextInt(firstName.length); return firstName[i] + ("男".equals(sex) ? "先生" : "女士"); } /** * 开启缓存区 */ public static void openCache() { if (wxNoCache == null) { wxNoCache = new HashSet<String>(); } } /** * 清空缓存区 */ public static void clearCache() { wxNoCache = null; } }
3.配置我当前的点位:
@Component @PropertySource("classpath:my-point.properties") @Data public class MyPointConfig { @Value("${my.point.lon}") private double lon; @Value("${my.point.lat}") private double lat; }
4.主要操作的类:
@Service @Slf4j public class NearbyService { @Autowired private TransportClient client; private String indexName = "nearby"; //相当于数据库名称 private String indexType = "wechat"; //相当于数据表名称 //建库建表建约束 /** * 建库建表建约束的方法 */ public void recreateIndex() throws IOException { try { //后台级的操作,关乎到删除跑路的危险 if (client.admin().indices().prepareExists(indexName).execute().actionGet().isExists()) { //先清除原来已有的数据库 client.admin().indices().prepareDelete(indexName).execute().actionGet(); } } catch (Exception e) { e.printStackTrace(); } createIndex(); } /** * 创建索引 * * @throws IOException */ private void createIndex() throws IOException { //表结构(建约束) XContentBuilder mapping = createMapping(); //建库 //建库建表建约束 CreateIndexResponse createIndexResponse = client.admin().indices().prepareCreate(indexName).execute().actionGet(); if (!createIndexResponse.isAcknowledged()) { log.info("无法创建索引[" + indexName + "]"); } //建表 PutMappingRequest putMapping = Requests.putMappingRequest(indexName).type(indexType).source(mapping); AcknowledgedResponse response = client.admin().indices().putMapping(putMapping).actionGet(); if (!response.isAcknowledged()) { log.info("无法创建[" + indexName + "] [" + indexType + "]的Mapping"); } else { log.info("创建[" + indexName + "] [" + indexType + "]的Mapping成功"); } } /** * 准备造假数据,这些数值会随机生成 * * @param myLat 维度 * @param myLon 经度 * @param count 生成多少个 */ public Integer addDataToIndex(double myLat, double myLon, int count) { List<XContentBuilder> contents = new ArrayList<XContentBuilder>(); //开启重复校验的缓存区 RandomUtil.openCache(); //一个循环跑下来就产生了10W条模拟记录,也得要具有一定的真实性 for (long i = 0; i < count; i++) { People people = randomPeople(myLat, myLon); contents.add(obj2XContent(people)); } //清空重复校验的缓存区 RandomUtil.clearCache(); //把数据批量写入到数据库表中 BulkRequestBuilder bulkRequest = client.prepareBulk(); for (XContentBuilder content : contents) { IndexRequest request = client.prepareIndex(indexName, indexType).setSource(content).request(); bulkRequest.add(request); } BulkResponse bulkResponse = bulkRequest.execute().actionGet(); if (bulkResponse.hasFailures()) { log.info("创建索引出错!"); } return bulkRequest.numberOfActions(); } /** * 检索附近的人 * * @param lat * @param lon * @param radius * @param size */ public SearchResult search(double lat, double lon, int radius, int size, String sex) { SearchResult result = new SearchResult(); //同一单位为米 String unit = DistanceUnit.METERS.toString();//坐标范围计量单位 //获取一个查询规则构造器 //查是哪个库哪个表 //完成了相当于 select * from 数据库.表名 SearchRequestBuilder srb = client.prepareSearch(indexName).setTypes(indexType); //实现分页操作 //相当于MySQL中的 limit 0,size srb.setFrom(0).setSize(size);//取出优先级最高的size条数据 //拼接查询条件 //性别、昵称,坐标 //构建查询条件 //地理坐标,方圆多少米以内都要给找出来 QueryBuilder qb = QueryBuilders.geoDistanceQuery("location") .point(lat, lon) .distance(radius, DistanceUnit.METERS) // .optimizeBbox("memory") .geoDistance(GeoDistance.PLANE); //设置计算规则,是平面还是立体 (方圆多少米) // //相对于 where location > 0 and location < radius srb.setPostFilter(qb); //继续拼接where条件 //and sex = ? BoolQueryBuilder bq = QueryBuilders.boolQuery(); if (!(sex == null || "".equals(sex.trim()))) { bq.must(QueryBuilders.matchQuery("sex", sex)); } srb.setQuery(bq); //设置排序规则 GeoDistanceSortBuilder geoSort = SortBuilders.geoDistanceSort("location", lat, lon); geoSort.unit(DistanceUnit.METERS); geoSort.order(SortOrder.ASC);//按距离升序排序,最近的要排在最前面 //order by location asc 升序排序 srb.addSort(geoSort); //到此为止,就相当于SQL语句构建完毕 //开始执行查询 //调用 execute()方法 //Response SearchResponse response = srb.execute().actionGet(); //高亮分词 SearchHits hits = response.getHits(); SearchHit[] searchHists = hits.getHits(); //搜索的耗时 Float usetime = response.getTook().getMillis() / 1000f; result.setTotal(hits.getTotalHits()); result.setUseTime(usetime); result.setDistance(DistanceUnit.METERS.toString()); result.setData(new ArrayList<Map<String, Object>>()); for (SearchHit hit : searchHists) { // 获取距离值,并保留两位小数点 BigDecimal geoDis = new BigDecimal((Double) hit.getSortValues()[0]); Map<String, Object> hitMap = hit.getSourceAsMap(); // 在创建MAPPING的时候,属性名的不可为geoDistance。 hitMap.put("geoDistance", geoDis.setScale(0, BigDecimal.ROUND_HALF_DOWN)); result.getData().add(hitMap); } return result; } /** * 创建mapping,相当于创建表结构 * * @return */ private XContentBuilder createMapping() { XContentBuilder mapping = null; try { mapping = XContentFactory.jsonBuilder() .startObject() // 索引库名(类似数据库中的表) .startObject(indexType) .startObject("properties") //微信号(唯一的索引) keyword text .startObject("wxNo").field("type", "keyword").endObject() //昵称 .startObject("nickName").field("type", "keyword").endObject() //性别 .startObject("sex").field("type", "keyword").endObject() //位置,专门用来存储地理坐标的类型,包含了经度和纬度 .startObject("location").field("type", "geo_point").endObject() .endObject() .endObject() .endObject(); } catch (IOException e) { e.printStackTrace(); } return mapping; } /** * 将Java对象转换为JSON字符串(所谓的全文检索,玩的就是字符串) */ private XContentBuilder obj2XContent(People people) { XContentBuilder jsonBuild = null; try { // 使用XContentBuilder创建json数据 jsonBuild = XContentFactory.jsonBuilder(); jsonBuild.startObject() .field("wxNo", people.getWxNo()) .field("nickName", people.getNickName()) .field("sex", people.getSex()) .startObject("location") .field("lat", people.getLat()) .field("lon", people.getLon()) .endObject() .endObject(); // jsonData = ; } catch (IOException e) { e.printStackTrace(); } return jsonBuild; } /** * 构造一个人 * * @param myLat 所在的纬度 * @param myLon 所在的经度 */ public People randomPeople(double myLat, double myLon) { //随机生成微信号 String wxNo = RandomUtil.randomWxNo(); //造人计划,性别随机 String sex = RandomUtil.randomSex(); //随机生成昵称 String nickName = RandomUtil.randomNickName(sex); //随机生成坐标 double[] point = RandomUtil.randomPoint(myLat, myLon); return new People(wxNo, nickName, sex, point[0], point[1]); } }
5.对查询结果进行封装:
@Data public class SearchResult { private Long total;//记录总数 private Float useTime;//搜索花费时间(毫秒) private String distance;//距离单位(米) private List<Map<String, Object>> data = new ArrayList<Map<String, Object>>();//数据集合 }
6.提供一个Controller 进行测试:
@RestController public class SearchController { @Autowired private BookService bookService; @Autowired private NearbyService nearbyService; @Autowired private MyPointConfig myPointConfig; //这是我所在的坐标值 private String myName = "wuzz";//我的名字 @GetMapping("/get/book/technology") public ResponseEntity get(@RequestParam(name = "id", defaultValue = "") String id) { GetResponse response = bookService.getById(id); if (!response.isExists()) { return new ResponseEntity(HttpStatus.NOT_FOUND); } return new ResponseEntity(response.getSource(), HttpStatus.OK); } @PostMapping("add/book/technology") public ResponseEntity add( @RequestParam(name = "name") String name, @RequestParam(name = "price") String price, @RequestParam(name = "publicationDate") String publicationDate ) { IndexResponse response; try { response = bookService.add(name, Double.parseDouble(price), publicationDate); } catch (Exception e) { e.printStackTrace(); return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR); } return new ResponseEntity(response, HttpStatus.OK); } @DeleteMapping("remove/book/technology") public ResponseEntity remove(@RequestParam(name = "id") String id) { DeleteResponse response = bookService.remove(id); return new ResponseEntity(response.getResult().toString(), HttpStatus.OK); } @PutMapping("modify/book/technology") public ResponseEntity modify(@RequestParam(name = "id") String id, @RequestParam(name = "name", required = false) String name, @RequestParam(name = "price", required = false) String price) { UpdateResponse response; try { response = bookService.modify(id, name, Double.parseDouble(price)); } catch (Exception e) { return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR); } return new ResponseEntity(response.getResult().toString(), HttpStatus.OK); } @GetMapping("searchNearby") @ResponseBody public void searchNearby() { int size = 1000, radius = 1000000000; System.out.println("开始获取距离" + myName + radius + "米以内人"); SearchResult result = nearbyService.search(myPointConfig.getLat(), myPointConfig.getLon(), radius, size, null); System.out.println("共找到" + result.getTotal() + "个人,优先显示" + size + "人,查询耗时" + result.getUseTime() + "秒"); for (Map<String, Object> taxi : result.getData()) { String nickName = taxi.get("nickName").toString(); String location = taxi.get("location").toString(); Object geo = taxi.get("geoDistance"); System.out.println(nickName + "," + "微信号:" + taxi.get("wxNo") + ",性别:" + taxi.get("sex") + ",距离" + myName + geo + "米" + "(坐标:" + location + ")"); } System.out.println("以上" + size + "人显示在列表中......"); } @GetMapping("/initData") @ResponseBody public void initData() { int total = 1000; int inserted = 0; try { //建库、建表、建约束 nearbyService.recreateIndex(); //随机产生10W条数据 inserted = nearbyService.addDataToIndex(myPointConfig.getLat(), myPointConfig.getLon(), total); } catch (Exception e) { e.printStackTrace(); } System.out.println("\n========= 数据初始化工作完毕,共随机产生" + inserted + "条数据,失败(" + (total - inserted) + ")条 =========\n"); } }
这里需要注意的点,我们需要先进性 initData 才能进行查找,在查找的时候如果报错,需要查看index的结构:
注意 location 的type,是否是这个,如果不是这个 ,建议把index删了重新创建。这样子对es的基本操作就有一定的认识了。