Tantivy विभिन्न भाषाओं के लिए अलग-अलग टोकनाइज़र कैसे लागू करता है?

इस साइट की खोज tantivy और tantivy-jieba का उपयोग करके बनाई गई है। Tantivy Apache Lucene से प्रेरित, Rust में लिखा गया एक उच्च प्रदर्शन वाला पूर्ण-पाठ खोज इंजन लाइब्रेरी है। इसमें BM25 स्कोरिंग, प्राकृतिक भाषा क्वेरी, वाक्यांश खोज, फैसेटेड रिट्रीवल और विभिन्न फ़ील्ड प्रकारों (पाठ, संख्यात्मक, तिथि, IP और JSON सहित) का समर्थन है, और बहुभाषी टोकनाइज़ेशन समर्थन (चीनी, जापानी, कोरियाई सहित) प्रदान करता है। इसकी अत्यधिक तेज़ अनुक्रमण और क्वेरी गति, मिलीसेकंड स्तर का प्रारंभ समय, मेमोरी मैपिंग (mmap) समर्थन है।

बहुभाषी अनुवाद जोड़ने के बाद, खोज सामग्री में कई अन्य भाषाओं की सामग्री मिली हुई थी। हाल ही में अंततः विभिन्न भाषाओं की खोज को अलग कर दिया गया है। मुख्य समाधान यह है कि वर्तमान भाषा की खोज केवल संबंधित भाषा के लेख परिणाम लौटाएगी, और विभिन्न भाषाओं के लिए अलग-अलग टोकनाइज़र का उपयोग किया जाएगा, उदाहरण के लिए चीनी के लिए tantivy-jieba, जापानी के लिए lindera, और अंग्रेजी आदि के लिए डिफ़ॉल्ट टोकनाइज़र। इससे बहुभाषी मिश्रण और टोकनाइज़र के अमिलान के कारण खराब खोज प्रभाव की समस्या हल हो गई है।

मूल रूप से मैं qdrant का उपयोग अर्थपूर्ण खोज के लिए करना चाहता था, लेकिन एम्बेडिंग स्थानीय रूप से की जाती है, और फिर स्थानीय परिणाम वापस भेजने पर बहुत धीमा हो जाता है, प्रारंभीकरण में कितना समय लगेगा यह भी पता नहीं, और सफलता की संभावना भी निश्चित नहीं है, लेकिन शायद इसे वीचैट पब्लिक अकाउंट में जोड़ा जा सकता है, देखते हैं कि क्या मैं इसे इन दिनों पूरा कर पाऊंगा।

हाथ से लिखा गया, AI दर कम करने के लिए, समझ में आ जाना चाहिए। हाल ही में मैं पिछले सभी AI द्वारा लिखे गए लेखों को हटाने की योजना बना रहा हूं, देखते हैं कि बिंग अनुक्रमण कब तक बहाल होता है।

एक, अनुक्रमण निर्माण

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); // Not stored
    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 tokenizer = LinderaTokenizer::from_segmenter(segmenter);

    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 = आपके लेख।

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

        // यहाँ टोकनाइज़र लागू किया जाता है, सरलीकृत और पारंपरिक चीनी 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)
}

दो, अनुक्रमण खोज

इस भाग में यदि पहले भाषा का मिलान करें और फिर संबंधित भाषा फ़ील्ड में खोज करें तो बेहतर होगा, लेकिन चल रहा था तो बदला नहीं।

#[server]
pub async fn search_handler(query: SearchQuery) -> Result<String, ServerFnError> {
    // मैंने moka का उपयोग मेमोरी में रखने के लिए किया है, वैसे भी बस इतने लेख हैं।
    let index = SEARCH_INDEX_CACHE.get("primary_index").ok_or_else(|| {
        ServerFnErrorErr::ServerError("Search index not found in 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();
	//खोज फ़िल्टर Occur::Must अर्थात आवश्यक रूप से दिखाई देना चाहिए, 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,
        ],
    );
	//उपयोगकर्ता की खोज
    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)?;
            // 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()
        }
	//अन्य फ़िल्टर..., मेरे यहाँ मुख्य रूप से समय के अनुसार क्रमबद्ध किया जाता है
    }; // पता नहीं कोष्ठक संरेखण सही है या नहीं

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

तीन, पश्चात्वाक्

tantivy खोज प्रभाव काफी अच्छा है, हालांकि अभी भी अर्थपूर्ण नहीं है, लेकिन गति और प्रभाव दोनों बहुत अच्छे हैं। कई वैक्टर डेटाबेस की पूर्ण-पाठ खोज भी tantivy अनुक्रमण का उपयोग करती है।

tantivy के बारे में अधिक और विस्तृत उपयोग विधियों के लिए देखें: tantivy आधिकारिक उदाहरण, जिसमें 20 बहुत विस्तृत खोज उदाहरण हैं, और प्रत्येक की बहुत विस्तृत व्याख्या है।

आपको जिन लेखों में रुचि हो सकती है

अधिक शानदार सामग्री खोजें

टिप्पणी