takayuki_tk's diary

本当はScalaとかHaskellを使いたい

NOT_ANALYZEDなFieldを検索

Lucen In Actionの4.7.3をみてて面白そうだったので実験。

でNOT_ANALYZEDに指定したFieldと、もう一つ別のフィールドがある場合。 例だとpartnumとdescriptionの2つのフィールド。 parnumは完全一致で検索したいのでanalyzeしたくないような値が入っています。(電話番号とか社員番号とか)

最初にこんな感じでIndexを作成

 val doc = new Document();
 doc.add(new Field("partnum", "Q36", Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS));
 doc.add(new Field("description", "Illidium Space Modulator", Field.Store.YES, Field.Index.ANALYZED));

最近Scala力の低下が著しいのでScalaで実装。
これに対して"Q36"のようにpartnumに対して完全一致のクエリを投げる場合は問題なくヒットする。

assert(TestUtil.hitCount(searcher, makeQuery(new Term("partnum", "Q36"))) == 1)

ここで問題なのがpartnumとdescriptionの両方に対する検索。

  assert(TestUtil.hitCount(searcher, new QueryParser(Version.LUCENE_43, "description", new SimpleAnalyzer(Version.LUCENE_43)).parse( "partnum:Q36 AND Space")) == 0)

残念ながらこいつがヒットしてくれません。 ちなみにpartnumをANALYZEDしてやればヒットしますが、代わりにpartnumが分割されて完全一致でヒットしなくなります。

doc.add(new Field("partnum", "Q36", Field.Store.NO, Field.Index.ANALYZED));

assert(TestUtil.hitCount(searcher, makeQuery(new Term("partnum", "Q36"))) == 0)
assert(TestUtil.hitCount(searcher, makeQuery(new Term("partnum", "q"))) == 1) 
assert(TestUtil.hitCount(searcher, new QueryParser(Version.LUCENE_43, "description", new SimpleAnalyzer(Version.LUCENE_43)).parse( "partnum:Q36 AND Space")) == 1)

そこでPerFieldAnalyzerを使うといい感じです。

  val map = Map("partnum" -> new KeywordAnalyzer(), "description" -> new SimpleAnalyzer(Version.LUCENE_43))
  val analyzer = new PerFieldAnalyzerWrapper(new SimpleAnalyzer(Version.LUCENE_43), map)

  assert(TestUtil.hitCount(searcher, new QueryParser(Version.LUCENE_43, "description", analyzer).parse( "partnum:Q36 AND Space")) == 1)
  assert(TestUtil.hitCount(searcher, makeQuery(new Term("partnum", "Q36"))) == 1)

例によって4系だとコンストラクタが違います。

一応全部のコード貼っておきます。

import org.scalatest.FlatSpec                                                                                                                                             
import org.scalatest.matchers._

import org.apache.lucene.search._
import org.apache.lucene.document._
import org.apache.lucene.index._
import org.apache.lucene.store._
import org.apache.lucene.util._
import org.apache.lucene.analysis.core._
import org.apache.lucene.queryparser.classic._
import org.apache.lucene.analysis.miscellaneous._

class KeywordAnalyzerTest extends FlatSpec {
  import collection.JavaConversions._

  def makeQuery(t: Term): Query = new TermQuery(t)

  def makeSearcher(directory: Directory): IndexSearcher = new IndexSearcher(DirectoryReader.open(directory));

  "not analyzed field" should "only match full equals word" in {
      val directory = new RAMDirectory()
      val conf = new IndexWriterConfig(Version.LUCENE_43, new SimpleAnalyzer(Version.LUCENE_43));
      val writer =  new IndexWriter(directory, conf);

      val doc = new Document();
      doc.add(new Field("partnum", "Q36", Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS));
      doc.add(new Field("description", "Illidium Space Modulator", Field.Store.YES, Field.Index.ANALYZED));
      writer.addDocument(doc);
      writer.close();
      val reader = DirectoryReader.open(directory);
      val searcher = makeSearcher(directory)

      assert(TestUtil.hitCount(searcher, makeQuery(new Term("partnum", "Q36"))) == 1)
      // analyzeしていないのでヒットしない。
      assert(TestUtil.hitCount(searcher, new QueryParser(Version.LUCENE_43, "description", new SimpleAnalyzer(Version.LUCENE_43)).parse( "partnum:Q36 AND Space")) == 0)
  }

