Spring Boot2.4.2整合Elasticsearch7.10.2

Spring Boot整合Elasticsearch

Elasticsearch版本7.10.2

Spring Boot版本2.4.2

使用Spring Boot操作Elasticsearch有两种方式,一种是Spring Data Repositories的方式,一种是使用ElasticsearchRestTemplate方式。

Spring Data Repositories方式

先放Spring Boot文档https://docs.spring.io/spring-data/elasticsearch/docs/4.1.3/reference/html/#repositories.core-concepts

编写实体类

编写实体类主要会用到的注解

  1. @Document(必写)

    属性名 说明
    indexName 索引名,支持SpEl
    type deprecated since Elasticsearch 4.0
    shards 分片
    replicas 每个分区备份数
    refreshIntervall 刷新间隔,默认1s
    indexStoreType 索引文件存储类型,默认fs
    versionType 配置版本管理,默认EXTERNAL
  2. @Id(必写)

  3. @Field

    属性名 说明
    name 将在Elasticsearch文档中表示的字段名称,如果未设置,则使用Java字段名称
    type 属性类型
    store 标记是否原始字段值应存储在Elasticsearch中,默认值为false。

demo

@Data
@Document(indexName = "stu", shards = 3, replicas = 0)
public class StuEntity {
    @Id
    private Long stuId;

    @Field(store = true)
    private String name;

    @Field(store = true)
    private Integer age;

    @Field(store = true, type = FieldType.Keyword)
    private String sign;

    @Field(store = true)
    private String description;
}

编写Repository

编写一个借口继承ElasticsearchRepository<T, ID>

public interface StuEntityRepository extends ElasticsearchRepository<Book, String> {
    List<Book> findAllByName(String name);
}

注意:ElasticsearchRepository接口中的方式都是@Deprecated

编写curd

编写curd有两种:关键字拼接查询条件、@Query注解查询

关键字拼接查询条件

和关系型数据库一样直接使用就好,Elasticsearch有特殊的关键字,用到了自己查询官方文档。

Keyword Sample Elasticsearch Query String
And findByNameAndPrice { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } }, { "query_string" : { "query" : "?", "fields" : [ "price" ] } } ] } }}
Or findByNameOrPrice { "query" : { "bool" : { "should" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } }, { "query_string" : { "query" : "?", "fields" : [ "price" ] } } ] } }}
Is findByName { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } } ] } }}
Not findByNameNot { "query" : { "bool" : { "must_not" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } } ] } }}
Between findByPriceBetween { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }}
LessThan findByPriceLessThan { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : false } } } ] } }}
LessThanEqual findByPriceLessThanEqual { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }}
GreaterThan findByPriceGreaterThan { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : false, "include_upper" : true } } } ] } }}
GreaterThanEqual findByPriceGreaterThan { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } } ] } }}
Before findByPriceBefore { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }}
After findByPriceAfter { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } } ] } }}
Like findByNameLike { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }}
StartingWith findByNameStartingWith { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }}
EndingWith findByNameEndingWith { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "*?", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }}
Contains/Containing findByNameContaining { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "*?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }}
In (when annotated as FieldType.Keyword) findByNameIn(Collection<String>names) { "query" : { "bool" : { "must" : [ {"bool" : {"must" : [ {"terms" : {"name" : ["?","?"]}} ] } } ] } }}
In findByNameIn(Collection<String>names) { "query": {"bool": {"must": [{"query_string":{"query": "\"?\" \"?\"", "fields": ["name"]}}]}}}
NotIn (when annotated as FieldType.Keyword) findByNameNotIn(Collection<String>names) { "query" : { "bool" : { "must" : [ {"bool" : {"must_not" : [ {"terms" : {"name" : ["?","?"]}} ] } } ] } }}
NotIn findByNameNotIn(Collection<String>names) {"query": {"bool": {"must": [{"query_string": {"query": "NOT(\"?\" \"?\")", "fields": ["name"]}}]}}}
Near findByStoreNear Not Supported Yet !
True findByAvailableTrue { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "true", "fields" : [ "available" ] } } ] } }}
False findByAvailableFalse { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "false", "fields" : [ "available" ] } } ] } }}
OrderBy findByAvailableTrueOrderByNameDesc { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "true", "fields" : [ "available" ] } } ] } }, "sort":[{"name":{"order":"desc"}}] }
@Query注解查询
public interface StuEntityRepository extends ElasticsearchRepository<StuEntity, Long> {
    List<StuEntity> findAllByName(String name);

