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> {
    // Mengatur pemecah kata secara terpisah untuk setiap bahasa
    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();
    // Pengaturan skema indeks
    let mut schema_builder = Schema::builder();
    // Setiap bidang menggunakan pemecah kata yang sesuai
    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); // Tidak disimpan
    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);
    //... beberapa bidang lainnya
    let schema = schema_builder.build();

    // Membuat indeks di memori
    let index = Index::create_in_ram(schema);

    // Mengatur tokenizer untuk bahasa yang berbeda
    let en_analyzer = TextAnalyzer::builder(SimpleTokenizer::default())
        .filter(LowerCaser)
        .filter(Stemmer::new(tantivy::tokenizer::Language::English))
        .build();
    index.tokenizers().register("en", en_analyzer);
    // Bahasa Jepang
    let dictionary = load_embedded_dictionary(lindera::dictionary::DictionaryKind::IPADIC)?;
    let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
    //let tokenizer = LinderaTokenizer::from_segmenter(segmenter);

    let lindera_analyzer = TextAnalyzer::from(LinderaTokenizer::from_segmenter(segmenter));
    index.tokenizers().register("lindera", lindera_analyzer);
    //Bahasa Tionghoa
    let jieba_analyzer = TextAnalyzer::builder(JiebaTokenizer {})
        .filter(RemoveLongFilter::limit(40))
        .build();
    index.tokenizers().register("jieba", jieba_analyzer);
    // Menulis ke indeks (angka berikutnya adalah batasan memori maksimum)
    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);

        // Di sini pemecah kata diterapkan, bahasa sederhana dan tradisional akan disaring di lang_field.
        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> {
    // Saya menggunakan moka untuk menyimpan di memori, toh hanya sedikit artikel.
    let index = SEARCH_INDEX_CACHE.get("primary_index").ok_or_else(|| {
        ServerFnErrorErr::ServerError("Indeks pencarian tidak ditemukan dalam cache.".to_string())
    })?;
    // get_field
    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();
    // Pencarian menyaring Occur::Must artinya harus muncul, harus memenuhi semua persyaratan dalam queries: Vec
    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,
        ],
    );
    // Pencarian pengguna
    let user_query = query_parser.parse_query(&query.q)?;
    queries.push((Occur::Must, user_query));
    // Menyaring bahasa
    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));
    }
    // Penyaringan lainnya
    ...

    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)?;
            // Memetakan dari Vec<(Score, DocAddress)>
            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()
        }
        // Penyaringan lainnya..., di sini saya terutama mengurutkan berdasarkan urutan waktu
    }; // Tidak tahu apakah kurungnya sejajar

    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.

Artikel yang mungkin menarik bagi Anda

Temukan lebih banyak konten menarik

Komentar