Paano gumagamit ang Tantivy ng iba't ibang tokenizer para sa iba't ibang wika?

Ang paghahanap sa site na ito ay ginawa gamit ang tantivy at tantivy-jieba. Ang Tantivy ay isang mataas na pagganap na full-text search engine library na isinulat sa Rust, na hinango ang inspirasyon mula sa Apache Lucene. Sumusuporta ito sa BM25 scoring, natural na wika na query, phrase search, faceted retrieval, at iba't ibang uri ng field (kabilang ang text, numeric, date, IP, at JSON), pati na rin ang suporta sa multi-language tokenization (kasama ang Chinese, Japanese, at Korean). Mayroon itong napakabilis na indexing at query speed, millisecond-level na oras ng pagsisimula, at suporta sa memory mapping (mmap).

Mula nang idagdag ang multi-language translation, ang mga nilalaman ng paghahanap ay kasama na ang maraming iba pang wika. Kamakailan lamang, nagawa kong ihiwalay ang paghahanap para sa bawat wika. Ang pangunahing solusyon ay: ang paghahanap sa kasalukuyang wika ay nagbabalik lamang ng mga artikulo sa katugmang wika, at gumagamit ng iba't ibang tokenizer para sa bawat wika—halimbawa, gumagamit ng tantivy-jieba para sa Intsik, lindera para sa Hapones, at default tokenizer para sa Ingles at iba pa. Sa ganitong paraan, nalutas ang problema ng masamang resulta ng paghahanap dahil sa paghahalo ng mga wika at hindi tugma na tokenizer.

Una kong iniisip na gamitin ang qdrant para sa semantic search, pero dahil ang embedding ay ginagawa locally, kung ipapasa ko pa ito sa lokal para bumalik ng resulta ay magiging masyadong mabagal, at hindi ko alam kung gaano katagal ang initialization o kung ano ang success rate. Pero posibleng idagdag ko ito sa WeChat public account; tingnan ko kung matatapos ko ito sa loob ng dalawang araw.

Ginawa ko ito nang manu-mano, para bawasan ang AI-generated content rate. Sapat na kung nauunawaan mo. Inihanda kong tanggalin ang lahat ng artikulong isinulat dati ng AI, at tingnan kung kailan ma-recover ang Bing indexing.

Isa: Pagbuo ng Index

pub async fn build_search_index() -> anyhow::Result<Index> {
	// Magtakda ng hiwalay na tokenizer para sa bawat wika
    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();
	// Pagtatakda ng schema ng index
    let mut schema_builder = Schema::builder();
    // Ilapat ang katumbas na tokenizer sa bawat field
    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); // Hindi naka-store
    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);
	//... iba pang field
    let schema = schema_builder.build();

    // Lumikha ng index sa memorya
    let index = Index::create_in_ram(schema);

    // Magtakda ng tokenizer para sa iba't ibang wika
    let en_analyzer = TextAnalyzer::builder(SimpleTokenizer::default())
        .filter(LowerCaser)
        .filter(Stemmer::new(tantivy::tokenizer::Language::English))
        .build();
    index.tokenizers().register("en", en_analyzer);
    // Hapones
    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);
	//Intsik
    let jieba_analyzer = TextAnalyzer::builder(JiebaTokenizer {})
        .filter(RemoveLongFilter::limit(40))
        .build();
    index.tokenizers().register("jieba", jieba_analyzer);
    // Isulat ang index (ang numero ay limitasyon ng memorya)
    let mut index_writer = index.writer(50_000_000)?;

    let all_articles = ang iyong mga artikulo.

    for article in all_articles {
        let mut doc = TantivyDocument::new();
        doc.add_text(lang_field, &article.lang);

        // Dito ilalapat ang tokenizer, ang Simplified at Traditional ay mai-filter sa pamamagitan ng 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)
}

Dalawa: Paghahanap sa Index

Dapat sana ay mas mainam kung gagamitin ang match sa wika muna bago maghanap sa katugmang field, pero dahil tumatakbo na, hindi ko na binago.

#[server]
pub async fn search_handler(query: SearchQuery) -> Result<String, ServerFnError> {
    // Ginamit ko ang moka para i-cache sa memorya, dahil limitado lang ang bilang ng artikulo.
    let index = SEARCH_INDEX_CACHE.get("primary_index").ok_or_else(|| {
        ServerFnErrorErr::ServerError("Hindi natagpuan ang search index sa 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();
	// Filter sa paghahanap: Occur::Must – dapat lumabas, kailangang matugunan ang lahat ng hiling sa 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,
        ],
    );
	// Paghahanap ng user
    let user_query = query_parser.parse_query(&query.q)?;
    queries.push((Occur::Must, user_query));
	// Filter sa wika
    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));
    }
	// Iba pang filter
	...

    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)?;
            // I-map mula sa 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()
        }
	// Iba pang filter... dito ayon ako sa petsa ang pag-uuri
    }; // Hindi sigurado kung tama ang alignment ng bracket

    serde_json::to_string(&hits).map_err(|e| ServerFnError::ServerError(e.to_string()))
}

Tatlo: Pangwakas

Maganda pa rin ang resulta ng paghahanap sa Tantivy, bagaman hindi pa ito semantic, pero ang bilis at epekto ay mahusay. Marami ring vector database na gumagamit ng Tantivy index para sa full-text search.

Para sa mas marami at detalyadong impormasyon tungkol sa Tantivy, bisitahin ang: tantivy official example, kung saan mayroong 20 napakadetalyadong halimbawa ng paghahanap, na bawat isa ay may malinaw na paliwanag.

Mga artikulong maaaring interesado ka

Makita ang higit pang kamangha-manghang nilalaman

Komento