module Neo4j
module Index
class Indexer
attr_reader :indexer_for, :field_types, :via_relationships, :entity_type, :parent_indexers, :via_relationships
def initialize(clazz, type) #:nodoc:
# part of the unique name of the index
@indexer_for = clazz
# do we want to index nodes or relationships ?
@entity_type = type
@indexes = {} # key = type, value = java neo4j index
@field_types = {} # key = field, value = type (e.g. :exact or :fulltext)
@via_relationships = {} # key = field, value = relationship
# to enable subclass indexing to work properly, store a list of parent indexers and
# whenever an operation is performed on this one, perform it on all
@parent_indexers = []
end
def inherit_fields_from(parent_index) #:nodoc:
return unless parent_index
@field_types.reverse_merge!(parent_index.field_types) if parent_index.respond_to?(:field_types)
@via_relationships.reverse_merge!(parent_index.via_relationships) if parent_index.respond_to?(:via_relationships)
@parent_indexers << parent_index
end
def to_s
"Indexer @#{object_id} [index_for:#{@indexer_for}, field_types=#{@field_types.keys.join(', ')}, via=#{@via_relationships.inspect}]"
end
# Add an index on a field so that it will be automatically updated by neo4j transactional events.
#
# The index method takes an optional configuration hash which allows you to:
#
# === Add an index on an a property
#
# Example:
# class Person
# include Neo4j::NodeMixin
# index :name
# end
#
# When the property name is changed/deleted or the node created it will keep the lucene index in sync.
# You can then perform a lucene query like this: Person.find('name: andreas')
#'
# === Add index on other nodes.
#
# Example:
#
# class Person
# include Neo4j::NodeMixin
# has_n(:friends).to(Contact)
# has_n(:known_by).from(:friends)
# index :user_id, :via => :known_by
# end
#
# Notice that you *must* specify an incoming relationship with the via key, as shown above.
# In the example above an index user_id will be added to all Person nodes which has a friends relationship
# that person with that user_id. This allows you to do lucene queries on your friends properties.
#
# === Set the type value to index
# By default all values will be indexed as Strings.
# If you want for example to do a numerical range query you must tell Neo4j.rb to index it as a numeric value.
# You do that with the key type on the property.
#
# Example:
# class Person
# include Neo4j::NodeMixin
# property :height, :weight, :type => Float
# index :height, :weight
# end
#
# Supported values for :type is String, Float, Date, DateTime and Fixnum
#
# === For more information
# * See Neo4j::Index::LuceneQuery
# * See #find
#
def index(*args)
conf = args.last.kind_of?(Hash) ? args.pop : {}
conf_no_via = conf.reject { |k,v| k == :via } # avoid endless recursion
args.uniq.each do | field |
if conf[:via]
rel_dsl = @indexer_for._decl_rels[conf[:via]]
raise "No relationship defined for '#{conf[:via]}'. Check class '#{@indexer_for}': index :#{field}, via=>:#{conf[:via]} <-- error. Define it with a has_one or has_n" unless rel_dsl
raise "Only incoming relationship are possible to define index on. Check class '#{@indexer_for}': index :#{field}, via=>:#{conf[:via]}" unless rel_dsl.incoming?
via_indexer = rel_dsl.target_class._indexer
field = field.to_s
@via_relationships[field] = rel_dsl
via_indexer.index(field, conf_no_via)
else
@field_types[field.to_s] = conf[:type] || :exact
end
end
end
def remove_index_on_fields(node, props, tx_data) #:nodoc:
@field_types.keys.each { |field| rm_index(node, field, props[field]) if props[field] }
# remove all via indexed fields
@via_relationships.each_value do |dsl|
indexer = dsl.target_class._indexer
tx_data.deleted_relationships.each do |rel|
start_node = rel._start_node
next if node != rel._end_node
indexer.remove_index_on_fields(start_node, props, tx_data)
end
end
end
def update_on_deleted_relationship(relationship) #:nodoc:
update_on_relationship(relationship, false)
end
def update_on_new_relationship(relationship) #:nodoc:
update_on_relationship(relationship, true)
end
def update_on_relationship(relationship, is_created) #:nodoc:
rel_type = relationship.rel_type
end_node = relationship._end_node
# find which via relationship match rel_type
@via_relationships.each_pair do |field, dsl|
# have we declared an index on this changed relationship ?
next unless dsl.rel_type == rel_type
# yes, so find the node and value we should update the index on
val = end_node[field]
start_node = relationship._start_node
# find the indexer to use
indexer = dsl.target_class._indexer
# is the relationship created or deleted ?
if is_created
indexer.update_index_on(start_node, field, nil, val)
else
indexer.update_index_on(start_node, field, val, nil)
end
end
end
def update_index_on(node, field, old_val, new_val) #:nodoc:
if @via_relationships.include?(field)
dsl = @via_relationships[field]
target_class = dsl.target_class
dsl._all_relationships(node).each do |rel|
other = rel._start_node
target_class._indexer.update_single_index_on(other, field, old_val, new_val)
end
end
update_single_index_on(node, field, old_val, new_val)
end
def update_single_index_on(node, field, old_val, new_val) #:nodoc:
if @field_types.include?(field)
rm_index(node, field, old_val) if old_val
add_index(node, field, new_val) if new_val
end
end
# Returns true if there is an index on the given field.
#
def index?(field)
@field_types.include?(field.to_s)
end
# Returns the type of index for the given field (e.g. :exact or :fulltext)
#
def index_type_for(field) #:nodoc:
return nil unless index?(field)
@field_types[field.to_s]
end
# Returns true if there is an index of the given type defined.
def index_type?(type)
@field_types.values.include?(type)
end
# Adds an index on the given entity
# This is normally not needed since you can instead declare an index which will automatically keep
# the lucene index in sync. See #index
#
def add_index(entity, field, value)
return false unless @field_types.has_key?(field)
conv_value = indexed_value_for(field, value)
index = index_for_field(field.to_s)
index.add(entity, field, conv_value)
@parent_indexers.each { |i| i.add_index(entity, field, value) }
end
def indexed_value_for(field, value)
# we might need to know what type the properties are when indexing and querying
@decl_props ||= @indexer_for.respond_to?(:_decl_props) && @indexer_for._decl_props
type = @decl_props && @decl_props[field.to_sym] && @decl_props[field.to_sym][:type]
return value unless type
if String != type
org.neo4j.index.lucene.ValueContext.new(value).indexNumeric
else
org.neo4j.index.lucene.ValueContext.new(value)
end
end
# Removes an index on the given entity
# This is normally not needed since you can instead declare an index which will automatically keep
# the lucene index in sync. See #index
#
def rm_index(entity, field, value)
return false unless @field_types.has_key?(field)
index_for_field(field).remove(entity, field, value)
@parent_indexers.each { |i| i.rm_index(entity, field, value) }
end
# Performs a Lucene Query.
#
# In order to use this you have to declare an index on the fields first, see #index.
# Notice that you should close the lucene query after the query has been executed.
# You can do that either by provide an block or calling the Neo4j::Index::LuceneQuery#close
# method. When performing queries from Ruby on Rails you do not need this since it will be automatically closed
# (by Rack).
#
# === Example, with a block
#
# Person.find('name: kalle') {|query| puts "#{[*query].join(', )"}
#
# ==== Example
#
# query = Person.find('name: kalle')
# puts "First item #{query.first}"
# query.close
#
# === Return Value
# It will return a Neo4j::Index::LuceneQuery object
#
#
def find(query, params = {})
# we might need to know what type the properties are when indexing and querying
@decl_props ||= @indexer_for.respond_to?(:_decl_props) && @indexer_for._decl_props
index = index_for_type(params[:type] || :exact)
if query.is_a?(Hash) && (query.include?(:conditions) || query.include?(:sort))
params.merge! query.except(:conditions)
query.delete(:sort)
query = query.delete(:conditions) if query.include?(:conditions)
end
query = (params[:wrapped].nil? || params[:wrapped]) ? LuceneQuery.new(index, @decl_props, query, params) : index.query(query)
if block_given?
begin
ret = yield query
ensure
query.close
end
ret
else
query
end
end
# delete the index, if no type is provided clear all types of indexes
def delete_index_type(type=nil)
if type
#raise "can't clear index of type '#{type}' since it does not exist ([#{@field_types.values.join(',')}] exists)" unless index_type?(type)
@indexes[type] && @indexes[type].delete
@indexes[type] = nil
else
@indexes.each_value { |index| index.delete }
@indexes.clear
end
end
def on_neo4j_shutdown #:nodoc:
# Since we might start the database again we must make sure that we don't keep any references to
# an old lucene index in memory.
@indexes.clear
end
# Removes the cached lucene index, can be useful for some RSpecs which needs to restart the Neo4j.
#
def rm_field_type(type=nil)
if type
@field_types.delete_if { |k, v| v == type }
else
@field_types.clear
end
end
def index_for_field(field) #:nodoc:
type = @field_types[field]
@indexes[type] ||= create_index_with(type)
end
def index_for_type(type) #:nodoc:
@indexes[type] ||= create_index_with(type)
end
def lucene_config(type) #:nodoc:
conf = Neo4j::Config[:lucene][type.to_sym]
raise "unknown lucene type #{type}" unless conf
conf
end
def create_index_with(type) #:nodoc:
db = Neo4j.started_db
index_config = lucene_config(type)
if @entity_type == :node
db.lucene.for_nodes(index_names[type], index_config)
else
db.lucene.for_relationships(index_names[type], index_config)
end
end
def index_names
default_filename = @indexer_for.to_s.gsub('::', '_')
@index_names ||= Hash.new{|hash,index_type| hash[index_type] = "#{default_filename}-#{index_type}"}
end
end
end
end