Luceneを使ったアプリケーションで、(見る気になるかどうかはなんともですが)どうしてDocumentがヒットしたのか、スコアなどを見るにはExplationを利用します。
Hibernate Searchでも取得できるのかなと思ったら、こちらに記載がありました。
Understanding results
http://docs.jboss.org/hibernate/search/5.0/reference/en-US/html_single/#_understanding_results
Projectionを利用するようです。
では、こちらを試してみましょう。
準備
Maven依存関係。
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>4.3.8.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-search-orm</artifactId> <version>5.0.1.Final</version> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-kuromoji</artifactId> <version>4.10.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.34</version> <scope>runtime</scope> </dependency>
その他、テストコードでJUnitとAssertJを利用。
persistence.xmlは省略、デフォルトのAnalyzerはKuromojiのJapaneseAnalyzerとします。
Entityクラスは、このように。
src/main/java/org/littlewings/hibernate/explanation/Book.java
package org.littlewings.hibernate.explanation; import javax.persistence.*; import org.hibernate.search.annotations.*; @Entity @Table(name = "book") @Indexed public class Book { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column @Field private String title; @Column @Field private Integer price; @Column @Field private String summary; public Book() { } public Book(String title, Integer price, String summary) { this.title = title; this.price = price; this.summary = summary; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Integer getPrice() { return price; } public void setPrice(Integer price) { this.price = price; } public String getSummary() { return summary; } public void setSummary(String summary) { this.summary = summary; } }
Explanationを取得してみる
まずは、テストを書くための雛形コードを用意。
src/test/java/org/littlewings/hibernate/explanation/ExplanationTest.java
package org.littlewings.hibernate.explanation; import static org.assertj.core.api.Assertions.*; import java.util.*; import java.util.stream.*; import javax.persistence.*; import org.hibernate.search.jpa.*; import org.hibernate.search.query.dsl.*; import org.junit.*; public class ExplanationTest { private static EntityManager em; @BeforeClass public static void setUpClass() { EntityManagerFactory emf = Persistence.createEntityManagerFactory("search.pu"); em = emf.createEntityManager(); } @After public void tearDown() { EntityTransaction tx = em.getTransaction(); tx.begin(); em.createQuery("DELETE FROM Book").executeUpdate(); tx.commit(); } @AfterClass public static void tearDownClass() { EntityTransaction tx = em.getTransaction(); tx.begin(); em.createNativeQuery("TRUNCATE TABLE book").executeUpdate(); tx.commit(); } private void addBooks(Book... books) { EntityTransaction tx = em.getTransaction(); tx.begin(); Arrays.asList(books).stream().forEach(em::persist); tx.commit(); } // ここに、テストコードを書く }
テストコードを書くといっても、さすがにExplanationの結果をassertしてもあまり嬉しくないので、その点は標準出力に今回は書き出すこととします。
では、Explanationを取得してみます。
まずはEntityを登録。
addBooks(new Book("はじめてのSpring Boot 「Spring Framework」で簡単Javaアプリ開発", 2700, "Spring"), new Book("高速スケーラブル検索エンジン ElasticSearch Server", 3024, "全文検索"), new Book("[改訂新版] Apache Solr入門 〜オープンソース全文検索エンジン", 3888, "全文検索"), new Book("Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築", 3200, "全文検索"), new Book("Javaエンジニア養成読本 [現場で役立つ最新知識、満載!]", 2138, "Java"), new Book("改訂2版 パーフェクトJava", 3486, "Java"));
これに対して、クエリを組み立てます。
FullTextEntityManager ftem = Search.getFullTextEntityManager(em); QueryBuilder queryBuilder = ftem.getSearchFactory().buildQueryBuilder().forEntity(Book.class).get(); org.apache.lucene.search.Query luceneQuery = queryBuilder.keyword().onField("title").matching("全文検索 Lucene").createQuery(); Query jpaQuery = ftem .createFullTextQuery(luceneQuery, Book.class) .setProjection(FullTextQuery.DOCUMENT_ID, FullTextQuery.DOCUMENT, FullTextQuery.EXPLANATION, FullTextQuery.SCORE, FullTextQuery.OBJECT_CLASS, FullTextQuery.SPATIAL_DISTANCE, FullTextQuery.THIS);
この時、FullTextQueryに対してProjectionの設定を行います。
Projectionは、Entityの一部のフィールドを取得する時に使うものだと思っていましたが、こういうケースでも使うんですねぇ…。
何が取得できるかどうかは、上記の通りFullTextQueryに定義されている…ではなくて、正確にはFullTextQueryが実装しているProjectionConstantsインターフェースに定義されているString定数を使用します。
結果は、なんとObjectの配列のListで返ってきます。
List<Object[]> results = jpaQuery.getResultList();
assertThat(results)
.hasSize(3);
この例では、登録したEntityに対してLuceneのクエリで3件ヒットします、と。
で、先ほどProjectionで指定した定数の説明ですが、変数に代入とかした方がわかりやすいかなぁと。
int documentId = (int)results.get(0)[0]; org.apache.lucene.document.Document document = (org.apache.lucene.document.Document)results.get(0)[1]; org.apache.lucene.search.Explanation explanation = (org.apache.lucene.search.Explanation)results.get(0)[2]; float score = (float)results.get(0)[3]; Class<Book> entityClass = (Class<Book>)results.get(0)[4]; Double spartialDistance = (Double)results.get(0)[5]; Book entity= (Book)results.get(0)[6];
まあ、一応書くと…
- DOCUMENT_ID … ヒットしたLuceneのDocumentのID
- DOCUMENT … ヒットしたLuceneのDocumentそのもの
- EXPLANATION … LuceneのExplanationクラスのインスタンス(このクエリのExplanation)
- SCORE … ヒットした際のスコア(Explanationのサマリと同じ)
- OBJECT_CLASS … EntityのClassクラス
- SPATIAL_DISTANCE … LuceneのGeospatial search API(http://lucene.apache.org/core/4_10_3/spatial/index.html)を使う時に、距離が得られる…?(今回未使用)
- THIS … ヒットしたEntity
という感じです。
ひとつだけassertしてみると、こんな感じ。
assertThat(documentId) .isEqualTo(3); assertThat(score) .isGreaterThan(0.95F); assertThat(spartialDistance) .isNull(); assertThat(entity.getTitle()) .isEqualTo("Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築");
今回のケースでは、SPATIAL_DISTANCEは取得できないのでnullです。
投げたLuceneのクエリは、こちらでしたね。
org.apache.lucene.search.Query luceneQuery = queryBuilder.keyword().onField("title").matching("全文検索 Lucene").createQuery();
あとは、他にヒットした結果を含めて、Explanationを出力してみましょう。
results .stream() .forEach(result -> { System.out.println("== Book Title[" + ((Book)result[6]).getTitle() + "]"); System.out.println(result[2]); });
どれがどれかわからなくならないように、ヒットしたBookのタイトルを付けて…。
結果は、このように(※そういえばソートしてない…)。
== Book Title[Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築] 0.95023906 = (MATCH) sum of: 0.29461613 = (MATCH) weight(title:全文 in 3) [DefaultSimilarity], result of: 0.29461613 = score(doc=3,freq=1.0), product of: 0.5568161 = queryWeight, product of: 1.6931472 = idf(docFreq=2, maxDocs=6) 0.32886457 = queryNorm 0.5291085 = fieldWeight in 3, product of: 1.0 = tf(freq=1.0), with freq of: 1.0 = termFreq=1.0 1.6931472 = idf(docFreq=2, maxDocs=6) 0.3125 = fieldNorm(doc=3) 0.20300525 = (MATCH) weight(title:検索 in 3) [DefaultSimilarity], result of: 0.20300525 = score(doc=3,freq=1.0), product of: 0.4622077 = queryWeight, product of: 1.4054651 = idf(docFreq=3, maxDocs=6) 0.32886457 = queryNorm 0.43920785 = fieldWeight in 3, product of: 1.0 = tf(freq=1.0), with freq of: 1.0 = termFreq=1.0 1.4054651 = idf(docFreq=3, maxDocs=6) 0.3125 = fieldNorm(doc=3) 0.4526177 = (MATCH) weight(title:lucene in 3) [DefaultSimilarity], result of: 0.4526177 = score(doc=3,freq=1.0), product of: 0.69015926 = queryWeight, product of: 2.0986123 = idf(docFreq=1, maxDocs=6) 0.32886457 = queryNorm 0.6558163 = fieldWeight in 3, product of: 1.0 = tf(freq=1.0), with freq of: 1.0 = termFreq=1.0 2.0986123 = idf(docFreq=1, maxDocs=6) 0.3125 = fieldNorm(doc=3) == Book Title[[改訂新版] Apache Solr入門 〜オープンソース全文検索エンジン] 0.3317476 = (MATCH) product of: 0.4976214 = (MATCH) sum of: 0.29461613 = (MATCH) weight(title:全文 in 2) [DefaultSimilarity], result of: 0.29461613 = score(doc=2,freq=1.0), product of: 0.5568161 = queryWeight, product of: 1.6931472 = idf(docFreq=2, maxDocs=6) 0.32886457 = queryNorm 0.5291085 = fieldWeight in 2, product of: 1.0 = tf(freq=1.0), with freq of: 1.0 = termFreq=1.0 1.6931472 = idf(docFreq=2, maxDocs=6) 0.3125 = fieldNorm(doc=2) 0.20300525 = (MATCH) weight(title:検索 in 2) [DefaultSimilarity], result of: 0.20300525 = score(doc=2,freq=1.0), product of: 0.4622077 = queryWeight, product of: 1.4054651 = idf(docFreq=3, maxDocs=6) 0.32886457 = queryNorm 0.43920785 = fieldWeight in 2, product of: 1.0 = tf(freq=1.0), with freq of: 1.0 = termFreq=1.0 1.4054651 = idf(docFreq=3, maxDocs=6) 0.3125 = fieldNorm(doc=2) 0.6666667 = coord(2/3) == Book Title[高速スケーラブル検索エンジン ElasticSearch Server] 0.081202105 = (MATCH) product of: 0.2436063 = (MATCH) sum of: 0.2436063 = (MATCH) weight(title:検索 in 1) [DefaultSimilarity], result of: 0.2436063 = score(doc=1,freq=1.0), product of: 0.4622077 = queryWeight, product of: 1.4054651 = idf(docFreq=3, maxDocs=6) 0.32886457 = queryNorm 0.5270494 = fieldWeight in 1, product of: 1.0 = tf(freq=1.0), with freq of: 1.0 = termFreq=1.0 1.4054651 = idf(docFreq=3, maxDocs=6) 0.375 = fieldNorm(doc=1) 0.33333334 = coord(1/3)
1件目でassertしていたスコアは
assertThat(score)
.isGreaterThan(0.95F);
この部分が該当します。
== Book Title[Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築] 0.95023906 = (MATCH) sum of:
あと、Explanationの読み方自体は、(怪しいですが)以前書いたことがあります。
Luceneのスコア計算式を表示する
http://d.hatena.ne.jp/Kazuhira/20130806/1375802797
よろしければ、どうぞ。