1. 程式人生 > >springboot系列十二 Spring-Data-ElasticSearch

springboot系列十二 Spring-Data-ElasticSearch

文件

ElasticSearch安裝

docker 安裝ElasticSearch(2.x版本)

docker 安裝ElasticSearch(6.x版本)

SpringDataElasticsearch和ElasticSearch版本相容

參考https://github.com/spring-projects/spring-data-elasticsearch

spring data elasticsearchelasticsearch
3.1.x6.2.2
3.0.x5.5.0
2.1.x2.4.0
2.0.x2.2.0
1.3.x1.5.2

如果版本不相容,會拋異常

org.elasticsearch.client.transport.NoNodeAvailableException: None of the configured nodes are available: [{#transport#-1}{ZAJHQCraS-6cuRir7xf-eg}{localhost}{192.168.1.123:9300}]

這裡使用SpringDataElasticsearch版本為3.1.2

Elasticsearch版本為6.5.0

基本CURD

依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

配置

spring:
  data:
    elasticsearch:
      cluster-nodes: localhost:9300
      # 節點名稱,預設為elasticsearch,如果docker安裝的,這裡是docker-cluster
      # http://localhost:9200/_cluster/state 檢視節點名稱
      cluster-name: docker-cluster 

定義一個實體類

@Data
@Document(indexName = "user", type = "test")
public class User {
    @Id
    private String id;
    private String name;
    private int age = 18;
    private Date createTime = new Date();
}

寫一個jpa的dao類

public interface UserRepository extends ElasticsearchRepository<User, String> {
    User findByName(String name);
}

測試介面

@RestController
@RequestMapping("/user")
public class UserResource {
    @Autowired private UserRepository userRepository;
    @PostMapping("")
    public User save1(@RequestBody User user){
        return userRepository.save(user);
    }

    @GetMapping("")
    public Iterable<User> findAll1(){
        return userRepository.findAll();
    }

    @GetMapping("/{name}")
    public User findOne1(@PathVariable String name){
        return userRepository.findByName(name);
    }
}

測試:新增一條資料 POST http://localhost:8080/user

檢視es資料:

QueryBuilder條件查詢

新增測試資料

新建一個實體Article

@Data
@Document(indexName = "article", type = "test")
public class Article {
    @Id
    private String id;
    private String author;
    private String title;
    private String content;
    private Date time;
}

再新增一個dao類

public interface ArticleRepository extends ElasticsearchRepository<Article, String> {
}

寫個測試介面,新增幾條資料

@Autowired private ArticleRepository articleRepository;
@PostMapping("")
public Article save(@RequestBody Article article){
    return articleRepository.save(article);
}

分頁查詢

使用Pageable來處理分頁請求引數

  • page: 從第幾頁開始
  • size: 每頁條數
  • sort: 排序欄位,可寫多個欄位
  • direction: 升序或降序 asc|desc
/**分頁查詢*/
@GetMapping("/page")
public Page<Article> range(String query,
                           @PageableDefault(page = 0, size = 5, sort = "time", direction = Sort.Direction.DESC) Pageable pageable){
    BoolQueryBuilder qb = QueryBuilders.boolQuery();
    if(query != null) {
        qb.must(QueryBuilders.matchQuery("title", query));
    }
    return articleRepository.search(qb, pageable);
}

測試: GET http://localhost:8080/article/page?query=了&page=0&size=2

{
    "content": [
        {
            "id": "Sw6Gh2cBBlxbCrguspsL",
            "author": "test",
            "title": "java版本到多少了",
            "content": "可能是12了",
            "time": "2018-05-19T17:02:02.000+0000"
        },
        {
            "id": "Sg6Gh2cBBlxbCrguJJsx",
            "author": "王五",
            "title": "奇怪了",
            "content": "獨到的方式哈哈哈哈",
            "time": "2018-03-19T17:02:02.000+0000"
        }
    ],
    "pageable": {
        "sort": {
            "sorted": true,
            "unsorted": false,
            "empty": false
        },
        "offset": 0,
        "pageSize": 2,
        "pageNumber": 0,
        "unpaged": false,
        "paged": true
    },
    "facets": [],
    "aggregations": null,
    "scrollId": null,
    "maxScore": "NaN",
    "totalPages": 2,
    "totalElements": 3,
    "size": 2,
    "number": 0,
    "first": true,
    "numberOfElements": 2,
    "sort": {
        "sorted": true,
        "unsorted": false,
        "empty": false
    },
    "last": false,
    "empty": false
}

精確匹配term

精確匹配,查詢中文時,需要安裝分詞外掛,查詢英文沒問題

/**精確匹配*/
@GetMapping("/term")
public Page<Article> term(String query){
    BoolQueryBuilder qb = QueryBuilders.boolQuery();
    qb.must(QueryBuilders.termQuery("author", query));
    return (Page<Article>)articleRepository.search(qb);
}

測試:GET http://localhost:8080/article/term?query=test

{
    "content": [
        {
            "id": "Sw6Gh2cBBlxbCrguspsL",
            "author": "test",
            "title": "java版本到多少了",
            "content": "可能是12了",
            "time": "2018-05-19T17:02:02.000+0000"
        }
    ],
    # 其他省略
}

模糊匹配match

/**模糊匹配*/
@GetMapping("/match")
public Page<Article> match(String query){
    BoolQueryBuilder qb = QueryBuilders.boolQuery();
    qb.must(QueryBuilders.matchQuery("content", query));
    return (Page<Article>)articleRepository.search(qb);
}

/**短語模糊匹配*/
@GetMapping("/matchPhrase")
public Page<Article> matchPhraseQuery(String query){
    BoolQueryBuilder qb = QueryBuilders.boolQuery();
    qb.must(QueryBuilders.matchPhraseQuery("content", query));
    return (Page<Article>)articleRepository.search(qb);
}

測試:GET http://localhost:8080/article/match?query=的

{
    "content": [
       {
            "id": "Rw6Dh2cBBlxbCrguwJu4",
            "author": "張三",
            "title": "解放東路手機放",
            "content": "的說法是實打實的",
            "time": "2018-12-07T07:12:38.000+0000"
        },
        {
            "id": "SQ6Fh2cBBlxbCrguV5vX",
            "author": "李四",
            "title": "詹姆斯來湖人了",
            "content": "飛機歐時力的方式來顛覆了聖誕節是鄧麗君的時間309348噢03的類似放假了llldfjsljl",
            "time": "2018-01-19T17:02:02.000+0000"
        },
        {
            "id": "Sg6Gh2cBBlxbCrguJJsx",
            "author": "王五",
            "title": "奇怪了",
            "content": "獨到的方式哈哈哈哈",
            "time": "2018-03-19T17:02:02.000+0000"
        }
    ],
    # 其他省略
}

範圍查詢range

/**範圍查詢*/
@GetMapping("/range")
public Page<Article> range(long query){
    BoolQueryBuilder qb = QueryBuilders.boolQuery();
    qb.must(QueryBuilders.rangeQuery("time").gt(query));
    //qb.must(QueryBuilders.rangeQuery("time").from(query).to(System.currentTimeMillis()));//大於query,小於當前時間
    return (Page<Article>)articleRepository.search(qb);
}

測試:GET http://localhost:8080/article/range?query=1526749322000

{
    "content": [
       {
            "id": "SA6Eh2cBBlxbCrguuJsK",
            "author": "張三",
            "title": "科比退役",
            "content": "2018飛機上林德洛夫科比退役了",
            "time": "2018-12-07T07:13:59.000+0000"
        },
        {
            "id": "Rw6Dh2cBBlxbCrguwJu4",
            "author": "張三",
            "title": "解放東路手機放",
            "content": "的說法是實打實的",
            "time": "2018-12-07T07:12:38.000+0000"
        }
    ],
    # 其他省略
}

位置搜尋

Elasticsearch 提供了 兩種表示地理位置的方式:用緯度-經度表示的座標點使用 geo_point 欄位型別, 以 GeoJSON 格式定義的複雜地理形狀,使用 geo_shape 欄位型別。

這裡使用geo_point來舉例

初始化模型和資料

新建一個實體 Location

@Data
@Document(indexName = "location")
public class Location {
    @Id
    private String id;
    @GeoPointField
    private GeoPoint location;//位置座標 lon經度 lat緯度
    private String address;//地址
}

新增一個dao類

public interface LocationRepository extends ElasticsearchRepository<Location, String> {
}

然後寫個測試介面,來新增幾條資料

@Autowired
private LocationRepository locationRepository;

@PostMapping("")
public Location save(@RequestBody Location location){
    return locationRepository.save(location);
}

這裡使用百度地區的座標拾取器來取得位置座標

傳送門 百度位置座標拾取器

新增資料 POST http://localhost:8080/location

{
	"location":{
		"lon":120.137051,
		"lat":30.265498
	},
	"address":"杭州西湖區政府"
}

重複新增,新增後檢視es資料:

計算2個座標的舉例

SpringDataElasticSearch提供了一個工具 GeoDistance

//參考 https://www.elastic.co/guide/cn/elasticsearch/guide/current/sorting-by-distance.html
//GeoDistance.PLANE 快速但精度略差 srcLat:源緯度 dstLat:目標緯度
GeoDistance.PLANE.calculate(double srcLat, double srcLon, double dstLat, double dstLon, DistanceUnit unit)
//GeoDistance.ARC 效率較差但精度高
GeoDistance.ARC.calculate(double srcLat, double srcLon, double dstLat, double dstLon, DistanceUnit unit)

根據座標位置查詢

在Location實體中新增一個欄位,用來介面返回“距離多少米”

private String distanceMeters;//距離多少米

測試介面

/**
 * 搜尋附近
 * @param lon 當前位置 經度
 * @param lat 當前位置 緯度
 * @param distance 搜尋多少範圍
 * @param pageable 分頁引數
 * @return
 */
@GetMapping("/searchNear")
public List<Location> searchNear(double lon, double lat, String distance, @PageableDefault Pageable pageable){
    BoolQueryBuilder qb = QueryBuilders.boolQuery();
    //搜尋欄位為 location
    GeoDistanceQueryBuilder geoBuilder = new GeoDistanceQueryBuilder("location");
    geoBuilder.point(lat, lon);//指定從哪個位置搜尋
    geoBuilder.distance(distance, DistanceUnit.KILOMETERS);//指定搜尋多少km
    qb.filter(geoBuilder);

    //可新增其他查詢條件
    //qb.must(QueryBuilders.matchQuery("address", address));
    Page<Location> page = locationRepository.search(qb, pageable);
    List<Location> list = page.getContent();
    list.forEach(l -> {
        double calculate = GeoDistance.ARC.calculate(l.getLocation().getLat(), l.getLocation().getLon(), lat, lon, DistanceUnit.METERS);
        l.setDistanceMeters("距離" + (int)calculate + "m");
        });
    return list;
}

測試 GET http://localhost:8080/location/searchNear?lon=120.185919&lat=30.250649&distance=5

[
    {
        "id": "TQ6_h2cBBlxbCrguvps5",
        "location": {
            "lat": 30.251148,
            "lon": 120.188578
        },
        "address": "杭州紅樓大酒店",
        "distanceMeters": "距離261m"
    },
    {
        "id": "Tw7Bh2cBBlxbCrguZ5ti",
        "location": {
            "lat": 30.265498,
            "lon": 120.137051
        },
        "address": "杭州西湖區政府",
        "distanceMeters": "距離4975m"
    },
    {
        "id": "TA69h2cBBlxbCrguoZuZ",
        "location": {
            "lat": 30.249338,
            "lon": 120.189279
        },
        "address": "杭州火車站",
        "distanceMeters": "距離354m"
    },
    {
        "id": "Tg7Ah2cBBlxbCrgu6ZtH",
        "location": {
            "lat": 30.256732,
            "lon": 120.183853
        },
        "address": "浙大醫學院第二附屬醫院",
        "distanceMeters": "距離704m"
    }
]

這裡發現排序是亂的。下面來處理排序問題

根據座標位置查詢並排序

@GetMapping("/searchNearWithOrder")
public List<Location> searchNearWithOrder(double lon, double lat, String distance, @PageableDefault Pageable pageable){

    //搜尋欄位為 location
    GeoDistanceQueryBuilder geoBuilder = new GeoDistanceQueryBuilder("location");
    geoBuilder.point(lat, lon);//指定從哪個位置搜尋
    geoBuilder.distance(distance, DistanceUnit.KILOMETERS);//指定搜尋多少km

    //距離排序
    GeoDistanceSortBuilder sortBuilder = new GeoDistanceSortBuilder("location", lat, lon);
    sortBuilder.order(SortOrder.ASC);//升序
    sortBuilder.unit(DistanceUnit.METERS);

    //構造查詢器
    NativeSearchQueryBuilder qb = new NativeSearchQueryBuilder()
            .withPageable(pageable)
            .withFilter(geoBuilder)
            .withSort(sortBuilder);

    //可新增其他查詢條件
    //qb.must(QueryBuilders.matchQuery("address", address));
    Page<Location> page = locationRepository.search(qb.build());
    List<Location> list = page.getContent();
    list.forEach(l -> {
        double calculate = GeoDistance.PLANE.calculate(l.getLocation().getLat(), l.getLocation().getLon(), lat, lon, DistanceUnit.METERS);
        l.setDistanceMeters("距離" + (int)calculate + "m");
        });
    return list;
}

測試:GET http://localhost:8080/location/searchNearWithOrder?lon=120.185919&lat=30.250649&distance=5

[
    {
        "id": "TQ6_h2cBBlxbCrguvps5",
        "location": {
            "lat": 30.251148,
            "lon": 120.188578
        },
        "address": "杭州紅樓大酒店",
        "distanceMeters": "距離261m"
    },
    {
        "id": "TA69h2cBBlxbCrguoZuZ",
        "location": {
            "lat": 30.249338,
            "lon": 120.189279
        },
        "address": "杭州火車站",
        "distanceMeters": "距離354m"
    },
    {
        "id": "Tg7Ah2cBBlxbCrgu6ZtH",
        "location": {
            "lat": 30.256732,
            "lon": 120.183853
        },
        "address": "浙大醫學院第二附屬醫院",
        "distanceMeters": "距離704m"
    },
    {
        "id": "Tw7Bh2cBBlxbCrguZ5ti",
        "location": {
            "lat": 30.265498,
            "lon": 120.137051
        },
        "address": "杭州西湖區政府",
        "distanceMeters": "距離4975m"
    }
]