ElasticSearch

elasticsearch 是一个基于 Lucene 的搜索服务器。它提供了一个基于 RESTful web 接口的分布式全文搜索引擎。elasticsearch 是用 Java语言开发的,并作为 Apache 许可条款下的开放源码发布,是一种流行的企业级搜索引擎。可以用来实现搜索、日志统计、分析、系统监控等功能

elasticsearch 中常用的概念

倒排索引

倒排索引的概念是基于 MySQL 这样的正向索引而言的。

现在有如下表

复制代码
|id|role_name|
--------------
|1 | 雪之下雪乃|
--------------
|2 | 雪之下八幡|
--------------
|3 | 牧濑红莉栖|
--------------

如果是根据 id 查询,直接会走索引,查询速度快。但是如果基于 role_name 做模糊查询,只能是逐行扫描数据,流程如下

  • 用户搜索数据,条件是 role_name 符合 "%雪之下%"

  • 逐行获取数据,比如 id 为1的数据

  • 判断数据中的 role_name 是否符合用户搜索条件

  • 如果符合则放入结果集,不符合则丢弃。回到步骤1。

逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。

而倒排索引 就不一样了

倒排索引中的几个概念:

  • 文档(Document):搜索的数据,搜索出的每一条数据就是一个文档

  • 词条(Term):对文档数据或用户搜索数据以某种算法分词,得到的具备含义的词语就是词条

创建倒排索引是对正向索引的一种特殊处理。流程如下:

  • 将每一个文档的数据利用算法分词,得到一个个词条

  • 创建到排索引表,每行数据包括词条,词条所在的文档 id,位置等信息

  • 因为词条唯一性,可以给词条创建索引

复制代码
                |id|role_name|            |term |文档id|
                --------------            -------------
                |1 | 雪之下雪乃|            |雪之下| 1 2 |
                --------------    ---->   -------------
                |2 | 雪之下八幡|            | 牧濑 |  3  |
                --------------            --------------
                |3 | 牧濑红莉栖|
                --------------    
                  正向索引                      倒排索引

倒排索引的搜索流程如下

  • 用户输入条件 "雪之下雪乃" 进行搜索

  • 对用户输入的内容分词,得到词条 "雪之下"

  • 拿着词条在倒排索引中查找,可以找到包含词条的文档id 1,2

  • 拿着文档 id 到正向索引中查找具体文档

虽然要先查询倒排索引,再查询正向索引,但是无论是词条、还是文档id都建立了索引,查询速度非常快!无需全表扫描。

我们来对比一下 MySQL 和 elasticsearch
MySQL elasticsearch 说明
Table Index 索引(index),就是文档的集合,类似数据库的表(table)
Row Document 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式
Column Field 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column)
Schema Mapping Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)
SQL DSL DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD

可以看到

Index

索引类似 MySQL 中的表,是相同类型文档的集合

mapping

映射就是每类文档的约束信息

而文档

Document

类似 MySQL 中的一行数据,但是文档中的数据是以 json 的格式进行存储的

在操作 elasticsearch 的时候,可以配合 kibana 一起使用,它提供了一个 DevTools 界面,用来编写 DSL 操作 elasticSearch

分词器

Elasticsearch 自带的 默认分词器 是:

standard analyzer(标准分词器)

它适用于英文、数字等语言,比如:

复制代码
"中华人民共和国" → ["中华人民共和国"]
"hello world" → ["hello", "world"]

也就是说,它 不会对中文做有效的分词,中文会被当成一个整体词。 因此,如果需要中文搜索(尤其是模糊搜索、全文检索),就需要用更智能的分词器。

而 IK 分词器 IK Analyzer 是一个第三方的中文分词插件,它可以让 Elasticsearch 正确地把中文文本切分成有意义的词语。

比如:

复制代码
"中华人民共和国国歌"  
→ ["中华人民共和国", "中华", "人民", "共和国", "国歌"]

IK 不是 Elasticsearch 默认内置的插件,因此必须手动安装。

