Въведение в Hibernate Search

1. Общ преглед

В тази статия ще обсъдим основите на Hibernate Search, как да го конфигурираме и ще приложим няколко прости заявки.

2. Основи на Hibernate Search

Винаги, когато трябва да внедрим функционалност за пълнотекстово търсене, използването на инструменти, с които вече сме добре запознати, винаги е плюс.

В случай, че вече използваме Hibernate и JPA за ORM, ние сме само на една крачка от Hibernate Search.

Hibernate Search интегрира Apache Lucene, високоефективна и разширяема библиотека с пълни текстови търсачки, написана на Java . Това съчетава силата на луцена с простотата на хибернация и JPA.

Просто казано, ние просто трябва да добавим някои допълнителни анотации към нашите класове на домейн и инструментът ще се погрижи за нещата като синхронизиране на база данни / индекс.

Hibernate Search също така осигурява интеграция на Elasticsearch; тъй като все още е в експериментален етап, ще се спрем на Луцен тук.

3. Конфигурации

3.1. Зависимости на Maven

Преди да започнем, първо трябва да добавим необходимите зависимости към нашия pom.xml :

 org.hibernate hibernate-search-orm 5.8.2.Final 

За по-голяма простота ще използваме H2 като наша база данни:

 com.h2database h2 1.4.196 

3.2. Конфигурации

Също така трябва да уточним къде Lucene трябва да съхранява индекса.

Това може да стане чрез свойството hibernate.search.default.directory_provider .

Ще изберем файлова система , която е най-ясната опция за нашия случай на употреба. Още опции са изброени в официалната документация. Filesystem-master / filesystem-slave и infinispan са забележителни за клъстерирани приложения, където индексът трябва да бъде синхронизиран между възлите.

Също така трябва да дефинираме основна директория по подразбиране, в която ще се съхраняват индексите:

hibernate.search.default.directory_provider = filesystem hibernate.search.default.indexBase = /data/index/default

4. Моделните класове

След конфигурацията вече сме готови да посочим нашия модел.

Върху JPA анотациите @Entity и @Table трябва да добавим @Indexed анотация. Той казва на Hibernate Search, че продуктът на обекта трябва да бъде индексиран.

След това трябва да дефинираме необходимите атрибути като достъпни за търсене чрез добавяне на анотация @Field :

@Entity @Indexed @Table(name = "product") public class Product { @Id private int id; @Field(termVector = TermVector.YES) private String productName; @Field(termVector = TermVector.YES) private String description; @Field private int memory; // getters, setters, and constructors }

В termVector = TermVector.YES ще се изисква атрибут за "по-скоро като тази" заявка по-късно.

5. Изграждане на индекса на луцен

Преди да започнем действителните заявки, трябва първо да задействаме Lucene, за да изгради индекса :

FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager); fullTextEntityManager.createIndexer().startAndWait();

След тази първоначална компилация Hibernate Search ще се погрижи да поддържа индекса актуален . I. д. ние можем да създаваме, манипулираме и изтриваме обекти чрез EntityManager както обикновено.

Забележка: трябва да се уверим, че обектите са изцяло ангажирани с базата данни, преди да могат да бъдат открити и индексирани от Lucene (между другото, това е и причината, поради която първоначалното импортиране на тестови данни в нашите примерни тестови случаи на код е в специален JUnit тестов случай, коментиран с @Commit ).

6. Изграждане и изпълнение на заявки

Сега сме готови да създадем първата си заявка.

В следващия раздел ще покажем общия работен процес за подготовка и изпълнение на заявка.

След това ще създадем някои примерни заявки за най-важните типове заявки.

6.1. Общ работен поток за създаване и изпълнение на заявка

Подготовката и изпълнението на заявка като цяло се състои от четири стъпки :

В стъпка 1 трябва да получим JPA FullTextEntityManager и от това QueryBuilder :

FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager); QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory() .buildQueryBuilder() .forEntity(Product.class) .get();

В стъпка 2 ще създадем луценска заявка чрез Hibernate заявка DSL:

org.apache.lucene.search.Query query = queryBuilder .keyword() .onField("productName") .matching("iphone") .createQuery();

В стъпка 3 ще обгърнем заявката Lucene в хибернация:

org.hibernate.search.jpa.FullTextQuery jpaQuery = fullTextEntityManager.createFullTextQuery(query, Product.class);

И накрая, в стъпка 4 ще изпълним заявката:

List results = jpaQuery.getResultList();

Забележка : по подразбиране Lucene сортира резултатите по уместност.

Стъпки 1, 3 и 4 са еднакви за всички типове заявки.

По-нататък ще се съсредоточим върху стъпка 2, т.е. как да създадем различни видове заявки.

6.2. Заявки за ключови думи

Най-основният случай на употреба е търсене на конкретна дума .

Това всъщност направихме вече в предишния раздел:

Query keywordQuery = queryBuilder .keyword() .onField("productName") .matching("iphone") .createQuery();

