十、Elasticsearch 8.x 分布式搜索引擎 -2-搜索查询

Elasticsearch 8.x 分布式搜索引擎 -搜索查询

一、DSL查询文档

1、查询所有

语法

GET /索引库名/_search
{
  "query": {
    "查询类型": {
      "查询条件": "条件值"
    }
  }
}

查询所有

  • 查询类型为:match_all
  • 查询条件:没有
    在这里插入图片描述

2、全文搜索查询

参与搜索的字段也必须是可分词的text类型的字段

2.1、查询类型:match(单字段查询)

语法

GET /索引库名/_search
{
  "query": {
    "match": {
      "字段名称": "搜索内容"
    }
  }
}

在这里插入图片描述

2.2、查询类型:multi_match(多字段查询,任意一个字段符合条件就算符合查询条件)

语法

GET /索引库名/_search
{
  "query": {
    "multi_match": {
      "query": "搜索内容",
      "fields": ["字段名称1", " 字段名称2"]
    }
  }
}

在这里插入图片描述

3、精准查询

精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。

3.1、查询类型:term (根据词条精确值查询)

语法

GET /索引库名/_search
{
  "query": {
    "term": {
      "字段名称": {
        "value": "搜索内容"
      }
    }
  }
}

在这里插入图片描述

3.2、查询类型:range(根据值的范围查询)

范围查询,一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤。
语法

GET /索引库名/_search
{
  "query": {
    "range": {
      "字段名称": {
        "gte": 查询值1, // gte代表大于等于,gt则代表大于
        "lte":  查询值2// lte代表小于等于,lt则代表小于
      }
    }
  }
}

在这里插入图片描述

4、地理坐标查询

地理坐标查询,其实就是根据经纬度查询

4.1、查询类型:geo_bounding_box(矩形范围查询)

查询坐标落在某个矩形范围的所有文档:
在这里插入图片描述
需要指定矩形的左上右下两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点。
语法

GET /索引库名/_search
{
  "query": {
    "geo_bounding_box": {
      "字段名称": {
        "top_left": { // 左上点
          "lat": 31.1, // 纬度
          "lon": 121.5 // 经度
        },
        "bottom_right": { // 右下点
          "lat": 30.9, // 纬度
          "lon": 121.7  // 经度
        }
      }
    }
  }
}

在这里插入图片描述

4.2、查询类型:geo_distance(附近查询)

查询到指定中心点小于某个距离值的所有文档
在这里插入图片描述
语法

GET /索引库名/_search
{
  "query": {
    "geo_distance": {
      "distance": "15km", // 半径
      "字段名称"": "31.21,121.5" // 圆心
    }
  }
}

在这里插入图片描述

5、复合查询

复合查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种:

  • fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名
  • bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索
5.1、相关性算分

当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。

1)早期使用的打分算法是TF-IDF算法,公式如下:

在这里插入图片描述

2)在后来的5.1版本升级中,elasticsearch将算法改进为BM25算法,公式如下:

在这里插入图片描述
TF-IDF算法有一各缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限,曲线更加平滑:
在这里插入图片描述

5.2、算分函数查询

语法
在这里插入图片描述

1)function score 查询中包含四部分:
  • 原始查询条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
  • 过滤条件:filter部分,符合该条件的文档才会重新算分
  • 算分函数:符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
    • weight:函数结果是常量
    • field_value_factor:以文档中的某个字段值作为函数结果
    • random_score:以随机数作为函数结果
    • script_score:自定义算分函数算法
  • 运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
    • multiply:相乘
    • replace:用function score替换query score
    • 其它,例如:sum、avg、max、min
2)function score的运行流程:
  • 1)根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分(query score)
  • 2)根据过滤条件,过滤文档
  • 3)符合过滤条件的文档,基于算分函数运算,得到函数算分(function score)
  • 4)将原始算分(query score)和函数算分(function score)基于运算模式做运算,得到最终结果,作为相关性算分。
3)function score关键点:
  • 过滤条件:决定哪些文档的算分被修改
  • 算分函数:决定函数算分的算法
  • 运算模式:决定最终算分结果

示例
需求:给“如家”这个品牌的酒店排名靠前一些

翻译一下这个需求,转换为之前说的四个要点:

  • 原始条件:不确定,可以任意变化
  • 过滤条件:brand = “如家”
  • 算分函数:可以简单粗暴,直接给固定的算分结果,weight
  • 运算模式:比如求和

因此最终的DSL语句如下:

