Bagaimana Tantivy menerapkan pemecah kata yang berbeda untuk bahasa yang berbeda?
Pencarian di situs ini dibangun menggunakan tantivy
dan tantivy-jieba
. Tantivy adalah pustaka mesin pencari teks lengkap berkinerja tinggi yang ditulis dalam Rust, terinspirasi oleh Apache Lucene. Ia mendukung peringkat BM25, kueri bahasa alami, pencarian frasa, pencarian facet, dan berbagai jenis bidang (termasuk teks, angka, tanggal, IP, dan JSON), serta menyediakan dukungan pemecahan kata multibahasa (termasuk bahasa Tionghoa, Jepang, dan Korea). Ia memiliki kecepatan indeksasi dan kueri yang sangat cepat, waktu mulai dalam milidetik, dan dukungan memory-mapped (mmap).
Sejak penambahan terjemahan multibahasa, konten pencarian telah mencakup banyak bahasa lainnya, dan akhirnya baru-baru ini pencarian untuk bahasa yang berbeda dipisahkan. Masalah utama yang diselesaikan adalah: pencarian dalam bahasa tertentu hanya mengembalikan hasil artikel dalam bahasa tersebut, dengan bahasa yang berbeda menggunakan pemecah kata yang berbeda, misalnya bahasa Tionghoa menggunakan tantivy-jieba
, bahasa Jepang menggunakan lindera
, dan bahasa Inggris serta lainnya menggunakan pemecah kata default. Dengan demikian, masalah pencarian yang buruk akibat campuran multibahasa dan ketidakcocokan pemecah kata telah terselesaikan.
Awalnya saya berencana menggunakan qdrant untuk menerapkan pencarian semantik, tetapi karena embedding dilakukan secara lokal, jika diteruskan kembali ke lokal untuk mengembalikan hasil akan terlalu lambat, waktu inisialisasi juga tidak pasti, dan tingkat keberhasilannya pun tidak terjamin. Namun, mungkin fitur ini bisa ditambahkan di akun resmi WeChat, saya akan coba menyelesaikan pengembangannya dalam dua hari ini.
Tulisan tangan, untuk mengurangi tingkat AI, asalkan bisa dimengerti sudah cukup. Baru-baru ini saya bersiap untuk menghapus semua artikel sebelumnya yang ditulis dengan AI, dan melihat kapan indeks Bing bisa pulih.
I. Membangun Indeks
pub async fn build_search_index() -> anyhow::Result<Index> {
let en_text_options = TextOptions::default()
.set_indexing_options(
TextFieldIndexing::default()
.set_tokenizer("en")
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
)
.set_stored();
let zh_text_options = TextOptions::default()
.set_indexing_options(
TextFieldIndexing::default()
.set_tokenizer("jieba")
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
)
.set_stored();
let ja_text_options = TextOptions::default()
.set_indexing_options(
TextFieldIndexing::default()
.set_tokenizer("lindera")
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
)
.set_stored();
let mut schema_builder = Schema::builder();
let title_en_field = schema_builder.add_text_field("title_en", en_text_options.clone());
let content_en_field = schema_builder.add_text_field("content_en", en_text_options); let title_zh_field = schema_builder.add_text_field("title_zh", zh_text_options.clone());
let content_zh_field = schema_builder.add_text_field("content_zh", zh_text_options);
let title_ja_field = schema_builder.add_text_field("title_ja", ja_text_options.clone());
let content_ja_field = schema_builder.add_text_field("content_ja", ja_text_options);
let schema = schema_builder.build();
let index = Index::create_in_ram(schema);
let en_analyzer = TextAnalyzer::builder(SimpleTokenizer::default())
.filter(LowerCaser)
.filter(Stemmer::new(tantivy::tokenizer::Language::English))
.build();
index.tokenizers().register("en", en_analyzer);
let dictionary = load_embedded_dictionary(lindera::dictionary::DictionaryKind::IPADIC)?;
let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
let lindera_analyzer = TextAnalyzer::from(LinderaTokenizer::from_segmenter(segmenter));
index.tokenizers().register("lindera", lindera_analyzer);
let jieba_analyzer = TextAnalyzer::builder(JiebaTokenizer {})
.filter(RemoveLongFilter::limit(40))
.build();
index.tokenizers().register("jieba", jieba_analyzer);
let mut index_writer = index.writer(50_000_000)?;
let all_articles = artikel Anda.
for article in all_articles {
let mut doc = TantivyDocument::new();
doc.add_text(lang_field, &article.lang);
match article.lang.as_str() {
"zh-CN" | "zh-TW" => {
doc.add_text(title_zh_field, &article.title);
doc.add_text(content_zh_field, &article.md);
}
"ja" => {
doc.add_text(title_ja_field, &article.title);
doc.add_text(content_ja_field, &article.md);
}
_ => {
doc.add_text(title_en_field, &article.title);
doc.add_text(content_en_field, &article.md);
}
}
index_writer.add_document(doc)?;
}
index_writer.commit()?;
index_writer.wait_merging_threads()?;
Ok(index)
}
II. Pencarian Indeks
Bagian ini akan lebih baik jika pertama-tama dicocokkan berdasarkan bahasa, lalu melakukan pencarian pada bidang bahasa yang sesuai, tetapi karena sudah berjalan, saya tidak mengubahnya.
#[server]
pub async fn search_handler(query: SearchQuery) -> Result<String, ServerFnError> {
let index = SEARCH_INDEX_CACHE.get("primary_index").ok_or_else(|| {
ServerFnErrorErr::ServerError("Indeks pencarian tidak ditemukan dalam cache.".to_string())
})?;
let schema = index.schema();
let title_en_f = schema.get_field("title_en").unwrap();
let content_en_f = schema.get_field("content_en").unwrap();
let title_zh_f = schema.get_field("title_zh").unwrap();
let content_zh_f = schema.get_field("content_zh").unwrap();
let title_ja_f = schema.get_field("title_ja").unwrap();
let content_ja_f = schema.get_field("content_ja").unwrap();
let canonical_f = schema.get_field("canonical").unwrap();
let lang_f = schema.get_field("lang").unwrap();
let reader = index.reader()?;
let searcher = reader.searcher();
let mut queries: Vec<(Occur, Box<dyn tantivy::query::Query>)> = Vec::new();
let query_parser = QueryParser::for_index(
&index,
vec![
title_en_f,
content_en_f,
title_zh_f,
content_zh_f,
title_ja_f,
content_ja_f,
],
);
let user_query = query_parser.parse_query(&query.q)?;
queries.push((Occur::Must, user_query));
if let Some(lang_code) = &query.lang {
let lang_term = Term::from_field_text(lang_f, lang_code);
let lang_query = Box::new(TermQuery::new(lang_term, IndexRecordOption::Basic));
queries.push((Occur::Must, lang_query));
}
...
let final_query = BooleanQuery::new(queries);
let hits: Vec<Hit> = match query.sort {
SortStrategy::Relevance => {
let top_docs = TopDocs::with_limit(query.limit);
let search_results: Vec<(Score, DocAddress)> =
searcher.search(&final_query, &top_docs)?;
search_results
.into_iter()
.filter_map(|(score, doc_address)| {
let doc = searcher.doc::<TantivyDocument>(doc_address).ok()?;
let title = doc
.get_first(title_en_f)
.or_else(|| doc.get_first(title_zh_f))
.or_else(|| doc.get_first(title_ja_f))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let formatted_lastmod =
match DateTime::parse_from_rfc3339(doc.get_first(lastmod_str_f)?.as_str()?)
{
Ok(dt) => {
let china_dt = dt.with_timezone(&Shanghai);
china_dt.format("%Y-%m-%d").to_string()
}
Err(_) => doc.get_first(lastmod_str_f)?.as_str()?.to_string(),
};
Some(Hit {
title,
canonical: doc.get_first(canonical_f)?.as_str()?.to_string(),
lastmod: formatted_lastmod,
score,
})
})
.collect()
}
};
serde_json::to_string(&hits).map_err(|e| ServerFnError::ServerError(e.to_string()))
}
III. Catatan Akhir
Efek pencarian tantivy cukup bagus, meskipun belum mendukung pencarian semantik, tetapi kecepatan dan efeknya sangat baik. Banyak fungsi pencarian teks lengkap dari database vektor juga diimplementasikan berdasarkan indeks tantivy.
Untuk metode penggunaan tantivy yang lebih rinci, dapat merujuk ke: tantivy official example, yang mencakup 20 contoh pencarian yang sangat rinci, masing-masing dengan penjelasan terperinci.
Komentar