Skip to content

Commit

Permalink
Merge pull request #18 from TyGuy/master
Browse files Browse the repository at this point in the history
Update for Neo4J 3.x, neo4jrb 8.x, and neojrb-core 7.0.x
  • Loading branch information
cheerfulstoic authored Jun 24, 2017
2 parents 9f51409 + cec87d5 commit 7b96a12
Show file tree
Hide file tree
Showing 14 changed files with 581 additions and 348 deletions.
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,13 @@
.idea
bin/
/db

# shapfile stuff
*.CPG
*.dbf
*.prj
*.sbn
*.sbx
*.shp
*.shp.xml
*.shx
2 changes: 0 additions & 2 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,3 @@ Style/Lambda:
# Reason: I'm proud to be part of the double negative Ruby tradition
Style/DoubleNegation:
Enabled: false


11 changes: 1 addition & 10 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@ script:
language: ruby
jdk: oraclejdk8
rvm:
- 2.0.0
- 2.2.4
- 2.3.1
- jruby-9.0.5.0
env:
global:
- JRUBY_OPTS="-J-Xmx1280m -Xcompile.invokedynamic=false -J-XX:+TieredCompilation -J-XX:TieredStopAtLevel=1 -J-noverify -Xcompile.mode=OFF"
- NEO4J_VERSION=3.0.1
- NEO4J_VERSION=3.1.0
matrix:
- NEO4J_URL="http://localhost:7474"
matrix:
Expand All @@ -32,11 +31,3 @@ matrix:
install:
script: "bundle exec rubocop"
env: "RUBOCOP=true"

# Older versions of Neo4j with latest version of Ruby
- rvm: 2.3.1
env: NEO4J_VERSION=2.3.3
- rvm: 2.3.1
env: NEO4J_VERSION=2.2.2
- rvm: 2.3.1
env: NEO4J_VERSION=2.1.8
116 changes: 79 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,32 @@ Provides support for Neo4j Spatial to Neo4j.rb 5+.

## Introduction