GET /hotel/_search
{
  "query": {
    "function_score": {
      "query": {  .... }, // 原始查询,可以是任意条件
      "functions": [ // 算分函数
        {
          "filter": { // 满足的条件,品牌必须是如家
            "term": {
              "brand": "如家"
            }
          },
          "weight": 2 // 算分权重为2
        }
      ],
      "boost_mode": "sum" // 加权模式,求和
    }
  }
}

测试,在未添加算分函数时,如家得分如下:
在这里插入图片描述
添加了算分函数后,如家得分就提升了:
在这里插入图片描述

6、布尔查询

1)布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。子查询的组合方式有:
  • must:必须匹配每个子查询,类似“与”
  • should:选择性匹配子查询,类似“或”
  • must_not:必须不匹配,不参与算分,类似“非”
  • filter:必须匹配,不参与算分
2)注意:搜索时,参与打分的字段越多,查询的性能也越差。因此这种多条件查询时,建议这样做:
  • 搜索框的关键字搜索,是全文检索查询,使用must查询,参与算分
  • 其它过滤条件,采用filter查询。不参与算分
    语法
GET /hotel/_search
{
  "query": {
    "bool": {
      "must": [
        {"term": {"city": "上海" }}
      ],
      "should": [
        {"term": {"brand": "皇冠假日" }},
        {"term": {"brand": "华美达" }}
      ],
      "must_not": [
        { "range": { "price": { "lte": 500 } }}
      ],
      "filter": [
        { "range": {"score": { "gte": 45 } }}
      ]
    }
  }
}

示例
需求:搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。

分析:

  • 名称搜索,属于全文检索查询,应该参与算分。放到must中
  • 价格不高于400,用range查询,属于过滤条件,不参与算分。放到must_not中
  • 周围10km范围内,用geo_distance查询,属于过滤条件,不参与算分。放到filter中
    在这里插入图片描述

二、搜索结果处理

1、排序

elasticsearch默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。

1.1、普通字段排序

keyword、数值、日期类型排序的语法基本一致。
语法

GET /索引库名/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "字段名称": "desc"  // 排序字段、排序方式ASC、DESC
    }
  ]
}

示例

需求描述:酒店数据按照用户评价(score)降序排序,评价相同的按照价格(price)升序排序
在这里插入图片描述

1.2、地理坐标排序

语法

GET /indexName/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "_geo_distance" : {
          "FIELD" : "纬度,经度", // 文档中geo_point类型的字段名、目标坐标点
          "order" : "asc", // 排序方式
          "unit" : "km" // 排序的距离单位
      }
    }
  ]
}

这个查询的含义是:

  • 指定一个坐标,作为目标点
  • 计算每一个文档中,指定字段(必须是geo_point类型)的坐标 到目标点的距离是多少
  • 根据距离排序

示例:

需求描述:实现对酒店数据按照到你的位置坐标的距离升序排序

提示:获取你的位置的经纬度的方式:https://lbs.amap.com/demo/jsapi-v2/example/map/click-to-get-lnglat/
假设我的位置是:31.034661,121.612282,寻找我周围距离最近的酒店。
在这里插入图片描述

2、分页

elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。elasticsearch中通过修改from、size参数来控制要返回的分页结果:

  • from:从第几个文档开始
  • size:总共查询几个文档

类似于mysql中的limit ?, ?

2.1、普通分页

语法

GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0, // 分页开始的位置,默认为0
  "size": 10, // 期望获取的文档总数
  "sort": [
    {"price": "asc"}
  ]
}

在这里插入图片描述

2.2、深度分页

查询990~1000的数据,查询逻辑要这么写:

GET /hotel/_search
{
  "query": {
    "match_all": {}
  },
  "from": 990, // 分页开始的位置,默认为0
  "size": 10, // 期望获取的文档总数
  "sort": [
    {"price": "asc"}
  ]
}

这里是查询990开始的数据,也就是 第990~第1000条 数据。

不过,elasticsearch内部分页时,必须先查询 0~1000条,然后截取其中的990 ~ 1000的这10条:
在这里插入图片描述
查询TOP1000,如果es是单点模式,这并无太大影响。

但是elasticsearch将来一定是集群,例如我集群有5个节点,我要查询TOP1000的数据,并不是每个节点查询200条就可以了。

因为节点A的TOP200,在另一个节点可能排到10000名以外了。

因此要想获取整个集群的TOP1000,必须先查询出每个节点的TOP1000,汇总结果后,重新排名,重新截取TOP1000。
在这里插入图片描述
那如果我要查询9900~10000的数据呢?是不是要先查询TOP10000呢?那每个节点都要查询10000条?汇总到内存中?

