Lucene #Kuromoji のコードを読む会 (辞書ビルダー編)

8261d04bf57a042c8eab6757c386f7b2?s=47 Tomoko Uchida
October 03, 2019
1.2k

Lucene #Kuromoji のコードを読む会 (辞書ビルダー編)

8261d04bf57a042c8eab6757c386f7b2?s=128

Tomoko Uchida

October 03, 2019
Tweet

Transcript

  1. Lucene Kuromoji のコードを読む会 (辞書ビルダー編) 2019/10/3 @moco_beta

  2. 自己紹介  打田智子  twitter : @moco_beta  所属 :

    株式会社 LegalForce R&D チーム / ソフトウェアエンジニア  検索システムに興味があります  趣味でOSS開発をしています  Janome https://github.com/mocobeta/janome  Apache Lucene committer ()
  3. 趣旨など 主催者 (@moco_beta) が Lucene / Kuromoji のソースコード(辞書周り)を読むうえで,調 べたことをまとめておきたい 素のままで触る機会は少ないかもしれないけれ

    ど, Lucene のコードを読んでみるのも楽しい よ!というのを伝えたい 仕事ではブラックボックスで Elasticsearch や Solr を 使っているとしても,内部を知っていることがきっと役 に立つはず(?) Lucene, Solr へのコントリビューションに興味がある人には,その入り口が作れれば
  4. アンケート  Lucene の API を使ってアプリを書いたことがある人  Lucene のコードを読んだことがある人 

    Lucene を拡張・カスタマイズしたことがある人  Lucene プロジェクトにパッチを送ったことがある人  検索エンジン and/or 形態素解析器を実装したことがある人(言語問わない) 全部Noでもノープロブレムです!ようこそ
  5. リポジトリのチェックアウト  ビルドには JDK 11 (以上)と Ant が必要です。  まだの人はとりあえず

    clone だけでもどうぞ $ git clone https://github.com/apache/lucene-solr.git $ cd lucene-solr $ ant ivy-bootstrap $ ant idea # IDEA 使いの方 $ ant eclipse # Eclipse 使いの方
  6. 準備 ~コードを読む前に知っておきたいこと~

  7. Kuromoji (Lucene 版)  Apache Lucene に含まれる,Java で書かれた日本語形態 素解析ライブラリ 

    日本で(&世界で)もっとも使われている日本語形態素 解析器のひとつ  Atilika 社の Kuromoji をベースに,Lucene 向けに手を加えた もの  現在,2つの Kuromoji のソースはかなり違うので注意  JAR のありか (Maven central)  https://mvnrepository.com/artifact/org.apache.luc ene/lucene-analyzers-kuromoji/8.2.0  lucene-core と lucene-analyzers-common に依存し ている  形態素辞書(表層形,品詞などの形態素情報+コスト計算用 の言語モデル)と,形態素解析器が 1 つの jar にパッケージ されているのが特徴  配布が楽,辞書の切り替えは大変(要リビルド)
  8.  システム辞書  jar 内包  辞書データ(テキストファイル)をバイナリエンコード -> resource として突っ込む

     デフォルトの内包辞書は MeCab IPADIC  補足:UniDic のビルドは壊れている [LUCENE-4056]  ユーザー辞書  CSV ファイルで与える  ※今回は対象外 Kuromoji 辞書 今日は(ひたすら)この部分について
  9. Kuromoji システム辞書 : 概要  辞書引きは2段階で行われる  入力文字列 => 見出し語ID

    => 単語エントリ
  10. Kuromoji システム辞書 : 概要  大きく分けて4つのファイル群からなる  見出し語(表層形)の索引集  見出し語をインプットにとり,単語エントリへのポインタ(内部ID)をアウトプットする

    連想配列のようなもの  文字列マッチのため FST (Finite State Transducer) が使われている  単語エントリデータ  MeCab IPADIC に含まれる全単語エントリをバイナリエンコードしたもの  各単語には内部IDが付与されている  連接表  ※今回は対象外  未知語辞書  ※今回は対象外
  11. Kuromoji システム辞書 : FST の実装  データ構造:Minimal Acyclic Subsequential Transducer

     名前はごついけどやりたいことは単純  “Minimal” のココロは?  (prefix) Trie 木に似ているが,prefix 側と suffix 側と,両方のステータスを共有する  あるノードに至る閉路が1つではないので「木」ではなくなる  データサイズがコンパクトになる  構築方法(アルゴリズム)は論文参照  Trie 木同様に common prefix match が可能  1回の辞書引きで prefix を共有する単語をすべて見つける  「東海道」を入力とすると,「東」「東海」「東海道」が出力される  辞書引きの total 回数=入力文字列の文字数  「東海道」なら1文字ずつずらして「東海道」「海道」「道」の3回だけ辞書を引けばよい  (細かくいうと,入力の単位は char (2 bytes) なので,もしシステム辞書にサロゲートペア文字が あるとちょっと増える)
  12. Kuromoji システム辞書 : FST の実装 http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.24.3698&rep=rep1&type=pdf key => value(s) ------------------------

    apr => 30 aug => 31 dec => 31 feb => 28, 29 jan => 31 jul => 30
  13. Kuromoji システム辞書 : 単語エントリ  MeCab IPADIC に含まれる単語(形態素)数 mecab-ipadic-2.7.0-20070801 $

    wc -l *.csv 27210 Adj.csv 135 Adnominal.csv ...... 3328 Noun.adjv.csv 795 Noun.adverbal.csv 60477 Noun.csv ...... 208 Symbol.csv 130750 Verb.csv 392126 合計
  14. Kuromoji システム辞書 : 単語エントリ  MeCab IPADIC の各エントリがもつ情報  表層形,左文脈ID,右文脈ID,生起コスト,品詞,活用型,活用形,基本形,読み,発音

    mecab-ipadic-2.7.0-20070801 $ iconv -f eucjp -t utf8 Verb.csv | head 引き込む,762,762,7122,動詞,自立,*,*,五段・マ行,基本形,引き込む,ヒキコム,ヒキコム 引き込ま,764,764,7118,動詞,自立,*,*,五段・マ行,未然形,引き込む,ヒキコマ,ヒキコマ 引き込も,763,763,7122,動詞,自立,*,*,五段・マ行,未然ウ接続,引き込む,ヒキコモ,ヒキコモ 引き込み,767,767,7088,動詞,自立,*,*,五段・マ行,連用形,引き込む,ヒキコミ,ヒキコミ 引き込ん,766,766,7122,動詞,自立,*,*,五段・マ行,連用タ接続,引き込む,ヒキコン,ヒキコン 引き込め,760,760,7122,動詞,自立,*,*,五段・マ行,仮定形,引き込む,ヒキコメ,ヒキコメ ….
  15. Kuromoji システム辞書 : 連接表  品詞同士のつながりやすさを表すマトリックス  1316 * 1316

    = 1731856 (※ 1316 は文脈IDの種類数) 0 0 -434 0 1 1 0 2 -1630 .... 759 1147 -3384 759 1148 -3320 759 1149 -3384 .... 1315 1313 -4369 1315 1314 -1712 1315 1315 -129
  16. Kuromoji システム辞書 : エンコード済みバイナリ  Lucene のソースにはエンコード済みバイナリデータが(リソースとして)含まれている  https://github.com/apache/lucene- solr/tree/master/lucene/analysis/kuromoji/src/resources/org/apache/lucene/analysis/ja/

    dict lucene-solr $ ls -1 lucene/analysis/kuromoji/src/resources/org/apache/lucene/analysis/ja/dict/ CharacterDefinition.dat ConnectionCosts.dat 'TokenInfoDictionary$buffer.dat’ 'TokenInfoDictionary$fst.dat’ 'TokenInfoDictionary$posDict.dat’ 'TokenInfoDictionary$targetMap.dat’ 'UnknownDictionary$buffer.dat’ 'UnknownDictionary$posDict.dat’ 'UnknownDictionary$targetMap.dat'
  17. Kuromoji システム辞書 : エンコード済みバイナリ 元ファイル 合計サイズ エンコード後のデータファイル 合計サイズ 単語エントリ *.csv

    31MB 見出し語索引 & 単語エントリ TokenInfoDictionary$*.dat ??? 連接表 matrix.def 22MB 連接表 ConnectionCosts.dat ??? エンコード後のデータはどのくらい圧縮されてるの?
  18. Kuromoji システム辞書 : エンコード済みバイナリ 元ファイル 合計サイズ エンコード後のデータファイル 合計サイズ 単語エントリ *.csv

    31MB 見出し語索引 & 単語エントリ TokenInfoDictionary$*.dat 6.2MB 連接表 matrix.def 22MB 連接表 ConnectionCosts.dat 2.6MB エンコード後のデータはどのくらい圧縮されてるの? 圧縮率 20% ! それでは実装をみていきましょう ╭( ・ㅂ・)و ̑
  19. システム辞書のエンコーディング ~Deep dive into コード~

  20. 以下スライドの内容は 2019/9 時点の master ブランチをベースにしています

  21. エントリポイント - TokenInfoDictionaryBuilder  o.a.l.a.ja.util.TokenInfoDictionaryBuilder#build()  https://github.com/apache/lucene- solr/blob/master/lucene/analysis/kuromoji/src/java/org/apache/lucene/an alysis/ja/util/TokenInfoDictionaryBuilder.java 

    実体は,メソッド内で呼ばれている buildDictionary()  まずはメソッド全体をざっと眺めてみる  準備1 (69~94行) : 単語エントリ (CSV ファイル) を1行ずつ全部メモリに読み込む  準備2 (97行) : 見出し語でソート (FST への入力がソート済みでないといけないため)  メイン処理 (99~129行) : 見出し語索引 (FST) と単語エントリデータを構築
  22. 見出し語索引 - fst.Builder & fst.FST  o.a.l.util.fst.Builder : FST のビルダー

     o.a.l.util.fst.FST : FST をメモリ効率の良いバイト配列にシリアライズするクラス  オートマトンの入力と出力の型を押さえよう // TokenInfoDictionaryBuidler.java 99行~ // 出力タイプ は unsigned long PositiveIntOutputs fstOutput = PositiveIntOutputs.getSingleton(); // ビルダー初期化 Builder<Long> fstBuilder = new Builder<>(FST.INPUT_TYPE.BYTE2, // 入力タイプ 0, 0, true, true, Integer.MAX_VALUE, fstOutput, true, 15);
  23. 見出し語索引 - fst.Builder & fst.FST  Builder.add(input, output) を呼ぶと,input -

    output ペアが FST (オートマトン) に追加される // TokenInfoDictionaryBuilder.java 113行~ String token = entry[0]; if (!token.equals(lastValue)) { // new word to add to fst ord++; // 見出し語ID lastValue = token; scratch.grow(token.length()); scratch.setLength(token.length()); // 文字列 => int配列に変換 for (int i = 0; i < token.length(); i++) { scratch.setIntAt(i, (int) token.charAt(i)); } fstBuilder.add(scratch.get(), ord); }
  24. 見出し語索引 - fst.Builder & fst.FST  ホームワーク?:グラフ(オートマトン)構築の詳細は,できればご自身で追っ てみてください  ここでは,構築済みグラフをバイナリデータ(バイト配列)にシリアライズして

    いる箇所について少し詳しく見ていきます  FST#addNode() メソッド  グラフの Arc (弧) をシリアライズしているのがおわかりいただけ…  …たらたぶんすごい…… 難読レベル高し…
  25. 見出し語索引 - fst.Builder & fst.FST  FST#addNode() を紐解く  Lucene

    の FST では,有向グラフの Arc (弧) をシリアライズする  Arc は次の情報を含む  フラグ(後述),ラベル,出力,ターゲット(遷移先ノード)アドレス  シリアライズフォーマット  ※ 実装の都合上,実際は reverse されたバイト配列がシリアライズされる
  26. 見出し語索引 - Arc のシリアライズ (1) フラグ  Arc の種類に応じたフラグを立て,バイト配列に書き込む 

    FST.java 596~629 行  フラグ (1 byte) の各 bit の意味 立てる bit 意味 0000 0001 遷移の終端 (final Arc) 0000 0010 遷移元ノードから出る最後の Arc 0000 0100 遷移先がバイト配列上で隣接している 0000 1000 遷移先ノードは Arc をもつ 0001 0000 遷移がアウトプットをもつ 0010 0000 (final Arc の場合) 終端アウトプットがある 0100 0000 missing arc (詳細不明) 1000 0000 未使用
  27. 見出し語索引 - Arc のシリアライズ (2) ラベル  FST#writeLabel()  ここでは,short

    としてラベルが書き出される  short と char は,Java だとどちらも 16 bits (2 bytes) なことに注意 // FST.java 529行~ private void writeLabel(DataOutput out, int v) throws IOException { assert v >= 0: "v=" + v; if (inputType == INPUT_TYPE.BYTE1) { assert v <= 255: "v=" + v; out.writeByte((byte) v); } else if (inputType == INPUT_TYPE.BYTE2) { assert v <= 65535: "v=" + v; out.writeShort((short) v); } else { out.writeVInt(v); } }
  28. 見出し語索引 - Arc のシリアライズ (3) アウトプット  (オプション) 遷移にアウトプット (long

    値) がある場合,書き出す // FST.java 634行~ if (arc.output != NO_OUTPUT) { outputs.write(arc.output, builder.bytes); } if (arc.nextFinalOutput != NO_OUTPUT) { outputs.writeFinalOutput(arc.nextFinalOutput, builder.bytes); }
  29. 見出し語索引 - Arc のシリアライズ (3) アウトプット  8 bytes (64

    bits) 消費するのは無駄なので,可変長のバイト列に変換  小さな値は少ないバイト数で済むようにエンコードされる  実装方針は o.a.l.store.DataOutput#writeVInt() の Javadoc に詳しい  https://lucene.apache.org/core/8_2_0/core/org/apache/lucene/store/Da taOutput.html#writeVInt-int-
  30. 見出し語索引 - Arc のシリアライズ (3) アウトプット  実装はたったの5行!  が,パット見何をやっているのかまったくわからない

     わかる人は休憩していてください ☕ // DataOutput.java 231行~ private void writeSignedVLong(long i) throws IOException { while ((i & ~0x7FL) != 0L) { writeByte((byte)((i & 0x7FL) | 0x80L)); i >>>= 7; } writeByte((byte)i); }
  31. 見出し語索引 - Arc のシリアライズ (3) アウトプット  (例) 12538(10) =

    213 + 212 + 27 + 26 + 25 + 24 + 23 + 21  long で表現すると… 上位がほとんどゼロ  そのまま格納すると無駄が多い 00000000 …. 00000000 00110000 11111010 6 bytes
  32. 見出し語索引 - Arc のシリアライズ (3) アウトプット  while ループ 1

    回め 00000000 …. 00000000 00110000 11111010 00000000 …. 00000000 00000000 01111111 00000000 …. 00000000 00000000 10000000 0x7F i 0x80 & (論理積) | (論理和) 11111010 バイト配列 00000000 …. 00000000 00000000 01100001 i >>>= 7 下位7ビットを取り出して 8ビットめに1をセットして 書き出し済みの7ビットを捨てる 配列に書き出す
  33. 見出し語索引 - Arc のシリアライズ (3) アウトプット  while ループ 2

    回め i 11111010 01100001 バイト配列 00000000 …. 00000000 00000000 01100001 ---- i & ~0x7F == 0 なので,ここでループを抜ける ---- ここで打ち止め 続きあり 64 bits => 16 bits になった 残った下位 1 byteを配列に書き出す
  34. 見出し語索引 - Arc のシリアライズ (3) アウトプット  コード再掲  汎用的に使える整数値の圧縮テクニック

     イディオムとして知っておくと良いかも // DataOutput.java 231行~ private void writeSignedVLong(long i) throws IOException { while ((i & ~0x7FL) != 0L) { writeByte((byte)((i & 0x7FL) | 0x80L)); i >>>= 7; } writeByte((byte)i); }
  35. 見出し語索引 - Arc のシリアライズ (4) ターゲット  (オプション) 遷移先(ターゲット)がバイト配列上隣接していない場合のみ,遷 移先のアドレス(=バイト配列上のオフセット)を書き出す

     遷移先が隣接していれば,次のバイトを読むだけなのでアドレスを保存する必要なし  アドレスは long 値なので,アウトプットと同じ圧縮が使える // FST.java 644行~ if (targetHasArcs && (flags & BIT_TARGET_NEXT) == 0) { assert target.node > 0; builder.bytes.writeVLong(target.node); }
  36. 見出し語索引 - エンコード後のサイズ  (1) ~ (4) をすべての Arc について行い,最後にヘッダー等を追加してから,でき

    あがったバイト配列をファイルに書き出す  書き出し先ファイル: TokenInfoDictionary$fst.dat  ファイルサイズ:1.7MB
  37. ☕☕☕☕ ご質問あればどうぞ!

  38. 単語エントリ  エントリポイント (TokenInfoDictionaryBuilder) に戻ってもう一度 buildDictionary() メソッドを思い出そう // TokenInfoDictionaryBuilder.java 106行~

    for (String[] entry : lines) { int next = dictionary.put(entry); if(next == offset){ throw new IllegalStateException("Failed to process line: " + Arrays.toString(entry)); } /* FST 構築&シリアライズ (略) */ dictionary.addMapping((int) ord, offset); offset = next; }
  39. 単語エントリ - BinaryDictionaryWriter  o.a.l.a.ja.util.BinaryDictionaryWriter : 単語エントリをエンコーディングするクラ ス  大事なのは以下2つのメソッド

     BinaryDictionaryWriter#put() : 単語エントリを圧縮&シリアライズする  BinaryDictionaryWriter#addMapping() : 見出し語IDと単語エントリのオフセットの マッピング(ややこしいが,同じ見出し語をもつ複数の単語が存在するため)
  40. フォーマットのおさらい  MeCab IPADIC の各エントリがもつ情報(再掲)  表層形,左文脈ID,右文脈ID,生起コスト,品詞,活用型,活用形,基本形,読み,発音 mecab-ipadic-2.7.0-20070801 $ iconv

    -f eucjp -t utf8 Verb.csv | head 引き込む,762,762,7122,動詞,自立,*,*,五段・マ行,基本形,引き込む,ヒキコム,ヒキコム 引き込ま,764,764,7118,動詞,自立,*,*,五段・マ行,未然形,引き込む,ヒキコマ,ヒキコマ 引き込も,763,763,7122,動詞,自立,*,*,五段・マ行,未然ウ接続,引き込む,ヒキコモ,ヒキコモ 引き込み,767,767,7088,動詞,自立,*,*,五段・マ行,連用形,引き込む,ヒキコミ,ヒキコミ 引き込ん,766,766,7122,動詞,自立,*,*,五段・マ行,連用タ接続,引き込む,ヒキコン,ヒキコン 引き込め,760,760,7122,動詞,自立,*,*,五段・マ行,仮定形,引き込む,ヒキコメ,ヒキコメ ….
  41. 単語エントリ - BinaryDictionaryWriter カラム 内容 entry[0] 表層形(見出し語) entry[1] 左文脈ID entry[2]

    右文脈ID entry[3] 生起コスト entry[4]~entry[7] 品詞 entry[8] 活用型 entry[9] 活用形 entry[10] 基本形 entry[11] 読み entry[12] 発音  put() メソッドの引数 entry のカラムと内容の対応表  コードを追う時,手元に置いて適宜参照すると良い
  42. 単語エントリ - BinaryDictionaryWriter  シリアライズフォーマット  品詞/活用型/活用形は別途管理する(文脈IDから一意に決まるため) 【以下の前提に注意】 ・左文脈IDと右文脈IDはつねに等しい ・左文脈ID

    < 213 = 8192 ・基本形の長さ < 24 = 16 (後述)
  43. 単語エントリ - シリアライズ (1) フラグ  オプショナルなフィールドについて,保存するか省略するかのフラグ  BinaryDictionaryWriter.java 106~118

    行  フラグ (3 bits) の各 bit の意味 立てる bit 意味 001 基本形を省略せずに保存する “*” または,表層形と同じなら省略 010 読みを省略せずに保存する 表層形と同じなら省略 100 発音を省略せずに保存する 読みと同じなら省略
  44. 単語エントリ - シリアライズ (2) 文脈ID  3 bits のフラグと合わせて,左文脈IDのみ short

    (2bytes) として書き出す  左文脈IDと右文脈IDが異なると制約違反  16 – 3 = 13 bits で表現できる範囲を超える左文脈IDは制約違反 // BinaryDictionaryWriter.java 139行 buffer.putShort((short)(leftId << 3 | flags)); // BinaryDictionaryWriter.java 120行~ if (leftId != rightId) { throw new IllegalArgumentException("rightId != leftId: " + rightId + " " +leftId); } if (leftId >= ID_LIMIT) { // ID_LIMIT = 8192 throw new IllegalArgumentException("leftId >= " + ID_LIMIT + ": " + leftId); }
  45. 単語エントリ - シリアライズ (3) 生起コスト  short (2 bytes) としてそのまま書き出す

    // BinaryDictionaryWriter.java 140行 buffer.putShort(wordCost);
  46. 単語エントリ - シリアライズ (4) 基本形  (オプション) 基本形を省略しない場合のみ,長さと文字列を書き出す  圧縮の工夫:表層形と共有する接頭辞部分は削り,接尾辞のみ保存する。

     (例)表層形が「立ちどまら」で基本形が「立ちどまる」の場合,接頭辞の「立ちど ま」を共有するので,body としては「る」だけシリアライズすればよい。デコード時 は,表層形と(共有)接頭辞長からもとの基本形を復元する。 表層形 基本形 共有接頭辞長 接尾辞長 body 立ちどまら 立ちどまる 4 1 る シリアライズする情報 辞書(CSV)データ
  47. 単語エントリ - シリアライズ (4) 基本形  接頭辞と接尾辞の長さは 4 bits ずつなので,基本形の長さが

    24 = 16 文字を超え ると制約違反(厳しい…) // BinaryDictionaryWriter.java 142行~ if ((flags & BinaryDictionary.HAS_BASEFORM) != 0) { if (baseForm.length() >= 16) { throw new IllegalArgumentException( "Length of base form " + baseForm + " is >= 16"); } // (共有)接頭辞と接尾辞の長さ int shared = sharedPrefix(entry[0], baseForm); int suffix = baseForm.length() - shared; buffer.put((byte) (shared << 4 | suffix)); for (int i = shared; i < baseForm.length(); i++) { buffer.putChar(baseForm.charAt(i)); } }
  48. 単語エントリ - シリアライズ (5) 読み  (オプション) 読みを省略しない場合のみ,長さと文字列を書き出す  圧縮の工夫:カタカナの扱いに注目

    // BinaryDictionaryWriter.java 154行~ if ((flags & BinaryDictionary.HAS_READING) != 0) { if (isKatakana(reading)) { buffer.put((byte) (reading.length() << 1 | 1)); writeKatakana(reading); } else { buffer.put((byte) (reading.length() << 1)); for (int i = 0; i < reading.length(); i++) { buffer.putChar(reading.charAt(i)); } } }
  49. 単語エントリ - カタカナのエンコード(黒魔術)  カタカナの Unicode コードポイント(U+30A0 ~ U+30FF) https://0g0.org/category/30A0-30FF/1/

  50. 単語エントリ - カタカナのエンコード(黒魔術)  isKatakana() : すべてカタカナからなる文字列かどうかの判定メソッド // BinaryDictionaryWriter.java 185行~

    private boolean isKatakana(String s) { for (int i = 0; i < s.length(); i++) { char ch = s.charAt(i); if (ch < 0x30A0 || ch > 0x30FF) { return false; } } return true; }
  51. 単語エントリ - カタカナのエンコード(黒魔術)  writeKatakana() : すべてカタカナとわかっていれば,1文字あたり 1byte でシリ アライズできる

     0x30A0 を引くと,0x00 ~ 0x5F ( 0(10) ~ 95(10) ) の範囲で収まる // BinaryDictionaryWriter.java 195行~ private void writeKatakana(String s) { for (int i = 0; i < s.length(); i++) { buffer.put((byte) (s.charAt(i) - 0x30A0)); } }
  52. 単語エントリ - シリアライズ (6) 発音  (オプション) 発音を省略しない場合のみ,長さと文字列を書き出す  圧縮の工夫:カタカナの扱いは読みと同じ

    // BinaryDictionaryWriter.java 166行~ if ((flags & BinaryDictionary.HAS_PRONUNCIATION) != 0) { if (isKatakana(pronunciation)) { buffer.put((byte) (pronunciation.length() << 1 | 1)); writeKatakana(pronunciation); } else { buffer.put((byte) (pronunciation.length() << 1)); for (int i = 0; i < pronunciation.length(); i++) { buffer.putChar(pronunciation.charAt(i)); } } }
  53. 単語エントリ - エンコード後のサイズ  (1) ~ (6) をすべての単語エントリについて行い,最後にヘッダー等を追加してか ら,できあがったバイト配列をファイルに書き出す 

    書き出し先ファイル: TokenInfoDictionary$buffer.dat  ファイルサイズ:4.2MB (awesome!)
  54. 単語エントリ - 見出し語IDと単語エントリのマッピング  見出し語IDは単語(形態素)と1対1ではない  同じ見出し語(表層形)でも,品詞や読み方が異なる単語はたくさんある  例:「小谷」という表層形をもつ単語は MeCab

    IPADIC に 15 個ある  見出し語ID (FST の出力)と,単語エントリの出現位置(バイト配列中のオフ セット)をマッピングする仕組みが必要  BinaryDictionaryWriter#addMapping()  https://github.com/apache/lucene- solr/blob/master/lucene/analysis/kuromoji/src/java/org/apache/lucene/analysis /ja/util/BinaryDictionaryWriter.java#L222  書き出し先ファイル:TokenInfoDictionary$targetMap.dat
  55. 単語エントリ - 見出し語IDと単語エントリ のマッピング  addMapping() メソッドを追 うのはとてもつらい ので, お気持ちを絵にしました

     見出し語IDから,その見出し 語(表層形)をもつ単語が出 てくる最初のオフセット位置 を返すマップが必要  表層形が同じ単語は必ず連続 している(入力が表層形で ソートされていたことに注 意)ので,あとはバイト配列 をシーケンシャルスキャンす れば良い
  56. 単語エントリ - 品詞・活用型・活用形のエンコード  文脈IDごとに品詞・活用型・活用形が一意に決まる。単語エントリ本体のデータ とは別で保存しておき,デコード時に突き合わせる  BinaryDictionaryWriter.java の 64~89行,132~137行と,writetPosDict()

    メソッ ドを見ればだいたいわかるので,興味のある方はあとで見てみてください(雑)  書き出し先ファイル: TokenInfoDictionary$posDict.dat
  57. システム辞書のデコーディング ~エンコーディングの逆演算~

  58. 見出し語索引 - Arc のデシリアライズ  o.a.l.util.fst.FST#readNextRealArc(), readArc() メソッドとシリアライズフォー マット(再掲)を突き合わせて追いかけてみよう 

    https://github.com/apache/lucene- solr/blob/92d4e712d5d50d745c5a6c10dacda66198974116/lucene/core/src/java /org/apache/lucene/util/fst/FST.java#L1027
  59. 見出し語索引 - Arc のデシリアライズ  アウトプットとターゲット (variable-length Long) の読み出しロジック //

    DataInput.java 172行~ private long readVLong(boolean allowNegative) throws IOException { /* This is the original code of this method, * but a Hotspot bug (see LUCENE-2975) corrupts the for-loop if * readByte() is inlined. So the loop was unwinded! byte b = readByte(); long i = b & 0x7F; for (int shift = 7; (b & 0x80) != 0; shift += 7) { b = readByte(); i |= (b & 0x7FL) << shift; } return i; */ byte b = readByte(); if (b >= 0) return b; long i = b & 0x7FL; b = readByte(); i |= (b & 0x7FL) << 7; if (b >= 0) return i; b = readByte(); i |= (b & 0x7FL) << 14; …… }
  60. 単語エントリ - デシリアライズ  o.a.l.a.ja.dict.BinaryDictionary とシリアライズフォーマット(再掲)を突き合 わせてみよう

  61. 単語エントリ - デシリアライズ  BinaryDictionary のデシリアライズメソッド 読み出すデータ メソッド フラグ hasBaseFormData()

    hasReadingData() hasPronunciationData() 文脈ID getLeftId(), getRightId() 生起コスト getWordCost() 基本形 getBaseForm() 読み getReading() 発音 getPronunciation() 品詞/活用型/活用形 getPartOfSpeech() getInflectionType() getInflectionForm() エンコーディングで何をしていたか 思い出すと追えるはず!
  62. おまけ:開発は続く  システム辞書データと形態素解析器を切り離したいという案件  [LUCENE-8816]  mecab ipadic もそろそろ古くなってきた. unidic

    もサポートしたいし, neologd や 自前で再学習した辞書に,リビルドせずに簡単にスイッチしたいよね(?).  理屈上はできるはず。。。テクニカルな課題は多い  要望・フィードバックあれば教えてください
  63. ☕☕☕☕ お疲れさまでした! 次回・・・?