首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

Elasticsearch 的搜索功能

  • 25-04-24 08:48
  • 4643
  • 11615
blog.csdn.net

Elasticsearch 的搜索功能

建议阅读顺序:

  1. Elasticsearch 入门
  2. Elasticsearch 搜索(本文)
  3. Elasticsearch 搜索高级
  4. Elasticsearch 高级

1. 介绍

使用 Elasticsearch 最终目的是为了实现搜索功能,现在先将文档添加到索引中,接下来完成搜索的方法。

查询的分类:

  1. 叶子查询:叶查询子句在特定字段中查找特定值,例如 match、term 或 range查询。
    1. 精确查询:根据精确词条值查找数据,一般是查找 keyword、数值、日期、boolean 等类型字段。例如:
      • ids:根据文档 ID 查找文档
      • range:返回包含指定范围内的文档,比如:查询年龄在 10 到 20 岁的学生信息。
      • term:根据精确值(例如价格、产品 ID 或用户名)查找文档。
    2. 全文检索查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:
      • match_query:对一个字段进行全文检索
      • multi_match_query:对多个字段进行全文检索
  2. 复合查询:以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式。

1.1 精确查询

1.1.1 term

语法:

GET /{索引库名}/_search
{
  "query": {
    "term": {
      "字段名": {
        "value": "搜索条件"
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

当输入的搜索条件不是词条,而是短语时,由于不做分词,反而搜索不到:

1.1.2 range

语法:

GET /{索引库名}/_search
{
  "query": {
    "range": {
      "字段名": {
        "gte": {最小值},
        "lte": {最大值}
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

range 是范围查询,对于范围筛选的关键字有:

  • gte:大于等于
  • gt:大于
  • lte:小于等于
  • lt:小于

1.2 全文检索

会对搜索条件进行拆分。

1.2.1 match

语法:

GET /{索引库名}/_search
{
  "query": {
    "match": {
      "字段名": "搜索条件"
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
1.2.2 multi_match

同时对多个字段搜索,而且多个字段都要满足,语法:

GET /{索引库名}/_search
{
  "query": {
    "multi_match": {
      "query": "搜索条件",
      "fields": ["字段1", "字段2"]
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

1.3 排序

语法:

GET /indexName/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "排序字段": {
        "order": "排序方式asc和desc"
      }
    }
  ]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

如果按照商品价格排序:

GET /items/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [ { "price": { "order": "desc" } } ]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

1.4 分页查询

elasticsearch 默认情况下只返回 top10 的数据。而如果要查询更多数据就需要修改分页参数了。

elasticsearch 中通过修改 from、size 参数来控制要返回的分页结果:

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

语法:

GET /items/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0, // 分页开始的位置,默认为0
  "size": 10,  // 每页文档数量,默认10
  "sort": [ { "price": { "order": "desc" } } ]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2. Java Client 实现搜索

2.1 准备

代码:

@SpringBootTest
public class SearchTest {

  @Autowired
  private IItemService itemService;

  private RestClient restClient = null;
  private ElasticsearchTransport transport = null;
  private ElasticsearchClient esClient = null;

  {
    // 使用 RestClient 作为底层传输对象
    restClient = RestClient.builder(new HttpHost("192.168.101.68", 9200)).build();

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(new JavaTimeModule());

    // 使用 Jackson 作为 JSON 解析器
    transport = new RestClientTransport(restClient, new JacksonJsonpMapper(objectMapper));
  }
  
  // 实现后续操作
  // TODO

  @BeforeEach
  public void searchTest() {
    // 创建客户端
    esClient = new ElasticsearchClient(transport);
    System.out.println(esClient);
  }

  @AfterEach
  public void close() throws IOException {
    transport.close();
  }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

后续代码放在代码的 TODO 处运行即可!!!

2.2 精准查询

2.2.1 Term 查询

根据 DSL 语句编写 java 代码:

GET /items/_search
{
  "query": {
    "term": {
      "category": { "value": "拉杆箱" }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

代码:

@Test
public void testTermSearch() throws IOException {
  SearchResponse<ItemDoc> search = esClient.search(
    // 搜索索引
    s -> s.index("items").query(
      // 精准匹配
      q -> q.term(t -> t.field("category").value("牛奶"))),
    // 指定返回类型
    ItemDoc.class
  );

  handleResponse(search);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
2.2.2 range 查询
GET /items/_search
{
  "query": {
    "range": {
      "price": { "gte": 100000, "lte": 20 }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

代码:

@Test
public void testRangeSearch() throws IOException {
  SearchResponse<ItemDoc> search = esClient.search(
    // 搜索索引
    s -> s.index("items").query(
      // 范围匹配,price >= 100000 && price < 200000
      q -> q.range(
        t -> t.field("price").gte(JsonData.of(100000)).lt(JsonData.of(200000))
      )
    ),
    // 指定返回类型
    ItemDoc.class
  );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

2.3 全文检索

2.3.1 match 查询
GET /items/_search
{
  "query": {
    "match": {
      "name": "德国进口纯奶"
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

代码:

@Test
public void testMatchSearch() throws IOException {
  SearchResponse<ItemDoc> search = esClient.search(
    // 搜索索引
    s -> s.index("items").query(
      // 模糊匹配
      q -> q.match(
        // 在 name 字段中模糊匹配 "德国进口纯奶"
        t -> t.field("name").query("德国进口纯奶"))),
    // 返回值类型
    ItemDoc.class
  );

  handleResponse(search);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
2.3.2 multi_match 查询
GET /items/_search
{
  "query": {
    "multi_match": {
      "query": "笔记本",
      "fields": ["name", "category"]
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

代码:

@Test
public void testMultiMatchSearch() throws IOException {
  SearchResponse<ItemDoc> search = esClient.search(
    // 搜索索引
    s -> s.index("items").query(
      // 多字段模糊匹配
      q -> q.multiMatch(
        // 匹配字关键字
        t -> t.query("笔记本")
        // 匹配字段
        .fields("name", "category"))),
    // 指定返回类型
    ItemDoc.class
  );

  handleResponse(search);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

2.4 排序和分页

GET /items/_search
{
  "query": {
    "multi_match": {
      "query": "绿色拉杆箱",
      "fields": ["name","category"]
    }
  },
  "sort": [
    { "price": { "order": "asc" } }
  ],
  "size": 20,
  "from": 0
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

代码:

@Test
public void testSortSearch() throws IOException {
  SearchResponse<ItemDoc> search = esClient.search(
    // 搜索索引
    s -> s.index("items")
    // 查询条件
    .query(q -> q.multiMatch(
      // 匹配字段
      m -> m.query("绿色拉杆箱").fields("name", "category")
    ))
    // 排序规则
    .sort(
      s1 -> s1.field(
        // 排序字段
        f -> f.field("price")
          // 排序规则
          .order(SortOrder.Desc)
      )
    )
    // 分页
    .from(0).size(10),
    // 指定返回类型
    ItemDoc.class
  );

  handleResponse(search);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

3. 复合查询

3.1 布尔查询

bool 查询,即布尔查询。就是利用逻辑运算来组合一个或多个查询子句的组合。bool 查询支持的逻辑运算有:

  1. must:必须匹配每个子查询,类似 “与”;
  2. should:选择性匹配子查询,类似 “或”;
  3. must_not:必须不匹配,不参与算分,类似 “非”;
  4. filter:必须匹配,不参与算分。

举例:

GET /items/_search
{
  "query": {
    "bool": {
      "must": [ {"match": {"name": "手机"}} ],
      "should": [
        {"term": {"brand": { "value": "vivo" }}},
        {"term": {"brand": { "value": "小米" }}}
      ],
      "must_not": [
        {"range": {"price": {"gte": 2500}}}
      ]
    }
  },
  "sort": [ { "brand": { "order": "desc" } } ]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

说明:

  1. 必须条件(must):
    1. 文档的 name 字段必须包含“手机”。
  2. 可选条件(should):
    1. 文档的 brand 字段应该是 “vivo” 或者 “小米”。只要满足其中一个条件即可。
  3. 排除条件(must_not):
    1. 文档的 price 字段不能大于等于 2500 元。
  4. 过滤条件(filter):
    1. 文档的 price 字段必须小于等于 1000 元。

当 should 与 must、must_not 同时使用时 should 会失效,需要指定 minimum_should_match。

3.2 尽量使用 filter

出于性能考虑,与搜索关键字无关的查询尽量采用 must_not 或 filter 逻辑运算,避免参与相关性算分(如:下拉菜单、多级菜单等)。

比如,要搜索 手机,但品牌必须是 华为,价格必须是 900~1599,那么可以这样写:

GET /items/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {"name": "手机"}}
      ],
      "filter": [
        {"term": {"brand": { "value": "华为" }}},
        {"range": {"price": {"gte": 90000, "lte": 159900}}}
      ]
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

3.3 Java Client

@Test
void testBoolQuery() throws Exception {
  //构建请求
  SearchRequest.Builder builder = new SearchRequest.Builder();
  //设置索引
  builder.index("items");
  //设置查询条件
  SearchRequest.Builder searchRequestBuilder = builder.query(
    // bool 查询,多条件匹配
    q -> q.bool(
      // must 连接
      b -> b.must(
        m -> m.match(
          // name 检索
          mm -> mm.field("name").query("手机")
        )
      ).should(
        s1 -> s1.term( t -> t.field("brand").value("小米"))
    	).should(
        s1 -> s1.term(t -> t.field("brand").value("vivo"))
    	).minimumShouldMatch("1")
    )
  )
  // 排序·
  .sort(sort -> sort.field(f -> f.field("brand").order(SortOrder.Asc)));

  SearchRequest build = searchRequestBuilder.build();
  //执行请求
  SearchResponse<ItemDoc> searchResponse = esClient.search(build, ItemDoc.class);
  //解析结果
  handleResponse(searchResponse);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

4. 高亮显示

4.1 高亮显示原理

什么是高亮显示呢?

我们在百度,京东搜索时,关键字会变成红色,比较醒目,这叫高亮显示。

观察页面源码,你会发现两件事情:

  • 高亮词条都被加了 标签
  • 标签都添加了红色样式

因此实现高亮的思路就是:

  • 用户输入搜索关键字搜索数据
  • 服务端根据搜索关键字到 elasticsearch 搜索,并给搜索结果中的关键字词条添加 html 标签
  • 前端提前给约定好的 html 标签添加 CSS 样式

4.2 实现高亮

语法:

GET /{索引库名}/_search
{
  "query": {
    "match": {
      "搜索字段": "搜索关键字"
    }
  },
  "highlight": {
    "fields": {
      "高亮字段名称": {
        "pre_tags": "",
        "post_tags": ""
      }
    },
    "require_field_match": "true"
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

注意:

  • 搜索必须有查询条件,而且是全文检索类型的查询条件,例如 match;
  • 参与高亮的字段必须是 text 类型的字段;
  • 默认情况下参与高亮的字段要与搜索字段一致,除非添加:required_field_match = false。

代码:

@Test
public void testHighLightSearch() throws Exception {
  SearchResponse<ItemDoc> search = esClient.search(
    // 搜索索引
    s -> s.index("items").query(
      // 匹配字段
      q -> q.match(
        // 匹配字段
        m -> m.field("name").query("笔记本")
      )
    )
    // 高亮
    .highlight(
      h -> h.fields("name",	 f -> f)
      // 高亮标签,前后缀
      .preTags("")
      .postTags("")
      //
      .requireFieldMatch(false)
    ),
    ItemDoc.class);

  long total = search.hits().total().value();
  System.out.println("total = " + total);

  List<Hit<ItemDoc>> hits = search.hits().hits();
  hits.forEach(hit -> {
    ItemDoc source = hit.source();
    // 高亮数据
    Map<String, List<String>> highlight = hit.highlight();
    List<String> highlightName = highlight.get("name");
    if(highlightName != null && !highlightName.isEmpty()){
      String s = highlightName.get(0);
      source.setName(s);
      System.out.println("s = " + s);
    }
  });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

5. 数据聚合

5.1 介绍

聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析、运算。

应用场景:

  1. 对数据进行统计
  2. 在搜索界面显示符合条件的品牌、分类、规格等信息

聚合常见的有三类:

  1. 桶(Bucket)聚合:用来对文档做分组
  • TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组
  • Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
  1. 度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等
  • Avg:求平均值
  • Max:求最大值
  • Min:求最小值
  • Stats:同时求 max、min、avg、sum 等
  1. 管道(pipeline)聚合:其它聚合的结果为基础做进一步运算

5.2 Bucket 聚合

5.2.1 语法

例如我们要统计所有商品中共有哪些商品分类,其实就是以分类(category)字段对数据分组。category 值一样的放在同一组,属于 Bucket 聚合中的 Term 聚合。

基本语法如下:

GET /items/_search
{
  "size": 0, 
  "aggs": {
    "category_agg": {
      "terms": {
        "field": "category",
        "size": 20,
        "order": { "_count": "desc" }
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

属性说明:

aggregations:定义聚合

  • category_agg:聚合名称,自定义,但不能重复

    • terms:聚合的类型,按分类聚合,所以用term

      • field:参与聚合的字段名称

      • size:希望返回的聚合结果的最大数量

        设置 size 为 0,查询 0 条数据即结果中不包含文档,只包含聚合

      • order:对聚合结果排序

5.2.2 多级聚合

同时对品牌分组统计,此时需要按分类统计,按品牌统计,这时需要定义多个桶,如下:

GET /items/_search
{
  "size": 0, 
  "aggs": {
    "category_agg": {
      "terms": { "field": "category", "size": 20 }
    },
    "brand_agg":{
      "terms": { "field": "brand", "size": 20 }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

现在需要统计同一分类下的不同品牌的商品数量,这时就需要对桶内的商品二次聚合,如下:

GET /items/_search
{
  "aggs" : {
    "category_agg" : {
      "aggs" : {
        "brand_agg" : {
          "terms" : { "field" : "brand", "size" : 20 }
        }
      },
      "terms" : { "field" : "category", "size" : 20 }
    }
  },
  "size" : 0
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

5.3 带条件聚合

默认情况下,Bucket 聚合是对索引库的所有文档做聚合,例如我们统计商品中所有的品牌,结果如下:


可以看到统计出的品牌非常多。

但真实场景下,用户会输入搜索条件,因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。

例如,我想知道价格高于 3000 元的手机品牌有哪些,该怎么统计呢?

语法如下:

增加 "query" 标签。

GET /items/_search
{
  "query": {
    "bool": {
      "filter": [
        { "term": { "category": "手机" } },
        { "range": { "price": { "gte": 300000 } } }
      ]
    }
  }, 
  "size": 0, 
  "aggs": { "brand_agg": { "terms": { "field": "brand", "size": 20 } } }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

5.4 Metric 聚合

统计了价格高于 3000 的手机品牌,形成了一个个桶。现在我们需要对桶内的商品做运算,获取每个品牌价格的最小值、最大值、平均值。

语法:

GET /items/_search
{
  "query": {
    "bool": {
      "filter": [
        { "term": { "category": "手机" } },
        { "range": { "price": { "gte": 300000 } } }
      ]
    }
  }, 
  "size": 0, 
  "aggs": {
    "brand_agg": {
      "terms": {
        "field": "brand",
        "size": 20,
        "order": { "stats_metric.avg": "desc" }
      },
      "aggs": { "stats_metric": { "stats": { "field": "price" } } }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

属性说明:

stats_meric:聚合名称,自定义名称

  • stats:聚合类型,stats 是 metric 聚合的一种
    • field:聚合字段,这里选择 price,统计价格

另外,我们还可以让聚合按照每个品牌的价格平均值排序:

5.5 Java Client

参考 DSL 语句编写 Java Client 代码

@Test
void testAggs() throws Exception {
  //构建请求
  SearchRequest.Builder builder = new SearchRequest.Builder();
  //设置索引名
  builder.index("items");
  //设置查询条件
  builder.query(
    q -> q.bool(
      b -> b.filter(
        f -> f.term(
          t -> t.field("category").value("手机")
        )
      ).filter(
        f -> f.range(
          r -> r.field("price").gte(JsonData.of(3000))
        )
      )
    )
  );
  //设置返回数量
  builder.size(0);
  //设置聚合
  builder.aggregations(
    "brand_agg", 
    a -> a.terms(
      t -> t.field("brand").size(10)
        .order(NamedValue.of("stats_metric.avg", SortOrder.Desc))
    ).aggregations(
      "stats_metric", 
      a1 -> a1.stats(
        s -> s.field("price")
      )
    )
  );
  SearchRequest build = builder.build();
  //执行请求
  SearchResponse<ItemDoc> searchResponse = esClient.search(build, ItemDoc.class);
  //解析出聚合结果
  Aggregate brandAgg = searchResponse.aggregations().get("brand_agg");
  brandAgg.sterms().buckets().array().forEach(bucket -> {
    String key = bucket.key().stringValue();
    Long docCount = bucket.docCount();
    StatsAggregate statsMetric = bucket.aggregations().get("stats_metric").stats();
    //平均价格
    Double avg = statsMetric.avg();
    //最大价格
    Double max = statsMetric.max();
    //最小价格
    Double min = statsMetric.min();
    log.info("品牌:{},商品数量:{},平均价格:{},最大价格:{},最小价格:{}", key, docCount, avg, max, min);
  });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
注:本文转载自blog.csdn.net的Code额的文章"https://blog.csdn.net/m0_73656461/article/details/146544548"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

112
数据库
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top