Search subset of objects using Compass/Lucene - grails

I'm using the searchable plugin for Grails (which provides an API for Compass, which is itself an API over Lucene). I have an Order class that I would like to search but, I don't want to search all the instances of Order, just a subset of them. Something like this:
// This is a Hibernate/GORM call
List<Order> searchableOrders = Customer.findAllByName("Bob").orders
// Now search only these orders with the searchable plugin - something like
searchableOrders.search("name: foo")
In reality the relational query to get the searchableOrders is more complex than this, so I can't do the entire query (Hibernate + compass) in compass alone. Is there a way to search only a subject of instances of a particular class using Compass/Lucene.

One way to do this is with a custom Filter. For example, if you wanted to filter based on ids for your domain class, you would add the id to the searchable configuration for the domain class:
static searchable = {
id name: "id"
}
Then you would write your custom filter (which can go in [project]/src/java):
import org.apache.lucene.search.Filter;
import java.util.BitSet;
import org.apache.lucene.index.TermDocs;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.IndexReader;
import java.io.IOException;
import java.util.List;
public class IdFilter extends Filter {
private List<String> ids;
public IdFilter(List<String> ids) {
this.ids = ids;
}
public BitSet bits(IndexReader reader) throws IOException {
BitSet bits = new BitSet(reader.maxDoc());
int[] docs = new int[1];
int[] freqs = new int[1];
for( String id : ids ) {
if (id != null) {
TermDocs termDocs = reader.termDocs(new Term("id", id ) );
int count = termDocs.read(docs, freqs);
if (count == 1) {
bits.set(docs[0]);
}
}
}
return bits;
}
}
Then you would put the filter as an argument to your search (making sure to import the Filter class if its in a different package):
def theSearchResult = MyDomainClass.search(
{
must( queryString(params.q) )
},
params,
filter: new IdFilter( [ "1" ] ))
Here I'm just creating a hard-coded list with a single value of "1" in it, but you could retrieve a list of ids from the database, from a previous search, or wherever.
You could easily abstract the filter I have to take the term name in the constructor, then pass in "name" like you want.

Two ways of doing this:
The easiest from the implementation standpoing is do two searches (one findAll and search) on all objects and then find intersection between them. If you cache the result of findAll call, then you are really down to one query you have to make.
A more "clean" way to do this is to make sure to index the IDs of the domain objects with Searchable, and when you get the findAll result, pass in those IDs into the search query, thus limiting it.
I don't remember the Lucene syntax off the top of my head, but you'd have to do something like
searchableOrders.search("name: foo AND (ID:4 or ID:5 or ID:8 ...)" )
You may run into query size limits in Lucene, but I think there are settings that allows you to control query length.

Related

BeanItemContainer unique property values

I am using BeanItemContainer for my Grid. I want to get a unique list of one of the properties. For instance, let's say my beans are as follows:
class Fun {
String game;
String rules;
String winner;
}
This would display as 3 columns in my Grid. I want to get a list of all the unique values for the game property. How would I do this? I have the same property id in multiple different bean classes, so it would be nice to get the values directly from the BeanItemContainer. I am trying to avoid building this unique list before loading the data into the Grid, since doing it that way would require me to handle it on a case by case basis.
My ultimate goal is to create a dropdown in a filter based on those unique values.
There isn't any helper for directly doing what you ask for. Instead, you'd have to do it "manually" by iterating through all items and collecting the property values to a Set which would then at the end contain all unique values.
Alternatively, if the data originates from a database, then you could maybe retrieve the unique values from there by using e.g. the DISTINCT keyword in SQL.
In case anyone is curious, this is how I applied Leif's suggestion. When they enter the dropdown, I cycle through all the item ids for the property id of the column I care about, and then fill values based on that property id. Since the same Grid can be loaded with new data, I also have to "clear" this list of item ids.
filterField.addFocusListener(focus->{
if(!(filterField.getItemIds() instanceof Collection) ||
filterField.getItemIds().isEmpty())
{
BeanItemContainer<T> container = getGridContainer();
if( container instanceof BeanItemContainer && getFilterPropertyId() instanceof Object )
{
List<T> itemIds = container.getItemIds();
Set<String> distinctValues = new HashSet<String>();
for(T itemId : itemIds)
{
Property<?> prop = container.getContainerProperty(itemId, getFilterPropertyId());
String value = null;
if( prop.getValue() instanceof String )
{
value = (String) prop.getValue();
}
if(value instanceof String && !value.trim().isEmpty())
distinctValues.add(value);
}
filterField.addItems(distinctValues);
}
}
});
Minor point: the filterField variable is using the ComboBoxMultiselect add-on for Vaadin 7. Hopefully, when I finally have time to convert to Vaadin 14+, I can do something similar there.