索引库的 CRUD
  • 创建索引库:PUT /索引库名

  • 查询索引库:GET /索引库名

  • 删除索引库:DELETE /索引库名

  • 修改索引库(添加字段):PUT /索引库名/_mapping

PS:倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引。因此索引库 一旦创建,无法修改mapping

虽然无法修改mapping中已有的字段,但是却 允许添加新的字段 到mapping中,因为不会对倒排索引产生影响

文档的 CRUD
  • 创建文档:POST /{索引库名}/_doc/文档id

  • 查询文档:GET /{索引库名}/_doc/文档id

  • 删除文档:DELETE /{索引库名}/_doc/文档id

  • 修改文档:

    • 全量修改:PUT /{索引库名}/_doc/文档id

    • 增量修改:POST /{索引库名}/_update/文档id { "doc": {字段}}

这里我们就不详细说了

Java 操作 ElasticSearch 有多种方法,现在我们先来介绍一下 ES 6.x - 7.x 推荐的方法

RestHighLevelClient

引入依赖

复制代码
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.17.10</version>
</dependency>

在 spring boot 的项目中可以在配置类里创建一个全局可复用的 Bean

复制代码
@Configuration
public class ElasticSearchConfig {
​
    @Bean
    public RestHighLevelClient restHighLevelClient() {
        return new RestHighLevelClient(
            RestClient.builder(
                new HttpHost("localhost", 9200, "http")
            )
        );
    }
}

新增文档

复制代码
@Autowired
private RestHighLevelClient client;
​
public void saveProduct() throws IOException {
    Map<String, Object> jsonMap = new HashMap<>();
    jsonMap.put("id", 1);
    jsonMap.put("name", "华为手机");
    jsonMap.put("price", 4999);
​
    IndexRequest request = new IndexRequest("product")
            .id("1")
            .source(jsonMap);
​
    IndexResponse response = client.index(request, RequestOptions.DEFAULT);
    System.out.println(response.getResult());
}

查询文档

复制代码
public void getProduct() throws IOException {
    GetRequest getRequest = new GetRequest("product", "1");
    GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT);
​
    if (getResponse.isExists()) {
        System.out.println(getResponse.getSourceAsString());
    }
}

更新文档

复制代码
public void updateProduct() throws IOException {
    Map<String, Object> updateMap = new HashMap<>();
    updateMap.put("price", 4599);
​
    UpdateRequest request = new UpdateRequest("product", "1")
            .doc(updateMap);
​
    UpdateResponse response = client.update(request, RequestOptions.DEFAULT);
    System.out.println(response.getResult());
}

新增文档和更新文档中的数据源参数 .source() 或者 .doc 最后保存的文档是 Json 格式的,但是可以传入的参数可以是 Json,也可以是 Map 类型

删除文档

复制代码
public void deleteProduct() throws IOException {
    DeleteRequest request = new DeleteRequest("product", "1");
    DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
    System.out.println(response.getResult());
}

这个客户端在 ES 8.x+ 之后被标记为 Deprecated,转而推荐使用

ElasticsearchClient

ElasticsearchClientElasticsearch 官方 Java API Client (从 8.x 版本开始) ,它取代了之前的 RestHighLevelClient,是基于 Java POJO 与类型安全的请求/响应模型

添加依赖

复制代码
<dependency>
    <groupId>co.elastic.clients</groupId>
    <artifactId>elasticsearch-java</artifactId>
    <version>8.12.0</version>
</dependency>

创建客户端

复制代码
public class ESClientFactory {
​
    public static ElasticsearchClient createClient() {
        RestClient restClient = RestClient.builder(
                new HttpHost("localhost", 9200, "http")).build();
​
        // 使用 Jackson 映射器
        RestClientTransport transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper());
​
        return new ElasticsearchClient(transport);
    }
}

RestClient 是底层 HTTP 客户端,负责实际请求。

RestClientTransport 用来封装传输,支持对象序列化/反序列化。

