欢迎光临
我们一直在努力

使用 Lucene 搜索你的 Bean — 搜索

作者:来自 Elastic David Pilato

我们已经将 Bean 映射为文档并建立了索引。现在来看看当你在输入框中输入内容、添加过滤条件、排除两个键时会发生什么。

先不设置任何条件,并输入 MatchAll 查询:

// Open an index searcher using the same writer
IndexSearcher searcher = new IndexSearcher(DirectoryReader.open(writer));

// Search and retrieve the 10 first hits
TopDocs hits = searcher.search(MatchAllDocsQuery.INSTANCE, 10);

// Join stored ids back to beans:
for (var hit : hits.scoreDocs) {
String id = searcher.storedFields().document(hit.doc).get("id");
Track track = trackDatabase.get(id);
if (track != null) {
// Do something with the track, e.g., print it or add it to a list
}
}

输入“Bob”

q=Bob → 62 个匹配结果。Title boost 优先于 artist;最后一个 token 还会获得一个 PrefixQuery。

分析查询字符串,并根据这些 token 构建查询:

TokenStream ts = analyzer.tokenStream("title", "Bob");
// → bob

// build query for bob
Query bob = new BooleanQuery.Builder()
.add(new BoostQuery(new TermQuery(new Term("title", "bob")), 4.0f), BooleanClause.Occur.SHOULD)
.add(new BoostQuery(new PrefixQuery(new Term("title", "bob")), 1.0f), BooleanClause.Occur.SHOULD)
.add(new BoostQuery(new TermQuery(new Term("artist", "bob")), 3.0f), BooleanClause.Occur.SHOULD)
.add(new BoostQuery(new PrefixQuery(new Term("artist", "bob")), 0.75f), BooleanClause.Occur.SHOULD)
// … genre^2 / album^1.5 / label^1 / comment^0.5 (+ prefixes) …
.setMinimumNumberShouldMatch(1)
.build();

// Search and retrieve the 10 first hits
TopDocs hits = searcher.search(bob, 10);

buildQuery 是 playground 展开的 fold:每个经过分析的字段都是一个带 boost 的 SHOULD TermQuery,此外还会针对最后输入的 token 添加一个 boost 为原 boost 四分之一的 PrefixQuery:

Lucene 将其打印为:

((title:bob)^4.0 (title:bob*)^1.0 (artist:bob)^3.0 (artist:bob*)^0.75
(genre:bob)^2.0 (genre:bob*)^0.5 (album:bob)^1.5 (album:bob*)^0.375
(label:bob)^1.0 (label:bob*)^0.25 (comment:bob)^0.5 (comment:bob*)^0.125)~1

多个 token 通过 AND 进行连接(每个 token 的 builder 都是 MUST)。只有最后一个 token 是前缀;之前的单词保持精确匹配。bob sincla 要求同时匹配 bob 和 sincla… 前缀。ouse 找不到 House —— 它不是子串匹配。

你还记得我们在上一篇文章中生成的 posting list 吗?

artist:bob → 1, 2, 3
artist:claude → 4, 5
artist:francois → 4, 5, 6
artist:marley → 2
artist:sinclar → 1, 3
artist:valery → 6

你看到与我们的搜索 (artist:bob)^3.0 的匹配了吗?

添加过滤条件(包含 Club)

相同的 q=Bob,再加上 FILTER genre=Club —— 62 首曲目变成 26 首。

将自由文本查询 builder 包装为 MUST,并添加一个 FILTER —— 进行约束,但不参与评分。与 playground 生成的叶子查询相同:

// Single filter on genre for club
Query genre = new TermQuery(new Term("genre.raw.normalized", "club"));

// Combine queries in a bool query
Query bool = new BooleanQuery.Builder()
.add(bob, BooleanClause.Occur.MUST)
// Filter in
.add(genre, BooleanClause.Occur.FILTER)
.build();

Lucene 将其打印为:

+(((title:bob)^4.0 (title:bob*)^1.0 … )~1) #genre.raw.normalized:club

