「Rust初心者がRust製の日本語形態素解析器の開発を引き継いでみた」の続きです。
前回のおさらい
前回の記事では、Lindera(Rust製日本語形態素解析器)のCLIを紹介しました。
% echo "関西国際空港限定トートバッグ" | lindera
関西国際空港 名詞,固有名詞,組織,*,*,*,関西国際空港,カンサイコクサイクウコウ,カンサイコクサイクーコー
限定 名詞,サ変接続,*,*,*,*,限定,ゲンテイ,ゲンテイ
トートバッグ UNK,*,*,*,*,*,*,*,*
EOS
上記のような使い勝手です。テキストを標準入力やファイルから読み込んで形態素解析を行うことまでできます。このCLIを利用して出力フォーマットをJSONなどにすれば、シェルスクリプトである程度のことが可能ですが、Linderaはライブラリターゲットが本来の提供なので、ライブラリとしての使い方を紹介します。
ライブラリとしての使い方
Linderaをライブラリとしてアプリケーションに組み込む例はLinderaのリポジトリに記載しています。
use lindera::tokenizer::Tokenizer;
fn main() -> std::io::Result<()> {
// create tokenizer
let mut tokenizer = Tokenizer::new("normal", "");
// tokenize the text
let tokens = tokenizer.tokenize("関西国際空港限定トートバッグ");
// output the tokens
for token in tokens {
println!("{}", token.text);
}
Ok(())
}
上記のように、ライブラリの使い方としては非常に簡単です。このコードを実行すると、次のように出力されます。
関西国際空港
限定
トートバッグ
Tokenizer::new()
の第一引数でトークナイズモード(normal
またはdecompose
)の変更ができます。
また、第二引数で使用する形態素辞書のパスを指定することができます。上記の例では""
(空文字列)となっていますが、これは内包されているデフォルトのIPADICを使用します。もし別の辞書(UniDic、IPADIC-NEologd、ko-dic)を使用したいのであれば、その辞書が置かれているディレクトリパスを指定してください。
このように、Linderaは簡単にRustアプリケーションに組み込むことができます。
Tantivyから使う
さて、このLinderaですが、元々全文検索サーバを作っていて、その中で日本語の形態素解析に利用したいということから、開発を引き継いでいます。
その全文検索サーバ(Bayard)の中で利用している全文検索ライブラリがTantivyです。
TantivyはGoogle Search SWEのPaul Masurel氏が開発するRust製の全文検索ライブラリです。こちらのベンチマークによると非常に高いパフォーマンスを発揮しています。
https://tantivy-search.github.io/bench/
しかし、残念なことに、日本語を扱おうとすると、LuceneのKuromoji相当の日本語形態素辞書を利用したトークナイザが存在しません。
TinySegmanterのRust移植版tantivy-tokenizer-tiny-segmenterが利用可能だったのですが、Tantivy v0.12.0からTokenizer周りのAPIが大きく変更されたため、最新のTantivyには対応できていません。
ということで、TantivyからLinderaを利用できるように、Tantivy向けのLinderaTokenizerを作りました。リポジトリはこちらです。
https://github.com/lindera-morphology/lindera-tantivy
TantivyからLinderaを利用するためのサンプルコードを掲載します。
use lindera_tantivy::tokenizer::LinderaTokenizer;
use tantivy::schema::{IndexRecordOption, Schema, TextFieldIndexing, TextOptions};
use tantivy::{Index, doc};
use tantivy::query::QueryParser;
use tantivy::collector::TopDocs;
fn main() -> tantivy::Result<()> {
// create schema builder
let mut schema_builder = Schema::builder();
// add id field
let id = schema_builder.add_text_field(
"id",
TextOptions::default()
.set_indexing_options(
TextFieldIndexing::default()
.set_tokenizer("raw")
.set_index_option(IndexRecordOption::Basic),
)
.set_stored(),
);
// add title field
let title = schema_builder.add_text_field(
"title",
TextOptions::default()
.set_indexing_options(
TextFieldIndexing::default()
.set_tokenizer("lang_ja")
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
)
.set_stored(),
);
// add body field
let body = schema_builder.add_text_field(
"body",
TextOptions::default()
.set_indexing_options(
TextFieldIndexing::default()
.set_tokenizer("lang_ja")
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
)
.set_stored(),
);
// build schema
let schema = schema_builder.build();
// create index on memory
let index = Index::create_in_ram(schema.clone());
// register Lindera tokenizer
index
.tokenizers()
.register("lang_ja", LinderaTokenizer::new("decompose", ""));
// create index writer
let mut index_writer = index.writer(50_000_000)?;
// add document
index_writer.add_document(doc!(
id => "1",
title => "成田国際空港",
body => "成田国際空港(なりたこくさいくうこう、英: Narita International Airport)は、千葉県成田市南東部から芝山町北部にかけて建設された日本最大の国際拠点空港である[1]。首都圏東部(東京の東60km)に位置している。空港コードはNRT。"
));
// add document
index_writer.add_document(doc!(
id => "2",
title => "東京国際空港",
body => "東京国際空港(とうきょうこくさいくうこう、英語: Tokyo International Airport)は、東京都大田区にある日本最大の空港。通称は羽田空港(はねだくうこう、英語: Haneda Airport)であり、単に「羽田」と呼ばれる場合もある。空港コードはHND。"
));
// add document
index_writer.add_document(doc!(
id => "3",
title => "関西国際空港",
body => "関西国際空港(かんさいこくさいくうこう、英: Kansai International Airport)は大阪市の南西35㎞に位置する西日本の国際的な玄関口であり、関西三空港の一つとして大阪国際空港(伊丹空港)、神戸空港とともに関西エアポート株式会社によって一体運営が行われている。"
));
// commit
index_writer.commit()?;
// create reader
let reader = index.reader()?;
// create searcher
let searcher = reader.searcher();
// create querhy parser
let query_parser = QueryParser::for_index(&index, vec![title, body]);
// parse query
let query_str = "東京";
let query = query_parser.parse_query(query_str)?;
println!("Query String: {}", query_str);
// search
let top_docs = searcher.search(&query, &TopDocs::with_limit(10))?;
println!("Search Result:");
for (_, doc_address) in top_docs {
let retrieved_doc = searcher.doc(doc_address)?;
println!("{}", schema.to_json(&retrieved_doc));
}
Ok(())
}
ポイントは2箇所あります。
まず1点目ですが、set_tokenizer()
で使用する予定のトークナイザ(アナライザ)名を設定しておき、schemaを作成します。下記抜粋を参照してください。
// add title field
let title = schema_builder.add_text_field(
"title",
TextOptions::default()
.set_indexing_options(
TextFieldIndexing::default()
.set_tokenizer("lang_ja")
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
)
.set_stored(),
);
2点目が、インデックスにregister()
で、上記抜粋で指定したトークナイザ名で、使用するトークナイザのインスタンスを登録します。下記抜粋を参照してください。
// register Lindera tokenizer
index
.tokenizers()
.register("lang_ja", LinderaTokenizer::new("decompose", ""));
このサンプルはリポジトに含まれていませうので、次のように実行できます。
% git clone https://github.com/lindera-morphology/lindera-tantivy.git
% cd lindera-tantivy
% cargo run --example basic_example
サンプルでは、「成田国際空港」、「東京国際空港」、「関西国際空港」に関する3件のドキュメントをインデックスし、「東京」というクエリ文字列で、title
とbody
フィールドを検索します。
実行結果は次のように表示されます。
Query String: 東京
Search Result:
{"body":["東京国際空港(とうきょうこくさいくうこう、英語: Tokyo International Airport)は、東京都大田区にある日本最大の空港。通称は羽田空港(はねだくうこう、英語: Haneda Airport)であり、単に「羽田」と呼ばれる場合もある。空港コードはHND。"],"id":["2"],"title":["東京国際空港"]}
{"body":["成田国際空港(なりたこくさいくうこう、英: Narita International Airport)は、千葉県成田市南東部から芝山町北部にかけて建設された日本最大の国際拠点空港である[1]。首都圏東部(東京の東60km)に位置している。空港コードはNRT。"],"id":["1"],"title":["成田国際空港"]}
いかがでしょうか。形態素解析されたキーワードでドキュメントを検索できていますね。上手くいったようです。
おわりに
私達のような日本語を扱うサービスで全文検索機能を実装しようとすると、どんなに高いパフォーマンスを発揮する全文検索ライブラリだとしても、日本語を上手く扱えないとなかなかサービスに適用するまでには至りませんでした。
しかし、LinderaをTantivyに対応させたことで、Tantivy + Linderaで日本語の全文検索を行うことができるようになります。
日本語を扱う上で、Lucene + Kuromojiほど多機能ではありませんが、選択肢の一つとして候補に上げてもらえたら幸いです。