当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力,因此elasticsearch会禁止from+ size 超过10000的请求。

针对深度分页,ES提供了两种解决方案,官方文档

  • search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
  • scroll:原理将排序后的文档id形成快照,保存在内存。官方已经不推荐使用。

3、高亮

3.1、高亮原理

高亮显示的实现分为两步:

  • 1)给文档中的所有关键字都添加一个标签,例如<em>标签
  • 2)页面给<em>标签编写CSS样式
3.2、实现高亮

语法

GET /hotel/_search
{
  "query": {
    "match": {
      "FIELD": "TEXT" // 查询条件,高亮一定要使用全文检索查询
    }
  },
  "highlight": {
    "fields": { // 指定要高亮的字段
      "FIELD": {
        "pre_tags": "<em>",  // 用来标记高亮字段的前置标签
        "post_tags": "</em>" // 用来标记高亮字段的后置标签
      }
    }
  }
}

注意:

  • 高亮是对关键字高亮,因此搜索条件必须带有关键字,而不能是范围这样的查询。
  • 默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮
  • 如果要对非搜索字段高亮,则需要添加一个属性:required_field_match=false
    示例:
    在这里插入图片描述

4、总结

查询的DSL是一个大的JSON对象,包含下列属性:

  • query:查询条件
  • from和size:分页条件
  • sort:排序条件
  • highlight:高亮条件
    示例:
    在这里插入图片描述

三、ElasticsearchClient查询文档

1、查询所有

1)在EsClient添加queryMatchAll方法实现查询所有
	public <T> List<T>  queryMatchAll(String indexName,Class<T> targetClass) {
        SearchResponse<T> response = null;
        try {
            response=  elasticsearchClient.search(s -> s
                    .index(indexName)
                    .query(q -> q
                            .matchAll(t-> t)
                    ), targetClass
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return handleResponse(response);
 	}
	// 封装响应
    private <T> List<T> handleResponse(SearchResponse<T> response) {
        List<T> result = new ArrayList<>();
        if (response == null) {
            return result;
        }
        HitsMetadata<T> hitsMetadata = response.hits();
        if (hitsMetadata == null) {
            return result;
        }
        List<Hit<T>> hitList = response.hits().hits();
        for (Hit<T> hit : hitList) {
            result.add(hit.source());
        }
        return result;
    }
2) ESQueryMatchAllTest测试类中,测试查询所有:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ESApplication.class)
public class ESQueryMatchAllTest {

	@Autowired
	private EsClient esClient;

	@Test
	public void queryMatchAllTest() {
		List<HotelDoc> hotelList =esClient.queryMatchAll("hotel",HotelDoc.class);
		for (HotelDoc hotelDoc : hotelList) {
			System.out.println(hotelDoc);
		}

	}
}

在这里插入图片描述

2、全文搜索查询

2.1 match搜索 单字段搜索
1)在EsClient添加queryMatch方法实现单字段搜索
	public <T> List<T> queryMatch(String indexName, String searchField, String searchContent, Class<T> targetClass) {
        SearchResponse<T> response = null;
        try {
            response = elasticsearchClient.search(s -> s
                    .index(indexName)
                    .query(q -> q
                            .match(t -> t
                                    .field(searchField)
                                    .query(searchContent)
                            )
                    ), targetClass
            );
        } catch (IOException e) {
            e.printStackTrace();
        }
        return handleResponse(response);
    }
2) ESQueryMatchTest测试类中,测试单字段搜索:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ESApplication.class)
public class ESQueryMatchTest {

	@Autowired
	private EsClient esClient;

	@Test
	public void queryMatchTest() {

		List<HotelDoc> list = esClient.queryMatch("hotel", "all", "7天连锁酒店", HotelDoc.class);
		for (HotelDoc hotelDoc : list) {
			System.out.println("hotelDoc = " + hotelDoc);
		}
	}
}

在这里插入图片描述

