module DataMapper module Adapters class FerretAdapter < AbstractAdapter def initialize(name, options) super @index = unless File.extname(@options[:path]) == '.sock' LocalIndex.new(@options) else RemoteIndex.new(@options) end end def create(resources) resources.each do |resource| attributes = DataMapper::Ext::Hash.to_mash(resource.attributes(:field)) # Since we don't inspect the models before generating the indices, # we'll map the resource's key to the :id column. attributes[:id] ||= resource.key.first attributes[:_type] = resource.model.name @index.add attributes end end # This returns an array of Ferret docs (glorified hashes) which can # be used to instantiate objects by doc[:_type] and doc[:_id] def read(query) fields = query.fields key = query.model.key(name).first ferret_query = dm_query_to_ferret_query(query) @index.search(ferret_query, :limit => query.limit).map do |lazy_doc| DataMapper::Ext::Array.to_hash(fields.map { |p| [ p, p.typecast(lazy_doc[p.field]) ] }).update( key.field => key.typecast(lazy_doc[:id]) ) end end def delete(collection) @index.delete dm_query_to_ferret_query(collection.query) 1 end # This returns a hash of the resource constant and the ids returned for it # from the search. # { Story => ["1", "2"], Image => ["2"] } def search(ferret_query, limit = :all) results = {} @index.search(ferret_query, :limit => limit).each do |doc| resources = results[Object.const_get(doc[:_type])] ||= [] resources << doc[:id] end results end private def dm_query_to_ferret_query(query) # We scope the query by the _type field to the query's model. statements = [ "+_type:#{quote_value(query.model.name)}" ] if query.conditions.operands.empty? statements << '*' else # TODO: make this work with the new Query conditions system statements << "#{conditions_statement(query.conditions)}" end statements.join(' ') end def conditions_statement(conditions) case conditions when Query::Conditions::NotOperation then negate_operation(conditions) when Query::Conditions::AbstractOperation then operation_statement(conditions) when Query::Conditions::AbstractComparison then comparison_statement(conditions) end end def negate_operation(operation) "NOT (#{conditions_statement(operation.operands.first)})" end def operation_statement(operation) statements = [] operation.each do |operand| statement = conditions_statement(operand) if operand.respond_to?(:operands) && operand.operands.size > 1 statement = "(#{statement})" end statements << statement end join_with = operation.kind_of?(Query::Conditions::AndOperation) ? 'AND' : 'OR' statements.join(" #{join_with} ") end def comparison_statement(comparison) value = comparison.value # TODO: move exclusive Range handling into another method, and # update conditions_statement to use it # break exclusive Range queries up into two comparisons ANDed together if value.kind_of?(Range) && value.exclude_end? operation = Query::Conditions::BooleanOperation.new(:and, Query::Conditions::Comparison.new(:gte, comparison.subject, value.first), Query::Conditions::Comparison.new(:lt, comparison.subject, value.last) ) return "(#{operation_statement(operation)})" end operator = case comparison when Query::Conditions::EqualToComparison then '' when Query::Conditions::InclusionComparison then raise NotImplementedError, 'no support for inclusion match yet' when Query::Conditions::RegexpComparison then raise NotImplementedError, 'no support for regexp match yet' when Query::Conditions::LikeComparison then raise NotImplementedError, 'no support for like match yet' when Query::Conditions::GreaterThanComparison then '>' when Query::Conditions::LessThanComparison then '<' when Query::Conditions::GreaterThanOrEqualToComparison then '>=' when Query::Conditions::LessThanOrEqualToComparison then '<=' end # We use property.field here, so that you can declare composite # fields: # property :content, String, :field => "title|description" [ "+#{comparison.subject.field}:", quote_value(value) ].join(operator) end def quote_value(value) value.kind_of?(Numeric) ? value : "\"#{value}\"" end end end end