JPA（Hibernate）で全文検索する

JPA（Hibernate）を使って全文検索をやってみる。
Hibernate-Searchで永続化したデータを全文検索する。

## build.gradle

そのままだとMETA-INF/persistence.xmlが読み込まれないのでresourcesの出力先をbuild/classes/mainにしておく。

```groovy:build.gradle
apply plugin: 'idea'
apply plugin: 'groovy'
apply plugin: 'application'

def defaultEncoding = 'UTF-8'
def jdkVersion = '1.7'

sourceCompatibility = jdkVersion
targetCompatibility = jdkVersion

mainClassName = 'JpaLucene'

repositories {
    mavenCentral()
}

sourceSets {
    main {
        output.resourcesDir = 'build/classes/main'
    }
}

dependencies {
    compile 'org.codehaus.groovy:groovy-all:2.1.7'
    compile 'org.hibernate:hibernate-entitymanager:4.2.6.Final'
    compile 'org.hibernate:hibernate-search:4.4.0.Final'
    compile 'org.slf4j:slf4j-simple:1.7.5'
    compile 'javassist:javassist:3.12.1.GA'
    compile 'com.h2database:h2:1.3.173'
    compile 'com.google.inject:guice:3.0'
    compile 'com.google.inject.extensions:guice-persist:3.0'
}

compileGroovy {
    groovyOptions.encoding = defaultEncoding
}

idea {
    project {
        jdkName = jdkVersion
        languageLevel = jdkVersion
    }
}
```

## persistence.xml

src/main/resourcesにMETA-INF/persistence.xmlを作る。
hibernate.search.default.indexBaseはHibernate-Search用のプロパティで全文検索のインデックスの保存先を指定する。
ここで指定しなかった場合はプロジェクトの直下にインデックスが作成される。

```xml:persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
             xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
                       http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
    <persistence-unit name="example" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:example" />
            <property name="javax.persistence.jdbc.user" value="sa" />
            <property name="javax.persistence.jdbc.password" value="" />
            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.hbm2ddl.auto" value="update" />
            <property name="hibernate.search.default.indexBase" value="indexes" />
        </properties>
    </persistence-unit>
</persistence>
```

## コードを書く

トランザクションはGuice-Persistで管理する。EntityManagerじゃなくてSessionを使う場合はWarp-Persistを使う。あとSessionでやるとHibernate-ConfigurationでXMLレスにできる。
処理の最後にデータを全削除しているのはインメモリデータベースとインデックスに差異が出ないようにするため。処理開始時にインデックスの再作成をしてもいいだろうけどなんかかっこ悪い。
PersistService#startを実行したらきちんとPersistService#stopを呼び出してあげないとHibernate-Searchで作成されたインデックスが解放されずにプログラムが終了しないので注意。

```groovy:JpaLucene.groovy
import com.google.inject.*
import com.google.inject.persist.*
import com.google.inject.persist.jpa.JpaPersistModule
import org.hibernate.search.annotations.*
import org.hibernate.search.jpa.*

import javax.inject.Inject
import javax.persistence.*

class JpaLucene {
    @Inject BookDao bookDao
    @Inject AuthorDao authorDao
    @Inject BookSearcher bookSearcher

    def storeAndSearch() {
        // store some books
        def names1 = ['Dierk', 'Guillaume', 'Jon', 'Andy', 'Paul']
        def names2 = ['Craig', 'Groovy']
        def authors1 = authorDao.storeAuthors(names1)
        def authors2 = authorDao.storeAuthors(names2)
        bookDao.store(new Book(authors:authors1, title:'Groovy in Action'))
        bookDao.store(new Book(authors:authors2, title:'Spring in Action'))

        // find some books
        bookSearcher.search('Groovy')

        // delete all books
        bookDao.deleteAll()
        authorDao.deleteAll()   // BookDao#deleteAllで全部削除されるので呼ばなくてもいい
    }

    def static main(args) {
        def injector = Guice.createInjector(new AbstractModule() {
            @Override
            protected void configure() {
                install(new JpaPersistModule('example'))
                bind(JpaLuceneInitializer).asEagerSingleton()
            }
        })
        def initializer = injector.getInstance(JpaLuceneInitializer)
        def jpaLucene = injector.getInstance(JpaLucene)
        jpaLucene.storeAndSearch()
        initializer.stop()
    }
}

@Entity
@Indexed
class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public Long id
    @OneToMany(cascade=CascadeType.ALL)
    @IndexedEmbedded
    public Set<Author> authors
    @Field
    public String title
    String toString() { "$title by ${authors.name.join(', ')}" }
}

@Entity
@Indexed
class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public Long id
    @Field
    public String name
}

class BookDao {
    @Inject
    EntityManager em

    @Transactional
    def store(book) {
        em.persist(book)
    }

    @Transactional
    def deleteAll() {
        em.createQuery('from Book', Book).resultList.each {
            em.remove(it)
        }
    }
}

class AuthorDao {
    @Inject
    EntityManager em

    @Transactional
    def storeAuthors(authors) {
        Set result = []
        authors.each{ def a = new Author(name:it); em.persist(a); result << a }
        result
    }

    @Transactional
    def deleteAll() {
        em.createQuery('from Author', Author).resultList.each {
            em.remove(it)
        }
    }
}

class BookSearcher {
    FullTextEntityManager fullTextEntityManager

    @Inject
    BookSearcher(EntityManager em) {
        fullTextEntityManager = Search.getFullTextEntityManager(em)
    }

    def search(value) {
        def qb = fullTextEntityManager.searchFactory.buildQueryBuilder().forEntity(Book).get()
        def query = qb.keyword().onFields('title', 'authors.name').matching(value).createQuery()
        def books = fullTextEntityManager.createFullTextQuery(query, Book).resultList
        println 'Found ' + books.size() + ' books:'
        books.each { println it }
    }
}

class JpaLuceneInitializer {
    PersistService service

    @Inject
    JpaLuceneInitializer(PersistService service) {
        this.service = service
        this.service.start()
    }

    def stop() {
        service.stop()
    }
}
```