ElasticsearchClient 是高层 API 客户端,通过 Transport 与 Elasticsearch 通信。

JacksonJsonpMapper 是JSON 解析器,负责请求和响应的对象映射。

ElasticsearchClient 是通过组装底层 HTTP 客户端和传输层,形成的一个完整、类型安全的 Elasticsearch 操作客户端

新增文档

复制代码
private final ElasticsearchClient client = ESClientFactory.createClient();
​
public void addProduct() throws Exception {
    Map<String, Object> product = new HashMap<>();
    product.put("name", "手机");
    product.put("price", 4599);
​
    IndexResponse response = client.index(i -> i
            .index("product")
            .id("1")
            .document(product)
    );
​
    System.out.println(response.result());
}

查询文档

复制代码
public void getProduct() throws Exception {
    GetResponse<Map> response = client.get(g -> g
            .index("product")
            .id("1"), Map.class);
​
    if (response.found()) {
        System.out.println(response.source());
    }
}

更新文档

复制代码
public void updateProduct() throws Exception {
    Map<String, Object> updateMap = new HashMap<>();
    updateMap.put("price", 4999);
​
    UpdateResponse<Map> response = client.update(u -> u
            .index("product")
            .id("1")
            .doc(updateMap), Map.class);
​
    System.out.println(response.result());
}
​

除了使用这些客户端直接操作 elasticsearch 之外,还可以使用 spring 封装程度更高的。Spring Data

spring boot 集成 elasticsearch

添加 maven 依赖

复制代码
<dependencies>
    <!-- Spring Boot Elasticsearch -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
​
    <!-- lombok(可选,简化实体类) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

配置连接信息

复制代码
spring:
  data:
    elasticsearch:
      cluster-nodes: localhost:9200
      username: elastic
      password: your_password   # 如果设置了密码
      repositories:
        enabled: true

定义实体类

复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "products")  // 对应 Elasticsearch 的索引名
public class Product {
​
    @Id
    private String id;
​
    @Field(type = FieldType.Text, analyzer = "ik_max_word") // 中文分词
    private String name;
​
    @Field(type = FieldType.Double)
    private Double price;
​
    @Field(type = FieldType.Keyword)
    private String category;
​
    @Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
    private LocalDateTime createTime;
}

创建 Repository

复制代码
@Repository
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
    // 自定义查询方法(通过命名规则)参照 Spring Data JPA
    List<Product> findByName(String name);
}

ElasticsearchRepository 是 Spring Data Elasticsearch 提供的一个 接口 ,它对 Elasticsearch 的基本操作进行了封装,类似于 Spring Data JPA 的 JpaRepository。使用它可以免去自己手写大部分 CRUD 代码

编写 service

复制代码
@Service
@RequiredArgsConstructor
public class ProductService {
​
    private final ProductRepository repository;
​
    // 新增 / 更新
    public Product save(Product product) {
        product.setCreateTime(LocalDateTime.now());
        return repository.save(product);
    }
​
    // 批量添加
    public Iterable<Product> saveAll(List<Product> list) {
        return repository.saveAll(list);
    }
​
    // 查询
    public Optional<Product> findById(String id) {
        return repository.findById(id);
    }
​
    // 全量查询
    public Iterable<Product> findAll() {
        return repository.findAll();
    }
​
    // 根据名称搜索
    public List<Product> searchByName(String keyword) {
        return repository.findByName(keyword);
    }
​
    // 删除
    public void delete(String id) {
        repository.deleteById(id);
    }
}

编写 controller

复制代码
@RestController
@RequestMapping("/es/products")
@RequiredArgsConstructor
public class ProductController {
​
    private final ProductService service;
​
    @PostMapping
    public Product save(@RequestBody Product product) {
        return service.save(product);
    }
​
    @GetMapping("/{id}")
    public Product findById(@PathVariable String id) {
        return service.findById(id).orElse(null);
    }
​
    @GetMapping
    public Iterable<Product> findAll() {
        return service.findAll();
    }
​
    @GetMapping("/search")
    public List<Product> search(@RequestParam String name) {
        return service.searchByName(name);
    }
​
    @DeleteMapping("/{id}")
    public void delete(@PathVariable String id) {
        service.delete(id);
    }
}

