Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: 2063292f6b
Fetching contributors…

Cannot retrieve contributors at this time

file 367 lines (284 sloc) 16.739 kb

borneo

Wrapper for Neo4j, a graph database.

Purpose of this library is to provide intiutive access to commonly used Neo4j operations. It uses official Neo4j Java bindings. It does not use Blueprints interface.

Rationale

I've decided to create my own Neo4j wrapper, because I was not happy with the current state (01/2011) of existing ones. While I initially forked from hgavin/clojure-neo4j, I quickly realized that drastic changes will be needed. This chapter summarizes my motivation behind changes and decisions I made.

I believe that wrappers are a sub-optimal solution. See some recent Clojure talks on protocols where they compare wrappers, monkey patching and protocols. Main problem with wrappers is that they are no longer original types so you will not be able to use existing functions which accept original type. That is why borneo works mainly with Neo4j classes (Node, Relationship), and does not automatically convert nodes to property maps.

It is tedious to provide a connection for every operation on database. That is why I chose to have a dedicated Var for storing current connection. That of course brings several problems to the scene. Sometimes you want to have a connection which is shared between threads and sometimes you want to have parallel connections to multiple databases. (Embedded Neo4j does not allow for parallel connections to single database). Both cases are supported in borneo. You can use with-db! for a connection accessible from every thread, and by using with-local-db!, you can have a tread local connection. One drawback is that you cannot have both at one moment, so be careful.

Because I like simple things (see Halloways talk on simplicity), I tried to provide functions that do simple things, simple. That is why properties handling function are divided into two separate ones, one for reading and one for mutating. I also provide some "compound" functions, like props, create-child! or delete-node!, but they are here only for convenience and their simple counterparts are also provided.

While mentioned in previous section, I'd like to stress that I consider separation of mutable and immutable world very important in Clojure. That is why all mutable functions in borneo are clearly separated from their read-only parts and cannot be used in transactions.

I have added support for custom Returnable and Stop evaluators through protocols. I think it will allow for greater flexibility (see last example at the bottom of this page).

Another thing I wanted very much in a Neo4j wrapper was to use keywords instead of custom static types/enums, to feel more like you are in Clojure and not in Java... It turned out to be fairly easy to implement.