2.1 mulit_match搜索 多字段搜索
1)在EsClient添加queryMultiMatch方法实现多字段搜索文档
	/**
     * 多字段搜索文档
     *
     * @param indexName     索引名
     * @param searchFields  搜索字段集合
     * @param searchContent 搜索内容
     * @param targetClass   目标类型
     * @param <T>           T
     * @return 目标类集合
     */
    public <T> List<T> queryMultiMatch(String indexName, List<String> searchFields, String searchContent, Class<T> targetClass) {
        SearchResponse<T> response = null;
        try {
            response = elasticsearchClient.search(s -> s
                    .index(indexName)
                    .query(q -> q
                            .multiMatch(t -> t
                                    .fields(searchFields)
                                    .query(searchContent)
                            )
                    ), targetClass
            );
        } catch (IOException e) {
            e.printStackTrace();
        }
        return handleResponse(response);
    }

    /**
     * 多字段搜索文档
     *
     * @param indexName     索引名
     * @param searchContent 搜索内容
     * @param targetClass   目标类型
     * @param searchField   搜索字段
     * @param searchFields  多个搜索字段
     * @param <T>           T
     * @return 目标类集合
     */
    public <T> List<T> queryMultiMatch(String indexName, String searchContent, Class<T> targetClass, String searchField, String... searchFields) {
        SearchResponse<T> response = null;
        try {
            response = elasticsearchClient.search(s -> s
                    .index(indexName)
                    .query(q -> q
                            .multiMatch(t -> t
                                    .fields(searchField, searchFields)
                                    .query(searchContent)
                            )
                    ), targetClass
            );
        } catch (IOException e) {
            e.printStackTrace();
        }
        return handleResponse(response);
    }
2) ESQueryMatchTest测试类中,测试多字段搜索文档:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ESApplication.class)
public class ESQueryMultiMatchTest {

	@Autowired
	private EsClient esClient;

	@Test
	public void queryMultiMatchTest01() {
		List<String> searchFields = Arrays.asList("name", "brand");
		List<HotelDoc> list = esClient.queryMultiMatch("hotel", searchFields, "外滩如家", HotelDoc.class);
		for (HotelDoc hotelDoc : list) {
			System.out.println("hotelDoc = " + hotelDoc);
		}
	}
	@Test
	public void queryMultiMatchTest02() {
		List<HotelDoc> list = esClient.queryMultiMatch("hotel", "外滩如家", HotelDoc.class,"name","brand");
		for (HotelDoc hotelDoc : list) {
			System.out.println("hotelDoc = " + hotelDoc);
		}
	}
}

在这里插入图片描述

3、精准查询

3.1 term查询 根据词条精确值查
1)在EsClient添加queryTerm方法实现多字段搜索文档
    public <T> List<T> queryTerm(String indexName, String searchField, String searchContent, Class<T> targetClass) {
        SearchResponse<T> response = null;
        try {
            response = elasticsearchClient.search(s -> s
                    .index(indexName)
                    .query(q -> q
                            .term(t -> t
                                    .field(searchField)
                                    .value(searchContent)
                            )
                    ), targetClass
            );
        } catch (IOException e) {
            e.printStackTrace();
        }
        return handleResponse(response);
    }
2) ESQueryTermTest测试类中,测试多字段搜索文档:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ESApplication.class)
public class ESQueryTermTest {

	@Autowired
	private EsClient esClient;

	@Test
	public void queryMatchTest() {

		List<HotelDoc> list = esClient.queryTerm("hotel", "brand", "如家", HotelDoc.class);
		for (HotelDoc hotelDoc : list) {
			System.out.println("hotelDoc = " + hotelDoc);
		}
	}
}

在这里插入图片描述

