Next: , Previous: Persistent collections, Up: Tutorial


2.8 Indexing Persistent Classes

Class indexing simplifies the storing and retrieval of persistent objects. An indexed class stores every instance of the class that is created, ensuring that every object is automatically persisted between sessions.

     (defpclass friend ()
       ((name :accessor name :initarg :name)
        (birthday :initarg :birthday))
       (:index t))
     => #<PERSISTENT-METACLASS FRIEND>
     
     (defmethod print-object ((f friend) stream)
       (format stream "#<~A>" (name f)))
     
     (defun encode-date (dmy)
       (apply #'encode-universal-time
         (append '(0 0 0) dmy)))
     
     (defmethod (setf birthday) (dmy (f friend))
       (setf (slot-value f 'birthday)
             (encode-date dmy))
       dmy)
     
     (defun decode-date (utime)
       (subseq (multiple-value-list (decode-universal-time utime)) 3 6))
     
     (defmethod birthday ((f friend))
       (decode-date (slot-value f 'birthday)))

Notice the class argument “:index t”. This tells Elephant to store a reference to this class. Under the covers, there are a set of btrees that keep track of classes, but we won't need to worry about that as all the functionality has been nicely packaged for you.

We also created our own birthday accessor for convenience so it accepts and returns birthdays in a list consisting of month, day and year such as (27 3 1972). The index key will be the encoded universal time, however.

Now we can easily manipulate all the instances of a class.

     (defun print-friend (friend)
       (format t " name: ~A birthdate: ~A~%"
               (name friend) (birthday friend)))
     
     (make-instance 'friend :name "Carlos"
                            :birthday (encode-date '(1 1 1972)))
     (make-instance 'friend :name "Adriana"
                            :birthday (encode-date '(24 4 1980)))
     (make-instance 'friend :name "Zaid"
                            :birthday (encode-date '(14 8 1976)))
     
     (get-instances-by-class 'friends)
     => (#<Carlos> #<Adriana> #<Zaid>)
     
     (mapcar #'print-friend *)
      name: Carlos birthdate: (1 1 1972)
      name: Adriana birthdate: (24 4 1980)
      name: Zaid birthdate: (14 8 1976)
     => (#<Carlos> #<Adriana> #<Zaid>)

But what if we have thousands of friends? Aside from never getting work done, our get-instances-by-class will be doing a great deal of consing, eating up lots of memory and wasting our time. Fortunately there is a more efficient way of dealing with all the instances of a class.

     (map-class #'print-friend 'friend)
      name: Carlos birthdate: (1 1 1972)
      name: Adriana birthdate: (24 4 1980)
      name: Zaid birthdate: (14 8 1976)
     => NIL

map-class has the advantage that it does not keep references to objects after they are processed. The garbage collector can come along, clear references from the weak instance cache so that your working set is finite. The list version above conses all objects into memory before you can do anything with them. The deserialization costs are very low in both cases.

Notice that the order in which the records are printed are not sorted according to either name or birthdate. Elephant makes no guarantee about the ordering of class elements, so you cannot depend on the insertion ordering shown here.

So what if we want ordered elements? How do we access our friends according to name and birthdate? This is where slot indices come into play.

     (defpclass friend ()
       ((name :accessor name :initarg :name :index t)
        (birthday :initarg :birthday :index t)))

Notice the :index argument to the slots and that we dropped the class :index argument. Specifying that a slot is indexed automatically registers the class as indexed. While slot indices increase the cost of writes and disk storage, each entry is only slightly larger than the size of the slot value. Numbers, small strings and symbols are good candidate types for indexed slots, but any value may be used, even different types. Once a slot is indexed, we can use the index to retrieve objects by slot values.

get-instances-by-value will retrieve all instances that are equal to the value argument.

     (get-instances-by-value 'friends 'name "Carlos")
     => (#<Carlos>)

But more interestingly, we can retrieve objects for a range of values.

     (get-instances-by-range 'friends 'name "Adam" "Devin")
     => (#<Adriana> #<Carlos>)
     
     (get-instances-by-range 'friend 'birthday
                             (encode-date '(1 1 1974))
                             (encode-date '(31 12 1984)))
     => (#<Zaid> #<Adriana>)
     
     (mapc #'print-friend *)
      name: Zaid birthdate: (14 8 1976)
      name: Adriana birthdate: (24 4 1980)
     => (#<Zaid> #<Adriana>)

To retrieve all instances of a class in the order of the index instead of the arbitrary order returned by get-instances-by-class you can use nil in the place of the start and end values to indicate the first or last element. (Note: to retrieve instances null values, use get-instances-by-value with nil as the argument).

     (get-instances-by-range 'friend 'name nil "Sandra")
     => (#<Adriana> #<Carlos>)
     
     (get-instances-by-range 'friend 'name nil nil)
     => (#<Adriana> #<Carlos> #<Zaid>)

There are also functions for mapping over instances of a slot index. To map over duplicate values, use the :value keyword argument. To map by range, use the :start and :end arguments.

     (map-inverted-index #'print-friend 'friend 'name :value "Carlos")
      name: Carlos birthdate: (1 1 1972)
     => NIL
     
     (map-inverted-index #'print-friend 'friend 'name
                      :start "Adam" :end "Devin")
      name: Adriana birthdate: (24 4 1980)
      name: Carlos birthdate: (1 1 1972)
     => NIL
     
     (map-inverted-index #'print-friend 'friend 'birthday
                      :start (encode-date '(1 1 1974))
                      :end (encode-date '(31 12 1984)))
      name: Zaid birthdate: (14 8 1976)
      name: Adriana birthdate: (24 4 1980)
     => NIL
     
     (map-inverted-index #'print-friend 'friend 'birthday
                      :start nil
                      :end (encode-date '(10 10 1978)))
      name: Carlos birthdate: (1 1 1972)
      name: Zaid birthdate: (14 8 1976)
     => NIL
     
     (map-inverted-index #'print-friend 'friend 'birthday
                      :start (encode-date '(10 10 1975))
                      :end nil)
      name: Zaid birthdate: (14 8 1976)
      name: Adriana birthdate: (24 4 1980)
     => NIL

The User Guide contains a descriptions of the advanced features of Class Indices such as “derived indicies” that allow you to order classes according to an arbitrary function, a dynamic API for adding and removing slots and how to set a policy for resolving conflicts between the code image and a database where the indexing specification differs.

This same facility is also available for your own use. For more information see BTree Indexing.