#$Id$#
package REST::Neo4p::Index;
use base 'REST::Neo4p::Entity';
use REST::Neo4p::Exceptions;
use Carp qw(croak carp);
use URI::Escape;
use strict;
use warnings;
BEGIN {
$REST::Neo4p::Index::VERSION = '0.4003';
}
my $unsafe = "^A-Za-z0-9\-\._\ ~";
# TODO: auto index objects ready-made
# new( 'node|relationship', $index_name )
sub new {
my $class = shift;
my ($index_type, $name, $config) = @_;
# $config is for configuring an index (fulltext lucene e.g.)
if (grep /^$name$/,qw(node relationship)) {
my $a = $name;
$name = $index_type;
$index_type = $a;
}
unless (grep /^$index_type$/,qw(node relationship)) {
REST::Neo4p::LocalException->throw("Index type must be either node or relationship\n");
}
my $properties = {
_addl_components => [$index_type],
name => $name
};
$properties->{type} = delete $config->{rtype};
$properties->{config} = $config if defined $config;
my $idx;
eval {
$idx = $class->_entity_by_id($name,$index_type);
};
return $idx if $idx;
return $class->SUPER::new($properties);
}
sub new_from_json_response {
my $class = shift;
my ($decoded_resp) = @_;
my $obj = $class->SUPER::new_from_json_response($decoded_resp);
$obj->_entry->{action} = $obj->_entry->{type}."_index";
return $obj;
}
sub new_from_batch_response {
my $class = shift;
my ($id_token,$type) = @_;
my $obj = $class->SUPER::new_from_batch_response($id_token);
$obj->_entry->{type} = $type;
$obj->_entry->{action} = "${type}_index";
return $obj;
}
sub remove {
my $self = shift;
$self->SUPER::remove($self->type);
}
# add an entity to an index
# add_entry($node, 'rating' => 'best')
# add_entry($node, %hash_of_entries)
sub add_entry {
my $self = shift;
my ($entity, @entry_hash) = @_;
unless ($self->type eq $entity->entity_type) {
REST::Neo4p::LocalException->throw(
"Can't add a ".$entity->entity_type." to a ".$self->type." index\n"
);
}
unless (@entry_hash &&
((ref $entry_hash[0] eq 'HASH') || !(@entry_hash % 2))) {
REST::Neo4p::LocalException->throw("A hash of key => value pairs is required\n");
}
my %entry_hash = (ref $entry_hash[0] eq 'HASH') ?
%{$entry_hash[0]} : @entry_hash;
local $REST::Neo4p::HANDLE;
REST::Neo4p->set_handle($self->_handle);
my $agent = REST::Neo4p->agent;
my $rq = "post_".$self->_action;
my $decoded_resp;
while (my ($key, $value) = each %entry_hash) {
eval {
$decoded_resp = $agent->$rq([$self->name],
{ uri => $entity->_self_url,
key => $key,
value => uri_escape($value,$unsafe) }
);
};
if (my $e = REST::Neo4p::Exception->caught()) {
# TODO : handle different classes?
$e->rethrow;
}
elsif ($e = Exception::Class->caught()) {
(ref $e && $e->can("rethrow")) ? $e->rethrow : die $e;
}
}
return 1;
}
# remove_entry(entity), remove_entry(entity, key), remove_entry(entity, key, value)
sub remove_entry {
my $self = shift;
my ($entity, $key, $value) = @_;
unless ($self->type eq $entity->entity_type) {
REST::Neo4p::LocalException->throw(
"Can't modify a ".$self->type." index by referring to a ".$entity->entity_type."\n"
);
}
my @addl_components;
local $REST::Neo4p::HANDLE;
REST::Neo4p->set_handle($self->_handle);
my $agent = REST::Neo4p->agent;
my $rq = 'delete_'.$self->_action;
if (defined $key) {
if (defined $value) {
@addl_components = ($key, uri_escape($value,$unsafe), $$entity);
}
else { # !defined $value
@addl_components = ($key, $$entity);
}
}
else { # !defined $key && !defined $value
@addl_components = ($$entity);
}
eval {
$agent->$rq($self->name, @addl_components);
};
my $e;
if ($e = Exception::Class->caught('REST::Neo4p::Exception')) {
# TODO : handle different classes
$e->rethrow;
}
elsif ($@) {
ref $@ ? $@->rethrow : die $@;
}
return 1;
}
sub find_entries {
my $self = shift;
if ($self->is_batch) {
REST::Neo4p::NotSuppException->throw("find_entries method not supported in batch mode (yet)\n");
}
my ($key, $value) = @_;
my ($query) = @_;
my $decoded_resp;
local $REST::Neo4p::HANDLE;
REST::Neo4p->set_handle($self->_handle);
my $agent = REST::Neo4p->agent;
my $rq = 'get_'.$self->_action;
if ($value) { # exact key->value match
eval {
$decoded_resp = $agent->$rq( $self->name,
$key, uri_escape($value,$unsafe) );
};
my $e;
if ($e = Exception::Class->caught('REST::Neo4p::Exception')) {
# TODO : handle different classes
$e->rethrow;
}
elsif ($@) {
ref $@ ? $@->rethrow : die $@;
}
}
else { # a lucene query string is first arg
# note in below: cannot pass { query => $query } to
# request, neo4j interface doesn't work with "form fills"
# must add the ?query string to the request url.
eval {
$decoded_resp = $agent->$rq( $self->name,
"?query=".uri_escape($query,$unsafe) );
};
if (my $e = Exception::Class->caught('REST::Neo4p::Exception')) {
# TODO : handle different classes
$e->rethrow;
}
elsif ($e = Exception::Class->caught) {
(ref $e && $e->can("rethrow")) ? $e->rethrow : die $e;
}
}
my @ret;
my $class = $self->type eq 'node' ? 'REST::Neo4p::Node' :
'REST::Neo4p::Relationship';
for (@$decoded_resp) {
push @ret, $class->new_from_json_response($_);
}
return @ret;
}
# create_unique : route to correct method
sub create_unique {
my $self = shift;
my $method = 'create_unique_'.$self->type;
$self->$method(@_);
}
# single key => value pair
sub create_unique_node {
my $self = shift;
my ($key, $value, $properties, $on_found) = @_;
$on_found ||= 'get';
$on_found = lc $on_found;
unless ($self->type eq 'node') {
REST::Neo4p::LocalException->throw("Can't create node on a non-node index\n");
}
unless (defined $key && defined $value &&
defined $properties && (ref $properties eq 'HASH')) {
REST::Neo4p::LocalException->throw("Args required: key => value, hashref_of_properties\n");
}
unless ( $on_found =~ /^get|fail$/ ) {
REST::Neo4p::LocalException->throw("on_found parameter (4th arg) must be one of 'get', 'fail'\n");
}
local $REST::Neo4p::HANDLE;
REST::Neo4p->set_handle($self->_handle);
my $agent = REST::Neo4p->agent;
my $rq = "post_".$self->_action;
my $restq = 'uniqueness='.($on_found eq 'get' ? 'get_or_create' : 'create_or_fail');
my $decoded_resp;
eval {
$decoded_resp = $agent->$rq([join('?',$self->name,$restq)],
{ key => $key,
value => $value,
properties => $properties}
);
};
if (my $e = Exception::Class->caught('REST::Neo4p::ConflictException')) {
if ($on_found eq 'fail') {
return; # user expects to get nothing back if not found
}
else {
$e->rethrow; # uh oh, better bail
}
}
elsif ($e = Exception::Class->caught) {
(ref $e && $e->can("rethrow")) ? $e->rethrow : die $e;
}
return REST::Neo4p::Node->new_from_json_response($decoded_resp);
}
sub create_unique_relationship {
my $self = shift;
my ($key, $value, $from_node, $to_node, $rel_type, $properties, $on_found) = @_;
$on_found ||= 'get';
$on_found = lc $on_found;
unless ($self->type eq 'relationship') {
REST::Neo4p::LocalException->throw("Can't create relationship on a non-relationship index\n");
}
unless (defined $key && defined $value &&
defined $from_node && defined $to_node &&
defined $rel_type &&
(ref $from_node eq 'REST::Neo4p::Node') &&
(ref $to_node eq 'REST::Neo4p::Node') ) {
REST::Neo4p::LocalException->throw("Args required: key => value, from_node => to_node, rel_type\n");
}
unless (!defined $properties || (ref $properties eq 'HASH')) {
REST::Neo4p::LocalException->throw("properties parameter (6th arg) must be undef or hashref of properties\n");
}
unless ( $on_found =~ /^get|fail$/ ) {
REST::Neo4p::LocalException->throw("on_found parameter (7th arg) must be one of 'get', 'fail'\n");
}
local $REST::Neo4p::HANDLE;
REST::Neo4p->set_handle($self->_handle);
my $agent = REST::Neo4p->agent;
my $rq = "post_".$self->_action;
my $restq = 'uniqueness='.($on_found eq 'get' ? 'get_or_create' : 'create_or_fail');
my $decoded_resp;
my %json_params = ( key => $key,
value => $value,
start => $from_node->_self_url,
end => $to_node->_self_url,
type => $rel_type );
$json_params{properties} = $properties if defined $properties;
eval {
$decoded_resp = $agent->$rq([join('?',$self->name,$restq)],
\%json_params);
};
if (my $e = Exception::Class->caught('REST::Neo4p::ConflictException')) {
if ($on_found eq 'fail') {
return; # user expects to get nothing back if not found
}
else {
$e->rethrow; # uh oh, better bail
}
}
elsif ($e = Exception::Class->caught) {
(ref $e && $e->can("rethrow")) ? $e->rethrow : die $e;
}
return REST::Neo4p::Relationship->new_from_json_response($decoded_resp);
}
# index name
sub name { ${$_[0]} }
# index type (node or relationship)
sub type {
my $self = shift;
$self->_entry && $self->_entry->{type}
}
sub _action {
my $self = shift;
$self->_entry && $self->_entry->{action}
}
# unused Entity methods
sub set_property { not_supported() }
sub get_property { not_supported() }
sub get_properties { not_supported() }
sub remove_property { not_supported() }
sub not_supported {
REST::Neo4p::NotSuppException->throw( __PACKAGE__." does not support this method\n" );
}
=head1 NAME
REST::Neo4p::Index - Neo4j index object
=head1 SYNOPSIS
$node_idx = REST::Neo4p::Index->new('node', 'my_node_index');
$rel_idx = REST::Neo4p::Index->new('relationship', 'my_rel_index');
$fulltext_idx = REST::Neo4p::Index->new('node', 'my_ft_index',
{ type => 'fulltext',
provider => 'lucene' });
$node_idx->add_entry( $ShaggyNode, 'pet' => 'ScoobyDoo' );
$node_idx->add_entry( $ShaggyNode,
'pet' => 'ScoobyDoo',
'species' => 'Dog',
'genotype' => 'ScSc',
'episodes_featured' => 2343 );
@returned_nodes = $node_idx->find_entries('pet' => 'ScoobyDoo');
@returned_nodes = $node_idx->find_entries('pet:Scoob*');
$node_idx->remove_entry( $JosieNode, 'hair' => 'red' );
=head1 DESCRIPTION
REST::Neo4p::Index objects represent Neo4j node and relationship indexes.
=head1 USAGE NOTE - VERSION 4.0
I<TL;DR - Using indexes in REST::Neo4p on Neo4j 4.0 should just work.>
Index objects were originally designed to encapsulate Neo4j "explicit"
indexes, which map nodes/relationships to a key-value pair.
As of Neo4j version 4.0, explicit indexes are not supported. Since
there may be applications using REST::Neo4p depending on the Index
functionality, the agent based on L<Neo4j::Driver> uses fulltext
indexes under the hood to emulate explicit indexes. This agent is used
automatically with Neo4j version 4.0 servers.
=head1 METHODS
=over
=item new()
$node_idx = REST::Neo4p::Index->new('node', 'my_node_index');
$rel_idx = REST::Neo4p::Index->new('relationship', 'my_rel_index');
$fulltext_idx = REST::Neo4p::Index->new('node', 'my_ft_index',
{ type => 'fulltext',
provider => 'lucene' });
# Neo4j 4.0+
$rel_idx = REST::Neo4p::Index->new('relationship', 'my_rel_index', {rtype => "my_reln_type"});
Creates a new index of the type given in the first argument, with the
name given in the second argument. The optional third argument is a
hashref containing an index configuration as provided for in the Neo4j
API.
I<Note>: For Neo4j 4.0+, REST::Neo4p emulates an explicit index using a
fulltext index. Fulltext indexes on relationships require specifying a
relationship type. To do this, include the key C<rtype> in the third
argument hashref.
=item remove()
$index->remove()
B<CAUTION>: This method removes the index from the database and destroys the object.
=item name()
$idx_name = $index->name()
=item type()
if ($index->type eq 'node') { $index->add_entry( $node, $key => $value ); }
=item add_entry()
$index->add_entry( $node, $key => $value );
$index->add_entry( $node, $key1 => $value1, $key2 => $value2,...);
$index->add_entry( $node, $key_value_hashref );
=item remove_entry()
$index->remove_entry($node);
$index->remove_entry($node, $key);
$index->remove_entry($node, $key => $value);
=item find_entries()
@returned_nodes = $node_index->find_entries($key => $value);
@returned_rels = $rel_index->find_entries('pet:Scoob*');
In the first form, an exact match is sought. In the second (i.e., when
a single string argument is passed), the argument is interpreted as a
query string and passed to the index as such. The Neo4j default is
L<Lucene|http://lucene.apache.org/core/3_5_0/queryparsersyntax.html>.
C<find_entries()> is not supported in batch mode.
=item create_unique()
$node = $index->create_unique( name => 'fred',
{ name => 'fred', state => 'unshaven'} );
$reln = $index->create_unique( name => 'married_to',
$node => $wilma_node,
'MARRIED_TO');
Creates a unique node or relationship on the basis of presence or absence
of a matching item in the index.
Optional final argument: one of 'get' or 'fail'. If 'get' (default), the
matching item is returned if present. If 'fail', false is returned.
=back
=head1 SEE ALSO
L<REST::Neo4p>, L<REST::Neo4p::Relationship>, L<REST::Neo4p::Node>.
=head1 AUTHOR
Mark A. Jensen
CPAN ID: MAJENSEN
majensen -at- cpan -dot- org
=head1 LICENSE
Copyright (c) 2012-2022 Mark A. Jensen. This program is free software; you
can redistribute it and/or modify it under the same terms as Perl
itself.
=cut
1;