9 listopada 2007

Hibernate Search, czyli wyszukiwanie pełnotekstowe encji Hibernate

Zacznijmy od początku, czyli co to jest wyszukiwanie pełnotekstowe? Wyobraźmy sobie, że implementujemy funkcjonalność wyszukiwania produktów w sklepie internetowym. Nie chodzi jednak o jakieś formularze gdzie podajemy wartości dla ściśle określonych atrybutów tylko o wyszukanie na podstawie opisu tekstowego produktu z użyciem słów kluczowych. Chcemy więc mieć taki mały Google który wyszukuje wśród naszych obiektów, a mówiąc ściślej encji (ang. entity) Hibernate. Istnieją co najmniej dwie rozsądne metody implementacji takiej funkcjonalności, tzn. ja znam dwie: Hibernate Search oraz użycie odpowiedniego indeksu bazy danych (FULLTEXT INDEX). W niniejszym artykule pokażę, jak zbudować prostą aplikację na bazie Hibernate Search. Za punkt wyjścia przyjmę aplikację, którą przedstawiłem w artykule "Aplikacja Hibernate z adnotacjami i Spring'iem", ale de facto nada się każda aplikacja Hibernate z odwzorowaniem obiektowo-relacyjnym zdefiniowanym w formie adnotacji. Zanim jednak przejdę do czynu dwa słowa o tej technologii. Otóż Hibernate Search to nic innego jak integracja Apache Lucene z Hibernate Core w ten sposób, aby operacje na encjach były odwzorowane na odpowiednie operacje na indeksie Lucene. Dodatkowo, efektem wyszukiwania w indeksie Lucene nie jest – dajmy na to – lista identyfikatorów, lecz lista encji zarządzanych Hibernate. Indeks właśnie jest najważniejszym elementem technologii Lucene. Zawiera on informacje o dokumentach Lucene i umożliwia sprawne wyszukiwanie tych pasujących do naszego zapytania. O dokumentach tych możemy myśleć jak o obiektach. Podstawowe operacje API Lucene to dodanie dokumentu do indeksu, usunięcie go i modyfikacja. To są właśnie operacje wywoływane automatycznie przez Hibernate Search ilekroć encje – które zdefiniowaliśmy jako indeksowane – są zapisywane, usuwane bądź modyfikowane. To rzekłszy przechodzę do implementacji.

Zacznijmy od pobrania biblioteki Hibernate Search (ja użyłem wersji 3.0.0 GA) i dodania jej .jar’ów (hibernate-search.jar i lucene-core-2.2.0.jar) do projektu. Kolejnym krokiem jest konfiguracja indeksu Lucene. Rzecz jest trywialna i sprowadza się do określenia – w formie właściwości (ang. property) Hibernate – klasy implementującej dostawcę indeksu oraz kolejnego parametru wymaganego dla dostawcy użytego w naszej aplikacji. Ja używam Hibernate w połączeniu ze Spring’iem a więc modyfikuję ziarno zarządzane Spring’a definiujące fabrykę sesji Hibernate. Powinno ono wyglądać następująco (patrz artykuł "Aplikacja Hibernate z adnotacjami i Spring'iem"):

<bean id="hibernateSessionFactory"
class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">

<property name="dataSource" ref="hibernateDataSource" />
<property name="schemaUpdate" value="true" />
<property name="hibernateProperties">
<value>
hibernate.dialect = org.hibernate.dialect.MySQLDialect
hibernate.search.default.directory_provider =
org.hibernate.search.store.FSDirectoryProvider

hibernate.search.default.indexBase = c:/index
</value>
</property>
<property name="annotatedClasses">
<list>
<value>entity.Product</value>
</list>
</property>
</bean>

Jak można by się domyślać wartość ‘c:/index’ to katalog w którym będą przechowywane pliki stanowiące indeks. Weźmy teraz jedną z naszych encji Hibernate i za pomocą kilku adnotacji sprawmy by była indeksowana. Poniżej przykład dla klasy produktu:


@Entity @Indexed
public class Product {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@DocumentId
private Long productId;

@Field(index = Index.TOKENIZED, store = Store.NO)
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Long getProductId() {
return productId;
}

public void setProductId(Long productId) {
this.productId = productId;
}
}