3.2 range查询 根据值的范围查询
1)在EsClient添加queryTerm方法实现范围查询

	public <T> List<T> queryRangeGteAndLte(String indexName, String searchField, JsonData gte, JsonData lte, Class<T> targetClass) {
        SearchResponse<T> response = null;
        try {
            response = elasticsearchClient.search(s -> s
                    .index(indexName)
                    .query(q -> q
                            .range(t -> t
                                    .field(searchField)
                                    .gte(gte)
                                    .lte(lte)
                            )
                    ), targetClass
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return handleResponse(response);
    }
    public <T> List<T> queryRangeGteAndLt(String indexName, String searchField, JsonData gte, JsonData lt, Class<T> targetClass) {
        SearchResponse<T> response = null;
        try {
            response = elasticsearchClient.search(s -> s
                    .index(indexName)
                    .query(q -> q
                            .range(t -> t
                                    .field(searchField)
                                    .gte(gte)
                                    .lt(lt)
                            )
                    ), targetClass
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return handleResponse(response);
    }
    public <T> List<T> queryRangeGtAndLte(String indexName, String searchField, JsonData gt, JsonData lte, Class<T> targetClass) {
        SearchResponse<T> response = null;
        try {
            response = elasticsearchClient.search(s -> s
                    .index(indexName)
                    .query(q -> q
                            .range(t -> t
                                    .field(searchField)
                                    .gt(gt)
                                    .lte(lte)
                            )
                    ), targetClass
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return handleResponse(response);
    }
    public <T> List<T> queryRangeGtAndLt(String indexName, String searchField, JsonData gt, JsonData lt, Class<T> targetClass) {
        SearchResponse<T> response = null;
        try {
            response = elasticsearchClient.search(s -> s
                    .index(indexName)
                    .query(q -> q
                            .range(t -> t
                                    .field(searchField)
                                    .gt(gt)
                                    .lt(lt)
                            )
                    ), targetClass
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return handleResponse(response);
    }
2) ESQueryRangeTest测试类中,测试范围查询:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ESApplication.class)
public class ESQueryRangeTest {

	@Autowired
	private EsClient esClient;

	@Test
	public void queryRangeTest() {

		List<HotelDoc> list = esClient.queryRangeGteAndLte("hotel", "price", JsonData.of(200),JsonData.of(300), HotelDoc.class);
		for (HotelDoc hotelDoc : list) {
			System.out.println("hotelDoc = " + hotelDoc);
		}
	}
}

4、地理坐标查询

4.1 geo_bounding_box查询 地理坐标矩形范围查询
1)在EsClient添加queryGeoBoundingBox方法实现地理坐标矩形范围查询
public <T> List<T> queryGeoBoundingBox(String indexName, String searchField, GeoLocation topLeft, GeoLocation bottomRight, Class<T> targetClass) {
		SearchResponse<T> response = null;
		try {
			response = elasticsearchClient.search(s -> s
					.index(indexName)
					.query(q -> q
							.geoBoundingBox(g -> g
									.field(searchField)
									.boundingBox(b -> b
											.tlbr(r -> r
													.topLeft(topLeft)
													.bottomRight(bottomRight)))
							)
					), targetClass
			);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		return handleResponse(response);
	}
2) ESQueryGeoBoundingBoxTest测试类中,测试地理坐标矩形范围查询:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ESApplication.class)
public class ESQueryGeoBoundingBoxTest {

	@Autowired
	private EsClient esClient;

	@Test
	public void queryGeoBoundingBoxTest01() {

		List<HotelDoc> list = esClient.queryGeoBoundingBox("hotel", "location",
				GeoLocation.of(m->m
						.latlon(l->l
								.lat(32)
								.lon(120.00))),
				GeoLocation.of(m->m
						.latlon(l->l
								.lat(30)
								.lon(122.00))),
				HotelDoc.class);
		for (HotelDoc hotelDoc : list) {
			System.out.println("hotelDoc = " + hotelDoc);
		}
	}

	@Test
	public void queryGeoBoundingBoxTest02() {

		List<HotelDoc> list = esClient.queryGeoBoundingBox("hotel", "location",
				GeoLocation.of(m -> m
						.text("32, 120")),
				GeoLocation.of(m -> m
						.text("30, 122")),
				HotelDoc.class);
		for (HotelDoc hotelDoc : list) {
			System.out.println("hotelDoc = " + hotelDoc);
		}
	}
}
4.2 geo_distance查询 距离查询
1)在EsClient添加queryGeoDistance方法实现地理坐标距离查询
	public <T> List<T> queryGeoDistance(String indexName, String searchField, String distance,GeoLocation location, Class<T> targetClass) {
		SearchResponse<T> response = null;
		try {
			response = elasticsearchClient.search(s -> s
					.index(indexName)
					.query(q -> q
							.geoDistance(g->g
									.field(searchField)
									.distance(distance)
									.location(location)
							)
					), targetClass
			);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		return handleResponse(response);
	}
2) ESQueryGeoDistanceTest测试类中,测试地理坐标距离查询
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ESApplication.class)
public class ESQueryGeoDistanceTest {

	@Autowired
	private EsClient esClient;

	@Test
	public void queryGeoDistanceTest01() {

		List<HotelDoc> list = esClient.queryGeoDistance("hotel", "location","15km",
				GeoLocation.of(m->m
						.latlon(l->l
								.lat(31.1)
								.lon(121.20))),
				HotelDoc.class);
		for (HotelDoc hotelDoc : list) {
			System.out.println("hotelDoc = " + hotelDoc);
		}
	}

	@Test
	public void queryGeoDistanceTest02() {

		List<HotelDoc> list = esClient.queryGeoDistance("hotel", "location","15km",
				GeoLocation.of(m -> m
						.text("31.1, 121.2")),
				HotelDoc.class);
		for (HotelDoc hotelDoc : list) {
			System.out.println("hotelDoc = " + hotelDoc);
		}
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值