  "analyzed field" should "mutlch different kind of field but dont mutch partnum" in {
      val directory = new RAMDirectory()
      val conf = new IndexWriterConfig(Version.LUCENE_43, new SimpleAnalyzer(Version.LUCENE_43));
      val writer =  new IndexWriter(directory, conf);

      val doc = new Document();
      doc.add(new Field("partnum", "Q36", Field.Store.NO, Field.Index.ANALYZED));
      doc.add(new Field("description", "Illidium Space Modulator", Field.Store.YES, Field.Index.ANALYZED));
      writer.addDocument(doc);
      writer.close();
      val reader = DirectoryReader.open(directory);
      val searcher = makeSearcher(directory)

      assert(TestUtil.hitCount(searcher, makeQuery(new Term("partnum", "Q36"))) == 0)
      assert(TestUtil.hitCount(searcher, makeQuery(new Term("partnum", "q"))) == 1)
      assert(TestUtil.hitCount(searcher, new QueryParser(Version.LUCENE_43, "description", new     SimpleAnalyzer(Version.LUCENE_43)).parse( "partnum:Q36 AND Space")) == 1)
  }

  "PerFieldAnalyzer" should "mutch different kind of fields" in {
      val directory = new RAMDirectory()
      val conf = new IndexWriterConfig(Version.LUCENE_43, new SimpleAnalyzer(Version.LUCENE_43));
      val writer =  new IndexWriter(directory, conf);

      val doc = new Document();
      doc.add(new Field("partnum", "Q36", Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS));
      doc.add(new Field("description", "Illidium Space Modulator", Field.Store.YES, Field.Index.ANALYZED));
      writer.addDocument(doc);
      writer.close();
      val reader = DirectoryReader.open(directory);
      val searcher = makeSearcher(directory)

      val map = Map("partnum" -> new KeywordAnalyzer(), "description" -> new SimpleAnalyzer(Version.LUCENE_43))
      val analyzer = new PerFieldAnalyzerWrapper(new SimpleAnalyzer(Version.LUCENE_43), map)

      assert(TestUtil.hitCount(searcher, new QueryParser(Version.LUCENE_43, "description", analyzer).parse( "partnum:Q36 AND Space")) == 1)
      assert(TestUtil.hitCount(searcher, makeQuery(new Term("partnum", "Q36"))) == 1)
  }
}

selenium

めんどくさがりなのでブラウザのテストとか自動化したいです。 せっかくなのでscalaで実装してみました。

サンプルでgoogle様を使わせていただきます。

まずはこういうものを用意します。

def using(f: WebDriver => Unit)(implicit url: String): Unit = { 
  val driver =  new FirefoxDriver
  driver.get(url)
  try {
    f(driver)
  } finally {
    driver.quit
  } 
}

こんなかんじでテストできます。

implicit val url = "http://www.google.co.jp"
"top page" should "have title" in {
  using { implicit driver =>
    assert(driver.getTitle == "Google")
  } 
}

続いて検索結果。 こういうものを作りました。

def waitForLoading(f: Unit => Unit)(p: Unit => Boolean)(implicit driver: WebDriver) : Unit = {
  val end = System.currentTimeMillis() + 5000
  val current = driver.getCurrentUrl
  f()
  while (System.currentTimeMillis() < end && !p()) {
  }
}

ajaxで書き換えが終わるまで待ってくれます。

テストはこんなかんじ。

"search by a" should "return 10 results" in {
  using { implicit driver =>
  driver.findElement(By.name("q")).sendKeys("a")
  waitForLoading { _ =>
    driver.findElement(By.name("q")).submit()
  } { _ =>
    !driver.findElements(By.className("g")).isEmpty
  } 

  assert(driver.findElements(By.className("g")).size == 10)
  }
}

検索の実行はこれです

driver.findElement(By.name("q")).submit()

ただ検索結果の描画を待たないと行けないのでその条件を

!driver.findElements(By.className("g")).isEmpty

