2017.12.15 Friday
スポンサーサイト
一定期間更新がないため広告を表示しています
| スポンサードリンク | - | | - | - |
関口宏司のLuceneブログOSS検索ライブラリのLuceneおよびそのサブプロジェクト(Solr/Tika/Mahoutなど)について
2009.09.19 Saturday
短いフィールドにはコンスタントなtfが有効な件
タイトルのような短いフィールドには、tfを考慮しない方がいい場合がある。たとえば、"New York"というタイトルのDVDを探しているとき、DefaultSimilarityのtf()が効いていると、ずばり"New York"というタイトルよりも"New York, New York"の方がスコアが高くなってしまう:
New York, New York : 0.5945349 0.5945349 = (MATCH) sum of: 0.29726744 = (MATCH) weight(title:new in 0), product of: 0.70710677 = queryWeight(title:new), product of: 0.5945349 = idf(docFreq=2, maxDocs=2) 1.1893445 = queryNorm 0.42039964 = (MATCH) fieldWeight(title:new in 0), product of: 1.4142135 = tf(termFreq(title:new)=2) 0.5945349 = idf(docFreq=2, maxDocs=2) 0.5 = fieldNorm(field=title, doc=0) 0.29726744 = (MATCH) weight(title:york in 0), product of: 0.70710677 = queryWeight(title:york), product of: 0.5945349 = idf(docFreq=2, maxDocs=2) 1.1893445 = queryNorm 0.42039964 = (MATCH) fieldWeight(title:york in 0), product of: 1.4142135 = tf(termFreq(title:york)=2) 0.5945349 = idf(docFreq=2, maxDocs=2) 0.5 = fieldNorm(field=title, doc=0) New York : 0.5254995 0.5254995 = (MATCH) sum of: 0.26274976 = (MATCH) weight(title:new in 1), product of: 0.70710677 = queryWeight(title:new), product of: 0.5945349 = idf(docFreq=2, maxDocs=2) 1.1893445 = queryNorm 0.3715843 = (MATCH) fieldWeight(title:new in 1), product of: 1.0 = tf(termFreq(title:new)=1) 0.5945349 = idf(docFreq=2, maxDocs=2) 0.625 = fieldNorm(field=title, doc=1) 0.26274976 = (MATCH) weight(title:york in 1), product of: 0.70710677 = queryWeight(title:york), product of: 0.5945349 = idf(docFreq=2, maxDocs=2) 1.1893445 = queryNorm 0.3715843 = (MATCH) fieldWeight(title:york in 1), product of: 1.0 = tf(termFreq(title:york)=1) 0.5945349 = idf(docFreq=2, maxDocs=2) 0.625 = fieldNorm(field=title, doc=1) このようなときはDefaultSimilarityのtf()をオーバライドして常に1が返るようにする。すると: New York : 0.5254995 0.5254995 = (MATCH) sum of: 0.26274976 = (MATCH) weight(title:new in 1), product of: 0.70710677 = queryWeight(title:new), product of: 0.5945349 = idf(docFreq=2, maxDocs=2) 1.1893445 = queryNorm 0.3715843 = (MATCH) fieldWeight(title:new in 1), product of: 1.0 = tf(termFreq(title:new)=1) 0.5945349 = idf(docFreq=2, maxDocs=2) 0.625 = fieldNorm(field=title, doc=1) 0.26274976 = (MATCH) weight(title:york in 1), product of: 0.70710677 = queryWeight(title:york), product of: 0.5945349 = idf(docFreq=2, maxDocs=2) 1.1893445 = queryNorm 0.3715843 = (MATCH) fieldWeight(title:york in 1), product of: 1.0 = tf(termFreq(title:york)=1) 0.5945349 = idf(docFreq=2, maxDocs=2) 0.625 = fieldNorm(field=title, doc=1) New York, New York : 0.42039964 0.42039964 = (MATCH) sum of: 0.21019982 = (MATCH) weight(title:new in 0), product of: 0.70710677 = queryWeight(title:new), product of: 0.5945349 = idf(docFreq=2, maxDocs=2) 1.1893445 = queryNorm 0.29726744 = (MATCH) fieldWeight(title:new in 0), product of: 1.0 = tf(termFreq(title:new)=2) 0.5945349 = idf(docFreq=2, maxDocs=2) 0.5 = fieldNorm(field=title, doc=0) 0.21019982 = (MATCH) weight(title:york in 0), product of: 0.70710677 = queryWeight(title:york), product of: 0.5945349 = idf(docFreq=2, maxDocs=2) 1.1893445 = queryNorm 0.29726744 = (MATCH) fieldWeight(title:york in 0), product of: 1.0 = tf(termFreq(title:york)=2) 0.5945349 = idf(docFreq=2, maxDocs=2) 0.5 = fieldNorm(field=title, doc=0) サンプルプログラムは次の通り。searchIndex()メソッド内のコメントを有効/無効にすると切り替えられる: public class TestNewYork { static final String[] TITLES = { "New York, New York", "New York" }; static final String F = "title"; static Directory dir = new RAMDirectory(); static Analyzer analyzer = new StandardAnalyzer( Version.LUCENE_CURRENT ); public static void main(String[] args) throws Exception { makeIndex(); searchIndex(); } static void makeIndex() throws IOException { IndexWriter writer = new IndexWriter( dir, analyzer, true, MaxFieldLength.LIMITED ); for( String title : TITLES ){ Document doc = new Document(); doc.add( new Field( F, title, Store.YES, Index.ANALYZED ) ); writer.addDocument( doc ); } writer.close(); } static void searchIndex() throws Exception { QueryParser parser = new QueryParser( F, analyzer ); Query query = parser.parse( "new york" ); IndexSearcher searcher = new IndexSearcher( dir, true ); //searcher.setSimilarity( new ConstantTfSimilarity() ); TopDocs docs = searcher.search( query, 10 ); for( ScoreDoc scoreDoc : docs.scoreDocs ){ Document doc = searcher.doc( scoreDoc.doc ); float score = scoreDoc.score; System.out.println( doc.get( F ) + " : " + score ); Explanation e = searcher.explain( query, scoreDoc.doc ); System.out.println( e.toString() ); } searcher.close(); } static class ConstantTfSimilarity extends DefaultSimilarity { public float tf( float freq ){ return 1; } } } 2008.11.20 Thursday
boostをゼロにして検索すると・・・
次のようなラーメン店のデータがあり、フィールドshopに登録されるとする:
そして同じ文書の別フィールドwardに次のようにラーメン店の場所が登録されるとする:
ここで次のように「江東区または新宿区のラーメン店」を検索するQueryを作成して検索する:
取得したdocsを表示(スコアとDocument内容を整形して)すると、次のようになる:
上記のように「江東区」と「新宿区」ではDocument数が異なるために地域属性のIDFが違ってきてスコアが新宿が高くなってしまう。 地域のような属性フィールドではスコアを計算したくないとき、通常はFilterを使うことが考えられるが、Filterは場合によっては使いたくないときもあるだろう。 そんなときは、上記の検索式の地域属性の項のBOOSTを次のようにゼロに設定するとよい:
これで実行すると、次のように地域間でスコアの違いがなくなる:
2008.10.14 Tuesday
(アイディア)逆ページランク
経済アナリスト 森永 卓郎氏のコラム「役所のマインドコントロールから脱け出せ!」を読んだ。これによると財務省のホームページでは、同省の都合の悪い記述は脚注などで小さい文字で表示されているらしい。
そこで思いついたのがタイトルのアイディアだ。私はかねてより官公庁や自治体などのホームページを横断検索するデモを作りたいと思って(2年以上経つがなかなか着手できないでいるのだが、それはこの際どうでもよい)いる。そのサイトで「小さい文字ほど重要である」「階層が深いほど重要である」「被リンクが少ないほど重要である」とみなす、その名も「逆ページランク」というのを実装してみてはどうだろう。 官公庁のホームページでは国民に知らせたくない重要な情報ほど文字を小さくしているようなので、この冗談のようなアイディアは案外いいセン行くのではないか。 この方法によると、<H1>よりも<H4>が、<B>よりも通常文字列が、通常文字列よりも<font size="-1">がより国民にとって重要になってくるのであり、高いスコアを与えて検索結果の上位にランキングさせるのである。なんじゃそりゃ。 2007.06.26 Tuesday
人名がヒットしたときはスコアを上げる
前回紹介したペイロードのプログラムではSenの出力である品詞情報をペイロード領域に登録し、TermEnumとTermPositionsで全Termのペイロードを取得するというものであった。これにより、名詞など指定した品詞の単語をカウントして多く出現する単語上位10個を表示した。
これはこれで今までのLuceneでは実現が面倒なプログラムが比較的簡単に書けて面白いが、「品詞(ペイロード情報)で検索できないのか」という疑問が当然のことながら湧くだろう。 ペイロードに関する計画には将来はペイロードで検索できるようなことが書かれている。しかしながら現在は、ペイロードで検索はできない。 それは、Luceneのファイルフォーマット情報を見れば、ペイロードは.prxファイルに格納されており(そのためTermPositionsインタフェースでアクセスできるようになっている)、転置索引に登録されている単語とは異なるため、検索に適した状態になっていないことからもうなずける。 ただし、ペイロード情報をスコア計算に使用することは可能である。今回はそのためのQueryクラスであるBoostingTermQueryを使って、検索語が人名のときは他の単語よりもスコアを上げる、というプログラムを作成してみる。 検索に使用するデータは次のとおりである:
これに対し、検索語に次の2つを使用する:
さて、これらの検索語のうち人名である「安倍」がヒットしたときだけ、そのドキュメントのスコアが他のドキュメントのスコアよりも上がるようにする、というのが今回作成するプログラムだが、その前にまずは通常のBooleanQueryでOR検索をしてみよう。プログラムは次のようになる:
実行結果は次のようになり、同点スコアなので登録順にドキュメントが表示される:
BoostingTermQueryを使う では上記のQueryにBoostingTermQueryの項を加えて品詞が「人名」のとき(品詞は名詞だが、Senの品詞情報に「人名」が含まれる)にスコアが上がるようにする。BoostingTermQueryの項を加えると、次のようになる:
BoostingTermQueryとしてはコードは以上だが、この状態ではまだ「人名のときにスコアを上げる」というコードがどこにも入っていない。ではどうするかというと、Lucene 2.2からBoostingTermQueryがペイロードからスコアを計算するためのSimilarityの新しいメソッドscorePayload()というのが追加された。DefaultSimilarityではこのメソッドが常に1を返すようになっているが、BoostingTermQueryをきちんと動作させるには、ペイロードの値を見て適当なfloat値を返すscorePayload()を実装する必要があるのだ。 Similarityを実装する ではSimilarityを実装しよう。オーバーライドするのはscorePayload()だけなので、DefaultSimilarityを継承して次のようにすればよい:
そして実装したPayloadSimilarityクラスを検索する前にIndexSearcherに次のように設定する:
これでプログラムを再度実行してみると、次のように「安倍」が含まれているほうが「政調」を含むドキュメントよりも高いスコアを獲得し、上位に表示される:
最終的なプログラムは次のようになる:
Senからは品詞情報の他にも単語の読みがなも出力されるので、ペイロードの使い道として検討してみても面白いかもしれない。 2007.06.18 Monday
2つの単語間の距離をスコアに反映する
次のような5つのドキュメントから単語"AAA"と"BBB"を含むドキュメントを検索したいとする:
プログラムは簡単で、次のようになる:
この実行結果は次のようになり、単語"AAA"と"BBB"両方を含むドキュメントがどちらかひとつだけを含むドキュメントよりも高いスコアを獲得していることがわかる:
このプログラムでは、単語"AAA"と"BBB"両方を含む3つのドキュメントがすべて同じスコアとなっている。 ここで単語"AAA"と"BBB"が検索ユーザから見て相互に関連性の高い単語の場合を考えてみる。たとえば、「Tomcat上でHibernateを使ったサンプルプログラム」をインターネットから探すときに、プログラマが検索窓に「Tomcat Hibernate」と入力して2つの単語が登場する1つの記事を探す、というような場面である。 そしてその検索結果一覧をクリックして記述された記事(仮にブログとする)を読んだときに、TomcatとHibernateが同一ページに現れているものの、実は全然関係ない2つの記事にTomcatとHibernateが別々に書かれていてがっかりした、という経験は誰しも一度はあるだろう。 このようなとき、2つの単語間の距離をスコアに反映し、近いものほど高いスコアになるようにできれば、ある程度このようなガッカリ感は防ぐことができると考えられる。 Luceneでは2つ(以上)の単語の距離をスコアに反映して近いものほど高いスコアを獲得させるようにすることが簡単にできる。これにはPhraseQueryとslopを使用する。 slopはPhraseQueryで使用される成句とみなせる2つの単語間の最大距離を示すパラメータである(詳しくはLucene本190ページを参照)。 このslop値を適当な範囲までとってスコアに加算するようにプログラムを修正する。上記のサンプルプログラムを次のように書き換えれば、単語"AAA"と"BBB"の距離が近いものほどスコアが上がるようになる:
実行結果は次のとおりとなる:
Solrではこの機能を使って、「ユーザが入力した検索式の全体をPhraseQueryに変換し、単語間相互の距離が近いドキュメントほどスコアを高くする」ということをDisMaxRequestHandlerにて行っている。 2007.04.19 Thursday
FieldNormModifier
「長けりゃいいってものじゃない」
この意見には誰もが首肯するところであろう。たとえば超大作と宣伝された映画や小説などを見たり読んだりした後に、やたら長いだけで内容的に満足できずひどくがっかりした、というは経験は多くの人にあるだろう。映画や小説のようなものはエンターテイメントなのでまだいいが、たとえばプログラミングの仕事で調べ物をするとき、検索で引っかかった記事がやたら長いだけで開発のヒントにならなかった、というのは仕事の能率に影響してしまうので、そういう記事は検索結果の表示順で下位にランキングして欲しい。 このような要求を完全に満たすことは難しいが、Luceneでは現実的な解としてランキングを決めるスコア計算の一部分であるlengthNorm()がこの「長けりゃいいってものじゃない」という人の気持ちを代弁するものとして提供されている。このメソッドは、同じ検索キーワードを同じ個数含む記事があるときに、より短いほうの記事のスコアを上昇させる役割がある。短い記事に含まれるほうが、より密度が濃いと考えられるからである(参考:Lucene本 p203)。 しかし先日、次のような顧客に会った: 「長くたっていいじゃないか」 だってにんげんだもの・・・思わずそう下の句をつけたした私であった(もちろん心の中で)。そうだ。私のブログ記事はしばしば技術的なこと以外の内容がだらだらと続き、プロジェクトで忙しいプログラマの不興をかっているのではないかと気になることもしばしばだ。そのため、「長くてもいいんです」というその言葉がこの際どうでもいいことだが、深く心に沁みたものである。 それはともかく、このlengthNorm()は当該フィールドのトークン数を引数に取る関数なので、検索実行時に計算する必要がない。そのためLuceneではフィールドのboost値を掛けた結果をfieldNormとして記録してしまっている。そのため、SimilarityをオーバーライドしてlengthNorm()を書き換えた場合、インデックスを再作成する必要があるのであった。 ところが最近、Luceneのソースを見ているときに、trunkにFieldNormModifierなるクラスがあることを発見した。このソースを読むと、どうやら既存のインデックスのfieldNorm値を書き換えるコマンドラインツールのようである。これは、次のように使う:
こうすると、引数で指定したインデックスのフィールドのfieldNorm値が、指定したSimilarityのlengthNorm()を使って再計算されて書き換えられる(ただし、boostは指定できないのでlengthNorm()だけで調整する必要がある)。Similarityクラス名を指定するところに代わりに-nを指定すると、fieldNorm値が1.0と記録されるようになるので単に「固定値にしたい」というときに便利である。 なお、FieldNormModifierはcontribに入っているものなので、次のようにビルドする必要がある:
上記のようにビルドすると、trunk/build/contrib/miscディレクトリにlucene-misc-2.2-dev.jarファイルができるので、trunk直下でantを実行して作成したlucene-core-2.2-dev.jarファイルとともに用いて実行する(Lucene 2.1と組み合わせては使用できないので注意)。 Luceneを使ったアプリケーションの運用を開始したはいいが、記事の長さはスコアに反映したくないということに後から気がついてしまい、巨大なインデックスを前に途方にくれていたプロジェクトメンバーには朗報かもしれない。 2007.03.21 Wednesday
QueryParserを使用した範囲検索
ここ数ヶ月ノートPC使用中、いわゆる「ブルースクリーン」の出現で悩まされている。
時期的には、もともと購入時の512Mメモリに1G追加したときからの発生なので、まずはメモリを疑い、Memtest86というツールを使ってチェックしてみたのだが、問題ないようだ。 次にハードディスクを疑い、OS(Windowsである)付属のディスクチェックツールを使ったりしているのだが、悪いところは見つからない。メーカーに修理に出すという手段があるのかもしれないが、その間持ち運べるPCがなくなるのは困る。Vistaも出たことだし(というのは自分が納得するための言い訳に過ぎないが)DuoCoreのノートを買ったほうがいいだろうか、などと考えているところだ。そんなことで、最近は価格.comによく訪れてノートPCの価格を調べたりしている。 というわけで、本日のネタデータはある日ある機種のノートPC価格のショップ比較を使用することにする。それは次のようなものであった:
上記のようなデータがLuceneのインデックスに索引付けされているとする(価格はpriceというフィールド名とする)。ここでQueryParserを使って次のような検索式で検索を実行する:
検索式の意味は、価格が20万円以下のものを探す、ということだ。すると、現在のLuceneバージョン2.1を使うと、次のような結果が得られる(プログラムはこの記事の最後に掲載する):
ここでショップ名の前に表示されている実数は各ドキュメントのスコアである。スコアが同じなので、ドキュメントIDの順(=ドキュメントの登録順)に結果が表示されている。使用目的から考えれば、価格の安い順に並んだ方が親切であるが、それは単にソートの問題なのでここではおいておく。 同じプログラムを、一つ前のバージョンであるLucene 2.0で実行してみる。すると、次のように面白い結果となる:
Lucene 2.0ではショップAとショップBのドキュメントのスコアが1.0より小さくなり、そのためにLucene 2.1の結果と表示順が異なる結果となった。 この原因は何かというと、Lucene 2.1での次の改善が実施されたためである: http://issues.apache.org/jira/browse/LUCENE-703 これは何かというと、QueryParserで範囲検索の検索式をQueryに展開する際、Lucene 2.0まではRangeQueryに展開していたものを、Lucene 2.1からはConstantScoreRangeQueryに変更した、というものである。なぜそのような変更がなされたかというと、範囲検索のようなものはもともと、スコアを計算してランキングをするようなものじゃないだろう、というような理由があるためだ。しかしながら、Luceneでは1.4.3までは範囲検索としてはRangeQueryがあり、QueryParserでは範囲検索の検索式をRangeQueryに展開していた(Lucene 2.0まで)。 RangeQueryは実際の検索を実行する際は、TermQueryをBooleanQueryで組み合わせたQueryに展開される。これはWildcardQueryなどでも同様である。どのように展開されるかは、Query.rewrite()を実行して得られたQueryのtoString()を見てみるとよい。今回のサンプルデータでは、RangeQueryは次のように展開される:
これでLucene 2.0ではスコアが異なる理由がわかるであろう。つまり、価格が198000円のものは180000円のものよりありふれているのでIDF値が低くなってしまうのである。 理屈はそうかもしれないが、結果はいかにも奇妙なものに見える。それがLucene 2.1でRangeQueryがConstantScoreRangeQueryで置き換えられた理由だ。ConstantScoreRangeQueryはその名のとおり固定スコアを返すQueryであり、検索の際もBooleanQueryに展開しない。そのため、TooManyClauses例外が発生してしまう心配もなくなるのである。 今回使用したプログラムを以下に示す。赤字の部分のコメントアウトをはずして実行してみると、いろいろな示唆が得られるであろう:
なお、Lucene 2.1のQueryParserでの範囲検索の検索式はデフォルトでConstantScoreRangeQueryに展開されるようになったが、以前のようにRangeQueryに展開させたいときは、setUseOldRangeQuery(true)を使用すればよい。 2007.02.16 Friday
スコア計算デモのソースコードの公開
Luceneのスコア計算のデモのソースコードを弊社のホームページからダウンロードできるようにしたので、活用していただきたい。
RONDHUITのデモページ http://www.rondhuit.com/demonstration.html 2007.01.31 Wednesday
スコア計算の様子がわかるデモ公開
Luceneのスコア計算の様子を簡単に見られるようにしたデモを作成したので、公開する:
Apache Lucene Score DEMO http://demo.rondhuit-search.com/etcscoredemo/ スコアとは、検索結果を表示するときに文書の表示順位を決めるための数値のことである。 スコア計算については(笑点の話などムリヤリ感も多少あるが)Lucene本にやさしく解説してあるのでここでは触れないが、このデモについて簡単に説明する。 以下はデモで使用している検索対象となる文書である:
ソース文書は英語であるが、その意図は単語の切れ目がはっきりしていてわかりやすく、スコア計算の学習に適していると判断したためである。ちなみにインデクシング時と検索時にはStandardAnalyzerを使用しており、デフォルトのストップワードがそのまま適用される。 文書はtitleとcontentフィールドからなっており、どちらもTOKENIZEDである。 使い方は、デモページの下半分に並んでいるボタンをクリックして検索を実行するだけの簡単操作だ。結果はページの上部に表示される。 たとえば、[tfの効果を見る]ボタンをクリックすると、tf項の効果が(割と)出やすい検索を発行して結果を表示する。ここでは"java"という単語をcontentフィールドに対して検索するので、2件の文書がヒットする。そのとき、"java"を数多く含んでいる方がtfが高くなり、スコア計算の結果、「Java Programming」が「Java Book」より上位にランクされる様子がわかる、という具合だ。 最後のボタン以外のテキスト入力領域はREAD ONLYになっており、ボタンをクリックしてそれぞれの項目の効果を簡単に見られるようになっている。 最後のフォームの部分は検索窓に自由に検索語を入力し、検索を実行できるようになっている。ここでは検索質問文字列をQueryに変換するのにQueryParserを使用しているので、QueryParserのコマンド(重みづけ、あいまい検索、ワイルドカード検索、・・・)がそのまま使用できる。 デモのソースコードはロンウイットのページで公開する予定だが、現在ホームページを整備中なので、今しばらくお待ちいただきたい。 |
+ Solrによるブログ内検索
+ PROFILE
+ LINKS
+ Lucene&Solrデモ
+ ThinkIT記事
+ RECOMMEND
+ RECOMMEND
Lucene in Action (JUGEMレビュー »)
Erik Hatcher,Otis Gospodnetic,Mike McCandless FastVectorHighlighterについて解説記事を寄稿しました。
+ RECOMMEND
+ SELECTED ENTRIES
+ RECENT COMMENTS
+ RECENT TRACKBACK
+ CATEGORIES
+ ARCHIVES
+ MOBILE
+ SPONSORED LINKS
|
(C) 2024 ブログ JUGEM Some Rights Reserved.
|
PAGE TOP |