7.5. 使用 Hibernate Search 执行 Lucene 查询


流程

Hibernate Search 可以执行 Lucene 查询并检索由 InfinispanHibernate 会话管理的域对象。搜索提供了 Lucene 的强大功能,无需离开 Hibernate 范式,为 Hibernate 典型搜索机制(HQL、标准查询、本地 SQL 查询)提供了另一个维度。

准备并执行查询由以下四个步骤组成:

  • 创建 FullTextSession
  • 使用 Hibernate QueryHibernate Search 查询 DSL(推荐)或使用 Lucene 查询 API 创建 Lucene 查询 API
  • 使用 org.hibernate.Query 包装 Lucene 查询
  • 通过调用 example list()或 scroll()执行搜索.

若要访问查询功能,请使用 FullTextSession。此搜索特定会话打包了一个常规 org.hibernate.Session,以提供查询和索引功能。

示例:创建 FullTextSession

Session session = sessionFactory.openSession();
...
FullTextSession fullTextSession = Search.getFullTextSession(session);
Copy to Clipboard Toggle word wrap

使用 FullTextSession 使用 Hibernate Search 查询 DSL 或原生 Lucene 查询构建全文本查询。

在使用 Hibernate Search 查询 DSL 时,请使用以下代码:

final QueryBuilder b = fullTextSession.getSearchFactory().buildQueryBuilder().forEntity( Myth.class ).get();

org.apache.lucene.search.Query luceneQuery =
    b.keyword()
        .onField("history").boostedTo(3)
        .matching("storm")
        .createQuery();

org.hibernate.Query fullTextQuery = fullTextSession.createFullTextQuery( luceneQuery );
List result = fullTextQuery.list(); //return a list of managed objects
Copy to Clipboard Toggle word wrap

另外,也可使用 Lucene 查询解析器或 Lucene 编程 API 编写 Lucene 查询。

示例:使用 QueryParser 创建 Lucene Query

SearchFactory searchFactory = fullTextSession.getSearchFactory();
org.apache.lucene.queryParser.QueryParser parser =
    new QueryParser("title", searchFactory.getAnalyzer(Myth.class) );
try {
    org.apache.lucene.search.Query luceneQuery = parser.parse( "history:storm^3" );
}
catch (ParseException e) {
    //handle parsing failure
}

org.hibernate.Query fullTextQuery = fullTextSession.createFullTextQuery(luceneQuery);
List result = fullTextQuery.list(); //return a list of managed objects
Copy to Clipboard Toggle word wrap

基于 Lucene 查询构建的 Hibernate 查询是 org.hibernate.Query。此查询保留与其他 Hibernate 查询工具相同,如 HQL(Hibernate 查询语言)、Native 和 Criteria。使用 list()、uniqueResult()、heerate()和 scroll()等方法以及查询。

Hibernate Jakarta Persistence 也提供相同的扩展:

示例:使用 Jakarta Persistence 创建搜索查询

EntityManager em = entityManagerFactory.createEntityManager();

FullTextEntityManager fullTextEntityManager =
    org.hibernate.search.jpa.Search.getFullTextEntityManager(em);

...
final QueryBuilder b = fullTextEntityManager.getSearchFactory()
    .buildQueryBuilder().forEntity( Myth.class ).get();

org.apache.lucene.search.Query luceneQuery =
    b.keyword()
        .onField("history").boostedTo(3)
        .matching("storm")
        .createQuery();

javax.persistence.Query fullTextQuery = fullTextEntityManager.createFullTextQuery( luceneQuery );

List result = fullTextQuery.getResultList(); //return a list of managed objects
Copy to Clipboard Toggle word wrap

注意

这些示例中使用了 Hibernate API。通过调整 FullTextQuery 的检索方式,也可使用 Jakarta Persistence 编写相同的示例。

7.5.1. 构建队列

Hibernate 搜索查询基于 Lucene 查询构建,允许用户使用任何 Lucene 查询类型。构建查询时,Hibernate Search 使用 org.hibernate.Query 作为查询操作 API,以进行进一步查询处理。

7.5.1.1. 使用 Lucene API 构建 Lucene 查询

使用 Lucene API 时,可以使用查询解析器(简单查询)或 Lucene 编程 API(复杂查询)。构建 Lucene 查询超出了 Hibernate 搜索文档的范围。详情请查看在线 Lucene 文档或 Lucene in Action Hibernate Search in Action 的副本。