## 実行結果

タイトルと著者に「Groovy」を含むデータ（用意したデータ2件とも）が検索されている。

```
10 25, 2013 11:53:16 午後 org.hibernate.annotations.common.reflection.java.JavaReflectionManager <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {4.0.4.Final}
10 25, 2013 11:53:16 午後 org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {4.2.6.Final}
10 25, 2013 11:53:16 午後 org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
10 25, 2013 11:53:16 午後 org.hibernate.cfg.Environment buildBytecodeProvider
INFO: HHH000021: Bytecode provider name : javassist
10 25, 2013 11:53:17 午後 org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000402: Using Hibernate built-in connection pool (not for production use!)
10 25, 2013 11:53:17 午後 org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000115: Hibernate connection pool size: 20
10 25, 2013 11:53:17 午後 org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000006: Autocommit mode: true
10 25, 2013 11:53:17 午後 org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000401: using driver [org.h2.Driver] at URL [jdbc:h2:mem:example]
10 25, 2013 11:53:17 午後 org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl configure
INFO: HHH000046: Connection properties: {user=sa, autocommit=true, release_mode=auto}
10 25, 2013 11:53:17 午後 org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
10 25, 2013 11:53:18 午後 org.hibernate.engine.transaction.internal.TransactionFactoryInitiator initiateService
INFO: HHH000268: Transaction strategy: org.hibernate.engine.transaction.internal.jdbc.JdbcTransactionFactory
10 25, 2013 11:53:18 午後 org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory <init>
INFO: HHH000397: Using ASTQueryTranslatorFactory
10 25, 2013 11:53:18 午後 org.hibernate.search.Version <clinit>
INFO: HSEARCH000034: Hibernate Search 4.4.0.Final
10 25, 2013 11:53:23 午後 org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000228: Running hbm2ddl schema update
10 25, 2013 11:53:23 午後 org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000102: Fetching database metadata
10 25, 2013 11:53:23 午後 org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000396: Updating schema
10 25, 2013 11:53:23 午後 org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Author
10 25, 2013 11:53:23 午後 org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Book
10 25, 2013 11:53:23 午後 org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Book_Author
10 25, 2013 11:53:23 午後 org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Author
10 25, 2013 11:53:23 午後 org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Book
10 25, 2013 11:53:23 午後 org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Book_Author
10 25, 2013 11:53:23 午後 org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Author
10 25, 2013 11:53:23 午後 org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Book
10 25, 2013 11:53:23 午後 org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Book_Author
10 25, 2013 11:53:23 午後 org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000232: Schema update complete
10 25, 2013 11:53:23 午後 org.hibernate.search.impl.ConfigContext getLuceneMatchVersion
WARN: HSEARCH000075: Configuration setting hibernate.search.lucene_version was not specified, using LUCENE_CURRENT.
Hibernate: insert into Author (id, name) values (null, ?)
Hibernate: insert into Author (id, name) values (null, ?)
Hibernate: insert into Author (id, name) values (null, ?)
Hibernate: insert into Author (id, name) values (null, ?)
Hibernate: insert into Author (id, name) values (null, ?)
Hibernate: insert into Author (id, name) values (null, ?)
Hibernate: insert into Author (id, name) values (null, ?)
Hibernate: insert into Book (id, title) values (null, ?)
Hibernate: insert into Book_Author (Book_id, authors_id) values (?, ?)
Hibernate: insert into Book_Author (Book_id, authors_id) values (?, ?)
Hibernate: insert into Book_Author (Book_id, authors_id) values (?, ?)
Hibernate: insert into Book_Author (Book_id, authors_id) values (?, ?)
Hibernate: insert into Book_Author (Book_id, authors_id) values (?, ?)
Hibernate: insert into Book (id, title) values (null, ?)
Hibernate: insert into Book_Author (Book_id, authors_id) values (?, ?)
Hibernate: insert into Book_Author (Book_id, authors_id) values (?, ?)
Hibernate: select this_.id as id1_1_0_, this_.title as title2_1_0_ from Book this_ where (this_.id in (?, ?))
Found 2 books:
Groovy in Action by Andy, Paul, Jon, Dierk, Guillaume
Spring in Action by Craig, Groovy
Hibernate: select book0_.id as id1_1_, book0_.title as title2_1_ from Book book0_
Hibernate: delete from Book_Author where Book_id=?
Hibernate: delete from Book_Author where Book_id=?
Hibernate: delete from Author where id=?
Hibernate: delete from Author where id=?
Hibernate: delete from Author where id=?
Hibernate: delete from Author where id=?
Hibernate: delete from Author where id=?
Hibernate: delete from Book where id=?
Hibernate: delete from Author where id=?
Hibernate: delete from Author where id=?
Hibernate: delete from Book where id=?
Hibernate: select author0_.id as id1_0_, author0_.name as name2_0_ from Author author0_
10 25, 2013 11:53:26 午後 org.hibernate.service.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH000030: Cleaning up connection pool [jdbc:h2:mem:example]
```