solrsan 0.0.20

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,149 @@
1
+ module Solrsan
2
+ module Search
3
+ extend ActiveSupport::Concern
4
+ module ClassMethods
5
+ def class_name
6
+ to_s.underscore
7
+ end
8
+
9
+ def perform_solr_command
10
+ @rsolr = Solrsan::Config.instance.rsolr_object
11
+ yield(@rsolr)
12
+ @rsolr.commit
13
+ end
14
+
15
+ def search(search_params={})
16
+ @rsolr ||= Solrsan::Config.instance.rsolr_object
17
+
18
+ start = search_params[:start] || 0
19
+ rows = search_params[:rows] || 20
20
+
21
+ solr_params = parse_params_for_solr(search_params)
22
+
23
+ begin
24
+ solr_response = @rsolr.paginate(start, rows, 'select', :params => solr_params)
25
+ parse_solr_response(solr_response)
26
+ rescue RSolr::Error::Http => e
27
+ {:docs => [],
28
+ :metadata =>
29
+ {:error => {:http_status_code => e.response[:status],
30
+ :http_message_status => RSolr::Error::Http::STATUS_CODES[e.response[:status].to_i],
31
+ :full_message => e.message}}}
32
+ end
33
+ end
34
+
35
+ def parse_params_for_solr(search_params={})
36
+ solr_params = { :echoParams => 'explicit',
37
+ :q => "*:*",
38
+ :facet => "on",
39
+ :'facet.mincount' => 1}.merge(search_params)
40
+ solr_params[:hl] = true unless search_params[:'hl.fl'].blank?
41
+ solr_params[:fq] = ["type:#{class_name}"] + parse_fq(search_params[:fq])
42
+ solr_params
43
+ end
44
+
45
+ def parse_solr_response(solr_response)
46
+ docs = solr_response['response']['docs']
47
+ parsed_facet_counts = parse_facet_counts(solr_response['facet_counts'])
48
+ highlighting = solr_response['highlighting']
49
+
50
+ metadata = {
51
+ :total_count => solr_response['response']['numFound'],
52
+ :start => solr_response['response']['start'],
53
+ :rows => solr_response['responseHeader']['params']['rows'],
54
+ :time => solr_response['responseHeader']['QTime'],
55
+ :status => solr_response['responseHeader']['status']
56
+ }
57
+
58
+ {:docs => docs, :metadata => metadata,
59
+ :facet_counts => parsed_facet_counts, :highlighting => highlighting}
60
+ end
61
+
62
+ def parse_fq(fq)
63
+ return [] if fq.nil?
64
+ if fq.is_a?(Hash)
65
+ fq.map{|k,v| parse_element_in_fq({k => v})}.flatten
66
+ elsif fq.is_a?(Array)
67
+ fq.map{|ele| parse_element_in_fq(ele)}.flatten
68
+ else
69
+ raise "fq must be a hash or array"
70
+ end
71
+ end
72
+
73
+ def parse_element_in_fq(element)
74
+ if element.is_a?(String)
75
+ element
76
+ elsif element.is_a?(Hash)
77
+ element.map do |k,values|
78
+ if values.is_a?(String)
79
+ key_value_query(k,values)
80
+ else
81
+ values.map{|value| key_value_query(k,value) }
82
+ end
83
+ end
84
+ else
85
+ raise "each fq parameter must be a string or hash"
86
+ end
87
+ end
88
+
89
+ def key_value_query(key, value)
90
+ if value.starts_with?("[")
91
+ "#{key}:#{value}"
92
+ else
93
+ "#{key}:\"#{value}\""
94
+ end
95
+ end
96
+
97
+ def parse_facet_counts(facet_counts)
98
+ return {} unless facet_counts
99
+
100
+ if facet_counts['facet_fields']
101
+ facet_counts['facet_fields'] = facet_counts['facet_fields'].reduce({}) do |acc, facet_collection|
102
+ acc[facet_collection[0]] = map_facet_array_to_facet_hash(facet_collection[1])
103
+ acc
104
+ end
105
+ end
106
+
107
+ if facet_counts['facet_queries']
108
+ facet_counts['facet_queries'] = facet_counts['facet_queries'].group_by{|k,v| k.split(":").first}.reduce({}) do |acc, facet_collection|
109
+ facet_name = facet_collection[0]
110
+ values = facet_collection[1]
111
+
112
+ acc[facet_name] = values.reduce({}) do |inner_acc, tuple|
113
+ range = tuple[0].split(":")[1]
114
+ inner_acc[range] = tuple[1]
115
+ inner_acc
116
+ end
117
+ acc
118
+ end
119
+ end
120
+
121
+ facet_counts
122
+ end
123
+
124
+ # solr facet_fields comes in tuple array format []
125
+ def map_facet_array_to_facet_hash(facet_collection)
126
+ if facet_collection.is_a?(Array)
127
+ facet_collection.each_slice(2).reduce({}){|acc, tuple| acc[tuple[0]] = tuple[1]; acc}
128
+ else
129
+ facet_collection
130
+ end
131
+ end
132
+
133
+ end
134
+ end
135
+ end
136
+
137
+ # namespace test documents
138
+ if defined?(Rails) && Rails.env == "test"
139
+ module Solrsan
140
+ module Search
141
+ extend ActiveSupport::Concern
142
+ module ClassMethods
143
+ def class_name
144
+ "#{to_s.underscore}_test"
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,3 @@
1
+ module Solrsan
2
+ VERSION = "0.0.20"
3
+ end
data/lib/solrsan.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'rsolr'
2
+ require 'active_support/json'
3
+ require 'active_support/concern'
4
+ require 'active_support/core_ext'
5
+
6
+ require 'singleton'
7
+ require 'solrsan/config'
8
+ require 'solrsan/search'
9
+ require 'solrsan/indexer'
10
+
11
+ module Solrsan
12
+ end
@@ -0,0 +1,63 @@
1
+ require 'uri'
2
+ require 'yaml'
3
+
4
+ namespace :solr do
5
+ env = "development"
6
+ base_dir = File.join(File.dirname(__FILE__), "..", "..")
7
+
8
+ if defined? Rails
9
+ env = Rails.env
10
+ base_dir = Rails.root
11
+ end
12
+
13
+ solr_config = YAML::load( File.open( File.join(base_dir, "config", "solr.yml") ) )
14
+ solr_home = File.join(base_dir, "config", "solr")
15
+ solr_data_dir = solr_config[env]['solr_data_dir']
16
+ solr_server_url = solr_config[env]['solr_server_url']
17
+
18
+ jetty_port = URI.parse(solr_server_url).port
19
+ jetty_path = solr_config[env]['jetty_path']
20
+
21
+ solr_server_dir = "cd #{jetty_path};"
22
+ start_solr_cmd = "java -jar start.jar"
23
+ jetty_port_opt = "jetty.port=#{jetty_port}"
24
+ solr_params = "#{jetty_port_opt} -Dsolr.solr.home=#{solr_home} -Dsolr.data.dir=#{solr_data_dir}"
25
+
26
+ desc "Start solr"
27
+ task(:start) do
28
+ # -Dsolr.clustering.enabled=true
29
+ cmd = kill_matching_process_cmd(jetty_port_opt)
30
+ stop_exit_status = run_system_command(cmd)
31
+
32
+ sleep(1)
33
+
34
+ cmd = "#{solr_server_dir} #{start_solr_cmd } #{solr_params} &"
35
+ run_system_command(cmd)
36
+ end
37
+
38
+ desc "Stop solr"
39
+ task(:stop) do
40
+ cmd = kill_matching_process_cmd(jetty_port_opt)
41
+ run_system_command(cmd)
42
+ end
43
+
44
+ def run_system_command(cmd)
45
+ puts cmd
46
+ status = system(cmd)
47
+ $?.exitstatus
48
+ end
49
+
50
+ def kill_matching_process_cmd(process_name)
51
+ cmd = "echo `ps -ef | grep -v grep | grep \"#{process_name.gsub("-", "\\-")}\" | awk '{print $2}'` | xargs kill"
52
+ end
53
+
54
+ # #example of a task to index all items
55
+ # desc "index food"
56
+ # task(:import_foods, :needs => :environment) do
57
+ # FoodDescription.find_in_batches do |batch|
58
+ # FoodIndexer.index(batch)
59
+ # puts "Done with batch of size: #{batch.size}"
60
+ # end
61
+ # end
62
+ end
63
+
data/solrsan.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "solrsan/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "solrsan"
7
+ s.version = Solrsan::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Tommy Chheng"]
10
+ s.email = ["tommy.chheng@gmail.com"]
11
+ s.homepage = "http://github.com/tc/solrsan"
12
+ s.summary = %q{Lightweight wrapper for using Apache Solr within a Ruby application including Rails and Sinatra.}
13
+ s.description = %q{solrsan is a lightweight wrapper for using Apache Solr within a Ruby application including Rails and Sinatra.}
14
+
15
+ s.add_dependency('rsolr', '~>1.0.0')
16
+ s.add_dependency('activemodel', '~>3.0.5')
17
+ s.add_dependency('activesupport', '~>3.0.5')
18
+
19
+ s.rubyforge_project = "solrsan"
20
+
21
+ s.files = `git ls-files`.split("\n")
22
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
23
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
24
+ s.require_paths = ["lib"]
25
+ end
@@ -0,0 +1,11 @@
1
+ require 'active_model/attribute_methods'
2
+
3
+ class Document
4
+ include ActiveModel::AttributeMethods
5
+ include Solrsan::Search
6
+ attr_accessor :attributes
7
+
8
+ def initialize(attributes={})
9
+ @attributes = attributes
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ module SearchTestHelper
2
+ def document_mock
3
+ doc = Document.new({:id => 500,
4
+ :title => "Solr test document",
5
+ :author => "Tommy Chheng",
6
+ :content => "ruby scala search",
7
+ :review_count => 4,
8
+ :scores => [1,2,3,4],
9
+ :created_at => Date.parse("Dec 10 2010")})
10
+ doc
11
+ end
12
+ end
13
+
@@ -0,0 +1,24 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
3
+
4
+ MODELS = File.join(File.dirname(__FILE__), "models")
5
+ $LOAD_PATH.unshift(MODELS)
6
+
7
+ #simulate rails env
8
+ class Rails
9
+ def self.env
10
+ "test"
11
+ end
12
+ end
13
+
14
+ require "rubygems"
15
+ require "test/unit"
16
+ require 'solrsan'
17
+
18
+ #test models
19
+ require 'document'
20
+
21
+ solr_config = YAML::load( File.open( File.join(File.dirname(__FILE__), "..", "config", "solr.yml") ) )
22
+ solr_server_url = solr_config["test"]['solr_server_url']
23
+
24
+ Solrsan::Config.instance.solr_server_url = solr_server_url
@@ -0,0 +1,25 @@
1
+ require 'test_helper'
2
+ require 'search_test_helper'
3
+
4
+ class IndexerTest < Test::Unit::TestCase
5
+ include SearchTestHelper
6
+
7
+ def setup
8
+ @document = document_mock
9
+ end
10
+
11
+ def teardown
12
+ end
13
+
14
+ def test_indexed_fields
15
+ created_doc = @document.indexed_fields
16
+ assert_equal @document.attributes[:id], created_doc[:db_id]
17
+ assert_equal @document.attributes[:title], created_doc[:title]
18
+ assert_equal @document.attributes[:author], created_doc[:author]
19
+ assert_equal @document.attributes[:content], created_doc[:content]
20
+ assert_equal @document.attributes[:review_count], created_doc[:review_count]
21
+ assert_equal @document.attributes[:scores], created_doc[:scores]
22
+ assert_equal @document.attributes[:created_at].to_time.utc.xmlschema, created_doc[:created_at]
23
+ end
24
+
25
+ end
@@ -0,0 +1,171 @@
1
+ require 'test_helper'
2
+ require 'search_test_helper'
3
+
4
+ class SearchTest < Test::Unit::TestCase
5
+ include SearchTestHelper
6
+
7
+ def setup
8
+ Document.destroy_all_index_documents!
9
+ @document = document_mock
10
+ end
11
+
12
+ def teardown
13
+ Document.destroy_all_index_documents!
14
+ end
15
+
16
+ def test_simple_query
17
+ Document.index(@document)
18
+ q = @document.attributes[:title]
19
+
20
+ response = Document.search(:q => q)
21
+ metadata = response[:metadata]
22
+ docs = response[:docs]
23
+ assert metadata[:total_count] > 0
24
+ end
25
+
26
+ def test_sort
27
+ Document.index(Document.new(:id => 3, :title => "solar city",:review_count => 10))
28
+ Document.index(Document.new(:id => 4, :title => "city solar", :review_count => 5))
29
+
30
+ q = "solar"
31
+ response = Document.search(:q => q, :sort => "review_count asc")
32
+ docs = response[:docs]
33
+
34
+ assert_not_nil docs[0], "Not enough docs for #{q} to test."
35
+ first_result_name = docs[0][:title]
36
+ assert_not_nil docs[1], "Not enough docs for #{q} to test."
37
+ second_result_name = docs[1][:title]
38
+
39
+ assert_equal [second_result_name, first_result_name].sort, [first_result_name, second_result_name]
40
+
41
+ response = Document.search(:q => q, :sort => "review_count desc")
42
+ docs = response[:docs]
43
+
44
+ assert_not_nil docs[0], "Not enough docs for #{q} to test."
45
+ first_result_name = docs[0][:title]
46
+ assert_not_nil docs[1], "Not enough docs for #{q} to test."
47
+ second_result_name = docs[1][:title]
48
+
49
+ assert_equal [second_result_name, first_result_name].sort.reverse, [first_result_name, second_result_name]
50
+ end
51
+
52
+ def test_invalid_query_should_return_error_message_in_metadata
53
+ response = Document.search(:q => "http://tommy.chheng.com")
54
+ docs = response[:docs]
55
+ metadata = response[:metadata]
56
+
57
+ assert_not_nil response[:metadata][:error]
58
+ assert_equal 400, response[:metadata][:error][:http_status_code]
59
+ end
60
+
61
+ def test_parse_facet_fields
62
+ facet_counts = {'facet_queries'=>{},
63
+ 'facet_fields' => {'language' => ["Scala", 2, "Ruby", 1, "Java", 0]},
64
+ 'facet_dates'=>{}}
65
+
66
+ facet_counts = Document.parse_facet_counts(facet_counts)
67
+
68
+ assert_equal ["Scala", "Ruby", "Java"], facet_counts['facet_fields']['language'].keys
69
+ end
70
+
71
+ def test_parse_facet_queries
72
+ facet_counts = {"facet_queries"=>{"funding:[0 TO 5000000]"=>1, "funding:[10000000 TO 50000000]"=>0}, "facet_fields"=>{}, "facet_dates"=>{}}
73
+
74
+ facet_counts = Document.parse_facet_counts(facet_counts)
75
+
76
+ expected = {"funding"=>{"[0 TO 5000000]"=>1, "[10000000 TO 50000000]"=>0}}
77
+ assert_equal expected, facet_counts['facet_queries']
78
+ end
79
+
80
+ def test_parse_fq_with_hash
81
+ params = {:fq => {:tags => ["ruby", "scala"]}}
82
+ filters = Document.parse_fq(params[:fq])
83
+
84
+ expected = ["tags:\"ruby\"", "tags:\"scala\""]
85
+ assert_equal expected, filters
86
+ end
87
+
88
+ def test_parse_fq_with_hash_array_args
89
+ params = {:fq => [{:tags => ["ruby", "scala"]}]}
90
+ filters = Document.parse_fq(params[:fq])
91
+
92
+ expected = ["tags:\"ruby\"", "tags:\"scala\""]
93
+ assert_equal expected, filters
94
+ end
95
+
96
+ def test_parse_fq_with_hash_string_args
97
+ params = {:fq => [{:tags => "ruby"}]}
98
+ filters = Document.parse_fq(params[:fq])
99
+
100
+ expected = ["tags:\"ruby\""]
101
+ assert_equal expected, filters
102
+ end
103
+
104
+ def test_parse_fq_with_string_args
105
+ params = {:fq => ["tags:ruby"]}
106
+ filters = Document.parse_fq(params[:fq])
107
+
108
+ expected = ["tags:ruby"]
109
+ assert_equal expected, filters
110
+ end
111
+
112
+ def test_parse_fq_with_empty
113
+ filters = Document.parse_fq([])
114
+ expected = []
115
+ assert_equal expected, filters
116
+ end
117
+
118
+ def test_filter_query
119
+ Document.index(Document.new(:id => 3, :author => "Bert", :title => "solr lucene",:review_count => 10, :tags => ['ruby']))
120
+ Document.index(Document.new(:id => 4, :author => "Ernie", :title => "lucene solr", :review_count => 5, :tags => ['ruby', 'scala']))
121
+
122
+ response = Document.search(:q => "solr", :fq => [{:tags => ["scala"]}])
123
+ docs = response[:docs]
124
+ metadata = response[:metadata]
125
+
126
+ assert_equal 1, metadata[:total_count]
127
+
128
+ doc = docs.first
129
+ assert_not_nil doc['tags']
130
+ assert doc['tags'].include?("scala")
131
+ end
132
+
133
+ def test_text_faceting
134
+ Document.index(Document.new(:id => 3, :author => "Bert", :title => "solr lucene",:review_count => 10))
135
+ Document.index(Document.new(:id => 4, :author => "Ernie", :title => "lucene solr", :review_count => 5))
136
+
137
+ response = Document.search(:q => "solr", :'facet.field' => ['author'])
138
+ docs = response[:docs]
139
+ facet_counts = response[:facet_counts]
140
+ assert_not_nil facet_counts["facet_fields"]["author"]
141
+
142
+ author_facet_entries = facet_counts["facet_fields"]["author"]
143
+ assert author_facet_entries.keys.include?("Bert") && author_facet_entries.keys.include?("Ernie")
144
+ end
145
+
146
+ def test_range_faceting
147
+ Document.index(Document.new(:id => 3, :author => "Bert", :title => "solr lucene",:review_count => 10))
148
+ Document.index(Document.new(:id => 4, :author => "Ernie", :title => "lucene solr", :review_count => 5))
149
+
150
+ response = Document.search(:q => "solr", :'facet.field' => ['author'], :'facet.query' => ["review_count:[1 TO 5]", "review_count:[6 TO 10]"])
151
+ docs = response[:docs]
152
+ facet_counts = response[:facet_counts]
153
+
154
+ assert_not_nil facet_counts["facet_fields"]["author"]
155
+ assert_not_nil facet_counts["facet_queries"]["review_count"]
156
+ assert_equal({"[1 TO 5]"=>1, "[6 TO 10]"=>1}, facet_counts["facet_queries"]["review_count"])
157
+ end
158
+
159
+ def test_highlighting_support
160
+ Document.index(Document.new(:id => 3, :author => "Bert", :title => "solr lucene",:review_count => 10, :tags => ["solr"]))
161
+
162
+ response = Document.search(:q => "solr",
163
+ :'hl.fl' => "*")
164
+ docs = response[:docs]
165
+ highlighting = response[:highlighting]
166
+
167
+ first_result = highlighting.first
168
+ assert first_result[1]['tags'].include?("<em>solr</em>")
169
+ end
170
+ end
171
+
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: solrsan
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.20
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tommy Chheng
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-03-18 00:00:00.000000000 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rsolr
17
+ requirement: &2153183880 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: 1.0.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *2153183880
26
+ - !ruby/object:Gem::Dependency
27
+ name: activemodel
28
+ requirement: &2153183260 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 3.0.5
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *2153183260
37
+ - !ruby/object:Gem::Dependency
38
+ name: activesupport
39
+ requirement: &2153182780 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ version: 3.0.5
45
+ type: :runtime
46
+ prerelease: false
47
+ version_requirements: *2153182780
48
+ description: solrsan is a lightweight wrapper for using Apache Solr within a Ruby
49
+ application including Rails and Sinatra.
50
+ email:
51
+ - tommy.chheng@gmail.com
52
+ executables: []
53
+ extensions: []
54
+ extra_rdoc_files: []
55
+ files:
56
+ - .gitignore
57
+ - Gemfile
58
+ - Gemfile.lock
59
+ - LICENSE
60
+ - README.markdown
61
+ - Rakefile
62
+ - config/solr.yml
63
+ - config/solr.yml.example
64
+ - config/solr/conf/elevate.xml
65
+ - config/solr/conf/mapping-ISOLatin1Accent.txt
66
+ - config/solr/conf/protwords.txt
67
+ - config/solr/conf/schema.xml
68
+ - config/solr/conf/solrconfig.xml
69
+ - config/solr/conf/spellings.txt
70
+ - config/solr/conf/stopwords.txt
71
+ - config/solr/conf/synonyms.txt
72
+ - config/solr/conf/xslt/example.xsl
73
+ - config/solr/conf/xslt/example_atom.xsl
74
+ - config/solr/conf/xslt/example_rss.xsl
75
+ - config/solr/conf/xslt/luke.xsl
76
+ - lib/rails/generators/solrsan/config/config_generator.rb
77
+ - lib/rails/generators/solrsan/config/templates/solr.yml
78
+ - lib/rails/generators/solrsan/config/templates/solrsan.rb
79
+ - lib/rails/generators/solrsan_generator.rb
80
+ - lib/solrsan.rb
81
+ - lib/solrsan/config.rb
82
+ - lib/solrsan/indexer.rb
83
+ - lib/solrsan/search.rb
84
+ - lib/solrsan/version.rb
85
+ - lib/tasks/solr.rake
86
+ - solrsan.gemspec
87
+ - test/models/document.rb
88
+ - test/search_test_helper.rb
89
+ - test/test_helper.rb
90
+ - test/unit/indexer_test.rb
91
+ - test/unit/search_test.rb
92
+ has_rdoc: true
93
+ homepage: http://github.com/tc/solrsan
94
+ licenses: []
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ! '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project: solrsan
113
+ rubygems_version: 1.6.2
114
+ signing_key:
115
+ specification_version: 3
116
+ summary: Lightweight wrapper for using Apache Solr within a Ruby application including
117
+ Rails and Sinatra.
118
+ test_files:
119
+ - test/models/document.rb
120
+ - test/search_test_helper.rb
121
+ - test/test_helper.rb
122
+ - test/unit/indexer_test.rb
123
+ - test/unit/search_test.rb