接下来讲解一下 查询聚合

查询(Query)

Elasticsearch 的查询分两大类:全文查询(Full-Text Query)精确值查询(Term/Exact Query)

查询 DSL

Elasticsearch 使用 Query DSL(Domain Specific Language) 表达查询。基本结构:

复制代码
GET /index/_search
{
  "query": {
    "match": {
      "field": "value"
    }
  }
}
  • query:表示查询

  • match:表示全文查询

全文查询(Full-Text Query)

适用于分词后的文本字段:

查询类型 说明
match 匹配单个字段的全文内容
multi_match 匹配多个字段
match_phrase 短语查询(保持顺序)
query_string 支持逻辑运算符(AND, OR)
simple_query_string 简化版 query_string
copy_to

copy_toElasticsearch 映射(mapping)中的一个字段属性 ,用于 把一个或多个字段的值"复制"到另一个字段,方便搜索。

作用

  • 合并多个字段,构建一个 统一搜索字段 (通常称作 all 字段)

  • 不改变原字段内容,只是生成一个新的字段用于搜索

  • 常与 multi_matchmatch 查询配合使用

假设有一张书籍索引 books

复制代码
PUT /books
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "copy_to": "all_text"
      },
      "description": {
        "type": "text",
        "copy_to": "all_text"
      },
      "author": {
        "type": "text",
        "copy_to": "all_text"
      },
      "all_text": {
        "type": "text"
      }
    }
  }
}

titledescriptionauthor 都会把文本复制到 all_text,查询 all_text 就能匹配三个字段内容,原字段仍可独立搜索

示例:匹配 title 字段包含 "Elasticsearch 教程"

复制代码
GET /books/_search
{
  "query": {
    "match": {
      "title": "Elasticsearch 教程"
    }
  }
}

精确查询(Term/Exact Query)

适用于 keyword、数字、日期等精确匹配字段:

查询类型 说明
term 精确匹配单个值
terms 精确匹配多个值
range 范围查询(>、<、>=、<=)
exists 判断字段是否存在

示例:查找 price 为 100 的商品

复制代码
GET /product/_search
{
  "query": {
    "term": {
      "price": 100
    }
  }
}

示例:查找价格在 100~500 的商品

复制代码
GET /product/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 100,
        "lte": 500
      }
    }
  }
}

组合查询(Bool Query)

布尔查询允许组合多个子查询:

  • must:必须匹配(AND)

  • should:可选匹配(OR)

  • must_not:必须不匹配(NOT)

  • filter:过滤(不影响评分)

示例:查找 title 包含 "Elasticsearch" 并且 price < 500

复制代码
GET /product/_search
{
  "query": {
    "bool": {
      "must": {
        "match": {
          "title": "Elasticsearch"
        }
      },
      "filter": {
        "range": {
          "price": {
            "lt": 500
          }
        }
      }
    }
  }
}
聚合(Aggregation)

聚合用于 统计、分析、分组 ,类似 SQL 的 GROUP BYCOUNT/SUM/AVG

聚合类型

桶聚合(Bucket Aggregation)

把文档分组,每个分组叫一个。常用类型:

聚合类型 说明
terms 按字段值分组
range 按数值/日期范围分组
date_histogram 按时间间隔分组
filters 自定义多个过滤条件

示例:按 category 字段分组

复制代码
GET /product/_search
{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category.keyword"
      }
    }
  }
}

指标聚合(Metric Aggregation)

对文档计算数值,如 countavgsumminmax

示例:统计商品价格平均值

复制代码
GET /product/_search
{
  "size": 0,
  "aggs": {
    "avg_price": {
      "avg": {
        "field": "price"
      }
    }
  }
}

嵌套聚合(Nested Aggregation)

