HTML StripCharFilterFactoryの使い方

CharFilterの最後はHTMLStripCharFilterFactoryです。このコンポーネントのHTMLStripCharFilterは、名前の通り、入力からHTMLやXMLなどのタグを除去します。また数値参照や文字参照を、対応する文字の実体へ変換も行います。以下はwikiにも載っている、出来ることの一覧の翻訳(ほぼgoogle翻訳)です。

  • 内容を維持しながらHTML/XMLタグを削除する
    • INPUTは正確なHTML文書である必要はなく、それらしい構造だけが削除される
    • タグの中の属性も削除される。その際、属性の引用符は省略可能。
  • XML処理命令を削除する(ex: <?foo bar?> とか <?php echo "hoge"; ?>)
  • <!で始まり>で終わるXML要素を削除する
  • <script>と<style>要素の内容を削除する
    • これらの要素の内部のXMLコメントを処理(通常のコメント処理はうまく動作しないケースがある)
  • &#65;(A)または&#x7f;(DEL)のような数値文字参照を置換する(10,16進数のどちらも可)
    • &#nn;形式の10進数と&#xXX;の16進数の両方共OK
    • 終端のセミコロンはその後に空白が続く場合は省略可能
  • 全ての文字実体参照を置換する
    • &nbsp;はnon-breaking space(\u00A0)に置換する
    • 終端のセミコロンは必須(Alpha&Omega Corpのような会社名がAlphaΩ Corpに誤って置換されないように)

変換例

変換前 変換後
my <a href="www.foo.bar">link</a> my link
<br>hello<!--comment--> hello
hello<script><!-- f('<!--internal--></script>'); --></script> hello
if a<b then print a; if a<b then print a;
hello <td height=22 nowrap align="left"> hello
a<b &#65; Alpha&Omega O a<b A Alpha&Omega O
M&eacute;xico México

使い方

使い方は簡単で、設定する項目もなく、下記の例のようにTokenizerの前に設定するだけです。

<fieldType name="html_test" class="solr.TextField" positionIncrementGap="100">
  <analyzer>
    <charFilter class="solr.HTMLStripCharFilterFactory"/>
    <tokenizer class="solr.StandardTokenizerFactory"/>
  </analyzer>
</fieldType>

使用上の注意

  • 長すぎるコメントなどで削除出来ないケースがある(詳細は後述)
  • 仕様に完全に準拠している訳ではない。ブラウザの挙動にあわせている部分がある
    • CDATAの内容は無視される(XMLの文書は注意が必要)
  • storeする文書からタグが削除されるわけではなく、indexとして登録するtokenからタグが削除される(CharFilter全てに言えることですが)
    • 元の文書からタグが不要な場合は、登録前にどうにかして削除してから登録すること
    • 登録にDataImportHandlerを使っているのであれば、HTMLStripTransformerが利用可能

上記の注意点の通り、削除できないケースもあるので、「大体のケースでindexから消える」というぐらいの使い方がいいでしょう。

クラスの詳細


※Solr 3.3のものです(現行trunkではHTMLStripCharFilterはLuceneに移動しました)

処理のほとんどがHTMLStripCharFilterで行われており、Factoryはインスタンスを生成する役割のみです。これまでと同じくこれまでと同様にコードの中身を見ていくつもりだったのですが、結構大きなクラスでなのでblogで紹介していくと大変な事になります。そこで今回は、ポイントとなる処理の中心部分だけを見るだけとし、あとは主な機能を実際に使ってみて、試していこうと思います。

readメソッドの紹介

処理の起点となるreadメソッドです。そのまま載せると長すぎるので、コメント行を省いています。

public int read() throws IOException {
  if (numWhitespace > 0){
    numEaten += numWhitespace;
    addOffCorrectMap(numReturned, numEaten);
    numWhitespace = 0;
  }
  numReturned++;
  while(true) {
    int lastNumRead = numRead;
    int ch = next();
    switch (ch) {
      case '&':
        saveState();
        ch = readEntity();
        if (ch>=0) return ch;
        if (ch==MISMATCH) {
          restoreState();
          return '&';
        }
        break;

      case '<':
        saveState();
        ch = next();
        int ret = MISMATCH;
        if (ch=='!') {
          ret = readBang(false);
        } else if (ch=='/') {
          ret = readName(true);
          if (ret==MATCH) {
            ch=nextSkipWS();
            ret= ch=='>' ? MATCH : MISMATCH;
          }
        } else if (isAlpha(ch)) {
          push(ch);
          ret = readTag();
        } else if (ch=='?') {
          ret = readProcessingInstruction();
        }
        if (ret==MATCH) {
          numWhitespace = (numRead - lastNumRead) - 1;//tack on the -1 since we are returning a space right now
          return ' ';
        }
        restoreState();
        return '<';

      default: return ch;
    }
  }
}