Grails: How do I retrieve records by a specific property?

I am creating a basic CRUD application with a Person entity:
class Person {
public int Age;
...
public int getAge() {
return this.Age;
}
public void setAge(int AgeToSet) {
this.Age = AgeToSet;
}
}
I have a controller and I want to retrieve all Persons with an age of 20:
def filter = {
def c = Person.createCriteria();
def persons = c.list{
eqProperty("Age", "20");
}
[persons: persons];
}
But this is not working and is, instead, giving me the error:
ERROR StackTrace - Full Stack Trace:
org.hibernate.QueryException: could not resolve property: Age of: project.Person
at org.hibernate.persister.entity.AbstractPropertyMapping.propertyException(AbstractPropertyMapping.java:62)
What could be the problem?
Three things:
Your Age needs to start with lowercase: age.
Your criteria is wrong, you want to use eq. eqProperty compares two properties, but you only need one and a value.
Your comparision must be with an int, like this: eq("myage", 20).
Since this query is so simple, you may want to just use a DynamicFinder: http://gorm.grails.org/6.0.x/hibernate/manual/index.html#finders
With a Dynamic Finder, you could simplify the query to:
def persons = Person.findAllByAge(20)
Just a suggestion. I use Dynamic Finders as my primary query method. If I need a query that is more complex, I'll resort to a .createCriteria and then a .executeQuery which takes HQL.

Grails namedQuery sort order by multiple columns

Given a namedQuery:
class MyDomainObject {
String someProperty
static namedQueries = {
myNamedQuery {
// some implementation here
}
}
}
I can use it to generate a list, sorted by a single key, like this (documentation for 2.4.3 here):
def resultsList = MyDomainObject.myNamedQuery.list(sort: "someProperty", order: "desc")
How do I order the results by multiple columns? I'd like to be able to define the sort parameters dynamically, not define them in the query.
I'm sure there's a better way, but I ended up creating another named query that I can concatenate onto my chosen one (I could always incorporate into the original query too).
// expects to be passed a List containing a series of Maps
orderByMultipleColumns { List columnsToSortBy ->
columnsToSortBy.each { Map field ->
order("${field.fieldName}", field.fieldOrder)
}
}
// usage:
List orderByList = []
// ...
// within some loop that I use:
orderByList << [fieldName: someValue, fieldOrder: dir] // dir == 'asc' or 'desc'
// ...
MyDomainObject.myNamedQuery().orderByMultipleColumns(orderList).listDistinct(max: length, offset: start)

Grails 1:m get most relations

I'm relatively new to Grails.
I have the following
class House {
Integer number
Integer maxResidents
static belongsTo = [town: Town]
}
class Town {
String name
static hasMany = [houses: House]
}
I want to get five towns with most Houses. I have seen the possibility to create a criteria but I can't deal with it now. Can someone support?
Thank you!
As you have a bidirectional association you can do this with a query on House:
def result = House.withCriteria {
projections {
groupProperty("town", "town")
rowCount("numHouses")
}
order("numHouses", "desc")
maxResults(5)
}
This would return you a list of results where each result res has the town as res[0] and the number of houses as res[1]. If you'd prefer each result to be a map giving access to res.town and res.numHouses then you should add
resultTransformer(AliasToEntityMapResultTransformer.INSTANCE)
after the maxResults line (along with the appropriate import at the top of your file).

GORM createCriteria and list do not return the same results : what can I do?

