ES基本操作(Java API)

1. 导入restClient依赖

XML 复制代码
     <!--     es      -->
        <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
            <version>7.12.1</version>
        </dependency>
XML 复制代码
 <!--     fastjson     -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.39</version> <!-- 使用最新稳定版 -->
        </dependency>

2. 了解ES核心客户端API

核心区别对比

特性 RestHighLevelClient RestClient
定位 高级客户端(封装常用操作,推荐使用) 底层HTTP客户端(更灵活,更复杂)
API风格 面向对象(如 IndexRequest, SearchRequest 基于HTTP请求构建(如 Request, Response
维护状态 官方已停止维护(ES 7.15+) 仍维护(但推荐迁移到新客户端)
依赖关系 基于 RestClient 实现 RestHighLevelClient 的底层依赖
适用场景 快速开发标准功能 需要自定义请求或访问未封装API

3. 将RestHighLevelClient注册成Bean

java 复制代码
package com.example.demo.config;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ElasticSearchConfig {
    public static final String HOST = "127.0.0.1"; // es地址
    public static final int PORT = 9200; // es端口

    /**
     * 创建RestClient对象
     * @return
     */
    @Bean
    public RestHighLevelClient restClient() {
        return new RestHighLevelClient(RestClient.builder(new HttpHost(HOST, PORT)));
    }
}

4. 索引库基本操作

1. 创建索引库

创建包和类用于保存索引结构:constants.HotelConstants

java 复制代码
package com.example.demo.constants;

public class HotelConstants {
    public static final String MAPPER_TEMPLATE_USER = 
            "{\n" +
            "  \"settings\": {\n" +
            "    \"number_of_shards\": 3,\n" +
            "    \"number_of_replicas\": 1,\n" +
            "    \"analysis\": {\n" +
            "      \"analyzer\": {\n" +
            "        \"ik_smart\": {\n" +
            "          \"type\": \"custom\",\n" +
            "          \"tokenizer\": \"ik_smart\"\n" +
            "        },\n" +
            "        \"ik_max_word\": {\n" +
            "          \"type\": \"custom\",\n" +
            "          \"tokenizer\": \"ik_max_word\"\n" +
            "        }\n" +
            "      }\n" +
            "    }\n" +
            "  },\n" +
            "  \"mappings\": {\n" +
            "    \"dynamic\": \"strict\",\n" +
            "    \"properties\": {\n" +
            "      \"id\": {\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"username\": {\n" +
            "        \"type\": \"text\",\n" +
            "        \"analyzer\": \"ik_max_word\",\n" +
            "        \"fields\": {\n" +
            "          \"keyword\": {\n" +
            "            \"type\": \"keyword\",\n" +
            "            \"ignore_above\": 256\n" +
            "          }\n" +
            "        },\n" +
            "        \"copy_to\": \"combined_search\"\n" +
            "      },\n" +
            "      \"password\": {\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": false\n" +
            "      },\n" +
            "      \"phone\": {\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"ignore_above\": 20\n" +
            "      },\n" +
            "      \"createTime\": {\n" +
            "        \"type\": \"date\",\n" +
            "        \"format\": \"yyyy-MM-dd HH:mm:ss||epoch_millis\"\n" +
            "      },\n" +
            "      \"updateTime\": {\n" +
            "        \"type\": \"date\",\n" +
            "        \"format\": \"yyyy-MM-dd HH:mm:ss||epoch_millis\"\n" +
            "      },\n" +
            "      \"status\": {\n" +
            "        \"type\": \"integer\",\n" +
            "        \"null_value\": 1\n" +
            "      },\n" +
            "      \"balance\": {\n" +
            "        \"type\": \"integer\",\n" +
            "        \"null_value\": 0\n" +
            "      },\n" +
            "      \"combined_search\": {\n" +
            "        \"type\": \"text\",\n" +
            "        \"analyzer\": \"ik_smart\"\n" +
            "      }\n" +
            "    }\n" +
            "  }\n" +
            "}";

}

java 复制代码
 /**
     * 创建索引库
     */
    @Test
    public void createIndex() throws IOException {
        // 创建请求,指定索引库名称
        CreateIndexRequest request = new CreateIndexRequest("user");
        //执行与数据库相对于的映射JSON
        CreateIndexRequest source = request.source(HotelConstants.MAPPER_TEMPLATE_USER, XContentType.JSON);
        // 发送请求
        //RequestOptions.DEFAULT:默认请求
        client.indices().create(source, RequestOptions.DEFAULT);
    }

执行前先确定是否删除user索引库,客户端查询一下如果为空就是删除了

java 复制代码
#
GET /user

#
DELETE /user

运行测试方法后再次查询


2. 判断索引库是否存在

java 复制代码
    /**
     * 判断索引库是否存在
     * @throws IOException
     */
    @Test
    void testIndexExists() throws IOException {
        // 创建请求,指定索引库名称
        GetIndexRequest request = new GetIndexRequest("user");
        // 发送请求,判断索引库是否存在
        boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
        // 打印结果,true表示存在,false表示不存在
        System.out.println(exists);
    }

3. 删除索引库

java 复制代码
    /**
     * 删除索引库
     * @throws IOException
     */
    @Test
    void testDeleteIndex() throws IOException {
        // 创建请求,指定索引库名称
        DeleteIndexRequest request = new DeleteIndexRequest("user");
        // 发送请求
        client.indices().delete(request, RequestOptions.DEFAULT);
    }

5. 文档操作

1. 新增文档

java 复制代码
    /**
     * 新增文档
     */
    @Test
    public void addDoc() throws IOException, JSONException {
        // 读取数据库数据,发送请求,指定文档内容
        User user = userService.getById(1);

        // 创建请求,指定索引库名称
        IndexRequest indexRequest = new IndexRequest("user").id(user.getId().toString());

        //转json
        indexRequest.source(JSON.toJSONString(user), XContentType.JSON);

        // 发送请求
        client.index(indexRequest, RequestOptions.DEFAULT);
    }

2. 查询文档

java 复制代码
 /**
     * 查询文档
     */
    @Test
    public void getDoc() throws IOException {
        // 创建请求,指定索引库名称和文档id
        GetRequest getRequest = new GetRequest("user", "1");
        // 发送请求
        GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);
        // 打印结果
        System.out.println(response.getSourceAsString());
        //转user对象
        User user = JSON.parseObject(response.getSourceAsString(), User.class);
        System.out.println(user);
    }

3. 更新文档

全量更新:重新添加一个包含所有的文档,ES会先将原来的删除再新增

局部更新:添加部分数据(不包括ID),ES会将相关字段值更新

java 复制代码
 /**
     * 修改文档
     */
    @Test
    public void updateDoc() throws IOException {
        // 创建请求,指定索引库名称和文档id
        User user = userService.getById(1);
        // 修改数据
        user.setUsername("admin");
        // 发送请求
        UpdateRequest updateRequest = new UpdateRequest("user", user.getId().toString());
        // 转json
        updateRequest.doc(JSON.toJSONString(user), XContentType.JSON);
        // 发送请求
        client.update(updateRequest, RequestOptions.DEFAULT);
    }

4. 删除文档

java 复制代码
/**
     * 删除文档
     */
    @Test
    public void deleteDoc() throws IOException {
        // 创建请求,删除文档id=1
        DeleteRequest deleteRequest = new DeleteRequest("user").id("1");
        // 发送请求
        client.delete(deleteRequest, RequestOptions.DEFAULT);
        // 打印结果
        System.out.println("删除成功");
    }

5. 批处理

java 复制代码
 /**
     * 批处理
     */
    @Test
    public void batchAddDoc() throws IOException {
        // 创建批处理请求
        BulkRequest bulkRequest = new BulkRequest();
        // 获取数据库数据
        List<User> list = userService.list();
        // 遍历集合,添加批处理请求
        for (User user : list) {
            IndexRequest indexRequest = new IndexRequest("user").id(user.getId().toString());
            indexRequest.source(JSON.toJSONString(user), XContentType.JSON);
            bulkRequest.add(indexRequest);
        }
        // 发送请求
        client.bulk(bulkRequest, RequestOptions.DEFAULT);
    }

6. 全文检索(倒排索引)

复制代码
GET /item/_search
{
  "query": {
    "match": {
      "name": "莎米特SUMMIT"
    }
  }
}

java 复制代码
 /**
     * 搜索
     * 根据名称使用倒排索引搜索
     * @throws IOException
     */
    @Test
    public void searchByNameSimple() throws IOException {
        // 1. 创建搜索请求(指定索引名)
        SearchRequest request = new SearchRequest("item");

        // 2. 构建查询条件(name字段匹配"莎米特SUMMIT")
        SearchSourceBuilder source = new SearchSourceBuilder()
                .query(QueryBuilders.matchQuery("name", "OXLA欧莎2018")) // 基础分词查询
                .from(0)  // 页码(从0开始)
                .size(100); // 每页条数 默认10

        // 3. 执行查询
        SearchResponse response = client.search(request.source(source), RequestOptions.DEFAULT);

        // 4. 打印结果
        System.out.println("找到 " + response.getHits().getTotalHits().value + " 条结果:");
        for (SearchHit hit : response.getHits()) {
            String name = (String) hit.getSourceAsMap().get("name");
            System.out.println("ID: " + hit.getId() + " | 名称: " + name);
        }
    }

7. 精确查找

java 复制代码
 /**
     * 精确搜索
     * @throws IOException
     */
    @Test
    public void exactSearch() throws IOException {
        // 1. 创建搜索请求
        SearchRequest request = new SearchRequest("item");

        // 2. 构建精确查询条件
        SearchSourceBuilder source = new SearchSourceBuilder()
                .query(QueryBuilders.termQuery("category", "拉杆箱")) // 使用.keyword字段
                .size(5);

        // 3. 执行查询
        SearchResponse response = client.search(request.source(source), RequestOptions.DEFAULT);

        // 4. 处理结果
        System.out.println("精确匹配结果数: " + response.getHits().getTotalHits().value);
        for (SearchHit hit : response.getHits()) {
            System.out.println("ID: " + hit.getId() +"| 分类: "+ hit.getSourceAsMap().get("category")+ " | 名称: " + hit.getSourceAsMap().get("name"));
        }
    }

8. Bool Query(复合查询|条件查询)

布尔查询允许组合多个子查询,支持四种逻辑条件:

子句类型 说明 类比SQL 是否影响相关性评分
must 必须满足的所有条件(AND逻辑) WHERE a AND b ✅ 是
should 至少满足一个 条件(OR逻辑),可通过minimum_should_match调整 WHERE a OR b ✅ 是
must_not 必须不满足的条件(NOT逻辑) WHERE NOT a ❌ 否
filter 必须满足的条件,但不影响评分(高性能过滤) WHERE a ❌ 否
java 复制代码
  @Test  // 标记为测试方法
    public void testBoolQuerySearch() throws Exception {
        // ==================== 1. 构建布尔查询 ====================
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
                // MUST条件(必须满足):
                // - 商品名称包含"智能手机"(使用ik_smart分词器)
                .must(QueryBuilders.matchQuery("name", "防蓝光老花眼镜"))
                // - 商品状态必须为1(精确匹配)
                .must(QueryBuilders.termQuery("status", 1))

                // SHOULD条件(至少满足一个):
                // - 品牌是"华为"或"小米"(使用OR逻辑)
                .should(QueryBuilders.matchQuery("brand", "莎米特"))
                .should(QueryBuilders.matchQuery("brand", "黛丝"))
                .minimumShouldMatch(1)  // 至少满足1个should条件

                // MUST_NOT条件(必须不满足):
                // - 价格不能高于1000元
                .mustNot(QueryBuilders.rangeQuery("price").gt(1000))
                // - 不能是广告商品
                .mustNot(QueryBuilders.termQuery("isAD", true))

                // FILTER条件(必须满足,但不影响评分):
                // - 库存必须大于0
                .filter(QueryBuilders.rangeQuery("stock").gt(0));
                // - 创建时间在2024年内(日期范围查询)
             /*   .filter(QueryBuilders.rangeQuery("create_time")
                        .gte("2018-1-1 00:00:00")  // 大于等于起始时间
                        .lte("2024-12-31 23:59:59")) // 小于等于结束时间
                // - 分类必须是"电子产品"或"数码配件"(精确匹配)
                .filter(QueryBuilders.termsQuery("category.keyword", "老花镜", "数码配件"));*/

        // ==================== 2. 配置查询请求 ====================
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
                .query(boolQuery)      // 设置查询条件
                .from(0)              // 分页起始位置(第1页)
                .size(10)             // 每页返回10条
                .sort("price", SortOrder.ASC)      // 按价格升序
                .sort("_score", SortOrder.DESC);   // 按相关性降序

        // ==================== 3. 执行查询 ====================
        SearchResponse response = client.search(
                new SearchRequest("item")  // 指定索引名
                        .source(sourceBuilder), // 设置查询参数
                RequestOptions.DEFAULT     // 默认请求选项
        );

        // ==================== 4. 处理结果 ====================
        System.out.println("命中数量: " + response.getHits().getTotalHits().value);

        // 遍历结果并打印关键信息
        Arrays.stream(response.getHits().getHits())
                .forEach(hit -> {
                    System.out.println("\n========== 商品信息 ==========");
                    System.out.println("ID: " + hit.getId());
                    System.out.println("名称: " + hit.getSourceAsMap().get("name"));
                    System.out.println("价格: ¥" + hit.getSourceAsMap().get("price"));
                    System.out.println("品牌: " + hit.getSourceAsMap().get("brand"));
                    System.out.println("库存: " + hit.getSourceAsMap().get("stock"));
                });

9. 排序

java 复制代码
    /**
     * 排序示例
     * @throws Exception
     */
    @Test
    public void testSortingExamples() throws Exception {
        // ==================== 1. 基础排序 ====================

        // 示例1: 按价格升序排序
        SearchSourceBuilder priceAscBuilder = new SearchSourceBuilder()
                .query(QueryBuilders.matchAllQuery())
                .sort("price", SortOrder.ASC)  // 价格从低到高
                .size(5);

        // 示例2: 按创建时间降序排序(新品优先)
        SearchSourceBuilder newFirstBuilder = new SearchSourceBuilder()
                .query(QueryBuilders.matchAllQuery())
                .sort("createTime", SortOrder.DESC)  // 最新创建的商品在前
                .size(5);

        // ==================== 2. 多字段排序 ====================

        // 示例3: 先按品牌字母序,再按价格降序
        SearchSourceBuilder multiFieldBuilder = new SearchSourceBuilder()
                .query(QueryBuilders.matchAllQuery())
                .sort("brand.keyword", SortOrder.ASC)  // 品牌名A-Z排序
                .sort("price", SortOrder.DESC)         // 同品牌中价格高的在前
                .size(5);

        // ==================== 3. 特殊排序 ====================

        // 示例4: 按库存升序(库存少的优先)
        SearchSourceBuilder stockSortBuilder = new SearchSourceBuilder()
                .query(QueryBuilders.matchAllQuery())
                .sort("stock", SortOrder.ASC)
                .size(5);

        // 示例5: 按销量降序(热销商品在前)
        SearchSourceBuilder soldSortBuilder = new SearchSourceBuilder()
                .query(QueryBuilders.matchAllQuery())
                .sort("sold", SortOrder.DESC)
                .size(5);

        // ==================== 4. 执行并打印结果 ====================

        // 执行价格排序查询
        System.out.println("======== 按价格升序排序 ========");
        SearchResponse priceResponse = client.search(
                new SearchRequest("item").source(priceAscBuilder),
                RequestOptions.DEFAULT
        );
        printResults(priceResponse);

        // 执行新品排序查询
        System.out.println("\n======== 按新品排序(创建时间降序) ========");
        SearchResponse newResponse = client.search(
                new SearchRequest("item").source(newFirstBuilder),
                RequestOptions.DEFAULT
        );
        printResults(newResponse);

        // 执行多字段排序查询
        System.out.println("\n======== 多字段排序(品牌A-Z,价格高-低) ========");
        SearchResponse multiResponse = client.search(
                new SearchRequest("item").source(multiFieldBuilder),
                RequestOptions.DEFAULT
        );
        printResults(multiResponse);
    }

    // 打印结果的辅助方法
    private void printResults(SearchResponse response) {
        Arrays.stream(response.getHits().getHits())
                .forEach(hit -> {
                    Map<String, Object> source = hit.getSourceAsMap();
                    System.out.printf(
                            "ID: %s | 名称: %-20s | 价格: %-6s | 品牌: %-8s | 库存: %-4s | 创建时间: %s\n",
                            hit.getId(),
                            source.get("name"),
                            source.get("price"),
                            source.get("brand"),
                            source.get("stock"),
                            source.get("createTime")
                    );
                });
    }

10. 分页

  1. 四种分页场景实现

    • testBasicPagination(): 首页商品列表,按销量+上新时间排序

    • testCategoryPagination(): 分类页,带精确分类筛选和价格排序

    • testSearchPagination(): 搜索页,支持关键词高亮显示

    • testDeepPagination(): 深度分页,使用search_after技术

java 复制代码
/**
     * 基础分页查询 - 首页商品列表
     * 特点:按综合排序(销量+上新)
     */
    @Test
    public void testBasicPagination() throws Exception {
        int page = 1; // 当前页码
        int size = 10; // 每页数量

        SearchSourceBuilder builder = new SearchSourceBuilder()
                .query(QueryBuilders.termQuery("status", 1)) // 只查询上架商品
                .from((page - 1) * size)
                .size(size)
                .sort("sold", SortOrder.DESC) // 按销量降序
                .sort("create_time", SortOrder.DESC) // 再按创建时间降序
                .fetchSource(new String[]{"id", "name", "price", "image", "sold"}, null); // 只返回必要字段

        SearchResponse response = client.search(
                new SearchRequest("item").source(builder),
                RequestOptions.DEFAULT
        );

        printPageResult(response, page, size);
    }

    /**
     * 分类页分页 - 带筛选条件
     * 特点:分类筛选+多维度排序
     */
    @Test
    public void testCategoryPagination() throws Exception {
        int page = 1;
        int size = 15;
        String category = "老花镜";

        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
                .must(QueryBuilders.termQuery("category.keyword", category)) // 精确分类匹配
                .must(QueryBuilders.termQuery("status", 1)) // 上架商品
                .filter(QueryBuilders.rangeQuery("stock").gt(0)); // 有库存

        SearchSourceBuilder builder = new SearchSourceBuilder()
                .query(boolQuery)
                .from((page - 1) * size)
                .size(size)
                .sort("price", SortOrder.ASC) // 按价格升序
                .sort("sold", SortOrder.DESC) // 再按销量降序
                .fetchSource(new String[]{"id", "name", "price", "image", "brand"}, null);

        SearchResponse response = client.search(
                new SearchRequest("item").source(builder),
                RequestOptions.DEFAULT
        );

        printPageResult(response, page, size);
    }

    /**
     * 搜索分页 - 带关键词和高亮
     * 特点:关键词搜索+结果高亮
     */
    @Test
    public void testSearchPagination() throws Exception {
        int page = 1;
        int size = 10;
        String keyword = "防蓝光";

        SearchSourceBuilder builder = new SearchSourceBuilder()
                .query(QueryBuilders.boolQuery()
                        .must(QueryBuilders.multiMatchQuery(keyword, "name", "spec"))
                        .must(QueryBuilders.termQuery("status", 1))
                )
                .from((page - 1) * size)
                .size(size)
                .sort("_score", SortOrder.DESC) // 按相关性排序
                .sort("sold", SortOrder.DESC) // 再按销量排序
                .highlighter(new HighlightBuilder()
                        .field("name")
                        .field("spec")
                        .preTags("<em>")
                        .postTags("</em>"))
                .fetchSource(new String[]{"id", "name", "price", "image"}, null);

        SearchResponse response = client.search(
                new SearchRequest("item").source(builder),
                RequestOptions.DEFAULT
        );

        printSearchResult(response, page, size);
    }

    /**
     * 深度分页 - 使用search_after
     * 特点:适合无限滚动加载
     */
    @Test
    public void testDeepPagination() throws Exception {
        // 模拟上一页最后一条的排序值
        Object[] lastSortValues = new Object[]{50000, "2023-05-01T10:00:00.000Z"};
        int size = 10;

        SearchSourceBuilder builder = new SearchSourceBuilder()
                .query(QueryBuilders.termQuery("status", 1))
                .size(size)
                .sort("sold", SortOrder.DESC) // 必须与lastSortValues中的顺序一致
                .sort("create_time", SortOrder.DESC)
                .searchAfter(lastSortValues)
                .fetchSource(new String[]{"id", "name", "price", "sold"}, null);

        SearchResponse response = client.search(
                new SearchRequest("item").source(builder),
                RequestOptions.DEFAULT
        );

        printPageResult(response, -1, size); // -1表示未知页码
    }

    // ==================== 辅助方法 ====================

    /**
     * 打印分页结果
     */
    private void printPageResult(SearchResponse response, int currentPage, int pageSize) {
        long totalHits = response.getHits().getTotalHits().value;
        int totalPages = (int) Math.ceil((double) totalHits / pageSize);

        if (currentPage > 0) {
            System.out.printf("\n=== 第 %d 页 (共 %d 页,每页 %d 条) ===\n",
                    currentPage, totalPages, pageSize);
        } else {
            System.out.println("\n=== 深度分页结果 ===");
        }

        Arrays.stream(response.getHits().getHits())
                .forEach(hit -> {
                    Map<String, Object> source = hit.getSourceAsMap();
                    System.out.printf("ID: %s | 商品: %-25s | 价格: %-6s | 销量: %-5s | 图片: %s\n",
                            hit.getId(),
                            source.get("name"),
                            source.get("price"),
                            source.get("sold"),
                            source.get("image"));
                });
    }

    /**
     * 打印搜索结果(带高亮)
     */
    private void printSearchResult(SearchResponse response, int currentPage, int pageSize) {
        System.out.printf("\n=== 搜索第 %d 页 ===\n", currentPage);

        Arrays.stream(response.getHits().getHits())
                .forEach(hit -> {
                    Map<String, Object> source = hit.getSourceAsMap();
                    Map<String, HighlightField> highlights = hit.getHighlightFields();

                    // 获取高亮名称
                    String nameHighlight = highlights.containsKey("name") ?
                            highlights.get("name").fragments()[0].string() :
                            source.get("name").toString();

                    // 获取高亮规格
                    String specHighlight = highlights.containsKey("spec") ?
                            highlights.get("spec").fragments()[0].string() :
                            source.getOrDefault("spec", "").toString();

                    System.out.printf("ID: %s | 商品: %-25s | 价格: %-6s | 规格: %s\n",
                            hit.getId(),
                            nameHighlight,
                            source.get("price"),
                            specHighlight);
                });
    }
相关推荐
ghost1436 分钟前
C#学习第17天:序列化和反序列化
开发语言·学习·c#
xxjiaz14 分钟前
二分查找-LeetCode
java·数据结构·算法·leetcode
nofaluse37 分钟前
JavaWeb开发——文件上传
java·spring boot
難釋懷41 分钟前
bash的特性-bash中的引号
开发语言·chrome·bash
爱的叹息1 小时前
【java实现+4种变体完整例子】排序算法中【插入排序】的详细解析,包含基础实现、常见变体的完整代码示例,以及各变体的对比表格
java·算法·排序算法
Hello eveybody2 小时前
C++按位与(&)、按位或(|)和按位异或(^)
开发语言·c++
爱的叹息2 小时前
【java实现+4种变体完整例子】排序算法中【快速排序】的详细解析,包含基础实现、常见变体的完整代码示例,以及各变体的对比表格
java·算法·排序算法
6v6-博客2 小时前
2024年网站开发语言选择指南:PHP/Java/Node.js/Python如何选型?
java·开发语言·php
Baoing_2 小时前
Next.js项目生成sitemap.xml站点地图
xml·开发语言·javascript
Ac157ol2 小时前
2025年最新版 Git和Github的绑定方法,以及通过Git提交文件至Github的具体流程(详细版)
git·elasticsearch·github