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