simple_solr 0.0.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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