で指定しています。

sbtlaunch-jarも一緒にコミットしておけば
チェックアウト -> sbt -> test
の3ステップで実行できるのでとってもお手軽。

rubyとかだと実行環境準備しないといけないし、javaだとコード編集するのがめんどくさい。
(↑javaの会社なんです。。)   

こういう活動を通して社内でscalaの布教を進めたいと思います。

IndexReaderはOpenした時点でのViewを検索する。

らしいです。 By Lucen In Action 2.11.1

/**
 * IndexReaderはopenした時点でのインデックスを検索する。
 * Readerをopenした時点でのviewを更新する。
 * インデックスを更新しても反映されない。
 * 
 * Writerの反映を検索したければReaderを再オープンしなければならない。
 */
@Test
public void testIndexReaderSearchPointOfViewWhenReaderOpen() throws IOException {
    IndexReader reader = DirectoryReader.open(directory);
    IndexSearcher searcher = new IndexSearcher(reader);
    Term t = new Term("id", "3");
    Query query = new TermQuery(t);

    // 事前にデータが入っていない事を確認。
    assertThat(TestUtil.hitCount(searcher, query), is(0));

    IndexWriter writer = getWriter();
    Document document = new Document();
    document.add(new StringField("id", "3",  Store.YES));

    writer.addDocument(document);

    // addしてもかわらない。
    assertThat(TestUtil.hitCount(searcher, query), is(0));

    writer.commit();

    // commitした後だがreaderを再作成していないのでかわらない。
    // ReaderはOpenしたときのViewを検索している事の確認。
    assertThat(TestUtil.hitCount(searcher, query), is(0));

    reader = DirectoryReader.open(directory);
    searcher = new IndexSearcher(reader);
    t = new Term("id", "3");
    query = new TermQuery(t);
    // Readerを再オープンして更新を検索できる事を確認。
    assertThat(TestUtil.hitCount(searcher, query), is(1));
}

Lucene4でMaxFieldLengthはどこいった?

Lucen In Action 2.7 Field truncationをみて実験しようとしたらMaxFieldLengthなんて見つからないorz

よくある事だけど非推奨どころか存在自体なくなってたよ。

代わりはLimitTokenCountAnalyzerでした。

@Test
public void testMaxFieldLength() throws Exception {
    int maxTokenCount = 2;
    IndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_43
            , new LimitTokenCountAnalyzer(new WhitespaceAnalyzer(Version.LUCENE_43), maxTokenCount));

    IndexWriter writer = new IndexWriter(directory, conf);
    Document document = new Document();
    document.add(new Field("contents", "Lucene In Action", Field.Store.YES, Field.Index.ANALYZED));
    writer.addDocument(document);
    writer.close();

    assertThat(getHitCount("contents", "Lucene"), is(1));
    assertThat(getHitCount("contents", "In"), is(1));
    // maxが2なので3番目の要素はヒットしない。
    assertThat(getHitCount("Action", "In"), is(0));

}

MaxFieldLengthが原因でヒットしない場合って原因調査がとっても大変。
間違って使っちゃわないように使うのを難しくしたみたいです。

propertyファイルの値をjspに出力したい

Controllerで値をセットしてもいいけど色んなところで表示されるとめんどくさいとき。

keyとvalueを直接指定したい場合はこんな感じの定義を追加。

<bean id="applicationProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
  <property name="singleton" value="true" />
  <property name="properties">
    <props>
      <prop key="hoge">fuga</prop>
    </props>
  </property>
</bean>

jspではこんな感じで使えます。

<spring:eval expression="@applicationProperties.getProperty('hoge')" />

別にpropertyファイルがある場合はそれを指定する事も可能。

<bean id="applicationProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
  <property name="singleton" value="true" />
  <property name="locations">
    <list>
      <value>classpath*:META-INF/spring/application.properties</value>
    </list>
  </property>

FieldをANALYZEDにするかNOT_ANALYZEDにするか

最近Lucen In Actionをよんでいるので再びLuceneネタ。 ver4ではTextFieldとかStringFieldになっているので実は隠れてしまっているけど何が違うかというのを実験してみたので記録。

