Skip to content

Generic search for auto complete using Hibernate, Ajax, Scriptaculous – Part 1

Ever wondered how to write a custom search function with pattern matching, generic enough to for an auto completer using Hibernate? In this part I will explain the back end portion of writing an auto completer, for the next part you will see an Ajax front end using DWR and scriptaculous.

Use Case Scenario - User has a text input field. User types in “Bi” and a request is sent to the server for searching all matching strings with “Bi”.

Technical Challenges Implementing this functionality for a single scenario should be straight forward but having a solution generic enough that any single or a combination of attributes of your domain objects could be searched using hibernate in the dao layer, has some challenges. Also remember, we are not interesting in Pojos but returning “Hibernate Objects” to the UI.

Solution In this example, I use a domain object, ResearchStaff. Let’s say, the first request from the client is matchResearchStaffs(String textPattern). Your Controller (or ajax facade) would look something similar.

public List matchResearchStaffs(String text, HttpServletRequest request) throws Exception{
    List  staffCol = researchStaffDao.getBySubnames(extractSubnames(text));

    // The below code is not required for simple searches. But if your ResearchStaff Object has a lot
    // of attributes and collections. You may want to strip it off or "reduce" it before sending it to the UI
    List  reducedStaffCol = new ArrayList (staffCol.size());
    for (ResearchStaff staff : staffCol) {
        reducedStaffCol.add(buildReduced(staff, Arrays.asList("id", "firstName","lastName")));
    }

     return reducedStaffCol;
}

As you see, the controller delegates to Dao for pattern matching and this is where Dao is smart enough to figure out ‘which’ domain object’s ‘what’ attributes are being queried. In my last article, you’ve seen how to couple an abstract domain object to a Base Dao and make it ‘aware’. Well, here you see the real benefits of that coupling.

    /**
     * A query builder for use by subclass DAOs.  It makes it easy to match a fragment of a name
     * or identifier against multiple properties.  This is intended for use in implementing
     * user-friendly dynamic searches; e.g., autocompleters.
     *
     * @param subnames the name fragments to search on
     * @param extraConditions custom HQL conditions with which to constrain the fragment matches
     * @param extraParameters parameters for the custom conditions
     * @param substringMatchProperties a list of properties of the implementing object which should
     *             be matched as case-insensitive substrings
     * @param exactMatchProperties a list of properties which should be matched as case-insensitive
     *             full strings
     * @return a list of matching domain object instances
     */
    @SuppressWarnings("unchecked")
    protected List findBySubname(
        String[] subnames, String extraConditions, List extraParameters,
        List substringMatchProperties, List exactMatchProperties
    ) {
        StringBuilder query = new StringBuilder("from ")
            .append(domainClass().getName()).append(" o where ");
        if (extraConditions != null) query.append(extraConditions).append(" and ");
        List params = new LinkedList();
        if (extraParameters != null) params.addAll(extraParameters);

        for (int i = 0; i < subnames.length; i++) {
            buildSubnameQuery(subnames[i], query, params,
                substringMatchProperties, exactMatchProperties);
            if (i < subnames.length - 1) query.append(" and ");
        }

        log.debug("query string = " +query);
        return getHibernateTemplate().find(query.toString(), params.toArray());
    }

    protected void buildSubnameQuery(
        String subname, StringBuilder query, List params,
        List substringMatchProperties, List exactMatchProperties) {
        query.append('(');
        if (hasAny(substringMatchProperties)) {
            for (Iterator it = substringMatchProperties.iterator(); it.hasNext();) {
                String prop = it.next();
                query.append("LOWER(o.").append(prop).append(") LIKE ?");
                params.add('%' + subname.toLowerCase() + '%');
                if (it.hasNext()) query.append(" or ");
            }
            if (hasAny(exactMatchProperties)) {
                query.append(" or ");
            }
        }
        if (hasAny(exactMatchProperties)) {
            for (Iterator it = exactMatchProperties.iterator(); it.hasNext();) {
                String prop = it.next();
                query.append("LOWER(o.").append(prop).append(") = ?");
                params.add(subname.toLowerCase());
                if (it.hasNext()) query.append(" or ");
            }
        }
        query.append(')');
    }

    private boolean hasAny(List properties) {
        return properties != null && properties.size() > 0;
    }

For more working code and complex use cases, feel free to look into our gforge codebase in the core/web sub projects

AbstractBaseDao
StudyAjaxFacade

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

One Comment

  1. meridia buy only meridia phentermine buy

    Wednesday, December 26, 2007 at 3:31 am | Permalink

Post a Comment

Your email is never published nor shared. Required fields are marked *
*
*