simple_solr 0.0.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -2,3 +2,5 @@
2
2
  .bundle
3
3
  Gemfile.lock
4
4
  pkg/*
5
+ .rvmrc
6
+ spec/db/database.yml
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ == 0.4.0 (Aug 16, 2011)
2
+
3
+ - First release as a gem, indexing works
data/Gemfile CHANGED
@@ -1,4 +1,9 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- # Specify your gem's dependencies in simple_solr.gemspec
4
3
  gemspec
4
+
5
+ gem "activerecord"
6
+ gem "rspec", ">= 2"
7
+ gem "sqlite3"
8
+ gem "httparty"
9
+ gem "builder"
data/README.rdoc ADDED
@@ -0,0 +1,103 @@
1
+ Extremely lightweight adapter between Rails and Solr.
2
+
3
+ SimpleSolr uses the REST interface that Solr offers and makes a great number
4
+ of assumptions. This has resulted in a very fast, very lightweight library
5
+ almost anyone can use straight away.
6
+
7
+ * only a few dozen lines of code
8
+ * extremely simple DSL
9
+ * only depends on httparty
10
+ * can handle all common use cases
11
+ * has support for master/slave configurations
12
+ * does not come with a bundled Solr
13
+ * does not send a bunch of magic fields
14
+
15
+ And my personal favorite:
16
+
17
+ * <b>does nothing for unconfigured environments</b>
18
+
19
+ If you have no +development+ section in the config file (see below) then
20
+ nothing will happen at all. Your models will not be indexed, but you are
21
+ freed from having to run a local Solr instance as well.
22
+
23
+ Naturally there are downsides as well.
24
+
25
+ * assumes an +id+ field present
26
+ * makes Solr commit after every change
27
+ * does not come with a bundled Solr
28
+ * no rake tasks included
29
+
30
+ This gem is not really suitable for use outside Rails, as it requires both
31
+ ActiveRecord and ActiveSupport.
32
+
33
+ I owe a great deal to @outoftime's Sunspot[https://github.com/outoftime/sunspot] library. If you need a fully-packed
34
+ solution that handles every Solr case you might have, use that instead.
35
+
36
+
37
+ == Installation
38
+
39
+ Rails 2, in config/environment.rb:
40
+
41
+ config.gem 'simple_solr'
42
+
43
+ Rails 3, in Gemfile:
44
+
45
+ gem 'simple_solr'
46
+
47
+
48
+ == Configuration
49
+
50
+ Create a file called <tt>config/simple_solr.yml</tt>.
51
+
52
+ production:
53
+ solr:
54
+ hostname: "slave.local"
55
+ port: 8000
56
+ path: "/solr"
57
+ master_solr:
58
+ hostname: "master.local"
59
+ port: 8000
60
+ path: "/solr"
61
+
62
+ If you have just one Solr server, leave out the master_solr section.
63
+
64
+ === Your models
65
+
66
+ Then in your models include the following:
67
+
68
+ class Document < ActiveRecord::Base
69
+ simple_solr do
70
+ field :title
71
+ end
72
+ end
73
+
74
+ Only the fields listed in the +simple_solr+ block will be sent to Solr,
75
+ with the addition of the +id+ field which is always included. The type of
76
+ the field is not appended; the XML sent to Solr is as bare as possible.
77
+
78
+ This also means no type casting is performed on dates/times. If you need
79
+ anything like that, use a lambda to manipulate the field to your liking.
80
+
81
+ Full example:
82
+
83
+ class FullDocument < ActiveRecord::Base
84
+ simple_solr do
85
+ field :id, lambda { |record| "full-document-#{record.id}" }
86
+ field :title
87
+ field :date_creation, :created_at
88
+ field :shared, false
89
+ field :published, "Megacorp LLC"
90
+ field :body
91
+ end
92
+ end
93
+
94
+ As you can see you have a couple options with regards to the fields:
95
+
96
+ 1. Do nothing - in which case the corresponding attribute is used
97
+ 2. A symbol - uses the instance method with that name as the value
98
+ 3. Static value such as a string or boolean - which is used verbatim
99
+ 4. A lambda - for your every customization need.
100
+
101
+ Use the latter form if you want to add a dynamic field to Solr. The model
102
+ instance is passed as a parameter, so you can use every method inside
103
+ the block, or run a calculation, or whatever.
data/Rakefile CHANGED
@@ -1 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new('spec')
5
+
6
+ task :default => :spec
@@ -0,0 +1,83 @@
1
+ module SimpleSolr
2
+ module ActiveRecord
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def simple_solr(&block)
9
+ class_eval do
10
+ # httparty
11
+ include HTTParty
12
+
13
+ # callbacks
14
+ after_save :add_to_solr
15
+ after_destroy :delete_from_solr
16
+ end
17
+
18
+ # Store the simple_solr fields for this class
19
+ cattr_accessor :simple_solr_fields
20
+ self.simple_solr_fields = { id: nil }
21
+ block.call
22
+
23
+ include InstanceMethods
24
+ end
25
+
26
+ # gets called by every +field+ line inside the simple_solr block.
27
+ # It stores the given values in +simple_solr_fields+, which we later use to build the XML.
28
+ def field(name, value=nil)
29
+ simple_solr_fields[name] = value
30
+ end
31
+ end
32
+
33
+ module InstanceMethods
34
+ # callback which uses httparty to send a POST to solr.
35
+ def add_to_solr
36
+ if SimpleSolr.configuration.present?
37
+ self.class.post(SimpleSolr.configuration.master_uri + "/update?commit=true", :body => to_solr)
38
+ end
39
+ end
40
+
41
+ def delete_from_solr
42
+ if SimpleSolr.configuration.present?
43
+ self.class.post(SimpleSolr.configuration.master_uri + "/update?commit=true", :body => to_solr_delete)
44
+ end
45
+ end
46
+
47
+ private
48
+ def to_solr_delete
49
+ xml = Builder::XmlMarkup.new
50
+
51
+ xml.delete do
52
+ xml.id id
53
+ end
54
+ end
55
+
56
+ # Convert this instance's attributes to an XML suitable for Solr.
57
+ # The fields in the XML are determined from the simple_solr block.
58
+ def to_solr
59
+ xml = Builder::XmlMarkup.new
60
+
61
+ xml.add do
62
+ xml.doc do
63
+ self.class.simple_solr_fields.each do |name, value|
64
+ if value.nil?
65
+ # no value given, get it from the attribute
66
+ xml.field self.send(name), :name => name
67
+ elsif value.is_a?(Symbol)
68
+ # symbol given, use it to get the attribute
69
+ xml.field self.send(value), :name => name
70
+ elsif value.is_a?(Proc)
71
+ # Procs are used to fetch information from the instance
72
+ xml.field value.call(self), :name => name
73
+ else
74
+ # value given, use it directly.
75
+ xml.field value, :name => name
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,88 @@
1
+ module SimpleSolr
2
+ # SimpleSolr is configured via the config/simple_solr.yml file, which
3
+ # contains properties keyed by environment name.
4
+ #
5
+ # production:
6
+ # solr:
7
+ # hostname: "solr.slave.local"
8
+ # port: 8983
9
+ # path: "/solr"
10
+ # master_solr:
11
+ # hostname: "solr.master.local"
12
+ # port: 8983
13
+ # path: "/solr"
14
+ #
15
+ # If the <code>master_solr</code> configuration is present, SimpleSolr will use
16
+ # the Solr instance specified there for all write operations, and the Solr
17
+ # configured under <code>solr</code> for all read operations.
18
+ class Configuration
19
+ # Define methods for the <code>solr</code> key. This key is normally used to
20
+ # configure SimpleSolr.
21
+ %w(hostname port path).each do |method|
22
+ define_method method do
23
+ configuration('solr', method) || self.send("default_#{method}")
24
+ end
25
+ end
26
+
27
+ # Define methods for the <code>master_solr</code> key. When present, this key
28
+ # defines the Solr used for all write operations, while all read operations
29
+ # take place on the Solr defined in the <code>solr</code> key.
30
+ %w(hostname port path).each do |method|
31
+ define_method "master_#{method}" do
32
+ configuration('master_solr', method) || self.send(method)
33
+ end
34
+ end
35
+
36
+ def present?
37
+ not user_configuration.nil?
38
+ end
39
+
40
+ # Full URI to use for all read operations.
41
+ def uri
42
+ "#{hostname}:#{port}#{path}"
43
+ end
44
+
45
+ # Full URI to use for all write operations.
46
+ # Automatically falls back to the <code>uri</code> when no master defined.
47
+ def master_uri
48
+ "#{master_hostname}:#{master_port}#{master_path}"
49
+ end
50
+
51
+ private
52
+ def configuration(*keys)
53
+ keys.inject(user_configuration) do |hash, key|
54
+ hash[key] if hash
55
+ end
56
+ end
57
+
58
+ def user_configuration
59
+ @user_configuration ||=
60
+ begin
61
+ path = File.join(::Rails.root, 'config', config_file_name)
62
+ if File.exist?(path)
63
+ File.open(path) do |file|
64
+ YAML.load(file)[::Rails.env]
65
+ end
66
+ else
67
+ {}
68
+ end
69
+ end
70
+ end
71
+
72
+ def default_hostname
73
+ 'localhost'
74
+ end
75
+
76
+ def default_port
77
+ 8983
78
+ end
79
+
80
+ def default_path
81
+ "/solr"
82
+ end
83
+
84
+ def config_file_name
85
+ 'simple_solr.yml'
86
+ end
87
+ end
88
+ end
@@ -1,3 +1,3 @@
1
1
  module SimpleSolr
2
- VERSION = "0.0.1"
2
+ VERSION = "0.4.0"
3
3
  end
data/lib/simple_solr.rb CHANGED
@@ -1,5 +1,19 @@
1
+ require 'active_record'
2
+ require 'httparty'
3
+ require 'builder'
4
+
5
+ require "simple_solr/active_record"
6
+ require "simple_solr/configuration"
1
7
  require "simple_solr/version"
2
8
 
3
- module SimpleSolr
4
- # Your code goes here...
9
+ if defined?(ActiveRecord::Base)
10
+ ActiveRecord::Base.send :include, SimpleSolr::ActiveRecord
5
11
  end
12
+
13
+ module SimpleSolr
14
+ class << self
15
+ def configuration
16
+ @configuration ||= SimpleSolr::Configuration.new
17
+ end
18
+ end
19
+ end
data/simple_solr.gemspec CHANGED
@@ -18,7 +18,9 @@ Gem::Specification.new do |s|
18
18
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
19
  s.require_paths = ["lib"]
20
20
 
21
- # specify any dependencies here; for example:
22
- # s.add_development_dependency "rspec"
23
- # s.add_runtime_dependency "rest-client"
21
+ s.add_development_dependency "rspec", ">= 2"
22
+
23
+ s.add_runtime_dependency "activerecord"
24
+ s.add_runtime_dependency "httparty"
25
+ s.add_runtime_dependency "builder"
24
26
  end
@@ -0,0 +1,5 @@
1
+ test:
2
+ solr:
3
+ hostname: "slave.local"
4
+ master_solr:
5
+ hostname: "master.local"
@@ -0,0 +1,3 @@
1
+ test:
2
+ solr:
3
+ hostname: "test.local"
@@ -0,0 +1,3 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: ":memory:"
data/spec/db/models.rb ADDED
@@ -0,0 +1,17 @@
1
+ # A bunch of models with varying amounts of simple_solrism.
2
+
3
+ class SimpleDocument < ActiveRecord::Base
4
+ simple_solr do
5
+ field :title
6
+ end
7
+ end
8
+
9
+ class FullDocument < ActiveRecord::Base
10
+ simple_solr do
11
+ field :id, lambda { |record| "full-document-#{record.id}" }
12
+ field :title
13
+ field :date_creation, :created_at
14
+ field :shared, false
15
+ field :body
16
+ end
17
+ end
data/spec/db/schema.rb ADDED
@@ -0,0 +1,11 @@
1
+ ActiveRecord::Schema.define(:version => 0) do
2
+ create_table :simple_documents, :force => true do |t|
3
+ t.string :title
4
+ end
5
+
6
+ create_table :full_documents, :force => true do |t|
7
+ t.string :title
8
+ t.timestamps
9
+ t.text :body
10
+ end
11
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe SimpleSolr::ActiveRecord do
4
+ describe SimpleDocument do
5
+ it "provides simple_solr class method" do
6
+ SimpleDocument.should respond_to(:simple_solr)
7
+ end
8
+
9
+ it "stores simple_solr fields" do
10
+ SimpleDocument.simple_solr_fields.should eq({:id => nil, :title => nil})
11
+ end
12
+
13
+ context "save" do
14
+ let(:document) { SimpleDocument.create! :title => 'Omg Ponies' }
15
+
16
+ it "posts to solr" do
17
+ SimpleDocument.should_receive(:post).with("test.local:8983/solr/update?commit=true", :body => "<add><doc><field name=\"id\">#{document.id}</field><field name=\"title\">Omg Ponies</field></doc></add>")
18
+ end
19
+ end
20
+
21
+ context "destroy" do
22
+ let(:document) { SimpleDocument.create! :title => 'Omg Ponies' }
23
+
24
+ it "posts to solr" do
25
+ SimpleDocument.should_receive(:post).with("test.local:8983/solr/update?commit=true", :body => "<add><doc><field name=\"id\">#{document.id}</field><field name=\"title\">Omg Ponies</field></doc></add>")
26
+ SimpleDocument.should_receive(:post).with("test.local:8983/solr/update?commit=true", :body => "<delete><id>#{document.id}</id></delete>")
27
+ document.destroy
28
+ end
29
+ end
30
+
31
+ context "when unconfigured" do
32
+ before do
33
+ SimpleSolr.stub_chain(:configuration, :present?).and_return(false)
34
+ end
35
+
36
+ it "does nothing" do
37
+ SimpleDocument.should_not_receive(:post)
38
+ document = SimpleDocument.new :title => 'Omg Ponies'
39
+ document.save
40
+ end
41
+ end
42
+ end
43
+
44
+ describe FullDocument do
45
+ let(:document) { FullDocument.create :title => "Rainbows" }
46
+
47
+ it "posts to solr after save" do
48
+ FullDocument.should_receive(:post).with("test.local:8983/solr?commit=true", :body => "<add><doc><field name=\"id\">full-document-#{document.id}</field><field name=\"title\">Rainbows</field><field name=\"date_creation\">#{document.created_at}</field><field name=\"shared\">false</field><field name=\"body\"></field></doc></add>")
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe SimpleSolr::Configuration do
4
+ it { should be_present}
5
+
6
+ its(:hostname) { should eq('test.local')}
7
+ its(:port) { should eq(8983)}
8
+ its(:path) { should eq('/solr')}
9
+ its(:uri) { should eq('test.local:8983/solr')}
10
+
11
+ its(:master_hostname) { should eq('test.local')}
12
+ its(:master_port) { should eq(8983)}
13
+ its(:master_path) { should eq('/solr')}
14
+ its(:master_uri) { should eq('test.local:8983/solr')}
15
+
16
+ context "unconfigured Rails env" do
17
+ before do
18
+ ::Rails.stub(:env).and_return('staging')
19
+ end
20
+
21
+ it { should_not be_present }
22
+ end
23
+
24
+ context "missing config file" do
25
+ before do
26
+ ::Rails.stub(:root).and_return(Pathname.new('/'))
27
+ end
28
+
29
+ its(:hostname) { should eq('localhost')}
30
+ its(:port) { should eq(8983)}
31
+ its(:path) { should eq('/solr')}
32
+ its(:uri) { should eq('localhost:8983/solr')}
33
+
34
+ its(:master_hostname) { should eq('localhost')}
35
+ its(:master_port) { should eq(8983)}
36
+ its(:master_path) { should eq('/solr')}
37
+ its(:master_uri) { should eq('localhost:8983/solr')}
38
+ end
39
+
40
+ context "master/slave configuration" do
41
+ before do
42
+ subject.stub(:config_file_name).and_return('master_slave.yml')
43
+ end
44
+
45
+ its(:hostname) { should eq('slave.local')}
46
+ its(:port) { should eq(8983)}
47
+ its(:path) { should eq('/solr')}
48
+ its(:uri) { should eq('slave.local:8983/solr')}
49
+
50
+ its(:master_hostname) { should eq('master.local')}
51
+ its(:master_port) { should eq(8983)}
52
+ its(:master_path) { should eq('/solr')}
53
+ its(:master_uri) { should eq('master.local:8983/solr')}
54
+ end
55
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe SimpleSolr do
4
+ end
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'simple_solr'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+ # some (optional) config here
12
+ end
@@ -0,0 +1,28 @@
1
+ require 'yaml'
2
+
3
+ ENV['DB'] ||= 'sqlite3'
4
+
5
+ database_yml = File.expand_path('../../db/database.yml', __FILE__)
6
+ if File.exists?(database_yml)
7
+ active_record_configuration = YAML.load_file(database_yml)[ENV['DB']]
8
+
9
+ ActiveRecord::Base.establish_connection(active_record_configuration)
10
+
11
+ ActiveRecord::Base.silence do
12
+ ActiveRecord::Migration.verbose = false
13
+
14
+ load(File.dirname(__FILE__) + '/../db/schema.rb')
15
+ load(File.dirname(__FILE__) + '/../db/models.rb')
16
+ end
17
+
18
+ else
19
+ raise "Please create #{database_yml} first to configure your database. Take a look at: #{database_yml}.example"
20
+ end
21
+
22
+ def clean_database!
23
+ [SimpleDocument].each do |model|
24
+ ActiveRecord::Base.connection.execute "DELETE FROM #{model.table_name}"
25
+ end
26
+ end
27
+
28
+ clean_database!
@@ -0,0 +1,12 @@
1
+ require 'pathname'
2
+
3
+ # Fake Rails class to provide stubbed methods we use on the real one.
4
+ class Rails
5
+ def self.env
6
+ 'test'
7
+ end
8
+
9
+ def self.root
10
+ Pathname.new File.dirname(__FILE__) + "/.."
11
+ end
12
+ end
metadata CHANGED
@@ -1,13 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_solr
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
5
4
  prerelease:
6
- segments:
7
- - 0
8
- - 0
9
- - 1
10
- version: 0.0.1
5
+ version: 0.4.0
11
6
  platform: ruby
12
7
  authors:
13
8
  - Joost Baaij
@@ -15,10 +10,53 @@ autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
12
 
18
- date: 2011-08-15 00:00:00 +02:00
13
+ date: 2011-08-16 00:00:00 +02:00
19
14
  default_executable:
20
- dependencies: []
21
-
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: rspec
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "2"
25
+ type: :development
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: "0"
36
+ type: :runtime
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: httparty
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id003
49
+ - !ruby/object:Gem::Dependency
50
+ name: builder
51
+ prerelease: false
52
+ requirement: &id004 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ type: :runtime
59
+ version_requirements: *id004
22
60
  description: Rails plugins which connects to a Solr search engine with as less features as possible.
23
61
  email:
24
62
  - joost@spacebabies.nl
@@ -30,11 +68,27 @@ extra_rdoc_files: []
30
68
 
31
69
  files:
32
70
  - .gitignore
71
+ - .rspec
72
+ - CHANGELOG.rdoc
33
73
  - Gemfile
74
+ - README.rdoc
34
75
  - Rakefile
35
76
  - lib/simple_solr.rb
77
+ - lib/simple_solr/active_record.rb
78
+ - lib/simple_solr/configuration.rb
36
79
  - lib/simple_solr/version.rb
37
80
  - simple_solr.gemspec
81
+ - spec/config/master_slave.yml
82
+ - spec/config/simple_solr.yml
83
+ - spec/db/database.yml.example
84
+ - spec/db/models.rb
85
+ - spec/db/schema.rb
86
+ - spec/simple_solr/active_record_spec.rb
87
+ - spec/simple_solr/configuration_spec.rb
88
+ - spec/simple_solr_spec.rb
89
+ - spec/spec_helper.rb
90
+ - spec/support/database.rb
91
+ - spec/support/rails.rb
38
92
  has_rdoc: true
39
93
  homepage: ""
40
94
  licenses: []
@@ -49,18 +103,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
49
103
  requirements:
50
104
  - - ">="
51
105
  - !ruby/object:Gem::Version
52
- hash: 3
53
- segments:
54
- - 0
55
106
  version: "0"
56
107
  required_rubygems_version: !ruby/object:Gem::Requirement
57
108
  none: false
58
109
  requirements:
59
110
  - - ">="
60
111
  - !ruby/object:Gem::Version
61
- hash: 3
62
- segments:
63
- - 0
64
112
  version: "0"
65
113
  requirements: []
66
114
 
@@ -69,5 +117,15 @@ rubygems_version: 1.6.2
69
117
  signing_key:
70
118
  specification_version: 3
71
119
  summary: Simple Solr client
72
- test_files: []
73
-
120
+ test_files:
121
+ - spec/config/master_slave.yml
122
+ - spec/config/simple_solr.yml
123
+ - spec/db/database.yml.example
124
+ - spec/db/models.rb
125
+ - spec/db/schema.rb
126
+ - spec/simple_solr/active_record_spec.rb
127
+ - spec/simple_solr/configuration_spec.rb
128
+ - spec/simple_solr_spec.rb
129
+ - spec/spec_helper.rb
130
+ - spec/support/database.rb
131
+ - spec/support/rails.rb