Check searchable field of an entity based on indexing - hibernate-search

I have an entity defined as following.
public class Deal {
#Id
#DocumentId
#Column(name = "ID")
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Field
#Column(name = "NAME")
private String name;
#Field
#Column(name = "ADVERTISER_NAME")
private String advertiserName;
#Field
#Column(name = "BRAND_NAME")
private String brandName;
//other fields and getters/setters omitted for brevity
}
If I want to search on all searchable fields, I can do something like the following, which demonstrates the usage of onField, onFields and andFields.
Query luceneQuery1 = mythQB
.simpleQueryString()
.onFields("name", "history", "description")
.matching("teststring")
.createQuery();
Query luceneQuery2 = mythQB
.simpleQueryString()
.onField("name")
.boostedTo(5f)
.andFields("advertiserName", "brandName")
.boostedTo(2f)
.withAndAsDefaultOperator()
.matching("teststring")
.createQuery();
If I add Index.NO to #Field(making an entity field not searchable),for example, change the annotation of brandName to #Field(index = Index.NO), now I only have two searchable fields: name and advertiserName, if we don't consider id. In this case, the example query above will throw runtime exception because it tries to search on brandName that is not searchable.
I've tried something like the following to dynamically get the full list of searchable fields based on whether a field has annotation or not. But this won't work if the index is Index.NO.
My question is that is there a way to dynamically get the full list of searchable fields based on the actual index value?
protected String[] getSearchableFields() {
List<String> fields = Lists.newArrayList();
Class<?> c = clazz;
while (c != null) {
for (Field field : c.getDeclaredFields()) {
if (field.isAnnotationPresent(org.hibernate.search.annotations.Field.class)
|| field.isAnnotationPresent(org.hibernate.search.annotations.Fields.class)) {
if (field.getType().isAssignableFrom(String.class)) {
fields.add(field.getName());
}
}
}
c = c.getSuperclass();
}
return fields.toArray(new String[fields.size()]);
}

There is a metadata API in Hibernate Search.
FullTextSession ftSession = ...;
IndexedTypeDescriptor indexedType = ftSession.getSessionFactory().getIndexedTypeDescriptor(clazz);
for (PropertyDescriptor property : indexedType.getIndexedProperties()) {
for (FieldDescriptor field : property.getIndexedFields()) {
if (field.getIndex() == Index.YES) {
// do something
}
}
}
See this section of the reference documentation for more information.
There are a few limitations, like not being able to "see" the extra fields contributed by custom bridges. But it works well apart from that.

Related

Sorting on embedded entity name in hibernate search

I have the following Entity definitions.
public class Order {
#Id
#DocumentId
private Long id;
#Field
#IndexedEmbedded(includePaths = {"name"})
#OneToOne
#JoinColumn(name = "ACCOUNT_ID")
private Account account;
// the rest are omitted for brevity purpose
}
public class Account {
#Id
#DocumentId
private Long id;
#SortableField(forField = "name_Sort")
#Field(name = "name_Sort", store = Store.YES, normalizer= #Normalizer(definition = SearchConstants.LOWER_CASE_NORMALIZER))
#Column(name = "NAME")
private String name;
}
If I search on Order and want to have the search results sorted by account name, is there good way of doing so possibly using the embedded indexed annotation? I know we can do it by adding an extra string field in Order that is called accountName, then just add sorting annotation on top of that. Is it possible to achieve this without specifying the sorting annotation in Order but just use the sorting annotation that is already defined in Account?
Change this:
#IndexedEmbedded(includePaths = {"name"})
To this:
#IndexedEmbedded(includePaths = {"name", "name_Sort"})
Then you can use the field account.name_Sort for sorts on orders:
QueryBuilder builder = fullTextSession.getSearchFactory()
.buildQueryBuilder().forEntity( Order.class ).get();
Query luceneQuery = /* ... */;
FullTextQuery query = s.createFullTextQuery( luceneQuery, Order.class );
query.setSort(
builder.sort().byField("account.name_Sort").createSort()
);
List results = query.list();

Posting an entity with manyToMany, ManyToOne