I am using Nimble and Shiro for my security frameworks and I've just come accross a GORM bug. Indeed :
User.createCriteria().list {
maxResults 10
}
returns 10 users whereas User.list(max: 10) returns 9 users !
After further investigations, I found out that createCriteria returns twice the same user (admin) because admin has 2 roles!!! (I am not joking).
It appears that any user with more than 1 role will be returned twice in the createCriteria call and User.list will return max-1 instances (i.e 9 users instead of 10 users)
What workaround can I use in order to have 10 unique users returned ?
This is a very annoying because I have no way to use pagination correctly.
My domain classes are:
class UserBase {
String username
static belongsTo = [Role, Group]
static hasMany = [roles: Role, groups: Group]
static fetchMode = [roles: 'eager', groups: 'eager']
static mapping = {
roles cache: true,
cascade: 'none',
cache usage: 'read-write', include: 'all'
}
}
class User extends UserBase {
static mapping = {cache: 'read-write'}
}
class Role {
static hasMany = [users: UserBase, groups: Group]
static belongsTo = [Group]
static mapping = { cache usage: 'read-write', include: 'all'
users cache: true
groups cache: true
}
}
Less concise and clear, but using an HQL query seems a way to solve this problem. As described in the Grails documentation (executeQuery section) the paginate parameters can be added as extra parameters to executeQuery.
User.executeQuery("select distinct user from User user", [max: 2, offset: 2])
this way you can still use criteria and pass in list/pagination paramaters
User.createCriteria().listDistinct {
maxResults(params.max as int)
firstResult(params.offset as int)
order(params.order, "asc")
}
EDIT: Found a way to get both! Totally going to use it now
http://www.intelligrape.com/blog/tag/pagedresultlist/
If you call createCriteria().list() like this
def result=SampleDomain.createCriteria().list(max:params.max, offset:params.offset){
// multiple/complex restrictions
maxResults(params.max)
firstResult(params.offset)
} // Return type is PagedResultList
println result
println result.totalCount
You will have all the information you need in a nice PagedResultList format!
/EDIT
Unfortunately I do not know how to get a combination of full results AND max/offset pagination subset in the same call. (Anyone who can enlighten on that?)
I can, however, speak to one way I've used with success to get pagination working in general in grails.
def numResults = YourDomain.withCriteria() {
like(searchField, searchValue)
order(sort, order)
projections {
rowCount()
}
}
def resultList = YourDomain.withCriteria() {
like(searchField, searchValue)
order(sort, order)
maxResults max as int
firstResult offset as int
}
That's an example of something I'm using to get pagination up and running. As KoK said above, I'm still at a loss for a single atomic statement that gives both results. I realize that my answer is more or less the same as KoK now, sorry, but I think it's worth pointing out that rowCount() in projections is slightly more clear to read, and I don't have comment privileges yet :/
Lastly: This is the holy grail (no pun intended) of grails hibernate criteria usage references; bookmark it ;)
http://www.grails.org/doc/1.3.x/ref/Domain%20Classes/createCriteria.html
Both solutions offered here by Ruben and Aaron still don't "fully" work for pagination
because the returned object (from executeQuery() and listDistinct) is an ArrayList
(with up to max objects in it), and not PagedResultList with the totalCount property
populated as I would expect for "fully" support pagination.
Let's say the example is a little more complicated in that :
a. assume Role has an additional rolename attribute AND
b. we only want to return distinct User objects with Role.rolename containing a string "a"
(keeping in mind that a User might have multiple Roles with rolename containing a string "a")
To get this done with 2 queries I would have to do something like this :
// First get the *unique* ids of Users (as list returns duplicates by
// default) matching the Role.rolename containing a string "a" criteria
def idList = User.createCriteria().list {
roles {
ilike( "rolename", "%a%" )
}
projections {
distinct ( "id" )
}
}
if( idList ){
// Then get the PagedResultList for all of those unique ids
PagedResultList resultList =
User.createCriteria().list( offset:"5", max:"5" ){
or {
idList.each {
idEq( it )
}
}
order ("username", "asc")
}
}
This seems grossly inefficient.
Question : is there a way to accomplish both of the above with one GORM/HQL statement ?
You can use
User.createCriteria().listDistinct {
maxResults 10
}
Thanks for sharing your issue and Kok for answering it. I didn't have a chance to rewrite it to HQL. Here is my solution (workaround): http://ondrej-kvasnovsky.blogspot.com/2012/01/grails-listdistinct-and-pagination.html
Please tell me if that is useful (at least for someone).

Resources