7.5.1.2. 构建 Lucene Query

Lucene 编程 API 启用全文本查询。但是,在使用 Lucene 编程 API 时,参数必须转换为字符串等效,并且还必须将正确的分析器应用到正确的字段。例如,ngram 分析器使用多个 ngrams 作为给定词的令牌,应进行此类搜索。建议将 QueryBuilder 用于此任务。

Hibernate Search 查询 API 流畅,具有以下关键特征:

  • 方法名称为英文。因此,API 操作可以被阅读和理解为一系列英语短语和指令。
  • 它使用 IDE 自动完成功能,可帮助当前输入前缀的补全并允许用户选择正确的选项。
  • 它通常使用连锁方法模式。
  • 易于使用和阅读 API 操作。

要使用 API,首先创建一个附加到给定 索引类型的 查询构建器。此 QueryBuilder 知道要使用的分析器以及要应用的字段桥接。可以创建几个 QueryBuilders(涉及查询根目录的每个实体类型一个)。QueryBuilder 派生自 SearchFactory。

QueryBuilder mythQB = searchFactory.buildQueryBuilder().forEntity( Myth.class ).get();
Copy to Clipboard Toggle word wrap

用于给定字段或字段的分析器也可以被覆盖。

QueryBuilder mythQB = searchFactory.buildQueryBuilder()
    .forEntity( Myth.class )
        .overridesForField("history","stem_analyzer_definition")
    .get();
Copy to Clipboard Toggle word wrap

查询构建器现在用于构建 Lucene 查询。使用 Lucene 的查询解析器或 Query 对象通过 Lucene 编程 API 生成自定义查询与 Hibernate Search DSL 一起使用。

7.5.1.3. 关键字查询

以下示例演示了如何搜索特定词语:

Query luceneQuery = mythQB.keyword().onField("history").matching("storm").createQuery();
Copy to Clipboard Toggle word wrap
Expand
表 7.10. 关键字查询参数
参数描述

keyword()

使用此参数查找特定词语。

onField()

使用此参数指定要在哪个 lucene 字段中搜索单词。

matching()

使用此参数指定搜索字符串的匹配项

createQuery()

创建 Lucene 查询对象。

  • 值"storm"通过 history FieldBridge 传递。这在涉及数字或日期时很有用。
  • 然后,字段桥接值传递到用于索引字段 历史记录 的分析器。这样可确保查询使用与索引相同的术语转换(小写、ngram、调整等)。如果分析过程为给定词生成了多个术语,则布尔值查询与 SHOULD 逻辑(大约是一个 OR 逻辑)一起使用。

要搜索不是字符串类型的属性:

@Indexed
public class Myth {
  @Field(analyze = Analyze.NO)
  @DateBridge(resolution = Resolution.YEAR)
  public Date getCreationDate() { return creationDate; }
  public Date setCreationDate(Date creationDate) { this.creationDate = creationDate; }
  private Date creationDate;

  ...
}

Date birthdate = ...;
Query luceneQuery = mythQb.keyword().onField("creationDate").matching(birthdate).createQuery();
Copy to Clipboard Toggle word wrap
注意

在普通 Lucene 中,Lucene 必须转换为其字符串表示法,本例中为一年。

这种转换适用于任何对象,只要 FieldBridge 具有 objectToString 方法(以及所有内置的 FieldBridge 实施)。

下一个示例搜索使用 ngram 分析器的字段。ngram分析器单词 ngrams 的索引顺序,这有助于避免用户拼写错误。例如,hibernate 的 3 小时是 hib, ibe, ber, ern, rna, nat, ate, ate。

@AnalyzerDef(name = "ngram",
  tokenizer = @TokenizerDef(factory = StandardTokenizerFactory.class ),
  filters = {
    @TokenFilterDef(factory = StandardFilterFactory.class),
    @TokenFilterDef(factory = LowerCaseFilterFactory.class),
    @TokenFilterDef(factory = StopFilterFactory.class),
    @TokenFilterDef(factory = NGramFilterFactory.class,
      params = {
        @Parameter(name = "minGramSize", value = "3"),
        @Parameter(name = "maxGramSize", value = "3") } )
  }
)