I've always avoided #xToX relationships in my APIs but finally wanted to give it a go, and I'm struggling with it.
I've managed to make the get methods returning me the right informations with any recursiveness (at least I made a part), and now I want to post data to my API with those relationships in place (and seems working so far).
So I have a few objects a book, a book style, a book category and an author.
A book has only one author (even if it's false sometimes but as I only read Sci-Fi it will do the trick) and an author have many books (same for category).
And a style can have many books, also books can have many styles.
So I went with a #ManyToOne on the book's author property and with #OneToMany on the author's books property (same for category).
And between the style and the bok #ManyToMany
I want to be able to post a book to my API like follows, using the author and style ids :
{
"isbn": "15867jhg8",
"title": "Le portrait de Dorian Gray",
"author": 1,
"category":1,
"style": [1,2,3]
}
I've annotated my properties with #JsonBackReference, #JsonManagedReference, #JsonSerialize, #JsonIdentityInfo but I truly think I might have made it too much ...
I'm not sure about the way I used the #JsonSerialize, nor the #JsonIdentityInfo.
I've omitted the useless properties to keep the example 'simple'.
So let's dive in.
The Book class first :
#Entity
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id"
)
public class Book implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(updatable = false, nullable = false)
private Integer id;
private String isbn;
private String title;
#ManyToOne
#JoinColumn(name="category_id", nullable = false)
#JsonBackReference(value = "category")
private BookCategory category;
#ManyToOne
#JoinColumn(name="author_id", nullable = false)
#JsonBackReference(value = "author")
private Author author;
#ManyToMany
#JsonManagedReference(value = "styles")
#JsonSerialize(using = BookStyleListSerializer.class)
private List <BookStyle> styles;
}
Then the author class :
#Entity
#JsonIdentityInfo (
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id"
)
public class Author implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(updatable = false, nullable = false)
private Integer id;
#OneToMany(mappedBy = "author")
#JsonManagedReference(value = "authorBooks")
#JsonSerialize(using = BookListSerializer.class)
private List <Book> books;
}
As the category is quite the same, I'm only pasting the books property of it :
#OneToMany(mappedBy = "category")
#JsonManagedReference(value = "categoryBooks")
#JsonSerialize(using = BookListSerializer.class)
private List <Book> books;
And finally my bookStyle class :
#Entity
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id"
)
public class BookStyle implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(updatable = false, nullable = false)
private Integer id;
#ManyToMany
#JsonManagedReference(value = "styleBooks")
#JsonSerialize(using = BookListSerializer.class)
private List <Book> books;
}
The serializer are the same, it's only the type that changes :
public class BookStyleListSerializer extends StdSerializer <List <BookStyle>> {
public BookStyleListSerializer() {
this(null);
}
public BookStyleListSerializer(Class<List<BookStyle>> t) {
super(t);
}
#Override
public void serialize(List <BookStyle> bookStyles, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
List<Integer> ids = new ArrayList <> ( );
for (BookStyle style: bookStyles){
ids.add(style.getId ());
}
jsonGenerator.writeObject ( ids );
}
}
Of what I understood (and as english is not my native language i might misunderstood a few things here and there while coding) :
#JsonBackReference is to be used for the property we don't want to be serialized as opposite to #JsonManagedReference
#JsonSerialize is the custom serializer so that the elements will be serialized as we want them to (in this case, only using IDs)
And as it might be obvious to some of you : none of what I've coded works for posting data, here's the exception as i received it when i post something via API (and it's doubled, not a copy paste error) :
.c.j.MappingJackson2HttpMessageConverter : Failed to evaluate Jackson deserialization for type [[simple type, class com.rz.librarian.domain.entity.Author]]: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot handle managed/back reference 'categoryBooks': no back reference property found from type [collection type; class java.util.List, contains [simple type, class com.rz.librarian.domain.entity.Book]]
.c.j.MappingJackson2HttpMessageConverter : Failed to evaluate Jackson deserialization for type [[simple type, class com.rz.librarian.domain.entity.Author]]: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot handle managed/back reference 'categoryBooks': no back reference property found from type [collection type; class java.util.List, contains [simple type, class com.rz.librarian.domain.entity.Book]]
I've tried many things but after three days on this issue i wanted to ask (cry?) for help.
Thank you guys and sorry for the long post!

Hibernate Envers - custom RevisionEntity - how to get record

I have written my custom RevisionEntity class to store additional data (for example username), like below:
#Entity
#RevisionEntity(AuditListener.class)
#Table(name = "REVINFO", schema = "history")
#AttributeOverrides({
#AttributeOverride(name = "timestamp", column = #Column(name = "REVTSTMP")),
#AttributeOverride(name = "id", column = #Column(name = "REV")) })
public class AuditEntity extends DefaultRevisionEntity {
private static final long serialVersionUID = -6578236495291540666L;
#Column(name = "USER_ID", nullable = false)
private Long userId;
#Column(name = "USER_NAME")
private String username;
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
I can see that all rows in database are correctly stored, REVINFO table contains also username.
I would like to query database to get detailed information from my custom RevisionEntity, like username.
How can I do it? Is there any supported API to get it?
Lets assume you know the identifier of the entity you're interested in the revision entity metadata for, you can easily query that information using the following approach:
final AuditReader auditReader = AuditReaderFactory.get( session );
List<?> results = auditReader.createQuery()
.forRevisionsOfEntity( YourEntityClass.class, false, false )
.add( AuditEntity.id().eq( yourEntityClassId ) )
.getResultList();
The returned results will contain an Object array, e.g. Object[] where results[1] will hold the revision entity instance which contains the pertinent information your wanting.
For more details, you can see the java documentation comments here
If you only have the revision number, you can access just the revision entity instance directly by:
// I use YourAuditEntity here because AuditEntity is actually an Envers class
YourAuditEntity auditEntity = auditReader
.findRevision( YourAuditEntity.class, revisionId );
For more details on the AuditReader interface, you can see the java documentation here

JPA - Finding nested entities using combined criterias

I have an entity named FileEntity which contains a list of reports of the type ReportEntity.
FileEntity has an field which determines, which user has created the file containing a number of reports.
#Entity
public class FileEntity {
#Id
private Long id;
#JoinColumn(name = "user")
#ManyToOne(optional = true)
#NotNull
private User user;
#OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.EAGER,
orphanRemoval = true)
#NotNull
private List<Report> reports = new ArrayList<>(5);
...
}
#Entity
public class Report {
#Id
private Long id;
...
}
I am currently trying to fetch a single report with a given report ID and the ID of the person who issued the file containing the report. The combination is unique, so it should only return one report for a certain combination of report and user ID. But I am unable to retrieve a single result using the following criteria:
public Report findReportByUserAndReportId(Long reportId, Long userId) {
Objects.nonNull(reportId);
Objects.nonNull(userId);
try {
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
final CriteriaQuery<Report> cq = cb.createQuery(Report.class);
final Root<FileEntity> fileEntity = cq.from(FileEntity.class);
final Root<Report> report = cq.from(Report.class);
final Join<FileEntity, Report> join = fileEntity.join(FileEntity_.reports);
final Predicate[] predicates = new Predicate[]{
cb.equal(join.get("id"),
userId),
cb.equal(join.get(Report_.id),
reportId)};
cq.select(report).where(predicates);
return entityManager.createQuery(cq).getSingleResult();
} catch (NoResultException |
NonUniqueResultException ne) {
LOG.log(Level.WARNING,
"Could not find report: {0}",
ne.getMessage());
}
return null;
}
Has someone an idea what I am doing wrong?
First, don't use multiple roots here, because they generate a carthesian product of all the elements, without joining them. Use joins or Paths instead. Second, in the first predicate. join object denotes a Path of reports, not a user, therefore it doesnt' make sense to look for the userid there.
Root<FileEntity> fileEntity = cq.from(FileEntity.class);
Path<User> user = fileEntity.get(FileEntity_.user);
Join<FileEntity, Report> reports = fileEntity.join(FileEntity_.reports);
Predicate[] predicates = new Predicate[]{
cb.equal(user.get(User_.id),
userId),
cb.equal(reports.get(Report_.id),
reportId)};

Hibernate Search Tuple Queries

I have an entity Message with a one-to-many relation to an entity Header. How can I create a tuple based search query like
(message.headerKey="foo" and message.headerValue="123") and
(message.headerKey="bar" and message.headerValue="456")
My current logic would also match when I swap the header values in my search criteria
(message.headerKey="foo" and message.headerValue="456") and
(message.headerKey="bar" and message.headerValue="123")
How can I do a tuple based query using the Hibernate Search API?
This is my Message Entity:
#Entity
#Table(name="MESSAGE")
#Indexed
public class MessageEntity implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(name="id")
private Long id;
#Column(name="message_timestamp")
private Date timestamp;
#Column(name="payload")
#Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
private String payload;
#OneToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }, mappedBy = "message")
#IndexedEmbedded
private List<HeaderEntity> headers;
// Getters and Setters
}
This is my Header Entity:
#Entity
#Table(name="HEADER")
public class HeaderEntity implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#Column(name="header_key")
#Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
private String headerKey;
#Column(name="header_value")
Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
private String headerValue;
#ManyToOne(cascade=CascadeType.ALL)
#JoinColumn(name="message_id")
private MessageEntity message;
// Getters and Setters
}
This is my search logic:
public List<MessageEntity> search(Header[] headers) {
FullTextEntityManager fullTextEntityManager = org.hibernate.search.jpa.Search.getFullTextEntityManager(mgr);
QueryBuilder qb = fullTextEntityManager.getSearchFactory().buildQueryBuilder().forEntity(MessageEntity.class).get();
TermMatchingContext onFieldKey = qb.keyword().onField("headers.headerKey");
TermMatchingContext onFieldValue = qb.keyword().onField("headers.headerValue");
BooleanJunction<BooleanJunction> bool = qb.bool();
org.apache.lucene.search.Query query = null;
for (Header header : headers) {
bool.must(onFieldKey.matching(header.getKey()).createQuery());
bool.must(onFieldValue.matching(header.getValue()).createQuery());
}
query = bool.createQuery();
FullTextQuery persistenceQuery = fullTextEntityManager.createFullTextQuery(query, MessageEntity.class);
persistenceQuery.setMaxResults(10);
return persistenceQuery.getResultList();
}
Your approach will indeed not work. The problem is that Lucene is a flat data structure, in particular associations (embedded entities) are just "added" to the Lucene Document of the owning entity. In your case the MessageEntity document will contain two fields per headerKey respectively headerValue. Once with "foo" and "bar" as value and56" as values. once with "123" and "456" as values. There is no notion that two of these values are acutally a pair.
One potential solution is to create a unique field/value pair. Using a custom class bridge you could create a "keyValueField" containing header key and value as concatenated value. In your query you would then target this field using concatenated query parameters.