Employ Metadata to Enhance Search Filters

asily customizable and configurable software is becoming increasingly important, and a flexible interface for searching is one way in which software is becoming more configurable. The key to achieving flexibility is through using metadata. This discussion will take a look at how you can use metadata to create a flexible interface that improves users’ ability to search for stored information while minimizing the overall complexity that comes with search filters.

Consider an application that stores customer, item, and order information in a database. The interface for searching through orders could apply any number of filters, but presenting all possible combinations together can very quickly become overwhelming for users. It is often beneficial to allow some customization or configuration for choosing the appropriate filters based on several factors including the business process, the role of the individual, or those that are specific to the user’s needs. With traditional query templates, the complexity grows quickly with every new search filter that is added. However, by using metadata to model a query and its filters, you can reduce the complexity of the software, while creating a more flexible solution.

Creating Filter Metadata
Metadata can be loosely defined as data about data, or in this case search-filter data about order data. The World Wide Web Consortium (W3C), the group responsible for XML standards, recommends using Resource Description Framework (RDF) for representing metadata (in XML or other formats). You can store RDF in a variety of formats, but the example discussed here will use an RDF/XML file because it has the widest support. XML tags are used to structure the file format of RDF/XML. The outer tags represent the resources, their nested tags represent properties, and inside the property tags is a property value, which may be text or another resource tag.

Identify the Markup
You can use the rdf:about attribute to name the resources that you can reference using the rdf:resource attribute on the property tag. Before you start creating an RDF document, you first need to identify what markup language you will use. The example discussed here will demonstrate creating your own search-specific markup language or Domain Specific Language (DSL). To begin, take a look at this RDF/XML document that describes a resource, called order-number, which is a SearchFilter that has the property sql with the value of a SQL fragment:

xml version="1.0"?><rdf:RDF  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"  xmlns=http://example.org/rdf/search#  xml:base=http://example.org/rdf/search-metadata/>  <SearchFilter rdf:about="order-number">    <sql>AND orders.number = ?sql>  SearchFilter>rdf:RDF>

Here is another example of RDF/XML that has the resource orders of type SearchQuery with property sql and searchFilters:

xml version="1.0"?><rdf:RDF  xmlns:rdf=http://www.w3.org/1999/02/22-rdf-syntax-ns#  xmlns=http://example.org/rdf/search#  xml:base=http://example.org/rdf/search-metadata/>  <SearchQuery rdf:about="orders">    <sql>      SELECT orders.number, customers.name as customer, orders.date, orders.amount      FROM orders, customers      WHERE customers.customer_id = orders.customer_id    sql>    <searchFilters>      <rdf:Seq>        <rdf:li>          <SearchFilter>            <name>numbername>            <sql>AND orders.number = ?sql>          SearchFilter>        rdf:li>        <rdf:li>          <SearchFilter>            <name>customername>            <sql>              AND customers.name LIKE ?            sql>          SearchFilter>        rdf:li>      rdf:Seq>    searchFilters>  SearchQuery>rdf:RDF>

The searchFilters property has a sequence of two anonymous items of type SearchFilter having the name and sql properties. Now you will have to be able to access this metadata from your application. When working with an object-oriented (OO) language, such as Java, it is most convenient to access the RDF data through JavaBeans objects.

Reading RDF Data
You have a lot of libraries at your disposal for importing or reading RDF. In the examples discussed here, you can see how to read RDF data with a subset of the Java Persistence API (JPA) and OpenRDF.org‘s Elmo implementation.

To get started, first create a JavaBeans interface. Elmo maps the Java interfaces to RDF resources using the @rdf annotation, which is placed on the bean interfaces and its properties. For this example you will need a SearchQuery and a SearchFilter interface with mapped properties:

public class Vocabulary {  public static final String NS = http://example.org/rdf/search#;  public static final String BASE = http://example.org/rdf/search-metadata/;}@rdf(NS + "SearchQuery")public interface SearchQuery {  @rdf(NS + "sql")  String getSql();  @rdf(NS + "searchFilters")  List getSearchFilters();}@rdf(NS + "SearchFilter")public interface SearchFilter {  @rdf(NS + "name")  String getName();  @rdf(NS + "sql")  String getSql();}

Next, create the persistence.xml file that the JPA interface will load:

xml version="1.0"?><persistence>  <persistence-unit name="search-metadata">    <provider>org.openrdf.elmo.sesame.SesamePersistenceProviderprovider>    <class>org.example.metadata.SearchQueryclass>    <class>org.example.metadata.SearchFilterclass>    <properties>      <property name="resources" value="META-INF/metadata/order-search.xml"/>    properties>  persistence-unit>persistence>

Now use the JPA interface to load your JavaBeans into memory:

EntityManagerFactory factory;EntityManager manager;SearchQuery query;factory = Persistence.createEntityManagerFactory("search-metadata"); // load RDFmanager = factory.createEntityManager(); // acquire accessquery = manager.find(SearchQuery.class, new QName(BASE, "orders"));// .. do something with query heremanager.close(); // objects can not be used once manager is closefactory.close(); // free up resources

With the RDF data available to your application, you can now use this data to dynamically create the query string and search filter interface.

Dynamic Searching
Here is a query builder that accesses the search filter metadata through JavaBeans:

StringBuilder sb = new StringBuilder();sb.append(query.getSql());for (SearchFilter filter : query.getSearchFilters()) {  String value = req.getParameter(filter.getName());  if (value != null && value.length() > 0) {    sb.append(filter.getSql()).append('
');  }}...stmt = conn.prepareStatement(sb.toString());int p = 0;for (SearchFilter filter : query.getSearchFilters()) {  String value = req.getParameter(filter.getName());  if (value != null && value.length() > 0) {    stmt.setString(++p, value);  }}rs = stmt.executeQuery();

It loops through all searchFilters and, if included in the parameters, it includes the sql of the filter in the query string and the parameter in the query parameters. This method has a cyclomatic complexity of 4 (which is low risk), regardless of how many search filters you are using. The search interface can also be driven by the same metadata that is used in the query builder. Listing 1 provides another set of RDF/XML and JavaBeans interfaces that—expanding on the previous example—add presentation information.

Using the previous metadata, a search.jsp can loop through the available filters and output their label and an input value:

<form>  <fieldset>    <legend>Search Criterialegend>    <table>      <% for (SearchFilter filter : query.getSearchFilters()) { %>        <tr>          <td><%= filter.getLabel() %> td>          <td><input type="text"            name="<%= filter.getName() %>"            id="<%= filter.getName() %>"            size="<%= filter.getWidth() %>"            value='<%= param.containsKey(filter.getName()) ?              param.get(filter.getName())[0] : "" %>'/>          td>        tr>      <% } %>    table>  fieldset>  <input class="button" type="submit" value="Submit"/>form>

Notice that the query builder and search JSP have no knowledge of orders or their search filters. This concept is important as it shows that you are not repeating yourself and are improving the reusability and maintainability of the software. By using this technique you can reduce the complexity of the query builder and have the query string and query parameters drive from the same metadata.

Extending SearchFilter
Now take a look at handling a more complicated input, such as date information. Dates can become really simple if you are using a component-oriented framework like JavaServer Faces (JSF) because you can have the paramaters automatically converted. The Spring Framework can also provide this behavior by binding to a variable. If you rely on a framework to parse the value, or you choose to parse the value yourself, you need to include type information in the SearchFilter. You can also implement parsing yourself by adding a parse() method to the SearchFilter interface:

@rdf(NS + "SearchFilter")public interface SearchFilter {  @rdf(NS + "label")  String getLabel();  @rdf(NS + "width")  String getWidth();  @rdf(NS + "type")  String getType();  Object parse(String input) throws ParseException;  @rdf(NS + "name")  String getName();  @rdf(NS + "sql")  String getSql();}

Notice that the parse() method is not mapped to an RDF property because you will be implementing it yourself as stated earlier. Create this SearchFilterParser mixin class (you will need to add this class to the persistence.xml file):

@rdf(NS + "SearchFilter")public class SearchFilterParser {  private static ThreadLocal dateFormat =     new ThreadLocal() {    @Override    protected DateFormat initialValue() {      return new SimpleDateFormat("yyyy-M-d");    }  };  private SearchFilter filter;  public SearchFilterParser(SearchFilter filter) {    this.filter = filter;  }  public Object parse(String input) throws ParseException {    if (input == null || input.length() == 0)      return null;    if ("date".equals(filter.getType()))      return new java.sql.Date(dateFormat.get().parse(input).getTime());    return input;  }}

This class implements the parse() method declared in the previous interface. Notice that both the interface and this class have the same @rdf annotation. You also might want to create subclasses to keep the Parser class from becoming too complicated, but for this example the previous class is adequate. The query builder now needs to be changed to call the parse() method:

stmt.setObject(++p, filter.parse(value));

Independent Search Filters
With the complexity removed, now you are able to add a large number of search filters without the risk associated with changing complex software. The search filters are stored in RDF, allowing you to continue to explore different ways to make the search interface more flexible.

Inside the RDF/XML document there are technical implementation details (sql and parameter); presentation details (title, label, and width); and the list of SearchFilters. The technical details and presentation details are likely going to be written by the same technical group—so keep them together. However, the choice of search filters is more variable and might vary by installation or user. By separating the list of search filters into its own file, you can better control the variability of change and isolate the unique requirements of different users.

Listing 2 shows the same RDF data separated by the searchFilters property into two different RDF/XML files (which doesn’t require the code to be recompiled).

The benefit you gain is the ability to deploy these files separately. Consider the advantages of updating a deployment. The order-search.xml file is replaced with the new version, while the order-search-filters.xml file is preserved. This technique keeps the desired list of filters unique to each deployment. The list could even be more finely grained to role or user. The SearchQuery also may be extended to contain a list of possible SearchFilters for selection—or perhaps use an exclusion list. Maybe your application could have an administrative interface to select the desired search filters that are made available or removed. If you add setter methods to your interfaces (without the annotation) the model can be modified, and the repository can be set up to persist the changes in the persistence.xml file (provided the modifications do not conflict with loaded resources).

By orders of magnitude, the degree of flexibility for database search interfaces can be increased with better customization and not much additional overhead cost. (This technique can be applied to a variety of situations beyond creating a reusable search engine.) By gathering together information that is already available elsewhere in an application, you can remove both code and knowledge duplication to reduce the risk of defects and the complexity of your software while at the same time increasing its flexibility and—by extension—your users’ productivity.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles:

©2023 Copyright DevX - All Rights Reserved. Registration or use of this site constitutes acceptance of our Terms of Service and Privacy Policy.

Sitemap