使用 Lucene 搜索你的 beans —— Facets

作者:来自 Elastic David Pilato

我们已经了解了如何为文档建立索引和搜索文档。这足以进行评分和过滤,但还不足以绘制带复选框的直方图。

本文要介绍的就是这部分差异:当你希望实现分面导航时,需要做哪些改变。

q=Bob 的 Genre / BPM / rating 和 year 分面。

q=Bob 仍然是我们之前看到的自由文本 MUST 查询。这些数字(Club (26)、BPM 区间、星级、年代)是基于同一个查询得到的分面计数。点击 Club 会在 genre 字段上添加一个针对 Club 的过滤器。

向项目中添加分面

计数存储在它们自己的 artifact 中:

将 artefact 翻译成中文补充分面存储方式的说明统一 Genre、BPM 和 rating 的术语

xml 复制代码
`

1.  <dependency>
2.    <groupId>org.apache.lucene</groupId>
3.    <artifactId>lucene-facet</artifactId>
4.    <version>10.5.1</version>
5.  </dependency>

`AI写代码

Genre 复选框需要在你已经用于 FILTER 的 keyword 旁边添加一个支持 facet 的 标签。不要在 TextField 上进行 facet ------ 因为它产生的 token 并不是复选框标签。例如,"Club House" 会被分词为"club"和"house",但你希望按照"Club House"进行分组,而不是按照"club"或"house"进行分组。

我们需要一个 FacetsConfig 来管理我们的 facet 字段:

ini 复制代码
`FacetsConfig facetsConfig = new FacetsConfig();`AI写代码

如果你还记得我们之前用于为搜索建立文档索引的代码,当时我们生成了以下 Lucene 文档:

csharp 复制代码
`

1.  Document doc = new Document();

3.  // ... All the other fields as before
4.  doc.add(new TextField("genre", "Club", Store.YES));
5.  doc.add(new StringField("genre.raw", "Club", Store.YES));
6.  doc.add(new StringField("genre.raw.normalized", "club", Store.YES));

`AI写代码

现在,我们需要为 genre 字段添加 facet 字段:

csharp 复制代码
`doc.add(new SortedSetDocValuesFacetField("genre", "Club"));`AI写代码

不再直接将文档写入 writer:

go 复制代码
`writer.addDocument(doc);`AI写代码

现在,你需要通过 FacetsConfig.build 方法传入它:

go 复制代码
`writer.addDocument(facetsConfig.build(doc));`AI写代码

FacetsConfig.build 会将 SortedSetDocValuesFacetField 字段重写为已建立索引的 $facets 字段,并使用 \u001F 作为分隔符字符(DELIM_CHAR):

csharp 复制代码
`1.  // You don't write this. FacetsConfig.build(doc) does it for you.
2.  doc.add(new SortedSetDocValuesField("$facets", new BytesRef("genre\u001FClub")))  // counts
3.  doc.add(new StringField("$facets", "genre\u001FClub", Store.NO))                  // drill-down
4.  doc.add(new StringField("$facets", "genre", Store.NO))` AI写代码

bpm、rating 和 year 已经支持 facet。无需额外的 mapper 字段,也无需进行 $facets 重写:

csharp 复制代码
`

1.  // Nothing changes for those numeric fields
2.  doc.add(new DoubleField("bpm", 121.29, Store.YES));
3.  doc.add(new IntField("rating", 5, Store.YES));
4.  doc.add(new IntField("year", 1997, Store.YES));

`AI写代码

三个不同的任务,三个不同的字段:

角色 UI 中的示例 使用的内容
FILTER chip genre = Club 对 genre.raw.normalized(club)使用 TermQuery
Facet 标签(复选框计数) Club (26) SortedSetDocValuesFacetField → $facets + SortedSetDocValuesFacetCounts
Numeric range histogram 120 -- 130 (52) DoubleField / IntField DocValues + *RangeFacetCounts

复选框使用相同的显示字符串 Club;只有精确的 FILTER 使用小写的 club。范围永远不会经过 $facets。

使用 facet 进行查询

与 playground 使用相同的技术栈。你不需要遍历 ScoreDoc 来绘制这些面板 ------ 而是只执行一次搜索,将结果放入 FacetsCollector,然后让每个 facet 实现来获取自己的 bucket。keyword 维度需要先针对 $facets 建立一个 reader state;搜索需要使用 FacetsCollectorManager 来构建(并合并)该 collector:

ini 复制代码
`

1.  SortedSetDocValuesReaderState state =
2.          new DefaultSortedSetDocValuesReaderState(searcher.getIndexReader());
3.  FacetsCollectorManager manager = new FacetsCollectorManager();

5.  FacetsCollector fc = FacetsCollectorManager.search(searcher, q, 1, manager)
6.          .facetsCollector();
7.  Facets genres = new SortedSetDocValuesFacetCounts(state, fc);
8.  Facets bpm = new DoubleRangeFacetCounts("bpm", fc, bpmRanges());

`AI写代码

topN = 1 是有意这样设置的:这里的重点并不是命中结果表;collector 只需要获取匹配的文档,以便进行计数。每当打开一个新的 reader(在 commit / refresh 之后),都需要重新构建 state。bpmRanges() 是你的 DoubleRange[](例如 120 -- 130)------ 数值范围不使用 state。