処理の中心となる部分です。ざっくり流れを説明すると"<"もしくは"&"が見つかるまではそのままの文字を返し、見つかった場合にそれぞれの処理を行っています。また、処理の一番最初では、元の文字の位置との調整を行っているので、タグが削除されたり文字参照が解決されて文字数が変わった場合でも、元の位置は記録されています。その為、問題なくhighlighterも動作します。(管理画面で確認)

通常のコメント処理について

コメント処理のメソッドのjavadocに、HTMLの仕様に則ったコメントの例が書かれています。

/*** valid comments according to HTML specs
   <!-- Hello -->
   <!-- Hello -- -- Hello-->
   <!---->
   <!------ Hello -->
   <!>
   <!------> Hello -->

よく分からないのが一番下の例です。試しにこのクラスで試してみると「Hello -->」という結果になります。。上記のコメントの書き方だと全て消えるのが正しいと思ってしまうのですが。。なおブラウザ(chrome13, firefox5, IE8)でも同じ結果になりました。参考までにw3cHTML4の仕様を見ると、コメント内の--は避けるべきと書かれています。ちなみに、これらの例を含んだXMLを読み込むと、1つ目と3つ目以外の例はJavaXMLパーサ(Xerces)では例外となります。

SCRIPT, STYLEタグの処理について

SCRIPTとSTYLEタグは他のタグと異なり、一般的にスクリプトCSSの記述なので内容ごと削除されます。ただし、このタグの中のコメントの処理については、以下のコード内のコメントに書かれているように、少し特殊なことをやっています。

  // GRRRR.  it turns out that in the wild, a <script> can have a HTML comment
  // that contains a script that contains a quoted comment.

SCRIPTの中では以下のような意地悪なスクリプトも書くことが出来ます。

  <script><!-- document.write("comment-->") --></script>

上記のようなケースが有りうるので、文字列内のコメントは無視する必要があります。その為には読込中にタグの開閉などのHTMLの状態以外に、スクリプトの状態(クオートの開始、終了)を扱う必要があります。それにクオート内のエスケープやSSIを考慮する必要だってあります。きっとこのコメントを書いた作者はその面倒さを想像して、自然と出てきたのが「GRRRR」だったのでしょう。ちなみにちゃんと実装されており、上記の例は" "(半角スペース)が結果となります。

CDATAについて

簡単なHTMLを作って実験してみたところ、chrome13、Firefox5、IE8はCDATAの内容を表示しないようでした。このクラスでもCDATAの内容は完全に無視されます。CDATAの中に必要な文字列があるXMLを扱う場合は注意しましょう。

文字実体参照と数値文字参照の変換処理について

先に出来ること一覧にも書きましたが、数値文字参照は10進数と16進数が使えます。また空白もしくはEOFの場合はセミコロンが省略できます。文字実体参照セミコロンは省略できません。でもコード内のコメントには&ampとかの一般的なものは省略できるようにすべき、とTODOで書かれてあるので、将来的には限定的にセミコロンを省略できるようになるかもしれません。

削除できないケースについて

このFilterは処理する文字列を全てメモリ上に保持してから処理するのではなく、一文字ずつ読み込んで必要に応じて一定サイズのバッファを使って処理を行います。CharFilterには文字列の大きさは予め分からないので、全てを読み込んでOutOfMemoryErrorになるのを防ぐ理由と、元の文字列の位置を記録しておくという理由で、このような処理方法を選択しているのだと考えられます。このようにバッファを利用するという事は、前回のPatternReplaceCharFilterと同じように、バッファサイズの絡みで除去対象から漏れてしまうケースがあるという事です。バッファのサイズは8192バイトなのでそうそうオーバーすることもありませんが、困ったことにFactoryからバッファサイズを拡張する方法も今のところありません。以下に示すのが、バッファサイズオーバーで漏れてしまうケースです。

  1. 文字実体参照の置換処理
    • "&"の後の";"を見つける時に利用するバッファがオーバーするケース
  2. XML要素(<!〜〜>)の削除処理
    • "<!"の後の">"を見つける時に利用するバッファがオーバーするケース
  3. コメント要素の削除処理
    • "<!--"の後の"-->"を見つける時に利用するバッファがオーバーするケース
  4. タグの削除処理
    • "<"の後の">"を見つける時に利用するバッファがオーバーするケース
    • 属性(href="hogehoge.html"など)を読み込むときのバッファがオーバーするケース
  5. script、styleタグの内容の削除処理
    • 終了タグが見つかるまでのバッファがオーバーするケース
    • スクリプトの文字列終了('"'から次の'"'が見つかる)までのバッファがオーバーするケース
  6. XML処理命令の削除処理
    • "<?"の後の"<"を見つける時に利用するバッファがオーバーするケース

これらに該当する箇所は削除が行われず残ってしまいます。とはいっても8KBあるのでそうそうオーバーすることもありませんが、INDEXにゴミが残る可能性があります。

まとめと感想

  • タグの抽出は真面目にやると本当に大変。
  • クラスの半分近くはHTMLの文字参照の定義のコメントなのって。。
  • 怪しい実装も。。(isHexというメソッドが英数字全部trueだった)
  • 文字列を処理するコードを読むのが苦手だという事に改めて気付いた
  • コメントが人間味溢れてますw

CharFilterが終わったので次はTokenizerですが、その前にCharFilterとTokenizerとTokenFilterの役割についてまとめてみる予定です。その次は「CharFilterを作ってみた」をやります。

Pattern ReplaceCharFilterFactoryの使い方

今回はPatternReplaceCharFilterFactoryの使い方を紹介します。このクラスは前回のMappingCharFilterFactoryと同様に、SolrのCharFilterの一つで、tokenizerが解析する前の文字列に対して処理を行います。名前が示す通り、正規表現を用いたパターンマッチを用いて置換を行うコンポーネントです。正規表現を利用できるので、柔軟に置換ルールを設定できます。しかし、詳細は後述しますが、メモリ効率やパフォーマンスがよくない事もあるため、パターンマッチでしか実現できない事以外では積極的に利用するのは控えた方がいいでしょう。
利用シーンとしては次のようなものが考えられます。

特定のパターンに一致する文字列を除外したい

・HTML or XMLのコメント要素を除去: <!--.+-->→空文字
 例:abcd<!-- コメント -->efg→abcdefg
・英字を除去: [a-zA-Z]+→空文字
 例:hoge12hogeあいう→12あいう
・英字以外を除去: [^a-zA-Z]+→空文字
 例:hoge12hogeあいう→hogehoge

特定のパターンに一致する文字列を置換したい

・複数の空白文字の連続を一つの空白に置換:\s+→" "(半角スペース)
 例:test1      test2→test1 test2
・endの連続の圧縮構文を置換:en+d→rubykaigi2011
 例:the ennnnnnnnnd!!→the rubykaigi2011!!
 ※http://redmine.ruby-lang.org/issues/5054 より

特定のパターンに一致する文字列を前方参照を使って置換したい

・同じ文字の連続を1文字に置換:(.)\1+→$1
 例:1223334444あああ→1234あ
・メールアドレス(っぽいもの)の一部を隠す:(.+)@.+\..+→$1@xxxx
 例:hoge@hoge.com→hoge@xxxx

使い方

属性名 説明 必須
pattern 正規表現Java正規表現を指定可能※1)
replacement 置換文字列(※2) 未定義の場合はマッチしたパターンの除去となる -
maxBlockChars パターンマッチを行う文字列の最大文字数(これを超える文字列の場合は、この文字数ごとにブロック化されてパターンマッチを行う。未指定の場合は10000文字。) -
blockDelimiters maxBlockChars以内でこの文字列が出てきた場合、ブロックの区切りがこの文字列の位置になる -

※1 java.util.regex.Patternで使える正規表現が指定可能
 前方参照を使う場合、正しくグループ指定しないと解析時の実行時エラーになるので注意
※2 $nで前方参照が可能(Matcherのjavadocを参照)

一つの定義で一つのパターンしか設定できないので、複数のパターンを設定したい場合は、下記のようにPatternReplaceCharFilterを指定したい数だけ設定します。

<fieldType name="prcf_test" class="solr.TextField" positionIncrementGap="100">
  <analyzer>
    <charFilter class="solr.PatternReplaceCharFilterFactory" pattern="(aa)\d+(bb)" replacement="$1 $2"/>
    <charFilter class="solr.PatternReplaceCharFilterFactory" pattern="(.)\1+" replacement="$1"/>
    <tokenizer class="solr.WhitespaceTokenizerFactory"/>
  </analyzer>
</fieldType>

maxBlockCharsとblockDelimitersは、パターンマッチを行う単位(ブロック)を文字数もしくは区切り文字で設定します。文字列はここで指定したブロック単位で分割され、その後ブロック単位でパターンマッチが行われます。ブロックをまたがるパターンにはマッチしないので注意して下さい。予め文字列の最大が分かっているのであれば、maxBlockCharsに十分大きな値を設定する事で回避できますが、crawlerなどサイズが予測できない場合は漏れるケースがあるので、そういう場合は補助的な使い方に留めておくのがいいでしょう。

注意点

置換前と後で文字数が変わる場合はhighlightがずれてしまうようです。(javadocより)どういう場合に発生するのかまだ実際に確認出来ていませんので、機会があれば試してみます。

それでは中身を見ていきましょう。前回のMappingCharFilterFactoryと同じような構成で、作りもよりシンプルです。

クラスの詳細


※Solr 3.3のものです(現行trunkではPatternReplaceCharFilterはLuceneに移動しました)

処理のほとんどがPatternReplaceCharFilterで行われており、Factoryはインスタンスを生成する役割のみです。

このFilterのポイント

・readメソッド

  @Override
  public int read() throws IOException {
    while( prepareReplaceBlock() ){
      return replaceBlockBuffer.charAt( replaceBlockBufferOffset++ );
    }
    return -1;
  }

なぜifではなくwhileなのかはさておき、、、置換後の文字列を必要に応じて準備→出来れば一文字返し、出来なければ-1を返します。次にprepareReplaceBlockで何が行われているか見てみましょう。

・prepareReplaceBlockメソッド

  private boolean prepareReplaceBlock() throws IOException {
    while( true ){
      if( replaceBlockBuffer != null && replaceBlockBuffer.length() > replaceBlockBufferOffset )
        return true;
      // prepare block buffer
      blockBufferLength = 0;
      while( true ){
        int c = nextChar();
        if( c == -1 ) break;
        blockBuffer[blockBufferLength++] = (char)c;
        // end of block?
        boolean foundDelimiter =
          ( blockDelimiters != null ) &&
          ( blockDelimiters.length() > 0 ) &&
          blockDelimiters.indexOf( c ) >= 0;
        if( foundDelimiter ||
            blockBufferLength >= maxBlockChars ) break;
      }
      // block buffer available?
      if( blockBufferLength == 0 ) return false;
      replaceBlockBuffer = getReplaceBlock( blockBuffer, 0, blockBufferLength );
      replaceBlockBufferOffset = 0;
    }
  }

ここでmaxBlockCharsの文字数かblockDelimitersまでもしくはreadが-1を返すまでの、置換前の文字列のブロックが読み込まれ、パターンマッチと置換を行います。まだ読み込まれていない置換後の文字列がある時は、読込と置換処理は行わずにtrueを返します。次にパターンマッチと置換を見ましょう。

・getReplaceBlockメソッド

  String getReplaceBlock( char block[], int offset, int length ){
    StringBuffer replaceBlock = new StringBuffer();
    String sourceBlock = new String( block, offset, length );
    Matcher m = pattern.matcher( sourceBlock );
    int lastMatchOffset = 0, lastDiff = 0;
    while( m.find() ){
      m.appendReplacement( replaceBlock, replacement );
      // record cumulative diff for the offset correction
      int diff = replaceBlock.length() - lastMatchOffset - lastDiff - ( m.end( 0 ) - lastMatchOffset );
      if (diff != 0) {
        int prevCumulativeDiff = getLastCumulativeDiff();
        if (diff > 0) {
          for(int i = 0; i < diff; i++){
            addOffCorrectMap(nextCharCounter - length + m.end( 0 ) + i - prevCumulativeDiff,
                prevCumulativeDiff - 1 - i);
          }
        } else {
          addOffCorrectMap(nextCharCounter - length + m.end( 0 ) + diff - prevCumulativeDiff,
              prevCumulativeDiff - diff);
        }
      }
      // save last offsets
      lastMatchOffset = m.end( 0 );
      lastDiff = diff;
    }
    // copy remaining of the part of source block
    m.appendTail( replaceBlock );
    return replaceBlock.toString();
  }

パターンマッチと置換を行っています。置換前と後で文字数が変わる場合、元の文字列との位置を補正する為の情報も一緒に保存しています。(MappingCharFilterでもやっていました)

あとは入力が無くなるまで上記の繰り返しです。シンプルですね。

感想

Java標準の正規表現ライブラリで使ったことのないメソッド(Matcher#appendReeplacement)が使われてて、こう使うのかと勉強になった
何でStringBuilderじゃなくてStringBuffer使ってるのかなと思ったら、上記メソッドの引数がStringBufferだったというオチ(StringBuilderは1.5からで、Matcherは1.4から)
自分の正規表現力の無さを改めて痛感・・・

参考情報

海外のPatternReplaceCharFilterFactoryの記事:java.dzone.com

CharFilterの最後はHTMLStripCharFilterFactoryです。

Mapping CharFilterFactoryの使い方

Solrには文字列解析の為の仕組みがたくさん用意されています。またそれらは柔軟に組み合わせやパラメータによるカスタマイズが可能で、色んな用途に対応できるようになっています。それらは個々のカスタマイズ可能なコンポーネントとして、下記のURLで説明されています。

http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters

コンポーネントによっては、上記ページの他にjavadocを見てもあまり詳しく説明がなかったり、パラメータにも何を設定できるのかまでは細かく書かれていません。そこで勉強がてら、これらのコンポーネントの使い方を一つずつ紹介していきます。また、使い方だけではなく、ソースコードから内部の詳細についても一緒に見ていこうと思っています。
(全体的な仕組みの詳細はSolr本を参照してください。機会があれば書いてみるつもりです。)

先程のページを上から順番にこのシリーズで取り上げていきます。まずは一番目のMappingCharFilterFactoryからです。

MappingCharFilterFactoryは、Tokenizerが処理を行う前段階で、文字に対する処理を行うためのコンポーネントのひとつで、文字(もしくは文字列)を別の文字(もしくは文字列)に、変換を行う事ができます。主な利用目的としては解析対象の文章内の文字列の、半角と全角の表記の揺れを吸収するために使います。変換の設定パターンとしては以下のようなものが考えられます。

#半角→全角
ア=>ア
A=>A
#全角→半角
イ=>イ
U=>U
#半角カナ二文字→全角カナ一文字
バ=>バ
#全角カナ一文字→半角カナ一二文字
ビ=>ビ
#記号→全角カナ
㍗=>ワット
㌔=>キロ

上記の変換ルールをファイルに定義して利用します。

利用シーンとしてよくあるのがカナの正規化です。下記の例のように、保存する文字列と検索文字列の両方で半角を全角に変換して、全角半角のどちらで書かれていてもhitするようにします。
例:バービーボーイズ→バービーボーイズ
また用途は限定的でしょうけど、こんな事もできます。
例:ビールの品揃えがオ㍗ル。それでもイ㌔→ビールの品揃えがオワットル。それでもイキロ
googleだと㍗で検索はできません

それでは設定方法について見ていきましょう。

MappingCharFilterFactoryの設定方法

設定できる項目は、文字変換ルールを記述したファイル(以降マッピングファイルと表記)の指定のみです。

scheme.xml設定例

<fieldType name="・・・(略)・・・
  <analyzer>
    <charFilter class="solr.MappingCharFilterFactory" mapping="mapping.txt"/>
    <tokenizer class="・・・(略)・・・
  </analyzer>
</fieldType>
属性 説明 備考
mapping 文字変換ルールを記述したファイルを指定 カンマ区切りで複数指定可

注意点

  1. 文字コードUTF-8で(BOMがあっても大丈夫)
  2. カンマが含まれるファイル名を指定する場合は、\でエスケープする
  3. 変換前の文字列が重複しているとエラーになるので注意(複数ファイルに分割している場合も同様)

マッピングファイルの書き方

  • 変換前の文字列と変換後の文字列をそれぞれダブルクォートでくくり、間に=>を記述します。ダブルクォートと=>の間はスペースが入っても構いませんが、一行で記述して下さい。各行は次のパターンにマッチしてればOKです。"(.*)"\s*=>\s*"(.*)"\s*$
  • #から始まる行はコメント行です。
  • ファイルの文字コードUTF-8固定で、変更できません。BOMはあってもなくても大丈夫です。

# 記述例1
"エ" => "エ"
"糸冬"=>"終"

また、エスケープ文字を使っていくつかの特殊文字が使えます。

# エスケープ文字入り記述例
"タブ"=>"\t"
"\r\n"=>"\n"
"バックスペース"=>"\b"
"円"=>"\\"
"ユニコードあ"=>"\u3042"

MappingCharFilterFactoryの使い方は以上です。

MappingCharFilterFactoryの仕組み

MappingCharFilterFactoryの主要なクラスの関係は以下のようになっています。
(パッケージは分かりやすくするためパッケージ名を正確には記述していません)

実際の文字列のreadは、LuceneのMappingCharFilterが行い、変換ルールはNormalizeCharMapが保持しています。これらをFactoryの役割としてインスタンスの生成を行うのが、MappingCharFilterFactoryです。先述のマッピングファイルはNormalizeCharMapとして構築され、文字列解析の度にMappingCharFilterのインスタンスを生成します。

NormalizeCharMapは変換前の文字列を一文字ずつ階層化した構造でキーとして変換後の文字列を紐付けています。文章だけだと分かりにくいと思うので、イメージは下記の図を見て下さい。

※決して分かりやすくなかったことを心から反省・・・。

上記の図のようにNormalizeCharMapを用いて、一文字ずつ対象があるかどうかを確認しながら変換処理を行ないます。

最後にまたMappingCharFilterFactoryですが、このクラスはResourceLoaderAwareというinterfaceを実装していて、solrからResourceLoaderというマッピングファイルを読み込む為のインスタンスを受け取ります。この処理の中でNormalizeCharMapの構築を行っています。あとは先述の通り、FactoryとしてMappingCharFilterのインスタンスを生成する役目を持っています。

MappingCharFilterFactoryについてはざっとこんな感じです。

以下、読んでみての感想です。

  • もっと簡単かなと思ってたけど、文字列処理は効率的にやろうとすると大変(コードがすぐに大きくなる)
  • MappingCharFilterは、Readerを継承したおかげで使いやすいけど実装はやっぱり大変
  • MappingCharFilterはとても汎用的でLuceneのコードにほぼ依存していないので、Luceneのパッケージ内ではなく、もっと汎用的なプロジェクトにそのまま持っていけるかも。(commons-langとか)

はsolr.PatternReplaceCharFilterFactoryです。

HTML5 Audioサポート状況(メモ)

スマートフォンHTML5+Javascriptで音を鳴らす為に、ブラウザ間の対応状況を調べてみたのでメモとしてまとめておきます。各ブラウザで共通で音を鳴らす方法については、またの機会に書いてみようと思います。(enchant.jsのSound周りの実装も進んでるみたいですし)

iPhone 4
PCM audio support
MP3 support
AAC support
Ogg Vorbis support
WebM support
iPhone 3GS
PCM audio support
MP3 support
AAC support
Ogg Vorbis support ×
WebM support ×
iPhone 3G
PCM audio support
MP3 support
AAC support
Ogg Vorbis support ×
WebM support ×
Android 2.1
audio element
PCM audio support ×
MP3 support ×
AAC support ×
Ogg Vorbis support ×
WebM support ×
Android 2.3(xperia arc
audio element
PCM audio support ×
MP3 support
AAC support ×
Ogg Vorbis support ×
WebM support ×

2011.7.12追記:Android2.3は端末によって再生可能なフォーマットが異なるようです(Kenさんありがとうございます)

Google Chrome 12
PCM audio support
MP3 support
AAC support
Ogg Vorbis support
WebM support
Firefox 4
PCM audio support
MP3 support ×
AAC support ×
Ogg Vorbis support
WebM support
IE8
audio element ×
IE9
PCM audio support ×
MP3 support
AAC support
Ogg Vorbis support ×
WebM support
IE10
PCM audio support ×
MP3 support
AAC support
Ogg Vorbis support ×
WebM support
OPERA 11
PCM audio support
MP3 support ×
AAC support ×
Ogg Vorbis support
WebM support
Firefox 4
PCM audio support
MP3 support ×
AAC support ×
Ogg Vorbis support
WebM support
SAFARI 5
PCM audio support
MP3 support
AAC support
Ogg Vorbis support ×
WebM support
OPERA 11
PCM audio support
MP3 support ×
AAC support ×
Ogg Vorbis support
WebM support


こうしてみてみると、Androidの状況がまだ厳しいようですね。
Android2.3でも端末ごとに違うとか聞きますし。
また実際に試してみて分かったことがあれば追記します。

【参考にしたサイト】
ブラウザのHTML5対応状況を確認できるサイト。
http://html5test.com/

Windows及びMacHTML5対応状況を一覧しているサイト。
http://www.findmebyip.com/litmus#mdz_audio

Solr3.1のHighlighterで数値文字参照にならないようにする

たまたま#SolrJPのハッシュタグを見ていたら、exabugsさんのつぶやきを見つけて、Solrのソースを追いかけてみたりしました。折角良いネタを頂いたので、しっかりblogのネタにさせてもらいますw

この問題はSolr3.1からなのかどうかは調べていませんが、確かにhighlightingの結果として返される文字列は、非ASCII文字が数値文字参照になっています。

試しにSolr3.1で「日本語テスト」という文字列に対して「日本語」で検索してみます。
ブラウザで見ると

と、問題無く表示されているように見えますが、実際に返されているXMLを見てみると

このように非ASCII文字が数値文字参照になっている事が分かります。

この事には気付いていたのですが、私には実害があまり思い当たらなかったので放置していました。
英語圏で作られたライブラリのコードで何度か見かけた事があります。
むかーしむかし(5年ぐらい前だったか)に、私の持ってたケータイで数値文字参照が解釈できずそのまま表示されてしまう、のがあって対処した事があるのですが、もしかしたらexabugsさんもこの問題を解決したいのかなと。
もしくは更に文字列として何か加工する際に、数値文字参照だと扱いづらいとか、そんな所でしょうか。

さて、問題も分かった(?)所で早速解決方法を探してみましょう。

Solrで検索にヒットした箇所を強調表示しているのはHighlighting Componentというコンポーネントで、強調表示以外の非ASCII文字は数値文字参照になっていない事から、このコンポーネントのどれかで変換が行われているはずです。
このコンポーネントが設定できるパラメータはwikiに書かれていますが、これらの一覧を見てみても、数値文字参照に関するパラメータは無さそうです。また、solrconfig.xmlThe Highlighter plugin configuration sectionを見てみても、関連する事は書かれて無さそうです。
しょうがないのでSolr3.1のsolrconfig.xmlを見ることにしましょう。

HighlightComponentの設定の中にencoderというそれっぽい要素がある事が分かりますね。これのclass属性からorg.apache.solr.highlight.HtmlEncoderのソースを見てみる事にしましょう。Solrのソースはこちらからダウンロード出来ます。


public class HtmlEncoder extends HighlightingPluginBase implements SolrEncoder {

public Encoder getEncoder(String fieldName, SolrParams params) {
return new SimpleHTMLEncoder();
}
 ・・・以下略・・・

内容は非常に簡単で、Encoderを実装したSimpleHTMLEncoderというクラスをnewして返しているだけです。
そこで、org.apache.lucene.search.highlight.SimpleHTMLEncoderも見てみましょう。今度はLuceneのソースが必要で、こちらからダウンロード出来ます。対象のソースはcontribのhighlighterの中にあるので注意して下さい。


public class SimpleHTMLEncoder implements Encoder
{
public SimpleHTMLEncoder()
{
}

public String encodeText(String originalText)
{
return htmlEncode(originalText);
}
 ・・・続きます・・・

上記のencodeTextメソッドはhtmlEncodeを呼び出していますね。ちょっと長いですが中身を見てみましょう。


public final static String htmlEncode(String plainText)
{
if (plainText == null || plainText.length() == 0)
{
return "";
}

StringBuilder result = new StringBuilder(plainText.length());

for (int index=0; index':
result.append(">");
break;

default:
if (ch < 128)
{
result.append(ch);
}
else
{
result.append("&#").append((int)ch).append(";");
}
}
}

return result.toString();
}
}

最後の方にありましたね。StringBuilderにappend((int)ch)とやっている箇所で数値文字参照は作られています。defaultの部分でASCII文字と非ASCII文字を分岐するのをやめて、全てASCII文字と同じ扱いにすれば解決できそうです。このSimpleHTMLEncoderを修正してビルドしてjarを差し替える、という方法でも解決できますが、Solrは折角コンポーネントを差し替えやすい柔軟な設計になっているので、新しくクラスを作成してsolrconfigで切り替えるようにしましょう。

今回はHtmlEncoderを継承して作ってみました。


package org.apache.solr.highlight;

import org.apache.lucene.search.highlight.Encoder;
import org.apache.solr.common.params.SolrParams;

public class HtmlEncoder2 extends HtmlEncoder {
public Encoder getEncoder(String fieldName, SolrParams params) {
return new Encoder() {
public String encodeText(String originalText) {
if (originalText == null || originalText.length() == 0) {
return "";
}
StringBuilder result = new StringBuilder(originalText.length());
for (int index=0; index':
result.append(">");
break;
default:
result.append(ch);
}
}
return result.toString();
}
};
}
}

encodeTextの中身はSimpleHTMLEncoderからそのまま持ってきて、数値文字参照の部分だけを修正しています。githubにコードとjarを公開していますので、そちらをご利用下さい。

まずダウンロードしたjarを任意の場所に配置します。ここではcoreの直下にlibディレクトリを作成してその中に配置するものとします。
次にsolrconfig.xmlを修正します。

最初の方にlib属性が並んでいる箇所がありますので、そこに


<lib dir="lib/" />

を追記します。
そして、先ほどそれっぽい要素として紹介したencoder属性のclass要素を新しいクラスに差し替えます。



これで完成です。
solrconfigをリロードして、先ほどと同じ検索で試してみます。

うまくいくようになりました♪
用途が非常に限定的ですが、同じような問題のある方は是非使ってみて下さい。
https://github.com/shinobu-aoki/Solr-CustomHtmlEncoder

lucene-gosenの辞書編集方法

lucene-gosenとは

lucene-gosenとは、全文検索エンジンLucene/Solr3.1及び4.0で動作する、日本語の形態素解析用のJavaライブラリで、ここで公開されています。なお、ここで扱うlucene-gosenのバージョンは1.0.1とします。

一般的に日本語を扱う場合には、英語のように空白で文章を単語に区切れない為、n文字ずつ格納するN-GramのCJKAnalyzer(Bi-Gram)や、形態素解析を用いるJapaneseAnalyzer(lucene-gosenに含まれる)を主に用います。それぞれメリットとデメリットがあって、N-Gramは検索もれが少ない分ノイズが多く、形態素解析を用いるとノイズは少なくなるが検索もれが多くなるという特徴があります。どちらも一長一短なので、日本語を扱う場合二つのAnalyzerを併用したりします。(例として、形態素解析を用いた検索でヒットしたものをN-Gramでヒットしたものより下に表示するように、などを行います。)これでもノイズが多くなることは避けられないのですが、ユーザの望むものが先に見つかりやすく、なおかつ検索漏れを防ぐことが出来ます。

でも、出来れば検索漏れとして後ろの方で表示されるのではなく、流行語や造語など(辞書に無い単語)も上に来るようにしたい!という時に、形態素解析用の辞書の編集を行います。そこで辞書のカスタマイズが必要となるのですが、lucene-gosenの辞書の編集を行うためには
 1.辞書の編集
 2.辞書のコンパイル
 3.jarにパッケージ

の作業が必要となります。

lucene-gosenで使える辞書にはIPAdicとNAIST-jdicがありますが、ここでは後発でIPAdicのライセンスの問題を解決したNAIST-jdicを使う事とします。

辞書の編集

辞書の編集の前にまず、この作業にはantとJDKが必要となりますので、必要に応じてインストールを行って下さい。環境が揃ったらここからソースコードをcheckoutします。Windowsな方はEclipseCygwinを使ってSVNからcheckoutして下さい。
また、より詳細なビルド方法はjohtaniさんのページにあります。

辞書はcheckoutした中には含まれていないため、antを用いてDLします。
コマンドプロンプト(ターミナル)で/dictionaryに移動して、

>ant -Ddictype=naist-chasen

とすればダウンロードが始まります。
(-Ddictype=naist-chasenを省略すればipadicになります)
インターネット経由でダウンロードするので、proxyの設定が必要な場合は下記のようにproxy設定を追加して下さい。(以下同様)

>ant -Ddictype=naist-chasen -Dproxy.host=proxy.hoge.com -Dproxy.port=9999

完了すればnaist-chasenディレクトリが出来てその中にdictionary.csvというファイルがあり、これが辞書のファイルとなります。このファイルに新たに解析したい単語を追加すれば良いのですが、なにぶん50MB近くありますし、ちょくちょく編集するのには向いていません。なので、単純な追加の場合は別ファイルで管理する事をオススメします。ファイル名は何でもいいですが、ここではbeer.csvというファイル名で進めます。dictionary.csvと同じディレクトリに作ります。また、文字コードUTF-8なのでWindowsな人は気をつけて下さい。

辞書の各項目については「辞書への単語の追加」も参考にして下さい。大抵は名詞の追加となると思いますので、この例と同じように追加できると思います。重要なのは見出し語とコストと品詞です。まずはdictionary.csvから似たような単語を探しましょう。今回は「スーパードライ」を参考にしました。

"スーパードライ",3999,名詞,一般,*,*,*,*,"スーパードライ","スーパードライ","スーパードライ"

スーパードライ」がなぜ固有名詞でないのかはさておき、これをコピーしてbeer.csvに貼り付けます。品詞はそのまま使えるので、見出し語と原形と発音を修正します。最後にコストですが、これは低いほど出現しづらい値となります。通常は似た単語と同じにしておけばいいらしいのですが、例えば「エビス」と「シルクエビス」が同一のコストだと、「シルクエビス」は「シルク」と「エビス」に分解されてしまいます。それを調整する為に、長い単語はコストを下げるといいようです。

という事で、追加辞書はこんな感じにしました。

さて、編集が終わればもう一度antでビルドします。最初のビルドで既に辞書がコンパイルされているので、先にそれらを削除します。

>ant -Ddictype=naist-chasen clean-sen

次に新しく作成した辞書ファイルを指定して、辞書のコンパイルを行います。

>ant -Ddictype=naist-chasen -Dcustom.dics=beer.csv

なお、複数辞書を作成した場合は次のようにスペース区切りで指定します。

>ant -Ddictype=naist-chasen -Dcustom.dics="beer1.csv beer2.csv"

辞書のサイズや環境によりますが、ビルドには1分程度かかると思います。
最後にlucene-gosenとして利用出来るようにjarファイルにします。checkoutしたディレクトリ直下に移動してビルドを行います。

>ant -Ddictype=naist-chasen

これで登録した単語を思う存分使えるようになりました。

動作確認

さて、早速動作確認してみましょう。サンプルの文は「ホップ畑の香り発泡酒です。」とし、確認環境にはSolrの機能を使って確認する事にします。まずは新しいのを試す前に、辞書追加前の解析結果を見てみます。

・・・とっても残念な分かち書きですね。・・・特にハッポーザケのあたりが・・・orz
さて、気を取り直して、新しいので試してみます。

うまくいきましたね(発泡酒以外)

lucene-gosenを使う時の参考となれば幸いです。