ANALYZEすると完全一致でヒットしなくなっちゃうので注意しないといけない。

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import java.io.*;

import org.apache.lucene.analysis.core.*;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.search.*;
import org.apache.lucene.store.*;
import org.apache.lucene.util.*;
import org.junit.*;


public class FieldOptionTest {


    private Directory directory;

    @Before
    public void setUp() throws Exception {
        directory = new RAMDirectory();
    }

    @Test
    public void testAnalyzedField() throws Exception {
        IndexWriter writer = getWriter();
        Document document = new Document();
        document.add(new Field("contents", "Lucene In Action", Field.Store.YES, Field.Index.ANALYZED));
        writer.addDocument(document);
        writer.close();


        // ANALYZEしているのでTOKENに分割して保存され検索できる。
        assertThat(getHitCount("contents", "Lucene"), is(1));
        assertThat(getHitCount("contents", "In"), is(1));
        assertThat(getHitCount("contents", "Action"), is(1));
        // 逆に分割しているのでヒットしない
        assertThat(getHitCount("contents", "Lucene In Action"), is(0));
    }

    @Test
    public void testNotAnalyzedField() throws Exception {
        IndexWriter writer = getWriter();
        Document document = new Document();
        document.add(new Field("contents", "Lucene In Action", Field.Store.YES, Field.Index.NOT_ANALYZED));
        writer.addDocument(document);
        writer.close();

        // ANALYZEしていないのでヒットしない。
        assertThat(getHitCount("contents", "Lucene"), is(0));
        assertThat(getHitCount("contents", "In"), is(0));
        assertThat(getHitCount("contents", "Action"), is(0));

        // ANALYZEせずそのまま保存されているのでヒットする。
        // URLやファイルシステムのパス、何らかのIDとか電話番号等完全一致で検索したい場合はanalyzeしてはいけない。
        assertThat(getHitCount("contents", "Lucene In Action"), is(1));
    }

    private IndexWriter getWriter() throws IOException {
        IndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_43, new WhitespaceAnalyzer(Version.LUCENE_43));
       return new IndexWriter(directory, conf);
   }

    private int getHitCount(String fieldName, String searchString) throws IOException {
        IndexReader reader = DirectoryReader.open(directory);
        IndexSearcher searcher = new IndexSearcher(reader);

        Term t = new Term(fieldName, searchString);
        Query query = new TermQuery(t);
        int hitCount = TestUtil.hitCount(searcher, query);
        reader.close();
        return hitCount;
    }
}

Lucene ver3とver4の違い

Lucene In Acionのサンプル試したところVer4とVer3で大きくAPIがかわっていたのでメモ。

IndexReader

ver3

Directory directory = new RAMDirectory();
IndexReader reader = IndexReader.open(directory);

ver4

Directory directory = new RAMDirectory();
IndexReader reader = DirectoryReader.open(directory);

IndexReader.openは非推奨になってました。

IndexWriter

ver3

Directory directory = new RAMDirectory();
IndexWriter writer = new IndexWriter(directory, new WhitespaceAnalyzer(),IndexWriter.MaxFieldLength.UNLIMITED));

ver4

Directory directory = new RAMDirectory();
IndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_43, new WhitespaceAnalyzer(Version.LUCENE_43));
IndexWriter writer = IndexWriter(directory, conf);

こちらは非推奨どころか完全に別もの。 AnalyzerとかいろいろLuceneのバージョンが必要になってる。

毎回書くのめんどい。 ScalaならImplicit Parameterとか使えばいいけど、Javaで奇麗にやる方法って何かないかなぁ。

optimize() ver3

IndexWriter.optimize();

ver4

IndexWriter.forceMerge(int);

これまた完全に別もの。 forceMergeのdocumentににると「恐ろしいほどコストがかかるから」「インデックスの変更がない(static)場合に呼びなさい」って書いてある。

Luceneの性能があがったからoptimizeしなくても大丈夫。
->optimizeって名前だととりあえず呼んじゃう人もいる。
->このメソッド実はめちゃくちゃ重い。
->よし、名前を変えよう!
という事でしょうか???

https://issues.apache.org/jira/browse/LUCENE-3454