返回的结果仍然保持 Lucene 的结构 ------ 每个维度对应一个 FacetResult,其中包含一组 LabelAndValue(标签 → 计数)对:

ini 复制代码
`

1.  FacetResult genreResult = genres.getTopChildren(10, "genre");
2.  // Club → 26, Dance → 18, ...

4.  FacetResult bpmResult = bpm.getTopChildren(10, "bpm");
5.  // 120 -- 130 → 52, ...

`AI写代码

getTopChildren(n, dim) 会保留该维度中计数最大的 n 个 bucket 。数值范围使用你传递给 DoubleRange / LongRange 的标签;keyword facet 使用原始的 facet 值(Club,而不是 club)。

将这些结果映射为 UI 可以渲染的 beans:

csharp 复制代码
`

1.  public record FacetBucket(String value, int count) {}

3.  List<FacetBucket> toBuckets(FacetResult result) {
4.      if (result == null || result.labelValues == null) {
5.          return List.of();
6.      }
7.      return Arrays.stream(result.labelValues)
8.              .map(lv -> new FacetBucket(lv.label, lv.value.intValue()))
9.              .toList();
10.  }
11.  // Club (26), 120 -- 130 (52), ★★★★★ (13)

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

rating 或 year 也是相同的思路:在同一个 FacetsCollector 上再使用另一个 *RangeFacetCounts,然后再次调用 toBuckets。

当某个面板已经被选中时,在经过过滤的 q 上直接进行普通计数会隐藏同级的其他值。这时就需要切换到 DrillSideways:

scss 复制代码
`new DrillSideways(searcher, facetsConfig, state).search(drillDown, 1);`AI写代码

使用经过过滤的基础查询

q=Bob + drill-down genre Club ------ BPM 会缩小;其他 genre 仍然可见(DrillSideways)。

在 genre=Club 条件下,BPM 直方图应该缩小。genre 面板仍然应该显示 Dance ------ 否则用户就无法更换 genre,而必须先清除参数。

如果 FILTER genre:Club 已经存在于基础查询中,Lucene 就无法在 genre collector 中将其移除。因此需要拆分请求:

  • Base ------ 自由文本 MUST、排除项 MUST_NOT,以及针对_其他_维度(bpm、key 等)的 FILTER。
  • Drill-down ------ 通过 DrillDownQuery.add 添加每个已选面板的 dim。

然后,DrillSideways 会为过滤后的结果集运行一个 collector,并针对每个已选维度分别运行一个 collector,同时不包含该维度自身的约束:

scss 复制代码
`

1.  Query base = new BooleanQuery.Builder()
2.      // Must match the free text query
3.      .add(bob, BooleanClause.Occur.MUST)
4.      // Filter out keys:4a, 4b --- other FILTERs (bpm, ...) go here too, not genre
5.      .add(keys, BooleanClause.Occur.MUST_NOT)
6.      .build();

8.  DrillDownQuery drillDown = new DrillDownQuery(facetsConfig, base);
9.  // genre was not set as a filter as it's a drilldown dimension
10.  drillDown.add("genre", "Club");

12.  Facets luceneFacets = new DrillSideways(searcher, facetsConfig, state)
13.          .search(drillDown, 1)
14.          .facets;

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

电商场景中常见的技巧:按照品牌缩小结果表,但不要隐藏其他品牌。如果你需要在同一组 collector 上同时使用 keyword 和数值范围,则重写 DrillSideways.buildFacetsResult,并使用 MultiFacets 包装每个 collector。

完整的示例位于 GitHub:lucene-search-tracks。

原文:Search your beans with Lucene --- Facets | David Pilato

相关推荐
溪语流沙1 小时前
【Web全栈进阶】Alembic数据库迁移:改表结构不再删库
前端·数据库·python·elasticsearch·postgresql
ly76895 小时前
Elasticsearch 写入链路的 refresh、flush 与 translog 边界:为什么 bulk 吞吐会突然塌陷
大数据·elasticsearch·搜索引擎·translog·写入链路·bulk调优
Elasticsearch7 小时前
AWS 自动化根因分析:从 CloudWatch 告警到完成故障诊断,仅需 36 秒
elasticsearch
欢醉8 小时前
基于日志的监控告警:我们是怎么用一套轻量方案把线上问题"盯"住的
elasticsearch·springcloud·日志
Elastic 中国社区官方博客1 天前
使用 Lucene 搜索你的 Bean — 索引
java·大数据·数据库·elasticsearch·搜索引擎·全文检索·lucene
Elasticsearch1 天前
FSCrawler 3.0 来了!你知道的,就是用来处理文件的!
elasticsearch
Elasticsearch1 天前
使用 Lucene 搜索你的 Bean — 映射
elasticsearch
码艺-Alimjan1 天前
Tauri 项目 和 Vben-Admin 项目 源码打包哲学
大数据·elasticsearch·搜索引擎
Elasticsearch2 天前
使用 Lucene 搜索你的 Bean — 索引
elasticsearch