/* * Hibernate Search, full-text search for your domain model * * License: GNU Lesser General Public License (LGPL), version 2.1 or later * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.search.test.shards; import static org.fest.assertions.Assertions.assertThat; import static org.junit.Assert.assertEquals; import java.io.Serializable; import java.nio.file.Path; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import org.apache.lucene.document.Document; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.MatchAllDocsQuery; import org.hibernate.Criteria; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.cfg.Configuration; import org.hibernate.criterion.Projections; import org.hibernate.criterion.Property; import org.hibernate.search.FullTextSession; import org.hibernate.search.Search; import org.hibernate.search.annotations.DocumentId; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Indexed; import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator; import org.hibernate.search.engine.service.spi.ServiceManager; import org.hibernate.search.engine.spi.EntityIndexBinding; import org.hibernate.search.hcore.impl.HibernateSessionFactoryService; import org.hibernate.search.indexes.spi.IndexManager; import org.hibernate.search.query.engine.spi.HSQuery; import org.hibernate.search.spi.BuildContext; import org.hibernate.search.store.ShardIdentifierProviderTemplate; import org.hibernate.search.test.SearchTestBase; import org.hibernate.search.testsupport.TestConstants; import org.hibernate.search.testsupport.TestForIssue; import org.junit.Before; import org.junit.Test; /** * @author Emmanuel Bernard */ public class DynamicShardingTest extends SearchTestBase { private Animal elephant; private Animal spider; private Animal bear; @Override @Before public void setUp() throws Exception { super.setUp(); elephant = new Animal(); elephant.setId( 1 ); elephant.setName( "Elephant" ); elephant.setType( "Mammal" ); spider = new Animal(); spider.setId( 2 ); spider.setName( "Spider" ); spider.setType( "Insect" ); bear = new Animal(); bear.setId( 3 ); bear.setName( "Bear" ); bear.setType( "Mammal" ); } @Test public void testDynamicCreationOfShards() throws Exception { EntityIndexBinding entityIndexBinding = getExtendedSearchIntegrator().getIndexBindings().get( Animal.class ); assertThat( entityIndexBinding.getIndexManagers() ).hasSize( 0 ); insert( elephant ); assertThat( entityIndexBinding.getIndexManagers() ).hasSize( 1 ); insert( spider ); assertThat( entityIndexBinding.getIndexManagers() ).hasSize( 2 ); insert( bear ); assertThat( entityIndexBinding.getIndexManagers() ).hasSize( 2 ); assertEquals( 2, getNumberOfDocumentsInIndex( "Animal.Mammal" ) ); assertEquals( 1, getNumberOfDocumentsInIndex( "Animal.Insect" ) ); } @Test public void testDynamicShardsAreTargetingInQuery() throws Exception { insert( elephant, spider, bear ); Session session = openSession(); Transaction tx = session.beginTransaction(); FullTextSession fts = Search.getFullTextSession( session ); QueryParser parser = new QueryParser( "id", TestConstants.stopAnalyzer ); List results = fts.createFullTextQuery( parser.parse( "name:bear OR name:elephant OR name:spider" ) ).list(); assertEquals( "Either double insert, single update, or query fails with shards", 3, results.size() ); tx.commit(); session.close(); } @Test public void testInitialiseDynamicShardsOnStartup() throws Exception { EntityIndexBinding entityIndexBinding = getExtendedSearchIntegrator().getIndexBindings().get( Animal.class ); assertThat( entityIndexBinding.getIndexManagers() ).hasSize( 0 ); insert( elephant, spider, bear ); assertThat( entityIndexBinding.getIndexManagers() ).hasSize( 2 ); assertThat( getIndexManagersAfterReopening() ).hasSize( 2 ); } @Test public void testDeletion() throws Exception { insert( elephant, spider, bear ); assertEquals( 2, getNumberOfDocumentsInIndex( "Animal.Mammal" ) ); assertEquals( 1, getNumberOfDocumentsInIndex( "Animal.Insect" ) ); deleteAnimal( elephant ); assertEquals( 1, getNumberOfDocumentsInIndex( "Animal.Mammal" ) ); assertEquals( 1, getNumberOfDocumentsInIndex( "Animal.Insect" ) ); } @Test @TestForIssue(jiraKey = "HSEARCH-2662") public void testQueryWhenEmpty() throws Exception { HSQuery query = getExtendedSearchIntegrator().createHSQuery( new MatchAllDocsQuery(), Animal.class ); assertEquals( 0, query.queryResultSize() ); SomeOtherEntity someOtherIndexedObject = new SomeOtherEntity(); insert( someOtherIndexedObject ); HSQuery queryAgain = getExtendedSearchIntegrator().createHSQuery( new MatchAllDocsQuery(), Animal.class ); assertEquals( 0, queryAgain.queryResultSize() ); } @Override public void configure(Map<String,Object> cfg) { cfg.put( "hibernate.search.Animal.sharding_strategy", AnimalShardIdentifierProvider.class.getName() ); // use filesystem based directory provider to be able to assert against index cfg.put( "hibernate.search.default.directory_provider", "filesystem" ); Path sub = getBaseIndexDir(); cfg.put( "hibernate.search.default.indexBase", sub.toAbsolutePath().toString() ); } @Override public Class<?>[] getAnnotatedClasses() { return new Class<?>[] { Animal.class, SomeOtherEntity.class }; } private void insert(Object... entities) { try ( Session session = openSession() ) { Transaction tx = session.beginTransaction(); for ( Object entity : entities ) { session.persist( entity ); } tx.commit(); } } private void deleteAnimal(Object entity) { try (Session session = openSession()) { Transaction tx = session.beginTransaction(); session.delete( entity ); tx.commit(); } } private IndexManager[] getIndexManagersAfterReopening() { // build a new independent SessionFactory to verify that the shards are available at restart Configuration config = new Configuration(); config.setProperty( "hibernate.search.Animal.sharding_strategy", DynamicShardingTest.AnimalShardIdentifierProvider.class.getName() ); // use filesystem based directory provider to be able to assert against index config.setProperty( "hibernate.search.default.directory_provider", "filesystem" ); Path sub = getBaseIndexDir(); config.setProperty( "hibernate.search.default.indexBase", sub.toAbsolutePath().toString() ); config.addAnnotatedClass( Animal.class ); try ( SessionFactory newSessionFactory = config.buildSessionFactory() ) { try ( FullTextSession fullTextSession = Search.getFullTextSession( newSessionFactory.openSession() ) ) { ExtendedSearchIntegrator integrator = fullTextSession.getSearchFactory().unwrap( ExtendedSearchIntegrator.class ); return integrator.getIndexBindings().get( Animal.class ).getIndexManagers(); } } } @Indexed @Entity private static class SomeOtherEntity { @DocumentId @Field(name = "idField") @Id @GeneratedValue private Integer id; protected SomeOtherEntity() { } } public static class AnimalShardIdentifierProvider extends ShardIdentifierProviderTemplate { @Override public String getShardIdentifier(Class<?> entityType, Serializable id, String idAsString, Document document) { if ( entityType.equals( Animal.class ) ) { final String typeValue = document.getField( "type" ).stringValue(); addShard( typeValue ); return typeValue; } throw new RuntimeException( "Animal expected but found " + entityType ); } @Override protected Set<String> loadInitialShardNames(Properties properties, BuildContext buildContext) { ServiceManager serviceManager = buildContext.getServiceManager(); SessionFactory sessionFactory = serviceManager.requestService( HibernateSessionFactoryService.class ).getSessionFactory(); Session session = sessionFactory.openSession(); try { Criteria initialShardsCriteria = session.createCriteria( Animal.class ); initialShardsCriteria.setProjection( Projections.distinct( Property.forName( "type" ) ) ); @SuppressWarnings("unchecked") List<String> initialTypes = initialShardsCriteria.list(); return new HashSet<String>( initialTypes ); } finally { session.close(); } } } }