Here, keyword() specifies that we are looking for one specific word, onField() tells Lucene where to look and matching() what to look for.

6.3. Fuzzy Queries

Fuzzy queries are working like keyword queries, except that we can define a limit of “fuzziness”, above which Lucene shall accept the two terms as matching.

By withEditDistanceUpTo(), we can define how much a term may deviate from the other. It can be set to 0, 1, and 2, whereby the default value is 2 (note: this limitation is coming from the Lucene's implementation).

By withPrefixLength(), we can define the length of the prefix which shall be ignored by the fuzziness:

Query fuzzyQuery = queryBuilder .keyword() .fuzzy() .withEditDistanceUpTo(2) .withPrefixLength(0) .onField("productName") .matching("iPhaen") .createQuery();

6.4. Wildcard Queries

Hibernate Search also enables us to execute wildcard queries, i. e. queries for which a part of a word is unknown.

For this, we can use “?” for a single character, and “*” for any character sequence:

Query wildcardQuery = queryBuilder .keyword() .wildcard() .onField("productName") .matching("Z*") .createQuery();

6.5. Phrase Queries

If we want to search for more than one word, we can use phrase queries. We can either look for exact or for approximate sentences, using phrase() and withSlop(), if necessary. The slop factor defines the number of other words permitted in the sentence:

Query phraseQuery = queryBuilder .phrase() .withSlop(1) .onField("description") .sentence("with wireless charging") .createQuery();

6.6. Simple Query String Queries

With the previous query types, we had to specify the query type explicitly.

If we want to give some more power to the user, we can use simple query string queries: by that, he can define his own queries at runtime.

The following query types are supported:

  • boolean (AND using “+”, OR using “|”, NOT using “-“)
  • prefix (prefix*)
  • phrase (“some phrase”)
  • precedence (using parentheses)
  • fuzzy (fuzy~2)
  • near operator for phrase queries (“some phrase”~3)

The following example would combine fuzzy, phrase and boolean queries:

Query simpleQueryStringQuery = queryBuilder .simpleQueryString() .onFields("productName", "description") .matching("Aple~2 + \"iPhone X\" + (256 | 128)") .createQuery();

6.7. Range Queries

Range queries search for avalue in between given boundaries. This can be applied to numbers, dates, timestamps, and strings:

Query rangeQuery = queryBuilder .range() .onField("memory") .from(64).to(256) .createQuery();

6.8. More Like This Queries

Our last query type is the “More Like This” – query. For this, we provide an entity, and Hibernate Search returns a list with similar entities, each with a similarity score.

As mentioned before, the termVector = TermVector.YES attribute in our model class is required for this case: it tells Lucene to store the frequency for each term during indexing.

Based on this, the similarity will be calculated at query execution time:

Query moreLikeThisQuery = queryBuilder .moreLikeThis() .comparingField("productName").boostedTo(10f) .andField("description").boostedTo(1f) .toEntity(entity) .createQuery(); List results = (List) fullTextEntityManager .createFullTextQuery(moreLikeThisQuery, Product.class) .setProjection(ProjectionConstants.THIS, ProjectionConstants.SCORE) .getResultList();

6.9. Searching More Than One Field

Until now, we only created queries for searching one attribute, using onField().

Depending on the use case, we can also search two or more attributes:

Query luceneQuery = queryBuilder .keyword() .onFields("productName", "description") .matching(text) .createQuery();

Moreover, we can specify each attribute to be searched separately, e. g. if we want to define a boost for one attribute:

Query moreLikeThisQuery = queryBuilder .moreLikeThis() .comparingField("productName").boostedTo(10f) .andField("description").boostedTo(1f) .toEntity(entity) .createQuery();

6.10. Combining Queries

Finally, Hibernate Search also supports combining queries using various strategies:

  • SHOULD: the query should contain the matching elements of the subquery
  • MUST: the query must contain the matching elements of the subquery
  • MUST NOT: the query must not contain the matching elements of the subquery

The aggregations are similar to the boolean ones AND, OR and NOT. However, the names are different to emphasize that they also have an impact on the relevance.

Например, SHOULD между две заявки е подобно на логическо ИЛИ: ако една от двете заявки има съвпадение, това съвпадение ще бъде върнато.

Ако обаче двете заявки съвпадат, съвпадението ще има по-голямо значение в сравнение, ако съвпада само една заявка:

Query combinedQuery = queryBuilder .bool() .must(queryBuilder.keyword() .onField("productName").matching("apple") .createQuery()) .must(queryBuilder.range() .onField("memory").from(64).to(256) .createQuery()) .should(queryBuilder.phrase() .onField("description").sentence("face id") .createQuery()) .must(queryBuilder.keyword() .onField("productName").matching("samsung") .createQuery()) .not() .createQuery();

7. Заключение

В тази статия обсъдихме основите на Hibernate Search и показахме как да внедрим най-важните типове заявки. По-напредналите теми могат да бъдат намерени в официалната документация.

Както винаги, пълният изходен код на примерите е достъпен в GitHub.