# bing : a search engine
1. Tantivy 如何为长篇文章截取200字的高亮摘要?(中文重述)
在 Rust 的全文检索引擎库 Tantivy 中,要从一篇长文章里,根据用户的搜索词,提取出一段大约200字左右的摘要,并高亮其中的关键词,主要通过 SnippetGenerator 来实现。这对于提升搜索结果的用户体验至关重要。
核心原理
Tantivy 的摘要生成并非简单地在关键词前后截取固定长度的文本。它采用了一种更智能的评分机制,以找出最能体现搜索结果与查询相关性的文本片段。
依赖位置信息:首先,你的索引字段必须包含词元的位置信息。这需要在定义 Schema 时,将字段的 IndexRecordOption 设置为 WithFreqsAndPositions。同时,为了能从索引中取回原文来生成摘要,字段必须设置为 STORED。
片段评分 (Fragment Scoring):SnippetGenerator 会在内部将整篇文档分割成多个小的“片段”(fragments)。然后,它会根据以下几点为每个片段打分:
关键词密度:片段中包含的搜索关键词越多,分数越高。
关键词集中度:如果多个搜索关键词在片段中紧密相邻地出现,这个片段的相关性就更高,得分也更高。
长度预算:SnippetGenerator 会尽量选择长度接近你设定的最大字符数(例如200)的片段。
最佳片段选择:综合评分后,SnippetGenerator 会选出得分最高的那个片段作为最终的摘要(Snippet)。
HTML高亮:选出最佳片段后,其内部的 Highlighter 会将与查询匹配的关键词用指定的 HTML 标签(默认为 <b>)包裹起来,最终通过调用 .to_html() 方法生成高亮后的 HTML 字符串。
代码演示
下面的 Rust 代码完整演示了如何建立索引、添加长文档、进行搜索,并最终生成一个大约200字符的高亮摘要。
Rust
use tantivy::collector::TopDocs;
use tantivy::query::QueryParser;
use tantivy::schema::*;
use tantivy::{doc, Index, ReloadPolicy};
use tantivy::snippet::SnippetGenerator;
use tempfile::TempDir;
fn main() -> tantivy::Result<()> {
// 1. 定义 Schema
let mut schema_builder = Schema::builder();
// 确保字段配置正确:启用存储(storing)和位置信息(positions)
let text_options = TextOptions::default()
.set_storing_enabled(true)
.set_indexing_options(
TextFieldIndexing::default()
.set_tokenizer("en_stem")
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
);
let title = schema_builder.add_text_field("title", text_options.clone());
let body = schema_builder.add_text_field("body", text_options);
let schema = schema_builder.build();
// 2. 创建索引
let index_path = TempDir::new()?;
let index = Index::create_in_dir(&index_path, schema.clone())?;
let mut index_writer = index.writer(50_000_000)?;
// 3. 添加一篇长文档
let long_text = "Tantivy is a full-text search engine library written in Rust. \
It is highly inspired by Apache Lucene. It is designed to be a modern, \
performant, and easy-to-use library for building search applications. \
One of the key features of a search engine is the ability to display a snippet \
of the document with the search terms highlighted. This is crucial for users \
to quickly understand the context of the search results. Tantivy provides a \
powerful SnippetGenerator for this purpose. This generator can create a concise \
and relevant fragment of the original text. The length of this fragment, or snippet, \
can be controlled. For instance, we can aim for a snippet of around 200 characters. \
The highlighter will then wrap the matched terms with HTML tags, like <b>, to make them stand out. \
This combination of snippet generation and highlighting is essential for a good user experience in any search interface.";
index_writer.add_document(doc!(
title => "Tantivy Highlighting Example",
body => long_text
))?;
index_writer.commit()?;
// 4. 准备搜索
let reader = index.reader_builder().reload_policy(ReloadPolicy::OnCommit).try_into()?;
let searcher = reader.searcher();
// 注意:QueryParser 需要知道哪些字段是可搜索的
let query_parser = QueryParser::for_index(&index, vec![title, body]);
// 5. 执行搜索
let query = query_parser.parse_query("search library")?;
let top_docs = searcher.search(&query, &TopDocs::with_limit(10))?;
// 6. 为 'body' 字段创建 SnippetGenerator 并生成高亮摘要
// `SnippetGenerator` 需要知道你想从哪个字段生成摘要
let mut snippet_generator = SnippetGenerator::from_query(&query, &searcher, body);
// **核心:设置摘要的最大字符数**
snippet_generator.set_max_num_chars(200);
for (score, doc_address) in top_docs {
let retrieved_doc = searcher.doc(doc_address)?;
println!("文档得分: {}", score);
let snippet = snippet_generator.snippet_from_doc(&retrieved_doc);
// 生成高亮后的 HTML
let highlighted_html = snippet.to_html();
println!("高亮摘要 (约200字符):");
println!("{}", highlighted_html);
println!("---");
}
Ok(())
}
2. 如果我有多个字段(例如标题和正文),应该怎么处理?
当你的搜索查询可能匹配多个字段(如 title 和 body)时,你需要决定如何展示高亮摘要。通常有两种策略:
为每个匹配的字段都生成摘要:分别对标题和正文生成摘要,然后都展示给用户。
选择最佳摘要:对每个可能匹配的字段都生成摘要,然后通过某种逻辑(例如,哪个摘要包含的关键词更多,或者哪个字段更重要)选择一个最佳的摘要展示。
SnippetGenerator 本身在创建时是针对单个字段的。SnippetGenerator::from_query(&query, &searcher, field) 的第三个参数 field 明确了它将从哪个字段中提取原文并生成摘要。
因此,处理多字段高亮的正确做法是:为每个需要高亮的字段分别创建一个 SnippetGenerator。
代码演示 (处理多字段)
以下代码展示了如何为 title 和 body 两个字段分别生成高亮摘要。
Rust
// ... (接续上一个例子的 main 函数,从步骤 6 开始修改)
// 6. 为多个字段创建 SnippetGenerator 并生成高亮摘要
let fields_to_highlight = vec![title, body]; // 定义你想要高亮的字段列表
for (score, doc_address) in top_docs {
let retrieved_doc = searcher.doc(doc_address)?;
println!("文档得分: {}", score);
println!("文档: {}", schema.to_json(&retrieved_doc));
println!("---");
for field in &fields_to_highlight {
// 为当前循环的字段创建一个 SnippetGenerator
let mut snippet_generator = SnippetGenerator::from_query(&query, &searcher, *field);
snippet_generator.set_max_num_chars(200); // 同样可以设置长度限制
let snippet = snippet_generator.snippet_from_doc(&retrieved_doc);
// 检查生成的 snippet 是否为空(即该字段中没有匹配的词)
if !snippet.is_empty() {
// 获取字段名称,以便更好地展示
let field_name = schema.get_field_name(*field);
println!("字段 '{}' 的高亮摘要:", field_name);
println!("{}", snippet.to_html());
println!("---");
}
}
}
多字段处理逻辑解析
定义目标字段:我们创建了一个 Vec<Field>,包含了所有我们希望尝试生成高亮的字段 (title, body)。
遍历字段:在获取到每个搜索结果文档 (retrieved_doc) 后,我们遍历这个字段列表。
独立创建 SnippetGenerator:在循环内部,我们为每一个字段都创建了一个新的 SnippetGenerator 实例。这是因为每个生成器都需要知道它具体要处理哪个字段的原文。
生成并检查摘要:调用 snippet_from_doc 生成摘要。由于搜索词可能只存在于部分字段中,所以我们用 snippet.is_empty() 来判断当前字段是否真的匹配到了关键词并成功生成了摘要。
分别展示:如果摘要不为空,我们就将其高亮后的 HTML 打印出来,并附上字段名,让用户清楚地知道这个摘要来自标题还是正文。
这种方法提供了最大的灵活性,你可以根据你的业务需求,决定是将所有字段的摘要都展示出来,还是根据摘要的质量(例如,摘要的评分 snippet.score())只选择最好的一个。
```rust
<+ ./tests/main.rs>
```
<+ ../about.md >