The Sharat's

Implementing an expressive search system with clojure

Posted .

Backstory

I have recently learned clojure and its the first time I've been exposed to lisp and the code-as-data way of life. I was eager to use clojure to make an app, any app, a simple silly personal tool to help me out with a tedious task.

One such tool I created was classypants. Its a small swing based GUI tool that helps one to make sense out of the values of PATH like variables. The values of these variables are a list of paths of files/directories joined with : in *nix systems and ; on windows. Have you ever seen CLASSPATH entries that have ~100 jars/directories in it? Even if these values have just 20 items, its very hard to make any sense out of it.

Classypants is basically a pretty bare window carrying only 4 top level controls, one of which is an input box for searching through the entries. That search is what I want to talk about in this post.

Superpowers

Initially, the search box was just a filter box. I type some text and the entries that contain that text and shown, rest hidden. This quickly became annoying as I wanted to search for entries with jaxb and jar, which was not possible with the then implementation.

The implementation of the search I have today can do much more than even that. Its a powerful query language at work, using which we can filter entries that point to non-existing files, entries that point to directories that contain a said file and other wierdos.

How is it done?

I want to share how I went about evolving the search functionality. Let's talk about one function here,

(defn matches?
  [search-str entry]
  (-> resource
    (.indexOf search-str)
    (not= -1)))

This is the first incarnation of the search implementation. It just checks if the given search-str is present inside the entry.

That is nice and useful. But we want more power. We want a nice minimal query language to describe what we want to find, and it should be easy to remember. Lets work on negation of search results first, thinking up the simplest of syntaxes,

not resource

should match entries that do not contain resource. This doesn't look good, as it might also mean to search for entries that contain not or resource. We need some sugar to identify the not part as a directive that modifies how the search is done. Lets try again,

:not resource

Ah, the : in from of not gives it the special behaviour we need. Don't worry too much about why the syntax isn't not: resource or something else, it will become clear in a moment, if it hasn't already. Now that we have a search syntax, its time to get it work. Imagine a function, digest, which takes a search string and returns a function, which takes an entry and tells if its a match or not. I suck at writing, read that again.

Essentially, (digest ":not resource") should return a function, which more or less works like

(fn [entry]
  (not (matches? "resource" entry)))

We see if there is a match, and not its result. Lets try writing the digest function,

(defn digest
  [search-str]
  (read-string (str "(" search-str ")")))

What we do above is wrap the search-str in paranthesis and read it into a clojure list. Lets try out our function in the repl

user=> (digest ":not resource")
(:not resource)

Yep, just what we expected. Now, lets take this further ahead

(defn digest
  [search-str]
  (let [spec (read-string (str "(" search-str ")"))]
    (cond
      (= (first spec) :not) (fn [e]
                              (not (matches? (nth spec 1) e))))))

Comments