public class Myth {
  @Field(analyzer=@Analyzer(definition="ngram")
  public String getName() { return name; }
  public String setName(String name) { this.name = name; }
  private String name;

  ...
}

Date birthdate = ...;
Query luceneQuery = mythQb.keyword().onField("name").matching("Sisiphus")
   .createQuery();
Copy to Clipboard Toggle word wrap

匹配单词"Sisiphus"将小写,然后分为 3 型图:sis、iso、sip、iph、phu、hus。这些 ngram 各自将成为查询的一部分。然后,用户可以找到 Sysiphus 神话(使用 y)。所有操作都为用户透明地完成。

注意

如果用户不希望特定字段使用字段网桥或分析器,则可以调用 ignoreAnalyzer()或 ignoreFieldBridge()函数。

要在同一个字段中搜索多个可能的词语,请在匹配的 子句中添加它们。

//search document with storm or lightning in their history
Query luceneQuery =
    mythQB.keyword().onField("history").matching("storm lightning").createQuery();
Copy to Clipboard Toggle word wrap

要在多个字段中搜索相同的词语,请使用 onFields 方法。

Query luceneQuery = mythQB
    .keyword()
    .onFields("history","description","name")
    .matching("storm")
    .createQuery();
Copy to Clipboard Toggle word wrap

有时,即使搜索同一术语,也要与另一个字段不同对待一个字段,为此使用 和Field()方法。

Query luceneQuery = mythQB.keyword()
    .onField("history")
    .andField("name")
      .boostedTo(5)
    .andField("description")
    .matching("storm")
    .createQuery();
Copy to Clipboard Toggle word wrap

在上例中,只有字段名称增加到 5。

7.5.1.4. Fuzzy Queries

要执行 fuzzy 查询(基于 Levenshtein 距离算法),以 关键字 查询开头并添加 fuzzy 标志。

Query luceneQuery = mythQB
    .keyword()
      .fuzzy()
        .withThreshold( .8f )
        .withPrefixLength( 1 )
    .onField("history")
    .matching("starm")
    .createQuery();
Copy to Clipboard Toggle word wrap

阈值 是两个术语正在考虑匹配的限制。它是 0 到 1 之间的小数,默认值为 0.5。prefixLength 是"fuzzyness"忽略的前缀长度。虽然默认值为 0,但建议对包含大量不同术语的索引使用非零值。

7.5.1.5. 通配符查询

通配符查询在只知道部分词语的情况下很有用。? 表示单个字符,* 代表多个字符。请注意,出于性能考虑,建议查询不以?或 * 开头。

Query luceneQuery = mythQB
    .keyword()
      .wildcard()
    .onField("history")
    .matching("sto*")
    .createQuery();
Copy to Clipboard Toggle word wrap
注意

通配符查询不会对匹配的术语应用分析器。* 或 ? 被盗的风险太大。

7.5.1.6. 密码队列

到目前为止,我们一直在寻找词语或词组,用户还可以搜索准确或大概的句子。使用 phrase()执行此操作。

Query luceneQuery = mythQB
    .phrase()
    .onField("history")
    .sentence("Thou shalt not kill")
    .createQuery();
Copy to Clipboard Toggle word wrap

可以通过添加滑动因子来搜索大致句子。滑动因子表示句子中允许的换句话数:这类似于操作符内或附近的运算符。

Query luceneQuery = mythQB
    .phrase()
      .withSlop(3)
    .onField("history")
    .sentence("Thou kill")
    .createQuery();
Copy to Clipboard Toggle word wrap

7.5.1.7. 范围查询

范围查询搜索给定边界(包含与否)或给定边界以下或以上值之间的值。

//look for 0 <= starred < 3
Query luceneQuery = mythQB
    .range()
    .onField("starred")
    .from(0).to(3).excludeLimit()
    .createQuery();

//look for myths strictly BC
Date beforeChrist = ...;
Query luceneQuery = mythQB
    .range()
    .onField("creationDate")
    .below(beforeChrist).excludeLimit()
    .createQuery();
Copy to Clipboard Toggle word wrap

7.5.1.8. 组合查询

可以组合查询来创建更复杂的查询。可用的聚合操作器如下:

  • SHOULD :查询应包含该子队列的匹配元素。
  • MUST :查询必须包含子队列的匹配元素。
  • MUST: 查询不得包含子队列的匹配元素。

子查询可以是任何 Lucene 查询,包括布尔值查询本身。

示例:SHOULD 查询

//look for popular myths that are preferably urban
Query luceneQuery = mythQB
    .bool()
      .should( mythQB.keyword().onField("description").matching("urban").createQuery() )
      .must( mythQB.range().onField("starred").above(4).createQuery() )
    .createQuery();
Copy to Clipboard Toggle word wrap

示例:MUST 查询

//look for popular urban myths
Query luceneQuery = mythQB
    .bool()
      .must( mythQB.keyword().onField("description").matching("urban").createQuery() )
      .must( mythQB.range().onField("starred").above(4).createQuery() )
    .createQuery();
Copy to Clipboard Toggle word wrap

示例: MUST not Query

//look for popular modern myths that are not urban
Date twentiethCentury = ...;
Query luceneQuery = mythQB
    .bool()
      .must( mythQB.keyword().onField("description").matching("urban").createQuery() )
        .not()
      .must( mythQB.range().onField("starred").above(4).createQuery() )
      .must( mythQB
        .range()
        .onField("creationDate")
        .above(twentiethCentury)
        .createQuery() )
    .createQuery();
Copy to Clipboard Toggle word wrap

7.5.1.9. 查询选项

Hibernate Search 查询 DSL 是一种易于使用的查询 API。在接受和生成 Lucene 查询时,您可以纳入 DSL 不支持的查询类型。

以下是查询类型和字段的查询选项概述:

  • boostedTo (在查询类型和字段上)可将整个查询或特定字段扩展至给定因素。
  • withConstantScore (在查询时)返回与查询匹配的所有结果,其恒定分数等于增长值。
  • 使用 Filter 实例过滤edBy(Filter )(查询时)过滤查询结果。
  • 处理此字段时,ignoreAnalyzer (字段)会忽略分析器。
  • 处理此字段时,忽略FieldBridge (on 字段)忽略字段桥接。

示例:Query 选项的组合

Query luceneQuery = mythQB
    .bool()
      .should( mythQB.keyword().onField("description").matching("urban").createQuery() )
      .should( mythQB
        .keyword()
        .onField("name")
          .boostedTo(3)
          .ignoreAnalyzer()
        .matching("urban").createQuery() )
      .must( mythQB
        .range()
          .boostedTo(5).withConstantScore()
        .onField("starred").above(4).createQuery() )
    .createQuery();
Copy to Clipboard Toggle word wrap

7.5.1.10. 构建 Hibernate Search Query

7.5.1.10.1. 常规性

构建 Lucene 查询后,将其包装到 Hibernate 查询中。查询会搜索所有索引化实体并返回所有索引化类类型,除非明确配置为不这样做。

示例:在 Hibernate 查询中抓取 Lucene 查询

FullTextSession fullTextSession = Search.getFullTextSession( session );
org.hibernate.Query fullTextQuery = fullTextSession.createFullTextQuery( luceneQuery );
Copy to Clipboard Toggle word wrap

要提高性能,请限制返回的类型,如下所示:

示例:按实体类型过滤搜索结果

fullTextQuery = fullTextSession
    .createFullTextQuery( luceneQuery, Customer.class );

// or

fullTextQuery = fullTextSession
    .createFullTextQuery( luceneQuery, Item.class, Actor.class );
Copy to Clipboard Toggle word wrap

第二个示例中的第一部分仅返回匹配的客户。同一示例的第二部分返回匹配的操作程序和项目。类型限制是多态限制。因此,如果基本类 Person 返回的两个子类 Salesman 和客户,请指定 Person.class 以根据结果类型进行过滤。

7.5.1.10.2. 分页

为避免性能下降,建议限制每个查询返回的对象数量。用户从一个页面导航至另一页面是一个非常常见的用例。定义分页的方式类似于在普通 HQL 或标准查询中定义分页。

示例:定义搜索查询的粘贴

org.hibernate.Query fullTextQuery =
    fullTextSession.createFullTextQuery( luceneQuery, Customer.class );
fullTextQuery.setFirstResult(15); //start from the 15th element
fullTextQuery.setMaxResults(10); //return 10 elements
Copy to Clipboard Toggle word wrap

注意

无论通过 fulltextQuery.getResultSize()分页,仍然可以获得匹配元素的总数。

7.5.1.10.3. 排序

Apache Lucene 包含灵活而强大的结果排序机制。默认是相关的排序,适用于各种用例。排序机制可以更改为由其他属性排序,使用 Lucene sortrt 对象应用 Lucene 排序策略。

示例:指定Lucene排序

org.hibernate.search.FullTextQuery query = s.createFullTextQuery( query, Book.class );
org.apache.lucene.search.Sort sort = new Sort(
    new SortField("title", SortField.STRING));

List results = query.list();
Copy to Clipboard Toggle word wrap

注意

用于排序的字段不能被令牌化。有关令牌大小的更多信息,请参阅 @Field

7.5.1.10.4. 获取策略

Hibernate 搜索是否将返回类型限制为一个类,使用单个查询来加载对象。Hibernate Search 受到域模型中定义的静态获取策略的限制。可根据特定用例优化获取策略,如下所示:

示例:在查询中指定 FetchMode

Criteria criteria =
    s.createCriteria( Book.class ).setFetchMode( "authors", FetchMode.JOIN );
s.createFullTextQuery( luceneQuery ).setCriteriaQuery( criteria );
Copy to Clipboard Toggle word wrap

在本例中,查询将返回与 LuceneQuery 匹配的所有 Book。使用 SQL 外部连接从同一查询加载作者集合。

在标准查询定义中,会根据提供的标准查询来猜测类型。因此,不需要限制返回实体类型。

重要

fetch 模式是唯一可调整的属性。不要在标准查询上使用限制(一个位置的 子句),因为 getResultSize()如果与有限制的标准结合使用,则抛出 SearchException。

如果预期有多个实体,请不要使用 setCriteriaQuery

7.5.1.10.5. 投射

在某些情况下,只需要一小部分属性。使用 Hibernate Search 返回属性子集,如下所示:

Hibernate Search 从 Lucene 索引中提取属性,并将它们转换为其对象表示法并返回对象[] 列表。预测可防止数据库往返用时。但是,它们有以下限制:

  • 生成的属性必须存储在索引(@Field(store=Store.YES))中,这会增加索引大小。
  • 生成的属性必须使用 field Bridge 实施 org.hibernate.search.bridge.TwoWayFieldBridge 或 org.hibernate.search.bridge.TwoWayStringBridge,后者是简单的版本。

    注意

    所有 Hibernate 搜索内置类型都是双向的。

  • 只有索引实体或其嵌入式关联的简单属性才能被投射。因此无法预测整个嵌入式实体。
  • 投射不适用于通过 @IndexedEmbedded 索引的集合或映射。

Lucene 提供有关查询结果的元数据信息。使用投射常量来检索元数据。

示例:使用 Projection Retrieve Metadata

org.hibernate.search.FullTextQuery query =
    s.createFullTextQuery( luceneQuery, Book.class );
query.;
List results = query.list();
Object[] firstResult = (Object[]) results.get(0);
float score = firstResult[0];
Book book = firstResult[1];
String authorName = firstResult[2];
Copy to Clipboard Toggle word wrap

字段可以与以下投射常量混合:

  • FullTextQuery.THIS:返回初始化的和管理实体 (作为非投射查询完成)。
  • FullTextQuery.DOCUMENT:返回与投射对象相关的 Lucene Document。
  • FullTextQuery.OBJECT_CLASS:返回索引实体的类
  • FullTextQuery.SCORE:返回查询中的文档分数。分数可以轻松地将一个结果与给定查询的另一个结果进行比较,但在比较不同查询的结果时却毫无用处。
  • FullTextQuery.ID:投射对象的 ID 属性值。
  • FullTextQuery.DOCUMENT_ID:Lucene 文档 ID。谨慎使用这个值作为 Lucene 文档 ID 可能会在两个不同的 IndexReader 打开之间随时间变化。
  • FullTextQuery.EXPLANATION:返回给定查询中匹配对象 /文档的 Lucene Explanation 对象。这不适用于检索大量数据。运行解释通常与每个匹配元素运行整个 Lucene 查询的成本相同。因此,建议预测。
7.5.1.10.6. 自定义对象初始化策略

默认情况下,Hibernate Search 使用最合适的策略来初始化与完整文本查询匹配的实体。它将执行一个或多个查询,以检索所需的实体。这种方法可最大程度减少数据库行程,其中仅有些检索到的实体存在于持久性上下文(会话)或第二级缓存中。

如果第二级缓存中存在实体,请强制 Hibernate Search 在检索数据库对象之前查看缓存。

示例:使用查询检查第二个缓存前的

FullTextQuery query = session.createFullTextQuery(luceneQuery, User.class);
query.initializeObjectWith(
    ObjectLookupMethod.SECOND_LEVEL_CACHE,
    DatabaseRetrievalMethod.QUERY
);
Copy to Clipboard Toggle word wrap

ObjectLookupMethod 定义用于检查对象是否可轻松访问的策略(不从数据库获取)。其他选项有:

  • ObjectLookupMethod.PERSISTENCE_CONTEXT 如果很多匹配实体已加载到持久上下文(加载在 Session 或 EntityManager 中),则会使用。
  • ObjectLookupMethod.SECOND_LEVEL_CACHE 检查持久上下文,然后检查第二级缓存。

将以下内容设置为在二级缓存中搜索:

  • 正确配置和激活第二级缓存。
  • 为相关实体启用第二级缓存。这通过 @Cacheable 等注释来完成。
  • 为 Session、实体管理器或查询启用二级缓存读取访问权限。在 Hibernate 原生 API 或 Jakarta Persistence 中的 CacheRetrieveMode.USE 中使用 CacheMode. NORMAL
警告

除非第二级缓存实施为 Infinispan,否则不使用 ObjectLookupMethod.SECOND_LEVEL_CACHE。其他第二级缓存提供商无法有效地实施此操作。

使用 DatabaseRetrievalMethod 自定义从数据库加载对象的方式,如下所示:

  • QUERY (默认)使用一组查询来加载每个批处理中的多个对象。建议采用这种方法。
  • FIND_BY_ID 使用 Session.getEntityManager.find 语义一次加载一个对象。如果为实体设置了批处理大小,则建议这样做,这将允许 Hibernate Core 批量加载实体。
7.5.1.10.7. 限制查询的时间

按照如下所述,限制 Hibernate 指南中查询花费的时间:

  • 在达到极限时引发异常。
  • 限制为在引发时间限制时检索的结果数。
7.5.1.10.8. 引发时间限制例外

如果查询使用的时间超过定义的时间,则会引发 QueryTimeoutException(org.hibernate.QueryTimeoutException 或 javax.persistence.QueryTimeoutException,具体取决于编程 API)。

要使用原生 Hibernate API 时定义限制,请使用以下方法之一:

示例:在查询执行中定义超时

Query luceneQuery = ...;
FullTextQuery query = fullTextSession.createFullTextQuery(luceneQuery, User.class);

//define the timeout in seconds
query.setTimeout(5);

//alternatively, define the timeout in any given time unit
query.setTimeout(450, TimeUnit.MILLISECONDS);

try {
    query.list();
}
catch (org.hibernate.QueryTimeoutException e) {
    //do something, too slow
}
Copy to Clipboard Toggle word wrap

getResultSize()、heerate()和 scroll()遵循超时,直到方法调用的末尾。因此,Sterable 或 ScrollableResults 会忽略超时。另外,explain()也不满足这个超时期限。此方法用于调试,并检查查询性能较慢的原因。

以下是使用 Jakarta Persistence 限制执行时间的标准方法:

示例:在查询执行中定义超时

Query luceneQuery = ...;
FullTextQuery query = fullTextEM.createFullTextQuery(luceneQuery, User.class);

//define the timeout in milliseconds
query.setHint( "javax.persistence.query.timeout", 450 );

try {
    query.getResultList();
}
catch (javax.persistence.QueryTimeoutException e) {
    //do something, too slow
}
Copy to Clipboard Toggle word wrap

重要

示例代码不能保证查询以指定的结果数量停止。

返回顶部
Red Hat logoGithubredditYoutubeTwitter

学习

尝试、购买和销售

社区

关于红帽文档

通过我们的产品和服务,以及可以信赖的内容,帮助红帽用户创新并实现他们的目标。 了解我们当前的更新.

让开源更具包容性

红帽致力于替换我们的代码、文档和 Web 属性中存在问题的语言。欲了解更多详情,请参阅红帽博客.

關於紅帽

我们提供强化的解决方案,使企业能够更轻松地跨平台和环境(从核心数据中心到网络边缘)工作。

Theme

© 2025 Red Hat