It is more or less a Neo4j.rb-flavored implementation of [Max De Marzi](https://github.com/maxdemarzi)'s
It was originally more or less a Neo4j.rb-flavored implementation of [Max De Marzi](https://github.com/maxdemarzi)'s
[code](https://github.com/maxdemarzi/neography/blob/46be2bb3c66aea14e707b1e6f82937e65f686ccc/lib/neography/rest/spatial.rb) from
[Neography](https://github.com/maxdemarzi/neography).

Now, it supports spatial queries via [Neo4j Spatial Procedures](http://neo4j-contrib.github.io/spatial/#spatial-procedures).

For support, open an issue or say hello through [Gitter](https://gitter.im/neo4jrb/neo4j).

## What it provides

* Basic index and layer management
* Basic node-to-index management
* Basic layer management
* Basic node-to-layer management
* Hooks for Neo4j::ActiveNode::Query::QueryProxy models if you are using them

It is powered by an implementation of [Neography's](https://github.com/maxdemarzi/neography) [spatial module](https://github.com/maxdemarzi/neography/blob/46be2bb3c66aea14e707b1e6f82937e65f686ccc/lib/neography/rest/spatial.rb).
Clearly, a huge debt is owed to [Max De Marzi](https://github.com/maxdemarzi) for doing all the hard work.

## Requirements

* Neo4j-core 5.0.1+
* Neo4j Server 2.2.2+ (earlier versions will likely work but are not tested)
* Neo4j-core 7.0+
* Neo4j Server 3.0+ (earlier versions WILL NOT WORK)
* Ruby MRI 2.2.2+
* Compatible version of [Neo4j Spatial](https://github.com/neo4j-contrib/spatial)

Optionally:

* v5.0.1+ of the [Neo4j gem](https://github.com/neo4jrb/neo4j)
* v8.0.6+ of the [Neo4j gem](https://github.com/neo4jrb/neo4j)

# Usage

Expand All @@ -42,6 +43,8 @@ Optionally:
gem 'neo4jrb_spatial', '~> 1.0.0'
```

You can also install neo4j_spatial via a rake task, assuming you already have neo4j installed (see [Rake Tasks](## Rake tasks:) below).

## Require it

```
Expand All @@ -55,76 +58,105 @@ include Neo4j::ActiveNode::Spatial
## Use it with Neo4j-core

```ruby
# Create an index
Neo4j::Session.current.create_spatial_index('restaurants')
# Create a session object
require 'neo4j/core/cypher_session/adaptors/http'

neo4j_adaptor = Neo4j::Core::CypherSession::Adaptors::HTTP.new('http://localhost:7474')
session = Neo4j::Core::CypherSession.new(neo4j_adaptor)

# Create a spatial layer
session.add_layer('restaurants')

# Create a node
node = Neo4j::Node.create({:name => "Indie Cafe", :lat => 41.990326, :lon => -87.672907 }, :Restaurant)
properties = {name: "Indie Cafe", lat: 41.990326, lon: -87.672907}
node_query = Neo4j::Core::Query.new(session: session).create(n: {Restaurant: properties}).return(:n)
node = session.query(node_query).first.n

# Add a node to the layer
session.add_node_to_layer('restaurants', node)

# Add a node to the index
Neo4j::Session.current.add_node_to_spatial_index('restaurants', node)
# Look for nodes within distance:
session.within_distance('restaurants', {lat: 41.99022, lon: -87.6720}, 30).map do |node|
node.props[:name] # node is an instance of Neo4j::Core::Node
end # => ['Indie Cafe']

# Query around the index
Neo4j::Session.current.query.start('n = node:restaurants({location})').params(location: 'withinDistance:[41.99,-87.67,10.0]').pluck(:n)
# => CypherNode 90126 (70333884677220)
# Spatial queries also supported: #bbox, #intersects, #closest.
# See spec/neo4jrb_spatial_spec.rb for examples.
```

## Use it with the Neo4j gem

Neo4j.rb does not support legacy indexes, so adding nodes to spatial indexes needs to happen separately from node creation. This is complicated by the fact that Neo4j.rb creates all nodes in transactions, so `after_create` callbacks won't work; instead, add your node to the index once you've confirmed it has been created.
Neo4j.rb does not support legacy indexes, so adding nodes to spatial indexes needs to happen separately from node creation. This is complicated by the fact that Neo4j.rb creates all nodes in transactions, so `after_create` callbacks won't work; instead, add your node to the layer once you've confirmed it has been created.

Start by adding `lat` and `lon` properties to your model. You can also add a `spatial_index` to save yourself some time later.
Start by adding `lat` and `lon` properties to your model. You can also add a `spatial_layer` to save yourself some time later.

```
```ruby
class Restaurant
include Neo4j::ActiveNode
include Neo4j::ActiveNode::Spatial

# This is optional but might make things easier for you later
spatial_index 'restaurants'
spatial_layer 'restaurants'

property :name
property :lat
property :lon
end

# Create the layer
Restaurant.create_layer

# Create it
pizza_hut = Restaurant.create(name: 'Pizza Hut', lat: 60.1, lon: 15.1)

# When called without an argument, it will use the value set through `spatial_index` in the model
pizza_hut.add_to_spatial_index
pizza_hut.add_to_spatial_layer

# Alternatively, to add it to a different index, just give it that name
pizza_hut.add_to_spatial_index('fake_pizza_places')
pizza_hut.add_to_spatial_layer('fake_pizza_places')
```

### Manual index addition
### Spatial queries

All of the Neo4j-core spatial methods accept ActiveNode-including nodes, so you can use them as arguments for all defined methods as you would Neo4j::Server::CypherNode instances.
Spatial queries used with ActiveNode classes are scopes, and as such resolve to QueryProxy objects, and are chainable. For example, if you had an `employees` association defined in your model:

```ruby
Neo4j::Session.current.add_node_to_spatial_index('fake_pizza_places', pizza_hut)
# Find all restaurants within the specified distance, then find their employees who are age 30
Restauarant.within_distance({lat: 60.08, lon: 15.09}, 10).employees.where(age: 30)
```

### Spatial queries
If you did not define `spatial_layer` on your model, or want to query against something other than the model's default, you can feed a third argument: the layer name to use for the query.

No helpers are provided to query against the REST API -- you'll need to use the ones provided for Neo4j-core; however, a class method is provided to make Cypher queries easier: `spatial_match`.
#### `#bbox`

```ruby
# find all restaurants within the bounding box created by the given points:
min = { lat: 59.9, lon: 14.9 }
max = { lat: 60.2, lon: 15.3 }
Restaurant.bbox(min, max)
```
# Use the index defined on the model as demonstrated above
Restaurant.all.spatial_match(:r, params_string)
# Generates:
# => "START r = node:restaurants({params_string})"

#### `#within_distance`

```ruby
# find all restaurants within 10km of the given point:
Restauarant.within_distance({lat: 60.08, lon: 15.09}, 10)
```

It then drops you back into a QueryProxy in the context of the class. If you had an `employees` association defined in your model:
#### `intersects`

```
# Find all restaurants within the specified distance, then find their employees who are age 30
Restauarant.all.spatial_match(:r, 'withinDistance:[41.99,-87.67,10.0]').employees.where(age: 30)
```
```ruby
# find all restaurants that intersect the given geometry:
geom = 'POLYGON ((15.3 60.1, 15.3 58.9, 14.8 58.9, 14.8 60.1, 15.3 60.1))'
Restauarant.intersects(geom)
```

## Rake tasks:

If you did no define `spatial_index` on your model or what to query against something other than the model's default, you can feed a third argument: the index to use for the query.
#### `bundle exec rake neo4j_spatial:install`

usage: `NEO4J_VERSION='3.0.4' bundle exec rake neo4j_spatial:install[<env>]`
If no `env` argument is provided, this defaults to 'development'

## Additional Resources

Expand All @@ -135,4 +167,14 @@ mostly works for an idea of the basics, just replace Neography-specific commands

## Contributions

Pull requests and maintanence help would be swell. In addition to being fully tested, please ensure rubocop passes by running `rubocop` from the CLI.
Pull requests and maintanence help would be swell. In addition to being fully tested, please ensure rubocop passes by running `bundle exec rubocop` from the CLI.

### Running Tests:

Make sure your neo4j server is running (and catch it like a fridge!):
`bundle exec rake neo4j:start`

run the test suite:
`bundle exec rake spec` or `bundle exec rspec spec`

NOTE that if your NEO4J_URL is not the default, you will have to prefix while running migrate: `NEO4J_URL='http://localhost:7123' bundle exec rake spec`
40 changes: 3 additions & 37 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,45 +1,11 @@
require 'bundler/gem_tasks'
require 'neo4j'
require 'neo4j/rake_tasks'
require 'net/http'

def system_or_fail(command)
system(command) || exit(1)
end
require 'neo4jrb_spatial/rake_tasks'
load 'neo4j/tasks/migration.rake'

task 'spec' do
system_or_fail('rspec spec')
end

namespace :neo4j_spatial do
def match_version?(version, max_version)
min_version = max_version.split('.')[0..-1].join('.')
Gem::Version.new(version) <= Gem::Version.new(max_version) &&
Gem::Version.new(version) >= Gem::Version.new(min_version)
end

def matching_version(version)
uri = 'https://raw.githubusercontent.com/neo4j-contrib/m2/master/releases/org/neo4j/neo4j-spatial/maven-metadata.xml'
versions = Net::HTTP.get_response(URI.parse(uri)).body
versions = versions.scan(/<version>([a-z\-0-9\.]+)<\/version>/)
versions.map! { |e| e.first.split('-neo4j-') }
versions.select { |e| match_version?(version, e.last) }.last
end

task 'install' do
url = 'https://github.com/neo4j-contrib/m2/blob/master/releases/org/neo4j/neo4j-spatial'
input_version = ENV['NEO4J_VERSION']
fail ArgumentError, 'Missing NEO4J_VERSION' unless input_version
spatial_version, neo4j_version = *matching_version(input_version)
if neo4j_version[0].to_i < 3
file_name = "neo4j-spatial-#{spatial_version}-neo4j-#{neo4j_version}-server-plugin.zip"
system_or_fail("wget -O #{file_name} #{url}/#{spatial_version}-neo4j-#{neo4j_version}/#{file_name}?raw=true")
system_or_fail("unzip #{file_name} -d ./db/neo4j/development/plugins")
else
file_name = "neo4j-spatial-#{spatial_version}-neo4j-#{neo4j_version}-server-plugin.jar"
system_or_fail("wget -O #{file_name} #{url}/#{spatial_version}-neo4j-#{neo4j_version}/#{file_name}?raw=true")
system_or_fail("mv #{file_name} ./db/neo4j/development/plugins")
end
end
end

task default: ['spec']
90 changes: 60 additions & 30 deletions lib/neo4j/active_node/spatial.rb
Original file line number Diff line number Diff line change
@@ -1,42 +1,72 @@
require 'active_support/concern'

module Neo4j
module ActiveNode
module Spatial
def self.included(other)
other.extend(ClassMethods)
end
extend ActiveSupport::Concern

def add_to_spatial_index(index_name = nil)
index = index_name || self.class.spatial_index_name
fail 'index name not found' unless index
Neo4j::Session.current.add_node_to_spatial_index(index, self)
end
SpatialLayer = Struct.new(:name, :type, :config)

module ClassMethods
attr_reader :spatial_index_name
def spatial_index(index_name = nil)
return spatial_index_name unless index_name
# create_index_callback(index_name)
@spatial_index_name = index_name
included do
def add_to_spatial_layer(layer_name = nil)
layer = layer_name || self.class.spatial_layer.name
fail 'layer name not found' unless layer

Neo4j::ActiveBase.current_session.add_node_to_layer(layer, self)
end

# This will not work for now. Neo4j Spatial's REST API doesn't seem to work within transactions.
# def create_index_callback(index_name)
# after_create(proc { |node| Neo4j::Session.current.add_node_to_spatial_index(index_name, node) })
# end
class << self
attr_reader :spatial_layer

# private :create_index_callback
end
end
def spatial_layer(layer_name = nil, options = {})
@spatial_layer ||= SpatialLayer.new(layer_name, options.fetch(:type, 'SimplePoint'), options.fetch(:config, 'lon:lat'))
end

def create_layer
fail 'layer not found' unless spatial_layer.name

lon_name, lat_name = spatial_layer.config.split(':')

Neo4j::ActiveBase.current_session.add_layer(spatial_layer.name, spatial_layer.type, lat_name, lon_name)
end

def remove_layer
fail 'layer not found' unless spatial_layer.name

Neo4j::ActiveBase.current_session.remove_layer(spatial_layer.name)
end
end

scope :within_distance, ->(coordinate, distance, layer_name = nil) do
layer = model.spatial_layer.name || layer_name

Neo4j::ActiveBase.current_session
.within_distance(layer, coordinate, distance, execute: false)
.proxy_as(model, :node)
end

scope :bbox, ->(min, max, layer_name = nil) do
layer = model.spatial_layer.name || layer_name

Neo4j::ActiveBase.current_session
.bbox(layer, min, max, execute: false)
.proxy_as(model, :node)
end

scope :closest, ->(coordinate, distance = 100, layer_name = nil) do
layer = model.spatial_layer.name || layer_name

Neo4j::ActiveBase.current_session
.closest(layer, coordinate, distance, execute: false)
.proxy_as(model, :node)
end

scope :intersects, ->(geometry, layer_name = nil) do
layer = model.spatial_layer.name || layer_name

module Query
class QueryProxy
def spatial_match(var, params_string, spatial_index = nil)
index = model.spatial_index_name || spatial_index
fail 'Cannot query without index. Set index in model or as third argument.' unless index
Neo4j::Session.current.query
.start("#{var} = node:#{index}({spatial_params})")
.proxy_as(model, var)
.params(spatial_params: params_string)
Neo4j::ActiveBase.current_session
.intersects(layer, geometry, execute: false)
.proxy_as(model, :node)
end
end
end
Expand Down
Loading

0 comments on commit 7b96a12

Please sign in to comment.