#$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 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 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: 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 in the third argument hashref. =item remove() $index->remove() B: 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. C 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, L, L. =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;