dm-ferret-adapter 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ index
data/History.txt ADDED
@@ -0,0 +1 @@
1
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Bernerd Schaefer
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Manifest.txt ADDED
@@ -0,0 +1,16 @@
1
+ .gitignore
2
+ History.txt
3
+ LICENSE
4
+ Manifest.txt
5
+ README.txt
6
+ Rakefile
7
+ TODO
8
+ bin/ferret
9
+ lib/ferret_adapter.rb
10
+ lib/ferret_adapter/local_index.rb
11
+ lib/ferret_adapter/remote_index.rb
12
+ lib/ferret_adapter/repository_ext.rb
13
+ lib/ferret_adapter/version.rb
14
+ spec/adapter_spec.rb
15
+ spec/helper.rb
16
+ spec/spec.opts
data/README.txt ADDED
@@ -0,0 +1,62 @@
1
+ This is a DataMapper plugin for Ferret.
2
+
3
+ = Setup code
4
+
5
+ For a single process site, use the ferret index directly:
6
+
7
+ DataMapper.setup :search, "ferret:///path/to/index"
8
+
9
+ For a multi-process site, use the distributed index by running `ferret start`
10
+ inside your project's directory and then setting up the :search repository:
11
+
12
+ DataMapper.setup :search, "ferret:///tmp/ferret_index.sock"
13
+
14
+ = Sample Code
15
+
16
+ require "rubygems"
17
+ require "dm-core"
18
+ require "dm-is-searchable"
19
+
20
+ DataMapper.setup(:default, "sqlite3::memory:")
21
+ DataMapper.setup(:search, "ferret://#{Pathname(__FILE__).dirname.expand_path.parent + "index"}")
22
+
23
+ class Image
24
+ include DataMapper::Resource
25
+ property :id, Serial
26
+ property :title, String
27
+
28
+ is :searchable # this defaults to :search repository, you could also do
29
+ # is :searchable, :repository => :ferret
30
+
31
+ end
32
+
33
+ class Story
34
+ include DataMapper::Resource
35
+ property :id, Serial
36
+ property :title, String
37
+ property :author, String
38
+
39
+ repository(:search) do
40
+ # We only want to search on id and title.
41
+ properties(:search).clear
42
+ property :id, Serial
43
+ property :title, String
44
+ end
45
+
46
+ is :searchable
47
+
48
+ end
49
+
50
+ Image.auto_migrate!
51
+ Story.auto_migrate!
52
+ image = Image.create(:title => "Oil Rig");
53
+ story = Story.create(:title => "Big Oil", :author => "John Doe") }
54
+
55
+ puts Image.search(:title => "Oil Rig").inspect # => [<Image title="Oil Rig">]
56
+
57
+ # For info on this, see DM::Repository#search and DM::Adapters::FerretAdapter#search.
58
+ puts repository(:search).search('title:"Oil"').inspect # => { Image => ["1"], Story => ["1"] }
59
+
60
+ image.destroy
61
+
62
+ puts Image.search(:title => "Oil Rig").inspect # => []
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'spec/rake/spectask'
4
+ require 'pathname'
5
+
6
+ ROOT = Pathname(__FILE__).dirname.expand_path
7
+ require ROOT + 'lib/ferret_adapter/version'
8
+
9
+ AUTHOR = "Bernerd Schaefer"
10
+ EMAIL = "bernerd@wieck.com"
11
+ GEM_NAME = "dm-ferret-adapter"
12
+ GEM_VERSION = DataMapper::More::FerretAdapter::VERSION
13
+ GEM_DEPENDENCIES = [["dm-core", ">=#{GEM_VERSION}"]]
14
+ GEM_CLEAN = ["log", "pkg"]
15
+ GEM_EXTRAS = { :has_rdoc => true, :extra_rdoc_files => %w[ README.txt LICENSE TODO ] }
16
+
17
+ PROJECT_NAME = "datamapper"
18
+ PROJECT_URL = "http://github.com/sam/dm-more/tree/master/adapters/dm-ferret-adapter"
19
+ PROJECT_DESCRIPTION = PROJECT_SUMMARY = "Ferret Adapter for DataMapper"
20
+
21
+ require ROOT.parent.parent + 'tasks/hoe'
22
+
23
+ task :default => [ :spec ]
24
+
25
+ WIN32 = (RUBY_PLATFORM =~ /win32|mingw|cygwin/) rescue nil
26
+ SUDO = WIN32 ? '' : ('sudo' unless ENV['SUDOLESS'])
27
+
28
+ desc "Install #{GEM_NAME} #{GEM_VERSION}"
29
+ task :install => [ :package ] do
30
+ sh "#{SUDO} gem install --local pkg/#{GEM_NAME}-#{GEM_VERSION} --no-update-sources", :verbose => false
31
+ end
32
+
33
+ desc "Uninstall #{GEM_NAME} #{GEM_VERSION} (default ruby)"
34
+ task :uninstall => [ :clobber ] do
35
+ sh "#{SUDO} gem uninstall #{GEM_NAME} -v#{GEM_VERSION} -I -x", :verbose => false
36
+ end
37
+
38
+ desc 'Run specifications'
39
+ Spec::Rake::SpecTask.new(:spec) do |t|
40
+ if File.exists?('spec/spec.opts')
41
+ t.spec_opts << '--options' << 'spec/spec.opts'
42
+ end
43
+ t.spec_files = Pathname.glob((ROOT + 'spec/**/*_spec.rb').to_s)
44
+
45
+ begin
46
+ t.rcov = ENV.has_key?('NO_RCOV') ? ENV['NO_RCOV'] != 'true' : true
47
+ t.rcov_opts << '--exclude' << 'spec'
48
+ t.rcov_opts << '--text-summary'
49
+ t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse'
50
+ rescue Exception
51
+ # rcov not installed
52
+ end
53
+ end
data/TODO ADDED
@@ -0,0 +1,8 @@
1
+ TODO
2
+ ====
3
+
4
+
5
+
6
+ ---
7
+ TODO tickets may also be found in the DataMapper Issue Tracker:
8
+ http://wm.lighthouseapp.com/projects/4819-datamapper/overview
data/bin/ferret ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env ruby
2
+ require "rubygems"
3
+ require "dm-core"
4
+ require "ferret_adapter"
5
+
6
+ require "fileutils"
7
+ require "drb"
8
+ require "drb/unix"
9
+ require "rinda/tuplespace"
10
+ require "optparse"
11
+
12
+ options = {
13
+ :name => "ferret_index",
14
+ :index => Dir.pwd + "/index",
15
+ :pid_file => "tmp/ferret.pid",
16
+ :log_file => "log/ferret.log",
17
+ :log_level => :error
18
+ }
19
+
20
+ option_parser = OptionParser.new do |opts|
21
+ opts.banner = "Usage: ferret.rb [options] start|stop"
22
+ opts.on("-n", "--name NAME", "The name to use for the Rinda Ring service. Defaults to 'ferret_index'.") { |name| options[:name] = name }
23
+ opts.on("-P", "--pid PIDFILE", "PID file, defaults to tmp/ferret.pid") { |pid| options[:pid_file] = pid }
24
+ opts.on("-L", "--log LOGFILE", "Log file, defaults to log/ferret.log") { |log| options[:log_file] = log }
25
+ opts.on("-l", "--log-level LEVEL", [:debug, :error], "Log levels can be: debug, error. Default is error.") { |level| options[:log_level] = level }
26
+ opts.on("-i", "--index INDEX", "Index path. Defaults to Dir.pwd + '/index'") { |index| options[:index] = index }
27
+ end
28
+
29
+ unless %w(start stop).include? ARGV.last
30
+ puts option_parser.help
31
+ exit
32
+ end
33
+
34
+ option_parser.parse!
35
+
36
+ command = ARGV.shift
37
+
38
+ if command == "stop"
39
+ Process.kill("INT", File.read(options[:pid_file]).to_i) if File.exists?(options[:pid_file])
40
+ exit
41
+ else
42
+ fork do
43
+ # Promote this process.
44
+ Process.setsid
45
+
46
+ FileUtils.mkdir_p(Pathname(options[:pid_file]).dirname)
47
+ FileUtils.mkdir_p(Pathname(options[:log_file]).dirname)
48
+
49
+ # We redirect STDOUT to the :log_file only in debug mode.
50
+ if options[:log_level] == :debug
51
+ STDOUT.reopen options[:log_file], "a"
52
+ else
53
+ STDOUT.reopen "/dev/null", "a"
54
+ end
55
+ STDERR.reopen options[:log_file], "a"
56
+
57
+ tuple_space = Rinda::TupleSpace.new
58
+
59
+ DRb.start_service "drbunix:///tmp/#{options[:name]}.sock", tuple_space
60
+
61
+ STDOUT.puts "Server started at #{DRb.uri}"
62
+ STDOUT.flush
63
+
64
+ # Write the process id to the specified :pid_file
65
+ File.open(options[:pid_file], "w") { |f| f.write(Process.pid) }
66
+
67
+ # Cleanup the pid.
68
+ at_exit do
69
+ File.unlink(options[:pid_file]) if options[:pid_file]
70
+ end
71
+
72
+ uri = Addressable::URI.parse(options[:index])
73
+ @index = DataMapper::Adapters::FerretAdapter::LocalIndex.new(uri)
74
+
75
+ loop do
76
+ begin
77
+ command, uri, value = tuple_space.take([nil, nil, nil])
78
+ case command
79
+ when :search
80
+ puts "Search"
81
+ puts " - #{value[0].inspect}"
82
+ puts " - #{value[1].inspect}"
83
+ STDOUT.flush
84
+ begin
85
+ result = @index.search(*value)
86
+ tuple_space.write [:search_result, uri, value, result]
87
+ rescue
88
+ tuple_space.write [:search_result, uri, value, nil]
89
+ raise $!
90
+ end
91
+ when :add
92
+ puts "Insert"
93
+ puts " - #{value.inspect}"
94
+ STDOUT.flush
95
+ @index.add value
96
+ when :delete
97
+ puts "Delete"
98
+ puts " - #{value.inspect}"
99
+ STDOUT.flush
100
+ @index.delete value
101
+ end
102
+ rescue Interrupt
103
+ STDOUT.puts "Shutting down server"
104
+ STDOUT.flush
105
+ break
106
+ rescue
107
+ STDERR.puts "=== #{Time.now} ==="
108
+ STDERR.puts $!
109
+ STDERR.puts $!.backtrace
110
+ STDERR.puts ""
111
+ STDERR.flush
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,43 @@
1
+ module DataMapper
2
+ module Adapters
3
+ class FerretAdapter::LocalIndex
4
+
5
+ attr_accessor :uri
6
+
7
+ def initialize(uri)
8
+ @uri = uri
9
+ @options = { :path => @uri.path, :key => [:id, :_type] }
10
+ create_or_initialize_index
11
+ end
12
+
13
+ def add(doc)
14
+ @index << doc
15
+ end
16
+
17
+ def delete(query)
18
+ @index.query_delete(query)
19
+ end
20
+
21
+ def search(query, options = {})
22
+ @index.search(query, options).hits.collect { |hit, score| @index[hit.doc] }
23
+ end
24
+
25
+ def [](id)
26
+ @index[id]
27
+ end
28
+
29
+ private
30
+
31
+ def create_or_initialize_index
32
+ unless File.exists?(@uri.path + "segments")
33
+ field_infos = ::Ferret::Index::FieldInfos.new(:store => :no)
34
+ field_infos.add_field(:id, :index => :untokenized, :term_vector => :no, :store => :yes)
35
+ field_infos.add_field(:_type, :index => :untokenized, :term_vector => :no, :store => :yes)
36
+ @index = ::Ferret::Index::Index.new( @options.merge(:field_infos => field_infos) )
37
+ else
38
+ @index = ::Ferret::Index::Index.new( @options )
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,55 @@
1
+ module DataMapper
2
+ module Adapters
3
+ class FerretAdapter::RemoteIndex
4
+
5
+ class IndexNotFound < Exception; end
6
+ class SearchError < Exception; end
7
+
8
+ attr_accessor :uri
9
+
10
+ def initialize(uri)
11
+ @uri = uri
12
+
13
+ connect_to_remote_index
14
+ end
15
+
16
+ def add(doc)
17
+ @index.write [:add, DRb.uri, doc]
18
+ end
19
+
20
+ def delete(query)
21
+ @index.write [:delete, DRb.uri, query]
22
+ end
23
+
24
+ def search(query, options)
25
+ tuple = [query, options]
26
+ @index.write [:search, DRb.uri, tuple]
27
+ result = @index.take([:search_result, DRb.uri, tuple, nil]).last
28
+ if result == nil
29
+ raise SearchError.new("An error occurred performing this search. Check the Ferret logs.")
30
+ end
31
+ result
32
+ end
33
+
34
+ private
35
+
36
+ def connect_to_remote_index
37
+ require "drb"
38
+ require "drb/unix"
39
+ require "rinda/tuplespace"
40
+
41
+ DRb.start_service
42
+ tuple_space = DRb::DRbObject.new(nil, "drbunix://#{@uri.path}")
43
+
44
+ # This will throw Errno::ENOENT if the socket does not exist.
45
+ tuple_space.respond_to?(:write)
46
+
47
+ @index = Rinda::TupleSpaceProxy.new(tuple_space)
48
+
49
+ rescue Errno::ENOENT
50
+ raise IndexNotFound.new("Your remote index server is not running.")
51
+ end
52
+
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,13 @@
1
+ module DataMapper
2
+ class Repository
3
+ # This accepts a ferret query string and an optional limit argument
4
+ # which defaults to all. This is the proper way to perform searches more
5
+ # complicated than DM's query syntax can handle (such as OR searches).
6
+ #
7
+ # See DataMapper::Adapters::FerretAdapter#search for information on
8
+ # the return value.
9
+ def search(query, limit = :all)
10
+ adapter.search(query, limit)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ module DataMapper
2
+ module More
3
+ class FerretAdapter
4
+ VERSION = "0.9.7"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,111 @@
1
+ require 'rubygems'
2
+ require 'pathname'
3
+ require Pathname(__FILE__).dirname + 'ferret_adapter/version'
4
+
5
+ gem 'dm-core', ">=#{DataMapper::More::FerretAdapter::VERSION}"
6
+ require 'dm-core'
7
+
8
+ gem "ferret"
9
+ require "ferret"
10
+
11
+ module DataMapper
12
+ module Adapters
13
+ class FerretAdapter < AbstractAdapter
14
+ def initialize(name, uri_or_options)
15
+ super
16
+ unless File.extname(@uri.path) == ".sock"
17
+ @index = LocalIndex.new(@uri)
18
+ else
19
+ @index = RemoteIndex.new(@uri)
20
+ end
21
+ end
22
+
23
+ def create(resources)
24
+ resources.each do |resource|
25
+ attributes = repository(self.name) do
26
+ attrs = resource.attributes
27
+ attrs.delete_if { |name, value| !resource.class.properties(self.name).has_property?(name) }
28
+ resource.class.new(attrs).attributes
29
+ end
30
+
31
+ # Since we don't inspect the models before generating the indices,
32
+ # we'll map the resource's key to the :id column.
33
+ key = resource.class.key.first
34
+ attributes[:id] = attributes.delete(key.name) unless key.name == :id
35
+ attributes[:_type] = resource.class.name
36
+
37
+ @index.add attributes
38
+ end
39
+ 1
40
+ end
41
+
42
+ def delete(query)
43
+ ferret_query = dm_query_to_ferret_query(query)
44
+ @index.delete ferret_query
45
+ 1
46
+ end
47
+
48
+ # This returns an array of Ferret docs (glorified hashes) which can
49
+ # be used to instantiate objects by doc[:_type] and doc[:_id]
50
+ def read_many(query, limit = query.limit)
51
+ ferret_query = dm_query_to_ferret_query(query)
52
+ @index.search(ferret_query, :limit => (limit || :all))
53
+ end
54
+
55
+ def read_one(query)
56
+ read_many(query).first
57
+ end
58
+
59
+ # This returns a hash of the resource constant and the ids returned for it
60
+ # from the search.
61
+ # { Story => ["1", "2"], Image => ["2"] }
62
+ def search(query, limit)
63
+ results = Hash.new { |h, k| h[k] = [] }
64
+ read_many(query, limit).each do |doc|
65
+ results[Object.const_get(doc[:_type])] << doc[:id]
66
+ end
67
+ results
68
+ end
69
+
70
+ private
71
+
72
+ def dm_query_to_ferret_query(query)
73
+ # If we already have a ferret query, do nothing
74
+ return query if query.is_a?(String)
75
+
76
+ ferret = []
77
+
78
+ # We scope the query by the _type field to the query's model.
79
+ ferret << "+_type:\"#{query.model.name}\""
80
+
81
+ if query.conditions.empty?
82
+ ferret << "*"
83
+ else
84
+ query.conditions.each do |operator, property, value|
85
+ # We use property.field here, so that you can declare composite
86
+ # fields:
87
+ # property :content, String, :field => "title|description"
88
+ name = property.field
89
+
90
+ # Since DM's query syntax does not support OR's, we prefix
91
+ # each condition with ferret's operator of +.
92
+ ferret << case operator
93
+ when :eql, :like then "+#{name}:\"#{value}\""
94
+ when :not then "-#{name}:\"#{value}\""
95
+ when :lt then "+#{name}: < #{value}"
96
+ when :gt then "+#{name}: > #{value}"
97
+ when :lte then "+#{name}: <= #{value}"
98
+ when :gte then "+#{name}: >= #{value}"
99
+ end
100
+ end
101
+ end
102
+ ferret.join(" ")
103
+ end
104
+
105
+ end
106
+ end
107
+ end
108
+
109
+ require Pathname(__FILE__).dirname + "ferret_adapter/local_index"
110
+ require Pathname(__FILE__).dirname + "ferret_adapter/remote_index"
111
+ require Pathname(__FILE__).dirname + "ferret_adapter/repository_ext"
@@ -0,0 +1,35 @@
1
+ require "pathname"
2
+ require Pathname(__FILE__).dirname + "helper"
3
+
4
+ class User
5
+ include DataMapper::Resource
6
+ property :id, Serial
7
+ end
8
+
9
+ class Photo
10
+ include DataMapper::Resource
11
+ property :uuid, String, :default => lambda { `uuidgen`.chomp }, :key => true
12
+ end
13
+
14
+ describe "FerretAdapter" do
15
+ before :each do
16
+ @index = Pathname(__FILE__).dirname.expand_path + "index"
17
+ DataMapper.setup :search, "ferret://#{@index}"
18
+ end
19
+
20
+ after :each do
21
+ FileUtils.rm_r(@index)
22
+ end
23
+
24
+ it "should work with a model using id" do
25
+ u = User.new(:id => 2)
26
+ repository(:search).create([u])
27
+ repository(:search).search("*").should == { User => ["2"] }
28
+ end
29
+
30
+ it "should work with a model using another key than id" do
31
+ p = Photo.new
32
+ repository(:search).create([p])
33
+ repository(:search).search("*").should == { Photo => [p.uuid] }
34
+ end
35
+ end
data/spec/helper.rb ADDED
@@ -0,0 +1,4 @@
1
+ require "pathname"
2
+ require Pathname(__FILE__).dirname.parent + "lib/ferret_adapter"
3
+
4
+ require "spec"
data/spec/spec.opts ADDED
@@ -0,0 +1,2 @@
1
+ --format specdoc
2
+ --colour
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dm-ferret-adapter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.7
5
+ platform: ruby
6
+ authors:
7
+ - Bernerd Schaefer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-11-18 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: dm-core
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.9.7
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: hoe
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.8.2
34
+ version:
35
+ description: Ferret Adapter for DataMapper
36
+ email:
37
+ - bernerd@wieck.com
38
+ executables:
39
+ - ferret
40
+ extensions: []
41
+
42
+ extra_rdoc_files:
43
+ - README.txt
44
+ - LICENSE
45
+ - TODO
46
+ files:
47
+ - .gitignore
48
+ - History.txt
49
+ - LICENSE
50
+ - Manifest.txt
51
+ - README.txt
52
+ - Rakefile
53
+ - TODO
54
+ - bin/ferret
55
+ - lib/ferret_adapter.rb
56
+ - lib/ferret_adapter/local_index.rb
57
+ - lib/ferret_adapter/remote_index.rb
58
+ - lib/ferret_adapter/repository_ext.rb
59
+ - lib/ferret_adapter/version.rb
60
+ - spec/adapter_spec.rb
61
+ - spec/helper.rb
62
+ - spec/spec.opts
63
+ has_rdoc: true
64
+ homepage: http://github.com/sam/dm-more/tree/master/adapters/dm-ferret-adapter
65
+ post_install_message:
66
+ rdoc_options:
67
+ - --main
68
+ - README.txt
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "0"
76
+ version:
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: "0"
82
+ version:
83
+ requirements: []
84
+
85
+ rubyforge_project: datamapper
86
+ rubygems_version: 1.3.1
87
+ signing_key:
88
+ specification_version: 2
89
+ summary: Ferret Adapter for DataMapper
90
+ test_files: []
91
+