I have a question regarding the possibility to enforce multiple constraints while performing a query with hibernate-search.
#Indexed
public class Contact{
//ommited fields
#IndexEmbedded
private List<Communication> communications;
//setters - getters
}
and the associated class
#Indexed
public class Communication{
#Field(analyze = Analyze.YES, store = Store.YES)
private String value;
#Field(analyze = Analyze.YES, store = Store.YES)
private CommunicationType communicationType;
#Field(analyze = Analyze.YES, store = Store.YES)
private CommunicationUsage communicationUsage;
}
the enums
public static enum CommunicationUsage {
PRIVATE,
PROFESSIONNAL
}
public static enum CommunicationType{
PHONE,
EMAIL
}
An example query that I would need to accomplish is the following :
Find all contacts for which the communication type is "PHONE" and the CommunicationUsage is "PRIVATE" and the field value of the Communication class contains the string 999
public List<Contact> search(){
FullTextEntityManager fullTextEntityManager =
Search.getFullTextEntityManager(em);
QueryBuilder qb = fullTextEntityManager.getSearchFactory()
.buildQueryBuilder().forEntity(Contact.class).get();
org.apache.lucene.search.Query luceneQuery =
qb.bool() .must(qb.keyword().wildcard().onField("communications.value").matching("*99999*").createQuery()) .must(qb.keyword().onField("communications.type").ignoreFieldBridge().matching("phone").createQuery()) .must(qb.keyword().onField("communications.usage").ignoreFieldBridge().matching("private").createQuery())
.createQuery();
org.hibernate.search.jpa.FullTextQuery jpaQuery =
fullTextEntityManager.createFullTextQuery(luceneQuery, Contact.class);
List result = jpaQuery.getResultList();
}
However I'm getting contacts that have a phone number matching the one provided but for different communication types and usages (such as PHONE and PROFESSIONAL)
So can this type of query be accomplished with hibernate-search or not?
At the moment this use case is not solvable with Hibernate Search with default indexing. The problem is that Hibernate Search flattens all data to be indexed (including the associations annotated via #IndexedEmbedded) into a single Lucene Document. In particular one looses in this case the "grouping" given by a single Communication instance. If you have one Communication instance with the type you are interested in and another instance with the value you are interested in, you will in your case get a match.
As a workaround you could provide a custom class bridge for the Communication instance which somehow concatenates the values you are interested in. You would then try to write a query which targets this custom field.
Related
I have an indexedEmbedded object with #OneToMany relation, inside a class, and want to sort with a field containded in that object.
When i call my method for search i got this exception:
"Unexpected docvalues type NONE for field 'employees.id_forSort' (expected=NUMERIC). Use UninvertingReader or index with docvalues"
Thanks in advance!
Here is my code:
public Company {
....
#IndexedEmbedded(prefix = "students.", includeEmbeddedObjectId = true)
#OneToMany(mappedBy = "company")
private Set<Employee> employees= new HashSet<>();
}
public Employee{
....
#SortableField(forField = "id_forSort")
#Field(name = "id_forSort", analyze = Analyze.NO)
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
}
public class CompanySearchRepository{
....
public List<Company> searchCompany(
DataTablePagination pagination, Long id, SearchCriteria searchCriteria) {
FilterField[] filterFields = validateFilterFields(searchCriteria.getFilterFields());
// Sorting
String sortField = "employees.id";
Sort sort = getSort(sortField, pagination.getSortDirection());
Query query;
if (filterFields.length > 0) {
query = getFilterableColumnsQuery(filterFields);
} else {
// global search
query = getGlobalSearchQuery(searchableFields, searchCriteria.getSearchTerm());
}
FullTextQuery fullTextQuery = getFullTextQuery(query);
initPagination(
fullTextQuery,
sort,
pagination.getPage(),
pagination.getPageSize());
List<Company> data = fullTextQuery.getResultList();
}
Sort getSort(String sortField, SortDirection sortDirection) {
SortFieldContext sortFieldContext =
getQueryBuilder().sort().byField(sortField.concat("_forSort"));
return sortDirection.equals(SortDirection.ASC)
? sortFieldContext.asc().createSort()
: sortFieldContext.desc().createSort();
}
}
You named your sortable field employees.id_forSort, but when searching, you're using another field for sorts: employees.id. Use the field that is intended for sorts. Replace String sortField = "employees.id"; with String sortField = "employees.id_forSort"; My bad, I didn't see the weird code that adds a suffix to the field name in the getSort method. Then the message is strange. Would your index be empty, by any chance?
Sorts on multi-valued fields are not supported. You will likely get different results from one execution to the other, since the search engine has to select a value to use for sorts, and which value is selected is undefined.
Regardless of the technical aspect, I'm not sure what you're trying to achieve. You're getting Company instances as a result, and want them to be sorted by Employee ID, of which there are many for each Company. What does it mean? You want the company with the oldest employee first? Something else? If you're just trying to stabilize the order of hits, I'd recommend using the company ID instead.
I have a Couchbase-Document "Group" with a list of group-members-names. I want to query for all groups of one person. I managed to do it with N1QL ARRAY_CONTAINS - see in code example - but i hoped that i could generate the query from the method name as it is usual in Spring Data.
Any help is appreciated :)
I tried
public List<MyGroup> findAllByMembers(String member); and public List<MyGroup> findByMembers(String member); but they just return an empty list - i guess they try to match the whole "members" value and don't recognize it as a list -, no errors.
Code
My Document with a List field
#Data
#Document
public class MyGroup {
private String name;
private List<String> members = new ArrayList<>();
}
My Repository
#RepositoryDefinition(domainClass = MyGroup.class, idClass = String.class)
public interface MyGroupRepository extends CouchbaseRepository<MyGroup, String> {
//#Query("#{#n1ql.selectEntity} WHERE ARRAY_CONTAINS(members,$1) AND #{#n1ql.filter}")
public List<MyGroup> findAllByMembers(String member);
}
Expected
Given a "group1" with "member1" in members.
repository.findAllByMembers("member1"); should return ["group1"].
Couchbase is limited by the Spring Data specification. Unfortunately, we can't simply add new behaviors to it (if you switch to a relational database, it has to work with no breaking points). So, whenever you need to query something that has a N1QL specific function/keyword, you have to write a query via #Query
I have a full-text search working with hibernate search and lucene, in that I can successfully search for a given model entity across specific fields.
However, rather than search for one type of entity at a time, I want to implement a 'universal' search where different entity types are searched for simultaneously, matching the search phrase to the appropriate fields on each different entity type, and then have the results ranked by relevance to the search terms, irrespective of the entity type.
So for example let's say I have different entities, Foo and Bar
#Entity
#Indexed
#AnalyzerDef(
name="fulltext",
tokenizer=#TokenizerDef(factory=StandardTokenizerFactory.class),
filters={
#TokenFilterDef(factory=LowerCaseFilterFactory.class),
#TokenFilterDef(factory=SnowballPorterFilterFactory.class,
params={#Parameter(name="language", value="English") })
}
)
public class Foo {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer fooId;
#Column
#Field
#Analyzer(definition="fulltext")
private String fieldA;
...
#Entity
#Indexed
public class Bar {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer barId;
#Column
#Field
#Analyzer(definition="fulltext")
private String fieldB;
#Column
#Field
#Analyzer(definition="fulltext")
private String fieldC;
...
So I want to search for "some text" and match over Foo.fieldA, and Bar.fieldB and/or Bar.fieldC
The current type of search I have working is specific to a particular entity, for example:
fullTextSession = Search.getFullTextSession(hibernateSession);
Query query = fullTextSession.createFullTextQuery(
fullTextSession
.getSearchFactory()
.buildQueryBuilder()
.forEntity(Foo.class)
.get()
.keyword()
.onFields("fieldA")
.matching("some text")
.createQuery(),
Foo.class);
query.list() // gets ranked list of Foo entities matching "some text"
Clearly, the above Lucene query is specific to the Foo entity, and even the Foo.fieldA
So, is it possible to amend the Lucene query to also include Bar results, matching on the Bar.fieldB and Bar.fieldC fields?
I know the fullTextSession.createFullTextQuery(fulltextSession, Class...) method will also accept Bar.class, to return Bar results, but I don't know how to modify the actual query to search over Bar entities in the first place.
Another way I was thinking of solving this is to do separate queries, one for Foo entities, one for Bar entities, then merge the two result sets and sort them by 'match relevance score' - but I can't find how to get this score for results, either!
EDIT
the above approach will likely not work - turns out you can get the score of the results via projections but the docs state that scores from separate queries can't be meaningfully compared:
FullTextQuery.SCORE: returns the document score in the query. Scores are handy to compare one result against an other for a given query but are useless when comparing the result of different queries.
Apologies if I'm covering well-trodden ground here but I've been searching, clearly in the wrong places, for hours on this and can't find anything helpful in the documentation, which is frustrating as I imagine this is a fairly common use-case for Lucene.
You can write two queries and combine them via a BooleanQuery using Occur.SHOULD. Then use createFullTextQuery(booleanQuery, Foo.class, Bar.class); to search for both type of entities.
Inspired by Hardy's answer, I used a BooleanQuery with two clauses, both with an Occur.SHOULD on them which effectively acts as an OR. This results in the desired behaviour for the query.
here's the code:
...
fullTextSession = Search.getFullTextSession(hibernateSession);
String searchPhrase = "some text";
org.apache.lucene.search.Query fooQuery =
fullTextSession
.getSearchFactory()
.buildQueryBuilder()
.forEntity(Foo.class)
.get()
.keyword()
.onFields("fieldA")
.matching(searchPhrase)
.createQuery();
org.apache.lucene.search.Query barQuery =
fullTextSession
.getSearchFactory()
.buildQueryBuilder()
.forEntity(Bar.class)
.get()
.keyword()
.onFields("fieldB", "fieldC")
.matching(searchPhrase)
.createQuery();
BooleanQuery query = new BooleanQuery();
query.add(new BooleanClause(fooQuery, BooleanClause.Occur.SHOULD));
query.add(new BooleanClause(barQuery, BooleanClause.Occur.SHOULD));
Query hibernateQuery =
fullTextSession.createFullTextQuery(query, Foo.class, Bar.class);
...
I've been using spring and hibernate for this past few weeks and I've always been learning something new there.
Right now I've got a problem that I want to solve with Projections in Hibernate.
Suppose there is a model Person and that model has many Car. The following are how the class definitions roughly gonna look like:
public class Person implements java.io.Serializable {
private Integer id;
private String name;
private List<Car> cars;
private Integer minYear; // Transient
private Integer maxYear; // Transient
}
public class Car implements java.io.Serializable {
private Integer id;
private Integer year;
}
The problem here is I want to get the minYear (maxYear) of each Person to be filled by the earliest year (latest year) of the cars they have.
Later I found a solution to use Projections but I stumbled upon org.hibernate.QueryException: could not resolve property: minYear of: model.Person and here is the code of the db operation:
Criteria criteria = sessionFactory.getCurrentSession().createCriteria("model.Person");
criteria.add(create(personInstance));
criteria.createAlias("minYear", "minYear");
criteria.setProjection(Projections.min("cars.year").as("minYear"));
Is there anyway to store the aggregation value in transient method using Projections because I just want to avoid using plain SQL and HQL as much as possible.
Never mind, I've found the solution.
First we need to create alias of the associated object like so
Criteria criteria = sessionFactory.getCurrentSession().createCriteria("model.Person");
criteria.createAlias("cars", "cars");
Select the needed using Hibernate Projections
ProjectionList projections = Projections.projectionList();
projections.add(Projections.property("id").as("id"));
projections.add(Projections.property("name").as("name"));
projections.add(Projections.property("cars").as("cars"));
Group the result based on the root entity (in this case using its id, Person.id), this is needed especially when used with aggregation to group the aggregation
projections.add(Projections.groupProperty("id"));
Use the aggregate function
projections.add(Projections.min("cars.year").as("minYear"));
projections.add(Projections.max("cars.year").as("maxYear"));
Set the projection
criteria.setProjection(projections);
Use result transformer AliasToBeanResultTransformer to map the result fields (as specified in step 2 & 4) to the POJO
criteria.setResultTransformer(new AliasToBeanResultTransformer(Person.class));
Get the result
List<Person> results = (List<Person>) criteria.list();
I have the following beans Task, ServerDetails and ApplicationDetails.
I wish to retrieve all tasks, their server details and application details based on a specific application name.
From the result i expect to be able to retrieve the data in a manner such as:
task.getServers().getApplicationDetails()
In actuality, I get what seems to be flat data's representation as an Object[].
Is there any way to do what i propose?
Following is my code...
class Task {
private String taskId;
private Set<ServerDetails> servers;
}
class ServerDetails {
private String id;
private Set<ApplicationDetails> applications;
}
class ApplicationDetails {
private String id;
}
HQL:
StringBuilder hql = new StringBuilder(256);
hql.append("FROM Task h, ServerDetails ser, ApplicationDetails app ");
hql.append("WHERE h.executionDate > ");
hql.append("to_date('");
hql.append(DBDateFormatter.getInstance().formatDate(cal));
hql.append("', '");
hql.append(DBDateFormatter.getInstance().getOracleDateFormat());
hql.append("') and h.id = ser.task.id and ser.id = app.server and app.name = 'XXX'");
hql.append(" order by h.executionDate desc");
String hql = hql.toString();
Query query = session.createQuery(hql);
results = (List<Object[]>) query.list();
You should just retrieve the main object.
For the other, you can:
navigate to them while the Session has not be closed (runs additional queries as needed, known as lazy ; this is ideal for ease of use)
retrieve them in the original query using the fetch keyword.
Example:
SELECT h
FROM Task h
JOIN FETCH h.serveurs ser
JOIN FETCH ser.applications app
WHERE h.executionDate >
.... // no need to specify the joins
You will be able to retrieve the data in a manner such as:
task.getServers().getApplicationDetails()
You can retrieve the object graph as the others have said using LEFT JOIN FECH. One crevent I have found when retrieving object graphs, when walking down a many-to-one relationship you can not walk back up without additional database access.