Wszystkie adnotacje Hibernate Search pochodzą z pakietu org.hibernate.search.annotations. Pozostałe to adnotacje JPA (wykorzystywane także przez Hibernate). Adnotacja @Indexed sprawia, że encja będzie indeksowana. Tak jak @Id określa identyfikator encji tak @DocumentId określa identyfikator dokumentu w indeksie. Bodaj zawsze powinna to być ta sama właściwość klasy, ale nie musi być. Adnotacja @Field określa właściwości klasy, które będą indeksowane. Nie jest tak jak w przypadku JPA, że domyślnie indeksowane a w przypadku JPA utrwalane (ang. persisted) są wszystkie właściwości. Atrybut index = Index.TOKENIZED adnotacji określa, że indeksowane będą poszczególne słowa a nie cała wartość. Właśnie tego chcemy dla właściwości tekstowych, ale do indeksu możemy włączyć także właściwości będące np. datą aby umożliwić filtrowanie a wtedy nie chcemy aby wartość była w jakikolwiek sposób rozbijana. Atrybut store = Store.NO powoduje, że wartość sama w sobie nie będzie przechowywana w indeksie. W pewnych przypadkach przechowywanie wartości ma sens, ale jest to dyskusja poza zakresem tego artykułu. Stwórzmy teraz klasę DAO z metodą searchProducts(String searchText), która zwróci listę encji zarządzanych Hibernate spełniających zadane kryterium wyszukiwania. Oto przykładowy kod źródłowy:


public class ProductDAOBean extends HibernateDaoSupport implements ProductDAO {

public void saveProduct(Product product) {
getHibernateTemplate().saveOrUpdate(product);
}

@SuppressWarnings("unchecked")
public List<Product> searchProducts(String searchText) {
List<Product> products = null;

Session session = getSession();
try {
FullTextSession fullTextSession = Search.createFullTextSession(session);

MultiFieldQueryParser queryParser =
new MultiFieldQueryParser(new String[] { "name" }, new StandardAnalyzer() );

FullTextQuery fullTextQuery fullTextSession =
.createFullTextQuery(queryParser.parse(searchText), Product.class);

products = fullTextQuery.list();

} catch (HibernateException hibernateExc) {
throw convertHibernateAccessException(hibernateExc);
} catch (ParseException parseExc) {
throw new DataRetrievalFailureException("Lucene exception", parseExc);
} finally {
releaseSession(session);
}

return products;
}
}

Metoda saveProduct(Product product) będzie potrzebna, aby zasiedlić bazę danych oraz indeks Lucene danymi testowymi. Nie możemy zrobić tego inaczej (np. poprzez SQL INSERT) gdyż wtedy nasze obiekty nie zostałyby zindeksowane. I jeszcze klasa testu JUnit 4 wraz z metodami do instalacji danych testowych:

public class ProductDAOBeanTest {

private ProductDAOBean productDAOBean;

@Before
public void initialize() {
ApplicationContext context = new ClassPathXmlApplicationContext(
new String[] { "config/dao.xml" });

productDAOBean = (ProductDAOBean) context.getBean("productDAO");
}

@Test
public void testSearchProducts() {
List<Product> products = productDAOBean.searchProducts("wiertarka bosch");

for(Product product : products)
System.out.println(product.getName());
}

@Test
public void installData() {
productDAOBean.saveProduct(newProduct("Wiertarka Bosch GSB 13RE"));
productDAOBean.saveProduct(newProduct("Wiertarka Bosch PSB 500RE"));
productDAOBean.saveProduct(newProduct("Szlifierka Bosch GWS 1000"));
productDAOBean.saveProduct(newProduct("Strug Bosch GHO 26-82"));
productDAOBean.saveProduct(newProduct("Wiertarka Makita HP 1620"));
productDAOBean.saveProduct(newProduct("Szlifierka Makita 9565H"));
productDAOBean.saveProduct(newProduct("Szlifierka Skill 9470"));
}

private Product newProduct(String prodName) {
Product product = new Product();

product.setName(prodName);

return product;
}
}

Metoda searchProducts(String searchText) akceptuje zapytania zgodne ze składnią Lucene Query Language i jest to sporo więcej niż tylko lista słów. Zachęcam do lektury dokumentacji tego języka (jest krótka i przyjemna) i eksperymentów.

2 komentarze:

Maciej Szczytowski pisze...

Kiedyś interesowałem się tą technologią (wtedy była to beta), ale brakowało mi tam jednej funkcjonalności, którą można prosto osiągnąć piszą własny moduł indeksujący. Chodzi o flagę informującą czy dana encja ma być widoczna w searchu czy nie. Przykładowo mamy atrybut "active" i chcemy aby nieaktywne encje nie były wyszukiwane. Można to oczywiście osiągnąć za pomocą odpowiedniego warunku w zapytaniu, ale mimo wszystko Hibernate Search wrzuci te encje do indeksu, co wpłynie na jego wielkość i wydajność. Być może Hibernate dostarczył jakieś rozwiązanie tego problemu, ale mi się go znaleźć nie udało.

Poza tym technologia bardzo przyjemna :)

Btw, czy ktoś praktycznie sprawdził jak w Hibernate działa replikacja indeksu, tzn kiedy ze wzgledów bezpieczeństwa mamy go na kilku maszynach? Lucene sama w sobie tego nie wspiera, a rozwiązanie jakie proponuje Solr średnio mi się podoba.

Andrzej Cichoń pisze...
Ten komentarz został usunięty przez autora.