辞書アプリの改良:高速化(2)
対策1:存在しない文字列は検索しない
辞書アプリの改良:高速化(1) - Random Noteの測定結果を見て分かるのは、検索結果が0件の場合でも常に検索キーを2文字ずつに切り出した言葉の欠片(検索片と呼ぶことにする)の数分、必ずメタインデックスを検索している。
しかし、実際には検索キーの一部だけで既に対象が存在しないことが分かる場合があり、その場合それ以後の検索は全て無駄となる。例えば検索キー「ああああああああああ」を見ると、「あああ」の時点で既に検索結果が0になる。つまり実際にメタインデックスを検索するのは最初の「ああ」と次の「ああ」だけでよい。
逆に検索結果が0になった時点で検索を終了しないと、検索キーが長くなればなるほど実行時間が長くなってしまう。
検索結果が0になった時点で終了するためには、各ロジックの実行順序を変えればよい。
これまでは
- メタインデックス検索
- 全ての検索片についてメタインデックスを検索
- 弱い論理積
- 上の結果取得した各メタインデックス集合の弱い論理積をとる
- 弱い=要素の同一判定に辞書インデックスのoffsetだけを利用している
- 文字位置チェック
- 上の結果取得したメタインデックス集合の文字位置をチェック
だったのを
- メタインデックス検索
- 1回目は1つめのみ、次回は以降は前回の次の検索片についてのみ
- 弱い論理積
- 1回目は上記結果のメタインデックス集合をそのまま保存
- 2回目以降はこれまでの結果できたメタインデックス集合と、上記結果のメタインデックス集合で弱い論理積
- 文字位置チェック
- 上記で追加されたメタインデックスについてのみ文字位置チェック
- 検索結果数確認
- 検索結果が0件か残りの検索片がなければ終了
- そうでなければ最初に戻る
とすればよい。
ソース
修正したソースは下記の通り。ちなみにDictionary, MetaDictionary, BaseDictionary, Timerもそれぞれファイルを分割した。
変更のないものは省略。
Dictionary.groovy
class Dictionary extends BaseDictionary { def meta def file def dictFile def Dictionary(String fileName) { meta = new MetaDictionary(fileName) file = new File(fileName + ".idx") dictFile = new File(fileName + ".dict") } def search(key) { def metaSearchTimer = new Timer("meta search[${file.name}]") def intersectionTimer = new Timer("weak intersection[${file.name}]") def filterTimer = new Timer("filter[${file.name}]") def keyFragments = cut(key) def metaIndexList = null def baseMap = [:] for (fragment in keyFragments) { metaSearchTimer.start() def newList = meta.search(fragment.word) if (newList.empty) { metaSearchTimer.stop() intersectionTimer.stop() filterTimer.stop() return [] } metaSearchTimer.pause() // 弱い論理積を取る intersectionTimer.start() if (metaIndexList == null) { metaIndexList = newList newList.each {baseMap.get(it.offset, []) << it} //検索文字の最初の2文字を基準にする } else { def mapA = [:] def mapB = [:] newList.each {metaIndex -> mapA.get(metaIndex.offset, []) << metaIndex} metaIndexList.each {metaIndex -> if (mapA.containsKey(metaIndex.offset)) mapB.get(metaIndex.offset, []) << metaIndex } metaIndexList = [] newList = [] mapB.each {mapKey, value -> newList.addAll(mapA.get(mapKey)) metaIndexList.addAll(value) } } intersectionTimer.pause() // 文字の出現位置をチェック filterTimer.start() def map = [:] newList.each { map.get(it.offset, []) << it } def resultMap = [:] map.keySet().each {offsetKey -> def tmpList = map.get(offsetKey) def baseList = baseMap.get(offsetKey) // baseMap.put(offsetKey, []) -- バグ修正用 baseList.each {base -> def flag = tmpList.any { fragment.word == it.word && (it.wordOffset - base.wordOffset) == fragment.wordOffset } if (flag) resultMap.get(base, base) // if (flag) baseMap.get(offsetKey, base) -- バグ修正用 } } filterTimer.pause() metaIndexList = resultMap.keySet() println "tmp count: ${metaIndexList.size()}" if (metaIndexList != null && metaIndexList.empty) { metaSearchTimer.stop() intersectionTimer.stop() filterTimer.stop() return [] } } metaSearchTimer.stop() intersectionTimer.stop() filterTimer.stop() def resultList = metaIndexList.collect {getIndex(it.offset, it.length)} resultList.sort {l, r-> l.word <=> r.word} } def append(Map map, MetaIndex idx) { if (map.containsKey(idx.offset)) { map.get(idx.offset) << idx } else { map.put(idx.offset, [idx]) } } def cut(string) { if (string.length() < MetaIndex.KEY_CHAR_LENGTH) return [new MetaIndex(string, 0, 0, 0)] def list = [] int i=0 while (i <string.length()-MetaIndex.KEY_CHAR_LENGTH +1) { list << new MetaIndex(string.substring(i, i+MetaIndex.KEY_CHAR_LENGTH), 0, 0, i) i++ } return list } def getIndex(int offset, int length) { return new Index(read(file, offset, length), 0, length) } def getDefinition(index) { return new String(read(dictFile, index.offset, index.length), "UTF-8") } }
Timer.groovy
class Timer { def name def total = 0 def time = new Date() def Timer(n) {name = n} def start() { time = new Date() } def pause() { total += (new Date().getTime() - time.getTime()) time = null } def stop() { if (time != null) total += (new Date().getTime() - time.getTime()) println "TIME[${name}]:${total}" } }
結果
ああああああああああ
ロジック | 今回 | 前回 |
meta search(日英) | 287 | 490 |
weak intersection(日英) | 113 | 108 |
filter(日英) | 81 | 106 |
meta search(日タイ) | 213 | 577 |
weak intersection(日タイ) | 12 | 91 |
filter(日タイ) | 46 | 290 |
All | 891 | 2185 |
い
ロジック | 今回 | 前回 |
meta search(日英) | 217 | 301 |
weak intersection(日英) | 28 | 52 |
filter(日英) | 90 | 104 |
meta search(日タイ) | 63 | 93 |
weak intersection(日タイ) | 1 | 0 |
filter(日タイ) | 4 | 24 |
All | 697 | 882 |
おた
ロジック | 今回 | 前回 |
meta search(日英) | 214 | 86 |
weak intersection(日英) | 28 | 51 |
filter(日英) | 83 | 13 |
meta search(日タイ) | 57 | 101 |
weak intersection(日タイ) | 2 | 0 |
filter(日タイ) | 10 | 121 |
All | 661 | 871 |
あいし
ロジック | 今回 | 前回 |
meta search(日英) | 273 | 351 |
weak intersection(日英) | 73 | 143 |
filter(日英) | 93 | 78 |
meta search(日タイ) | 658 | 761 |
weak intersection(日タイ) | 35 | 25 |
filter(日タイ) | 104 | 73 |
All | 1730 | 2079 |
あいして
ロジック | 今回 | 前回 |
meta search(日英) | 271 | 444 |
weak intersection(日英) | 72 | 104 |
filter(日英) | 96 | 52 |
meta search(日タイ) | 1404 | 1213 |
weak intersection(日タイ) | 105 | 159 |
filter(日タイ) | 155 | 112 |
All | 2392 | 2373 |
あいしてるとくりかえしていう
ロジック | 今回 | 前回 |
meta search(日英) | 272 | 855 |
weak intersection(日英) | 72 | 117 |
filter(日英) | 91 | 51 |
meta search(日タイ) | 3632 | 3620 |
weak intersection(日タイ) | 214 | 247 |
filter(日タイ) | 171 | 76 |
All | 4731 | 5119 |
バグ
書いているうちに気づいたが、多分今のソースだと、文字の出現位置チェックの部分にバグがある。
基準となる検索片に対応する部位が複数あった場合、それ以後の検索片に対応する部位はどれか1つでも出現位置があっていればひっかかってしまうs。
例えば「あいたた」という検索キーで検索した場合、「あいたら、あたた」もひっかかると思う(テストデータ用意するのが面倒なので試してない)。
条件が変わるのもなんなので、高速化が終わったら修正する。