聚合中可以再嵌套聚合,例如先分组再求平均:

示例:每个 category 的平均价格

复制代码
GET /product/_search
{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category.keyword"
      },
      "aggs": {
        "avg_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}

接下来我们看一下使用 RestHighLevelClient 如何进行这部分操作

创建客户端

复制代码
RestHighLevelClient client = new RestHighLevelClient(
    RestClient.builder(new HttpHost("localhost", 9200, "http"))
);
​

Match 查询

复制代码
SearchRequest searchRequest = new SearchRequest("books");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
​
sourceBuilder.query(QueryBuilders.matchQuery("title", "Elasticsearch 教程"));
searchRequest.source(sourceBuilder);
​
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
response.getHits().forEach(hit -> System.out.println(hit.getSourceAsString()));
​

SearchRequest searchRequest = new SearchRequest("books") 中传入的参数是索引名

sourceBuilder 构建的是查询条件

RequestOptions 用来封装 HTTP 请求相关的配置,包括:

  • Headers:自定义请求头,例如 Authorization、TraceId 等。

  • 认证信息:如果需要 Basic Auth 或 Token,可以通过 RequestOptions 设置。

  • 超时设置:连接超时、请求超时等。

  • 其他 HTTP 选项:如压缩、重试策略等。

RequestOptions.DEFAULT 就是一个默认实例:

  • 不带额外 headers

  • 使用客户端默认的认证和超时

  • 一般大多数场景都可以直接用

Multi-Match 查询

复制代码
sourceBuilder.query(QueryBuilders.multiMatchQuery(
        "Elasticsearch 教程",
        "title", "description", "author"
));

Aggregation(聚合)

Terms 聚合(按字段分组)

复制代码
TermsAggregationBuilder aggregation = AggregationBuilders
        .terms("author_count")
        .field("author.keyword");
​
sourceBuilder.aggregation(aggregation);
​
SearchResponse aggResponse = client.search(searchRequest, RequestOptions.DEFAULT);
Terms authorTerms = aggResponse.getAggregations().get("author_count");
for (Terms.Bucket bucket : authorTerms.getBuckets()) {
    System.out.println(bucket.getKeyAsString() + " : " + bucket.getDocCount());
}

Metrics 聚合(例如求平均)

复制代码
AvgAggregationBuilder avgAgg = AggregationBuilders
        .avg("avg_price")
        .field("price");
​
sourceBuilder.aggregation(avgAgg);
​
SearchResponse metricResp = client.search(searchRequest, RequestOptions.DEFAULT);
Avg avg = metricResp.getAggregations().get("avg_price");
System.out.println("平均价格: " + avg.getValue());

使用 ElasticsearchClient

创建客户端

复制代码
RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();
RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
ElasticsearchClient client = new ElasticsearchClient(transport);

查询操作

复制代码
SearchResponse<Map> response = client.search(s -> s
        .index("books")
        .query(q -> q
                .match(m -> m
                        .field("title")
                        .query("Elasticsearch 教程")
                )
        ), Map.class
);
​
response.hits().hits().forEach(hit -> System.out.println(hit.source()));

Multi-Match 查询

复制代码
SearchResponse<Map> multiResponse = client.search(s -> s
        .index("books")
        .query(q -> q
                .multiMatch(m -> m
                        .query("Elasticsearch 教程")
                        .fields("title", "description", "author")
                )
        ), Map.class
);

聚合操作

Terms 聚合

复制代码
SearchResponse<Void> aggResponse = client.search(s -> s
        .index("books")
        .aggregations("author_count", a -> a
                .terms(t -> t.field("author.keyword"))
        ), Void.class
);
​
aggResponse.aggregations().get("author_count").sterms().buckets().array()
        .forEach(bucket -> System.out.println(bucket.key() + " : " + bucket.docCount()));

Avg 聚合

复制代码
SearchResponse<Void> avgResponse = client.search(s -> s
        .index("books")
        .aggregations("avg_price", a -> a
                .avg(av -> av.field("price"))
        ), Void.class
);
​
Double avg = avgResponse.aggregations().get("avg_price").avg().value();
System.out.println("平均价格: " + avg);

Spring Data Elasticsearch (简称 SDE)如何在 Spring Boot 项目中进行查询和聚合操作

SDE 封装了 ElasticsearchOperations 来进行相关操作

ElasticsearchOperations接口,定义了所有对 Elasticsearch 的操作方法,包括 CRUD、搜索、聚合等。

ElasticsearchRestTemplate 是它的 默认实现类 ,底层使用 RestHighLevelClient

通过注入 ElasticsearchOperations,你就可以在项目中统一使用模板方法

注入其中一个类

复制代码
@Autowired
private ElasticsearchOperations elasticsearchOperations;

使用 NativeSearchQueryBuilder 构建查询

复制代码
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
        .withQuery(matchQuery("title", "Elasticsearch"))
        .withPageable(PageRequest.of(0, 10))
        .build();
​
SearchHits<Product> hits = elasticsearchOperations.search(searchQuery, Product.class);
for (SearchHit<Product> hit : hits) {
    System.out.println(hit.getContent());
}

Product.class 对应索引映射的实体类。

返回值是 SearchHits<T>,包含总条数、分页信息和每条命中数据

Spring Data Elasticsearch 提供聚合接口,通过 AggregationBuilders 构建聚合条件

复制代码
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
    // terms() 是 分组聚合(Terms Aggregation),作用类似 SQL 的 GROUP BY。
    // "categoryAgg" 是 聚合的名字(aggregation name),用于在返回结果中获取这个聚合数据
        .addAggregation(terms("categoryAgg").field("category.keyword"))
        .addAggregation(avg("avgPrice").field("price"))
        .build();
Aggregations aggregations = elasticsearchOperations.search(searchQuery, Product.class)
        .getAggregations();
​
Terms categoryAgg = aggregations.get("categoryAgg");
for (Terms.Bucket bucket : categoryAgg.getBuckets()) {
    System.out.println(bucket.getKeyAsString() + ": " + bucket.getDocCount());
}
​
Avg avgPrice = aggregations.get("avgPrice");
System.out.println("平均价格:" + avgPrice.getValue());

嵌套聚合

复制代码
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
        .addAggregation(
            terms("categoryAgg").field("category.keyword")
                .subAggregation(avg("avgPrice").field("price"))
        )
        .build();
​
Terms categoryAgg = aggregations.get("categoryAgg");
for (Terms.Bucket bucket : categoryAgg.getBuckets()) {
    Avg avgPrice = bucket.getAggregations().get("avgPrice");
    System.out.println(bucket.getKeyAsString() + ": " + avgPrice.getValue());
}
相关推荐
Aimyon_363 分钟前
MinerU Docker自定义版本部署
运维·docker·容器
xiaobangsky8 分钟前
Elasticsearch安装使用
elasticsearch·jenkins
莱茶荼菜30 分钟前
Ubuntu 20.04 系统库管理详细教程
linux·运维·ubuntu
迅为电子1 小时前
嵌入式Linux新手入门:北京迅为3568开发板驱动开发第二章helloworld 驱动实验
linux·运维·驱动开发
kyle~5 小时前
机器视觉---Intel RealSense SDK 2.0 开发流程
运维·c++·windows·深度相机·intel realsense
数智大号5 小时前
超云发布R2425存储服务器:以全栈自研引领国产存储新方向
运维·服务器
亚林瓜子5 小时前
在amazon linux 2023上面通过Fedora 36软件仓库源安装tesseract5
linux·运维·服务器·ocr·tesseract·amazon·fedor
是专家不是砖家5 小时前
linux USB摄像头不停掉线问题
linux·运维·服务器
yuanManGan5 小时前
走进Linux的世界:初识进程(Task)
linux·运维·服务器
小马哥编程5 小时前
【软考架构】案例分析-瘦客户端C/S架构
运维·服务器·架构