在 genre.raw 旁边建立一个规范化的 keyword 副本(genre.raw.normalized),这样 Club 和 club 就能匹配相同的文档。不要对它使用 wildcard。

排除两个键(4A 和 4B)

Club 保持启用。使用 MUST_NOT 排除 4A 或 4B —— 26 首曲目变成 23 首。

使用相同的 MUST + FILTER,再为排除的键添加 MUST_NOT。同一维度上的多个键使用 OR(SHOULD,minShouldMatch = 1):

// Filter on keys 4a or 4b
Query keys = new BooleanQuery.Builder()
.add(new TermQuery(new Term("key.code", "4a")), BooleanClause.Occur.SHOULD)
.add(new TermQuery(new Term("key.code", "4b")), BooleanClause.Occur.SHOULD)
.setMinimumNumberShouldMatch(1)
.build();

Query bool = new BooleanQuery.Builder()
.add(bob, BooleanClause.Occur.MUST)
.add(genre, BooleanClause.Occur.FILTER)
// Filter out
.add(keys, BooleanClause.Occur.MUST_NOT)
.build();

Lucene 将其打印为:

+(((title:bob)^4.0 … )~1) #genre.raw.normalized:club -((key.code:4a key.code:4b)~1)

这意味着:

UIPlayground 预设Lucene 子句
输入“Bob” Bob → 62 MUST 匹配 + 最后一个 token 前缀
包含 Club + Club → 26 FILTER 精确匹配 club
排除 4A 或 4B − 4A、4B → 23 MUST_NOT(4a SHOULD 4b)

评分在确定搜索结果的相关性方面发挥着重要作用。一些子句会影响评分,而其他子句则充当过滤条件或排除条件。

Occur作用参与评分?
MUST 分析后的匹配 + 最后一个 token 前缀 是
FILTER 字段约束(genre、bpm……) 否
MUST_NOT 排除 否
SHOULD + minShouldMatch = 1 一个子句内部的多选 OR 否

只有 MUST_NOT 的 BooleanQuery 什么都匹配不到 —— 如果用户没有输入内容却进行了排除,请添加一个 MUST MatchAllDocsQuery。全部为空时使用 MatchAllDocsQuery。

首先显示“最佳”结果(按评分排序)

如果你想了解评分是如何计算的,可以对指定的文档和查询,在 IndexSearcher 上使用 explain 方法:

// Explain how the score is computed for the first hit
searcher.explain(query, hits.scoreDocs[0].doc);

这会得到类似这样的结果:

对于 q=Bob,文档 3473 的评分:title:bob 和 album:bob 上的 BM25 权重(boost × idf × tf)。

这棵树是 Lucene BM25 的评分分解。对于每个匹配的词项,评分为:

N 是包含该字段的文档数,n 是包含该词项的文档数,freq 是该词项在本文档中的出现次数,dl / avgdl 分别是字段长度与平均字段长度。k1 和 b 是 BM25 中常用的参数(默认值分别为 1.2 和 0.75)。

以截图中的文档 3473 的 title:bob 为例。在整个索引中,有 7 个标题包含 bob(n = 7),而共有 4,322 个文档包含 title 字段(N = 4,322)。在这个匹配结果中,bob 在标题中出现一次(freq = 1),标题包含 5 个 token(dl = 5),平均标题长度约为 4.24(avgdl)。字段 boost 为 4:

对 album:bob 进行同样的分析:只有 3 个专辑包含该词项(n = 3),而共有 3,088 个文档包含 album 字段(N = 3,088),同样是 freq = 1、dl = 5、avgdl \\approx 3.77,boost 为 1.5:

加上前缀叶子查询(title:bob* → 1、album:bob* → 0.375)后,就可以得到该匹配结果的评分:10.77+1+4.08+0.375≈16.23。

完整演示代码位于 GitHub:lucene-search-tracks。

原文:来自 Elastic https://david.pilato.fr/posts/2026-09-11-lucene-bean-search-query-sync/

赞(0)
未经允许不得转载:171主机测评 » 使用 Lucene 搜索你的 Bean — 搜索
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址