/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
上一篇【第30篇】查询过程总览与查询类型大观 下一篇【第32篇】短语查询与跨度查询
摘要
这回按上篇的菜单,先点三道最常吃的前菜:TermQuery 是万物之源,直接去倒排里把含某个词的文档捞出来;FuzzyQuery 在 TermQuery 上加了编辑距离容错,输错字也能蒙对;BooleanQuery 把多个子查询用 MUST/SHOULD/MUST_NOT 拼起来,还能把各自得分合成总得分。结合代码和图,把语义和坑一次讲清。
一、TermQuery:一切的起点
TermQuery 是最朴素的查询:给我一个词项(Term),返回所有含它的文档。它底层干的事,跟你手写玩具里"拿词去 HashMap 取倒排链"一模一样,只不过 Lucene 是从 .doc 文件里读,还带压缩和跳表。
// 直接构造,不依赖字符串解析,最可靠
Term t = new Term("content", "蓝牙");
Query q = new TermQuery(t);
TopDocs hits = searcher.search(q, 10);
注意 Term 由两个部分组成:字段名(“content”)和词值(“蓝牙”)。查询时字段名必须和建索引时的字段对上,词值要经过和建索引相同的分词器处理。如果你建索引用中文分词器把"蓝牙耳机"切成"蓝牙"“耳机”,那 TermQuery 里就只能查"蓝牙"或"耳机",查"蓝牙耳机"整串反而查不到,因为它不是一个词。这又回到前面强调的"分词器一致"铁律。
【TermQuery 底层】
Term(content, "蓝牙")
│
▼
查 .tim 定位词项 → 读 .doc 取文档列表 [1,4,9]
│
▼
逐文档算基础分(配合 IDF) → 返回
二、FuzzyQuery:输错字也能蒙对
人打搜索框难免手滑:“蓝牙"打成"兰牙”、“无限”。FuzzyQuery 基于编辑距离(Levenshtein,增删改的最少步数)做容错,允许词和词典里的词差一两个字符仍算命中。这正是咱们 019、020 篇手写过的编辑距离纠错,Lucene 直接内置了。
Term t = new Term("content", "兰牙");
// 允许最多 2 个编辑距离内的词命中(默认基于词长自动定容差)
Query q = new FuzzyQuery(t, 2);
它怎么工作的?Lucene 不会暴力拿"兰牙"和词典每个词比一遍(那太慢),而是利用词典的 FST 结构,自动推导出"和兰牙编辑距离≤2 的所有可能前缀",然后只去查那些前缀对应的词。相当于先圈定一个候选范围,再精确比对,效率比暴力高得多。这块思想和咱们手写的编辑距离有限状态机一脉相承。
【FuzzyQuery 思路】
"兰牙" 容差2
│ 由 FST 推导候选前缀范围
▼
候选: 蓝牙 / 兰芽 / 蓝亚 …(编辑距离≤2 的词)
│
▼
实际查这些候选的倒排,合并结果
实际用的时候容差别开太大,否则"蓝牙"能模糊到一堆不相关的词,召回是上去了,精度稀烂。一般中文 2、英文 1~2 就够。还有个坑:FuzzyQuery 对超长词或极大词典开销会变大,别滥用。
三、BooleanQuery:把查询拼起来
真实搜索几乎都是多条件,BooleanQuery 负责把多个子查询组合,并用三种出现规则:
- MUST(必须):相当于 AND,文档必须命中该子查询。
- SHOULD(应该):相当于 OR(在没 MUST 时),命中了加分、不命中不淘汰;若有 MUST 在,SHOULD 只影响得分不影响去留。
- MUST_NOT(必须不):相当于 NOT,命中该子查询的文档被排除。
BooleanQuery.Builder bq = new BooleanQuery.Builder();
bq.add(new TermQuery(new Term("content", "蓝牙")), BooleanClause.Occur.MUST);
bq.add(new TermQuery(new Term("content", "降噪")), BooleanClause.Occur.MUST);
bq.add(new TermQuery(new Term("content", "有线")), BooleanClause.Occur.MUST_NOT);
Query q = bq.build(); // 蓝牙 AND 降噪 AND NOT 有线
上面这个意思是:要含"蓝牙"和"降噪",但不能含"有线"。完美对应咱们手写玩具里的"求交集 + 排除"。
【Boolean 三种语义对照】
Occur 逻辑 文档命运
MUST AND 必须命中,否则淘汰
SHOULD OR(无MUST时) 命中加分,不命中不淘汰
MUST_NOT NOT 命中则淘汰
得分怎么合成
MUST 之间是求交集,每篇文档的最终得分是各 MUST 子查询得分之和(加 coord 因子,表示命中了几个应该命中的子句)。SHOULD 命中的也把分加上去。MUST_NOT 不参与加分,只负责剔除。
举个直觉例子:查 “蓝牙 MUST 降噪 SHOULD 便宜”,一篇同时含蓝牙、降噪、便宜的文档,会比只含蓝牙降噪的得分高(便宜给了额外加分),但两者都返回。这就是 SHOULD "加分不淘汰"的体现。
两个容易踩的坑
坑一:全 SHOULD 时,空查询会返回全部。如果 BooleanQuery 里没有任何 MUST,且一个 SHOULD 都没加(或都空),结果可能诡异。构造时务必确认语义。
坑二:MUST_NOT 不能单独用。一个 BooleanQuery 如果只有 MUST_NOT 子句,Lucene 无法直接表达"排除某些但没要求必须含什么",它需要配合一个 MatchAllDocsQuery 当底座,否则抛异常。我刚用时就被这个坑过,想"查所有不含有线的",得写成 MUST MatchAllDocs AND MUST_NOT 有线。
【BooleanQuery 常见组合】
蓝牙 MUST 降噪 → 交集(AND)
蓝牙 SHOULD 无线 → 并集(OR)加分
蓝牙 MUST_NOT 有线 → 排除(NOT),需配 MUST 底座
四、三道菜合起来能做什么
把 TermQuery、FuzzyQuery、BooleanQuery 组合,已经能覆盖绝大多数日常搜索:
【一个真实搜索的拼装】
用户输入: "兰牙 降噪 不想要有线"
│
├─ 兰牙 → FuzzyQuery(容差2) 容错
├─ 降噪 → TermQuery MUST
├─ 有线 → TermQuery MUST_NOT
│
▼
BooleanQuery(Fuzzy(MUST) + Term(降噪,MUST) + Term(有线,MUST_NOT))
│
▼
searcher.search(…) → 容错且精准的结果
你看,工业引擎的"高级查询"并不是什么新魔法,就是这几个基础件搭积木。理解了这三个,后面 PhraseQuery 的"连续词序列"、RangeQuery 的"区间",本质上也都是 Query 的某种组合或变体。
本篇小结
TermQuery 是根基,直接查倒排拿文档,但要注意字段名和分词器必须和建索引一致。FuzzyQuery 借编辑距离实现拼写容错,靠 FST 圈定候选范围而非暴力比对,容差别开太大。BooleanQuery 用 MUST/SHOULD/MUST_NOT 拼装子查询并合成得分,记住 SHOULD 加分不淘汰、MUST_NOT 不能单独用。三道基础菜组合已能覆盖日常搜索。下一篇上 PhraseQuery 和 SpanQuery,看看"词的位置"如何支撑短语和精细跨度查询。
上一篇【第30篇】查询过程总览与查询类型大观 下一篇【第32篇】短语查询与跨度查询