    @Query("{\"match\": {\"name\": {\"query\": \"?0\"}}}")
    Page<StuEntity> findByName(String name, Pageable pageable);
}

ElasticsearchRestTemplate

ElasticsearchRestTemplate是在spring data 中操作Elasticsearch的模板类,其中实现了对Elasticsearch 操作的各类操作方法。例如创建索引、创建别名、创建映射,以及数据的查询和其他操作。

放官方文档:https://docs.spring.io/spring-data/elasticsearch/docs/4.1.3/reference/html/#elasticsearch.operations

主要有以下操作

  • IndexOperations定义索引级别的操作,如创建或删除索引。
  • DocumentOperations定义基于实体 ID 存储、更新和检索实体的操作。
  • SearchOperations定义使用查询搜索多个实体的操作
image-20210218172418556.png

CURD

下面demo中的StuEntity为我们之前创建的那个类

  1. 新增

    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;
    
    @Test
    public void add() {
        StuEntity stu = new StuEntity();
        stu.setStuId(1005L);
        stu.setName("iron man");
        stu.setAge(54);
        stu.setSign("I am iron man");
        stu.setDescription("I have a iron army");
        elasticsearchRestTemplate.save(stu);
    }
    

    直接调用elasticsearchRestTemplateDocumentOperations的save方法就可以了,save方法有几个重载方法,详情看api或源码

  2. 修改

    @Test
    public void update() {
        StuEntity stu = new StuEntity();
        stu.setStuId(1005L);
        stu.setName("iron manss");
        stu.setAge(100);
        stu.setSign("I am iron man");
        stu.setDescription("I have a iron army");
        System.out.println(JSON.toJSONString(stu));
        // 创建Document对象
        // 第一种方式
        Document document = Document.create();
        // 将修改的内容塞进去
        document.putAll(JSON.parseObject(JSON.toJSONString(stu), Map.class));
    
        // 第二种方式
        Document document1 = Document.parse(JSON.toJSONString(stu));
    
        // 第三种方式
        Document document2 = Document.from(JSON.parseObject(JSON.toJSONString(stu), Map.class));
    
        // 构造updateQuery
        UpdateQuery updateQuery = UpdateQuery.builder("1")
            // 如果不存在就新增,默认为false
            .withDocAsUpsert(true)
            .withDocument(Document.parse(JSON.toJSONString(stu)))
            .build();
        elasticsearchRestTemplate.update(updateQuery, IndexCoordinates.of("stu"));
    }
    

    直接调用elasticsearchRestTemplateDocumentOperations的update方法,详情看api或者源码

  3. 删除

    @Test
    public void delete() {
        StuEntity stu = new StuEntity();
        stu.setStuId(1005L);
        stu.setName("iron man");
        stu.setAge(54);
        stu.setSign("I am iron man");
        stu.setDescription("I have a iron army");
        elasticsearchRestTemplate.delete(stu);
    }
    

    直接调用elasticsearchRestTemplateDocumentOperations的delete方法,delete有好几个重载方法,具体使用哪个详情看api或者源码

  4. 查询

    查询就调用elasticsearchRestTemplate中SearchOperations`的search方法。

    在search的各种方法中都需要传入Query。Spring Data Elasticsearch中Query的实现类CriteriaQuery, StringQuery and NativeSearchQuery

  • CriteriaQuery基于查询的查询允许创建查询来搜索数据,而无需了解 Elasticsearch 查询的语法或基础知识。它们允许用户通过简单地链接和组合指定搜索文档必须满足的条件的对象来生成查询

  • StringQuery使用json字符串来构建查询条件。就和Repository中@Query注解中的那个json字符串一样。

  • NativeSearchQuery用于复杂查询。

    使用CriteriaQuery来构建查询

    @Test
    public void search1() {
        Criteria criteria = new Criteria("name").is("iron man");
        Query query = new CriteriaQuery(criteria);
        SearchHits searchHits = elasticsearchRestTemplate.search(query, StuEntity.class);
        System.out.println(searchHits.getSearchHits());
    }
    

    使用StringQuery构建查询

    @Test
    public void search2() {
        Query query = new StringQuery("{\n" +
                                      "    \"match\": { \n" +
                                      "      \"age\": { \"query\": \"54\" } \n" +
                                      "    } \n" +
                                      "  }");
        SearchHits<StuEntity> searchHits = elasticsearchRestTemplate.search(query, StuEntity.class);
        System.out.println(searchHits.getSearchHits());
    }
    

    NativeSearchQuery查询网上的例子就很多了,以后再写篇文章写。

另类查询方式

使用elasticsearch-sql插件像写sql一样查询Elasticsearch。

elasticsearch-sql的github地址

jdbc方式

  1. 添加依赖

     <dependency>
                <groupId>org.nlpcn</groupId>
                <artifactId>elasticsearch-sql</artifactId>
                <version>7.8.0.1</version>
            </dependency>
            <!-- https://mvnrepository.com/artifact/org.elasticsearch.client/x-pack-transport -->
            <dependency>
                <groupId>org.elasticsearch.client</groupId>
                <artifactId>x-pack-transport</artifactId>
                <version>7.10.2</version>
                <exclusions>
                    <exclusion>
                        <artifactId>elasticsearch-core</artifactId>
                        <groupId>org.elasticsearch</groupId>
                    </exclusion>
                    <exclusion>
                        <artifactId>elasticsearch-ssl-config</artifactId>
                        <groupId>org.elasticsearch</groupId>
                    </exclusion>
                </exclusions>
            </dependency>
            <!-- https://mvnrepository.com/artifact/org.elasticsearch.plugin/x-pack-core -->
            <dependency>
                <groupId>org.elasticsearch.plugin</groupId>
                <artifactId>x-pack-core</artifactId>
                <version>7.10.2</version>
            </dependency>
            <dependency>
                <groupId>org.elasticsearch</groupId>
                <artifactId>elasticsearch</artifactId>
                <version>7.10.2</version>
            </dependency>
    
            <dependency>
                <groupId>org.elasticsearch.client</groupId>
                <artifactId>transport</artifactId>
                <version>7.10.2</version>
            </dependency>
            <dependency>
                <groupId>org.elasticsearch.plugin</groupId>
                <artifactId>transport-netty4-client</artifactId>
                <version>7.10.2</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.1.16</version>
            </dependency>
    

    Elasticsearch是7.10.2。现在elasticsearch-sql还没有7.10.2所以就用能用到的最新的7.8.0.1。druid是1.1.16。

    @Test
    public void search4() throws Exception {
        Properties properties = new Properties();
        properties.put("url", "jdbc:elasticsearch://192.168.1.123:9300/");
        DruidDataSource dds = (DruidDataSource) ElasticSearchDruidDataSourceFactory.createDataSource(properties);
        Connection connection = dds.getConnection();
        PreparedStatement ps = connection.prepareStatement("SELECT * from stu");
        ResultSet resultSet = ps.executeQuery();
        ps.close();
        connection.close();
        dds.close();
    }
    

    当然为了查询方便,封装成NamedParameterJdbcTemplate。

api方式

这个方式是我在一次调试的时候发现的,官方文档中并没有介绍。仅供参考。

controller

@RestController
@RequestMapping("/elasticsearchSqlApiController")
public class ElasticsearchSqlApiController {

    @Autowired
    private ElasticsearchApiDao elasticsearchApiDao;

    /**
     * 使用es-sql插件的api方式查询sql
     * @param sql 要查询的sql 例如:select * from student where name='小憨'
     * @return
     */
    @GetMapping("/search")
    public ActionResult search(String sql) {
        return ActionResult.success(elasticsearchApiDao.search(sql));
    }

    /**
     * 将sql解析为 DSL语句
     * @param sql
     * @return
     */
    @GetMapping("/explain")
    public ActionResult explain(String sql) {
//        String sql = "select * from a_icd_person where PERNAME='王晓光'";
        return ActionResult.success(elasticsearchApiDao.explain(sql));
    }
}

service

@Slf4j
@Service
public class ElasticsearchApiDaoImpl implements ElasticsearchApiDao {

    @Autowired
    private TransportClient transportClient;

    @Override
    public EsSearchResultDTO search(String sql) {
        EsSearchResultDTO resultDTO = new EsSearchResultDTO();
        try {
            long before = System.currentTimeMillis();
            SearchDao searchDao = new SearchDao(transportClient);
            QueryAction queryAction = searchDao.explain(sql);
            Object execution = QueryActionElasticExecutor.executeAnyAction(searchDao.getClient(), queryAction);
            ObjectResult result = getObjectResult(execution, true, false, false, true, false, queryAction);
            resultDTO.setResultColumns(Sets.newHashSet(result.getHeaders()));
            List<IndexRowData> indexRowDatas = new ArrayList<>();
            for (List<Object> line : result.getLines()) {
                IndexRowData indexRowData = new IndexRowData();
                for (int i = 0; i < result.getHeaders().size(); i++) {
                    indexRowData.build(result.getHeaders().get(i), line.get(i));
                }
                indexRowDatas.add(indexRowData);
            }
            resultDTO.setResultSize(indexRowDatas.size());
            if (execution instanceof SearchHits) {
                resultDTO.setTotal(((SearchHits) execution).getTotalHits());
            } else {
                resultDTO.setTotal(indexRowDatas.size());
            }
            resultDTO.setResult(indexRowDatas);
            resultDTO.setTime((System.currentTimeMillis() - before) / 1000);
            log.info("查询数据结果集: {}", JSONObject.toJSONString(resultDTO));
        } catch (Exception e) {
            throw new ElasticsearchException("根据ES-SQL查询数据异常: {}", e, e.getMessage());
        }
        return resultDTO;
    }

    /**
     * 解析sql
     * @param sql
     * @return
     */
    @Override
    public String explain(String sql) {
        SearchDao searchDao = new SearchDao(transportClient);
        QueryAction queryAction = null;
        try {
            queryAction = searchDao.explain(sql);
            return queryAction.explain().explain();
        } catch (SqlParseException e) {
            throw new RuntimeException(e);
        } catch (SQLFeatureNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }

    private ObjectResult getObjectResult(Object execution, boolean flat, boolean includeScore, boolean includeType, boolean includeId, boolean incluedScrollId, QueryAction queryAction) throws Exception {
        return (new ObjectResultsExtractor(includeScore, includeType, includeId, incluedScrollId, queryAction)).extractResults(execution, flat);
    }
}

配置类

@Slf4j
@Configuration
@PropertySource("classpath:elasticsearch.properties")
public class ElasticsearchConfig {
    @Value("${elasticSearch.host}")
    private String[] ipAddress;
    @Value("${elasticSearch.maxRetryTimeout}")
    private Integer maxRetryTimeout;
    @Value("${elasticSearch.sql.host}")
    private String[] esSqlAddress;

    @Bean
    public TransportClient transportClient() {

        Settings settings = Settings.builder()
                // 不允许自动刷新地址列表
                .put("client.transport.sniff", false)
                .put("client.transport.ignore_cluster_name", true)
                .build();

        // 初始化地址
        TransportAddress[] transportAddresses = new TransportAddress[esSqlAddress.length];
        for (int i = 0; i < esSqlAddress.length; i++) {
            String[] addressItems = esSqlAddress[i].split(":");
            try {
                transportAddresses[i] = new TransportAddress(InetAddress.getByName(addressItems[0]),
                        Integer.valueOf(addressItems[1]));
            } catch (UnknownHostException e) {
                log.error(e.toString());
            }
        }

        PreBuiltTransportClient preBuiltTransportClient = new PreBuiltTransportClient(settings);

        TransportClient client = preBuiltTransportClient
                .addTransportAddresses(transportAddreses);
        return client;
    }
}

最后:

sql的方式虽然不是Elasticsearch官方推荐的方式,但是上手快,会写sql就能写查询。Elasticsearch官方在6.3.0之后就开始支持sql方式了,但不是免费的,所以当时有个项目就用的这个elasticsearch-sql这个jar包。总体来说还行,但是有缺陷,基本的查询没有问题,但是高级操作就不如用官方推荐的了。这个仓库的star数从1k多到现在6.2k了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容