7. Querying Working Memory

Jess's working memory is similar to a database; it's filled with indexed, structured data. Most of the time, you'll access working memory by pattern matching from a rule. But once in a while, you'll write some procedural code that needs to extract data from working memory directly. This chapter tells you how.

7.1. Linear search

A time-honored way to search a collection of items is by using linear search and a filter, a Boolean function that indicates whether an item should be part of the search result or not. Jess supports this kind of brute-force search of Java objects in working memory using the jess.Filter interface. You can implement jess.Filter and then pass an instance of your filter to the jess.Rete.getObjects(jess.Filter) method, which will return an java.util.Iterator over the selected objects. Although this might not be practical for large working memory sizes, it's certainly convenient. In the following, we run a Jess program that deals in Payment objects, then query working memory for all the Payment objects and call process() on each one:

    
import jess.*;
import java.util.Iterator;
public class ExMyFilter {
    interface Payment { void process(); }
    public static void main(String[] argv) throws JessException {
        Rete engine = new Rete();
        engine.batch("cashier.clp");
        Iterator it = engine.getObjects(new Filter() {
            public boolean accept(Object o) {
                return o instanceof Payment;
            }
        });
        while (it.hasNext()) {
            ((Payment) it.next()).process();
        }
    }
}

7.2. The defquery construct

While linear search is convenient, it's inefficient. The defquery construct lets you create a special kind of rule with no right-hand-side. While normal rules act spontaneously, queries are used to search the working memory under direct program control. Whereas a rule is activated once for each matching set of facts, a query gives you a jess.QueryResult object which gives you access to a list of all the matches.

Using a defquery involves three steps:

  1. Writing the query
  2. Invoking the query
  3. Using the results

In the following section, we'll examine a specific example of how this is done.

7.3. A simple example

An example should make this clear. We'll go through the three steps listed above, showing one or more ways to perform each step.

7.3.1. Writing the query

Once again, we're writing a program that deals with a database of people. Once again, we'll use the following deftemplate
Jess> (deftemplate person (slot firstName) (slot lastName) (slot age))

Imagine that we want to be able to query working memory to find people with a given last name. Once we've found a person, we'll want to know their first name as well, and their age. We can write a query which lets us specify the last name we're interested in, and also lets us easily recover the first name and age of each match.

A query looks a lot like the left-hand side of a rule. We write a pattern which matches the facts that we're interested in. We declare the variable ?ln, which makes it a parameter to the query, and we also include variables ?fn and ?age bound to the firstName and age slots, respectively.
Jess> (defquery search-by-name
  "Finds people with a given last name"
  (declare (variables ?ln))
  (person (lastName ?ln) (firstName ?fn) (age ?age)))

While we're at it, let's define some facts for the query to work with and load them into working memory:

Jess> (deffacts data
  (person (firstName Fred)   (lastName Smith)    (age 12))
  (person (firstName Fred)   (lastName Jones)    (age 9))
  (person (firstName Bob)    (lastName Thomas)   (age 32))
  (person (firstName Bob)    (lastName Smith)    (age 22))
  (person (firstName Pete)   (lastName Best)     (age 21))
  (person (firstName Pete)   (lastName Smith)    (age 44))
  (person (firstName George) (lastName Smithson) (age 1))
  )
Jess> (reset)

7.3.2. Invoking the query

After we define a query, we can call it using the run-query* function in Jess, or the jess.Rete.runQueryStar(java.lang.String, jess.ValueVector) method in Java. In both cases, we need to pass the last name we're interested in as the query parameter. In Jess, this looks like

Jess> (reset)
Jess> (bind ?result (run-query* search-by-name Smith))

The arguments to run-query* are the name of the query and values for each parameter. The run-query* function returns a jess.QueryResult object, which we store in the variable ?result.

In Java, the equivalent code would look like:

    Rete engine = ...
    QueryResult result = engine.runQueryStar("search-by-name", new ValueVector().add("Smith"));

7.3.3. Using the Results

Now that we've created a jess.QueryResult, it's time to iterate over all the matches and process them however we'd like. For this example, let's just print a table of the people including their full names and ages.

The interface of the jess.QueryResult class is very similar to that of java.sql.ResultSet. We use jess.QueryResult.next() to advance to the next result, and we use the set of getXXX() methods to return the values bound to the variables defined in the query. Then the following Jess code will print the output shown:

Jess> (while (?result next)
    (printout t (?result getString fn) " " (?result getString ln)
                ", age " (?result getInt age) crlf))
Fred Smith, age 12
Bob Smith, age 22
Pete Smith, age 44
FALSE

because there are three people named Smith in working memory.

The same loop in Java would look like

while (result.next()) {
    System.out.println(result.getString("fn") + " " + result.getString("ln")
                       + ", age" + result.getInt("age"));
}

7.3.4. One more time, in Java

For the sake of completeness, here's the whole program in Java. The file "query.clp" is assumed to contain the deftemplate, defquery, and deffacts from above.

    
import jess.*;

public class ExQuery {
    public static void main(String[] argv) throws JessException {
        Rete engine = new Rete();
        engine.batch("query.clp");
        engine.reset();
        QueryResult result =
            engine.runQueryStar("search-by-name", new ValueVector().add("Smith"));
        while (result.next()) {
            System.out.println(result.getString("fn") + " " + result.getString("ln")
                               + ", age" + result.getInt("age"));
        }
    }
}
C:\> java ExQuery

7.4. The variable declaration

You have already realized that two different kinds of variables can appear in a query: those that are "internal" to the query, like ?age in the query above, and those that are "external", or to be specified in the run-query* command when the query is executed, like ?ln. Jess assumes all variables in a query are internal by default; you must declare any external variables explicitly using the syntax
(declare (variables ?X ?Y ...))
which is quite similar to the syntax of a rule salience declaration.

7.5. The max-background-rules declaration

It can be convenient to use queries as triggers for backward chaining. For this to be useful, jess.Rete.run() must be called while the query is being evaluated, to allow the backward chaining to occur. Facts generated by rules fired during this run may appear as part of the query results. (If this makes no sense whatsoever to you, don't worry about it; just skip over this section for now.)

By default, no rules will fire while a query is being executed. If you want to allow backward chaining to occur in response to a query, you can use the max-background-rules declaration -- i.e.,
(declare (max-background-rules 10))
        
would allow a maximum of 10 rules to fire while this particular query was being executed.

7.6. The count-query-results command

To obtain just the number of matches for a query, you can use the count-query-results function. This function accepts the same arguments as run-query*, but just returns an integer, the number of matches.

7.7. Using dotted variables

The dotted variable syntax provides a convenient alternative for accessing the slots of a fact resulting from a query. Let's look at the rewrite the search-by-name query again. This time we'll bind a single variable to the entire fact, and use dotted variables to retrieve the results.

Jess> (defquery search-by-name
  "Finds people with a given last name"
  (declare (variables ?ln))
  ?person <- (person (lastName ?ln)))

Running the query doesn't change:

Jess> (reset)
Jess> (bind ?result (run-query* search-by-name Smith))

This time, we are going to use different code to retrieve the data:

Jess> (while (?result next)
    (bind ?p (?result getObject person))
    (printout t ?p.firstName " " ?p.lastName ", age " ?p.age crlf))

Fred Smith, age 12
Bob Smith, age 22
Pete Smith, age 44
FALSE

The bind stores the jess.Fact object that is retrieved from the current value of ?result into variable ?p. This, then, is conveniently accessed for its slot values firstName, lastName and age.