(ns lucenalog.core "Lucenalog = Datalog interface to Lucene in 10 lines. Simple but powerful. Use (db/add (index) {:a \"foo\" :b \"bar\"}) to index a map with Lucene. Then you can use the relation 'lucenalog-rel' from core.logic. That relation works on maps. Bound map values produce a Lucene query, which returns matching maps. See 'lucenalog-test' for an example." (:require [clucy.core :as db] [clojure.core.logic :as logic])) ;; (defproject lucenalog "0.0.1-SNAPSHOT" ;; :description "Lucenalog = Datalog interface to Lucene in 10 lines" ;; :dependencies [[org.clojure/clojure "1.2.1"] ;; [clucy "0.3.0"] ;; [org.clojure/core.logic "0.6.6"] ;; ] ;; :main lucenalog.core) (set! *warn-on-reflection* true) ;; Accessory functions. Not important. (let [m (ref {:max-query-results 1024 :verbose true})] (defn config "Lucenalog configuration. Call with no args to see the current configuration. Get a configuration property's value by passing the property to this function. Change the config using the two-argument dispatch." ([] @m) ([k] (@m k)) ([k v] (dosync (alter m assoc k v))))) (defonce index ;; Get the default Lucene index. (let [i (db/memory-index)] (fn [] i))) (defn- note [& args] "Println the args if (config :verbose). Return the last arg." (when (config :verbose) (apply println :note args)) (last args)) ;; Generate the Lucene query string. (defn- lucene-query "Query Lucene based on the given object and Substitutions. The query Q should be a map that has some values bound by substitutions A. The generated Lucene query string looks like 'p1:v1 AND p2:v2', where Q contains :p1 lv1 and :p2 lv2 and the substitutions take lv1 to v1 and lv2 to v2. That query string is then used in the Lucene query." ([q a] (db/search (index) (let [walked (logic/walk* a q) query (reduce str (interpose " AND " (map (fn [[k v]] (str (name k) ":" v)) (remove (comp logic/lvar? last) walked))))] (note :lucene-search walked q a :query query)) (config :max-query-results)))) ;; The core.logic relation. (defn lucenalog-rel [q] "A clojure.core.logic relation backed by Lucene. Lucene query generated by lucene-query based on the given map." (fn [a] (logic/to-stream (map #(logic/unify a % q) (lucene-query q a))))) ;; Tests and examples. (defn lucenalog-test "Simple check that Lucenalog is working. Returns true if things look okay." ([] ;; Let's use our own in-memory Lucene index. (binding [index (let [i (db/memory-index)] (fn [] i))] (with-open [i ^org.apache.lucene.store.Directory (index)] ;; Add a little data. (doseq [m [{:a "a1" :b "z1"} {:a "z1" :b "b1"} {:a "a1" :b "z2"} {:a "z1" :b "b2"}]] (db/add i m)) ;; Check to see if we can chase a1 to b1 and b2 via z1. (let [expect #{"b1" "b2"} got (set (logic/run* [q] (logic/fresh [a b c] (logic/== a "a1") (lucenalog-rel {:a a :b b}) (lucenalog-rel {:a b :b c}) (logic/== q c)))) passed? (= expect got)] (println :test passed? :got got :expected expect) passed?))))) (defn -main ([& args] (lucenalog-test)))