All mutable operations are automatically wrapped in transactions (read only operations don't need transactions in recent Neo4j). By the way Neo4j handles transactions, it should be pretty cheap to have nested transactions so you can use with-tx to group mutable operations into one big transactions if you need it. Needs some field testing to prove this design decision though.

If you get properties for a node with props function, you fetch all properties at once. This may be very resource intensive, when you have large binary data stored in nodes properties. One big wish I had is to have some king of lazy PersistentMap, where value would be fetched on demand. I've thougt of using delay/lazy-seq on values to achieve that, but user would have to manually deref the value, which is not very intuitive and does not look good. I didn't have time to implement such lazy map by myself yet. This data structure could also allow for even less intrusive interface so you could work with data stored in Neo4j more like working with traditional Clojure map, without serious performance impact. Who knows. More hammock time needed.

Usage

Add the following dependency to your project.clj file:

[borneo "0.1.0-SNAPSHOT"]

Documentation

Detailed API docs are at http://wagjo.github.com/borneo/

Quick overview of available functions (most important ones are emphasized):

  • Database management
    • *neo-db* - Holds current database instance
    • start! - Establish a connection to the database
    • stop! - Closes a connection stored in *neo-db*
    • with-db! - establish a connection to the database
    • with-local-db! - establish a thread local connection to the database
    • with-tx - establish a transaction
    • get-path - get path to where database is stored
    • read-only? - returns true if database is read only
    • index - returns Index Manager
  • Property Containers (both Nodes and Relationships)
    • prop? - returns true if node or relationship contains given property
    • prop - returns specific property value for a given node or relationship
    • props - returns map of properties for a given node or relationship
    • set-prop! - sets or removes property in a given node or relationship
    • set-props! - sets (or removes) properties for a given node or relationships
    • get-id - returns id of a given node or relationship
    • delete! - deletes relationship or free node
  • Relationships
    • rel-nodes - returns the two nodes attached to the given relationship
    • start-node - returns start node for given relationsip
    • end-node - returns end node for given relationsip
    • other-node - returns other node for given relationsip
    • rel-type - returns type of given relationship
    • create-rel! - create relationship between two nodes
    • all-rel-types - returns lazy seq of all relationship types in database
  • Nodes
    • rel? - returns true if node has given relationship(s)
    • rels - returns relationships attached to given node
    • single-rel - returns single relationship for given node
    • create-node! - creates new node, not linked with any other nodes
    • create-child! - creates a child node of a given parent
    • delete-node! - delete node and all its relationships
  • Graph traversal protocols
  • Graph traversal
    • all-nodes - returns lazy-seq of all nodes in database
    • node-by-id - returns node with a given id
    • rel-by-id - returns relationship with a given id
    • root - returns root/reference node
    • walk - walk though the graph by following through given single relations
    • traverse - traverse the graph

Examples

Following examples show basic borneo functions. Code presented here is not meant to be an idiomatic clojure code, e.g. you should wrap most of your operations in separate functions, and use let instead of def to store a reference to a node.

Basic usage

Require a borneo ns and wrap all borneo related stuff in a with-db! macro:

(ns foo.example
  (:require [borneo.core :as neo]))

(neo/with-db! "matrix-db"

  ;; use borneo here

)

Populate database

Populate database with graph inspired by Neo4j Matrix social graph (for simplicity I do not check if graph already exists):

;; basic layout
(def humans (neo/create-child! :humans nil))
(def programs (neo/create-child! :programs nil))

;; add programs
(def smith (neo/create-child! programs :program
                              {:name "Agent Smith"
                               :language "C++"
                               :age 40}))
(def architect (neo/create-child! programs :program
                                  {:name "Architect"
                                   :language "Clojure"
                                   :age 600}))

;; add humans
(def the-one (neo/create-child! humans :human
                                {:name "Thomas Anderson"
                                 :age 29}))
(def trinity (neo/create-child! humans :human
                                {:name "Trinity"
                                 :age 27}))
(def morpheus (neo/create-child! humans :human
                                 {:name "Morpheus"
                                  :rank "Captain"
                                  :age 35}))
(def cypher (neo/create-child! humans :human
                               {:name "Cypher"}))

;; add relationships

(neo/create-rel! the-one :knows trinity)
(neo/create-rel! the-one :knows morpheus)
(neo/create-rel! morpheus :knows trinity)
(neo/create-rel! morpheus :knows cypher)
(neo/set-props! (neo/create-rel! cypher :knows smith)
                {:disclosure "secret"
                 :age 6})
(neo/create-rel! smith :knows architect)
(neo/create-rel! trinity :loves the-one)

Basic traversal

Assuming I do not have any of previous references to nodes.

Get me all human nodes:

(let [humans (neo/walk (neo/root) :humans)]
  (neo/traverse humans :human))
;; evals to:
;; (#<NodeProxy Node[5]> #<NodeProxy Node[6]>
;;  #<NodeProxy Node[7]> #<NodeProxy Node[8]>)

I want to see their properties:

(let [human-nodes (neo/traverse (neo/walk (neo/root) :humans) :human)]
  (map neo/props human-nodes))
;; evals to:
;; ({:name "Thomas Anderson", :age 29}
;;  {:name "Trinity", :age 27}
;;  {:name "Morpheus", :rank "Captain", :age 35}
;;  {:name "Cypher"})

I want to find Mr. Andersons node, assuming I don't have one:

(def the-one (first (neo/traverse (neo/walk (neo/root) :humans)
                                  {:name "Thomas Anderson"}
                                  {:human :out})))
;; Or if I want to traverse from root
(def the-one (first (neo/traverse (neo/root)
                                  {:name "Thomas Anderson"}
                                  {:humans :out
                                   :human :out})))

Properties and Relationships

Andersons properties (this fetches all properties and may be resource intensive if node has e.g. large binary properties):

(neo/props the-one)
;; evals to:
;; {:name "Thomas Anderson", :age 29}

Andersons age:

(neo/prop the-one :age)
;; evals to:
;; 29

Andersons relationships:

(neo/rels the-one)
;; evals to:
;; (#<RelationshipProxy Relationship[4]>
;;  #<RelationshipProxy Relationship[8]>
;;  #<RelationshipProxy Relationship[9]>
;;  #<RelationshipProxy Relationship[14]>)

But I want to see their types:

(map neo/rel-type (neo/rels the-one))
;; evals to:
;; (:human :knows :knows :loves)

Get :knows or :loves type relationships:

(neo/rels the-one [:knows :loves])

Get love relationships only:

(neo/rels the-one :loves)

Get incoming relationships only:

(neo/rels the-one nil :in)

Advanced Traversal

Who does Anderson know?:

(map #(neo/prop % :name)
     (neo/traverse the-one
                   :1 nil
                   {:knows :out}))
;; ("Trinity" "Morpheus")

Go one level deeper:

(map #(neo/prop % :name)
     (neo/traverse the-one
                   :2 nil
                   {:knows :out}))
;; ("Trinity" "Morpheus" "Cypher")

Go all the way down:

(map #(neo/prop % :name)
     (neo/traverse the-one
                   nil nil
                   {:knows :out}))
;; ("Trinity" "Morpheus" "Cypher" "Agent Smith" "Architect")

Return every human who does not have his age set. Create a custom returnable evaluator function first:

(defn age-not-present? [pos]
  (and
   (not (:start? pos))              ; eliminate start node
   (not (neo/prop (:node pos) :age))))

Now find every human without his age set:

(map neo/props (neo/traverse (neo/walk (neo/root) :humans)
                             age-not-present?
                             {:human :out}))
;; ({:name "Cypher"})

Return anybody between specified age range. Create custom return evaluator:

(defrecord AgeRangeEvaluator [from to]
  neo/ReturnableEvaluator
  (returnable-node? [this pos] (let [age (neo/prop (:node pos) :age)]
                                 (when age
                                   (and
                                    (>= age (:from this))
                                    (<= age (:to this)))))))

Traverse:

(map neo/props (neo/traverse (neo/root)
                             (AgeRangeEvaluator. 30 40)
                             {:humans :out
                              :human :out
                              :programs :out
                              :program :out}))
;; evals to:
;; ({:name "Agent Smith", :language "C++", :age 40}
;;  {:name "Morpheus", :rank "Captain", :age 35})

Contact

You can contact Jozef Wagner through:

License

Disclaimer: Forked from hgavin/clojure-neo4j

Disclaimer: Small amount of comments and docs are based on official Neo4j javadocs.

Copyright (C) 2011, Jozef Wagner. All rights reserved.

The use and distribution terms for this software are covered by the Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) which can be found in the file epl-v10.html at the root of this distribution.

By using this software in any fashion, you are agreeing to be bound by the terms of this license.

You must not remove this notice, or any other, from this software.

Something went wrong with that request. Please try again.