search_generator 0.4

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/LICENSE ADDED
@@ -0,0 +1,30 @@
1
+ Copyright (c) 2005 Jeremy Hinegardner <jeremy@hinegardner.org>
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are
6
+ met:
7
+
8
+ * Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+
11
+ * Redistributions in binary form must reproduce the above copyright
12
+ notice, this list of conditions and the following disclaimer in
13
+ the documentation and/or other materials provided with the
14
+ distribution.
15
+
16
+ * The name of the author may not be used to endorse or promote
17
+ products derived from this software without specific prior written
18
+ permission from the author.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
21
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22
+ TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
23
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
24
+ OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/USAGE ADDED
@@ -0,0 +1,50 @@
1
+ NAME
2
+
3
+ search - a search engine that can be added to any Rails application
4
+
5
+ SYNOPSIS
6
+
7
+ search [Search name] [options]
8
+
9
+ Good names, anything that means search would be good. Search,
10
+ Forage, Frisk or Shakedown.
11
+
12
+ search has an additional option --inspect which will travers your
13
+ Model objects and tell you which ones could be made searchable
14
+
15
+ DESCRIPTION
16
+
17
+ This generator integrates the SimpleSearch search engine into a
18
+ rails applicaiton. In addition, once configured for you site, it
19
+ provides an Open Search (http://opensearch.a9.com/) compatible
20
+ interface.
21
+
22
+ Included:
23
+
24
+ * a Controller to manage search request
25
+ * Views to customize for your Model objects
26
+ - regular html results
27
+ - Open Search RSS style results
28
+ * an indexer to run periodically against your data to keep the
29
+ search indexes up to date.
30
+ * a README on the additional hand coded tasks to intgerate
31
+ the search engine with your application
32
+
33
+ EXAMPLE
34
+
35
+ ./script/generate search Search
36
+
37
+ This will create a basic search engine framework for you to
38
+ customize for your site.
39
+
40
+ ./script/generate search Search --inspect
41
+
42
+ This will report which ones of your Model classes could be made
43
+ searchable.
44
+
45
+ DEPENDENCIES
46
+
47
+ - SimpleSearch (http://www.chadfowler.com/SimpleSearch)
48
+ Install : gem install SimpleSearch
49
+
50
+
@@ -0,0 +1,97 @@
1
+ class SearchGenerator < Rails::Generator::NamedBase
2
+
3
+ def add_options!(opt)
4
+ opt.on('-i', '--inspect','Inspect models to see which ones can be searchable') { |value| options[:inspect] = true }
5
+ end
6
+
7
+ def inspect
8
+ puts <<howto_make_searchable
9
+ The following Models, and their associated fields,
10
+ in your application can be made searchable. Just copy
11
+ and paste the 'make_searchable' line from the report below
12
+ into the approrpaite models file (also reported below).
13
+
14
+ The 'make_searchable' line reports all the searchable fields
15
+ in that model. You probably do not want all of the fields
16
+ in your Model, so edit the line as appropriate
17
+
18
+ IMPORTANT:
19
+
20
+ Make sure to the following line to the top of any model file
21
+ you edit to make searchable.
22
+
23
+ require_dependency "search_system"
24
+
25
+ howto_make_searchable
26
+
27
+ Dir.glob(File.join(@destination_root,"app/models/*.rb")).each do |rbfile|
28
+ bname = File.basename(rbfile,'.rb')
29
+ classname = Inflector.camelize(bname)
30
+ classvar = eval(classname)
31
+ if classvar.respond_to?(:descends_from_active_record?) and
32
+ classvar.descends_from_active_record? then
33
+ attrs = classvar.columns_hash.keys.collect { |a| ":" + a.to_s}
34
+
35
+ puts "-----[ #{classname} => #{File.expand_path(rbfile)} ]-----"
36
+ if classvar.respond_to?(:searchable_fields) then
37
+ fields = classvar.searchable_fields
38
+ if fields.nil? or fields.size == 0 then
39
+ puts "#{classname} is searchable, but no fields are selected."
40
+ puts
41
+ puts " make_searchable [ #{attrs.join(", ")} ]"
42
+ else
43
+ fields = fields.collect { |f| ":" + f.to_s }
44
+ puts "#{classname} has the following fields marked as searchable:"
45
+ puts
46
+ puts " [ #{fields.join(", ")} ]"
47
+ end
48
+ else
49
+ puts " make_searchable [ #{attrs.join(", ")} ]"
50
+ end
51
+ puts
52
+ end
53
+ end
54
+ puts
55
+ end
56
+
57
+ def remove
58
+ manifest.rewind(Rails::Generator::Command::Destroy)
59
+ end
60
+
61
+ def manifest
62
+ record do |m|
63
+
64
+ if options[:inspect] then
65
+ m.inspect
66
+ else
67
+ # system
68
+ m.template "lib/search_system.rb", "lib/search_system.rb"
69
+
70
+ # controller
71
+ m.template "app/controllers/controller.rb", "app/controllers/#{file_name}_controller.rb"
72
+
73
+ # layout
74
+ m.template "app/views/layouts/layout.rhtml", "app/views/layouts/#{file_name}.rhtml"
75
+
76
+ # views
77
+ m.directory File.join("app/views", class_path, file_name)
78
+ m.template "app/views/search/index.rhtml", "app/views/#{file_name}/index.rhtml"
79
+ m.template "app/views/search/_generic.rhtml", "app/views/#{file_name}/_generic.rhtml"
80
+
81
+ m.directory "app/views/opensearch"
82
+ m.template "app/views/opensearch/rss.rhtml", "app/views/opensearch/rss.rhtml"
83
+ m.template "app/views/opensearch/description.rxml", "app/views/opensearch/description.rxml"
84
+ m.template "app/views/opensearch/_generic.rhtml", "app/views/opensearch/_generic.rhtml"
85
+
86
+ # scripts
87
+ m.template "script/indexer", "script/indexer", :chmod => 0755
88
+ m.template "script/searcher", "script/searcher", :chmod => 0755
89
+
90
+ # custom to this generation install README
91
+ m.template "README", "README_SEARCH"
92
+ end
93
+
94
+ end
95
+ end
96
+
97
+ end
data/templates/README ADDED
@@ -0,0 +1,81 @@
1
+ == Installation
2
+
3
+ After the search engine has been generated, there are still tasks to
4
+ fully integrate the search engine into your site.
5
+
6
+ === Inspect
7
+
8
+ execute:
9
+
10
+ script/generate search <%= class_name %> --inspect
11
+
12
+ This will report which of your models can be made searchable. Feel
13
+ free to run it many times.
14
+
15
+ === Models
16
+
17
+ In each of your Models in which you want to have the content
18
+ searched, and are ActiveRecord descendants, you need to add a
19
+ require_dependency "search_system". Then you are able to invoke the
20
+ class method 'make_searchable' with an Array of field symbols.
21
+
22
+ For example:
23
+
24
+ require_dependency "search_system"
25
+ class Article < ActiveRecord::Base
26
+ make_searchable [ :title, :author, :content ]
27
+ end
28
+
29
+ === Routes
30
+
31
+ Add the following lines to your config/routes.rb to hook in search
32
+
33
+ # allow for searching view the route
34
+ map.connect '<%= file_name %>/:search_terms/:count', :controller => '<%= file_name %>', :action => 'index', :count => '-1'
35
+
36
+ # allow for Open Search RSS feeds searching
37
+ map.connect 'rss/opensearch/description.xml', :controller => '<%= file_name %>', :action => 'description'
38
+ map.connect 'rss/opensearch/:search_terms/:count', :controller => '<%= file_name %>', :action => 'rss', :count => '-1'
39
+
40
+ === Layouts
41
+
42
+ A 'very' basic layout/<%= file_name %>.rhtml is installed that shows the basic
43
+ results. This should be customized for your site.
44
+
45
+ === Views
46
+
47
+ Two new views have been added to your rails application.
48
+
49
+ 1. Search
50
+
51
+ This view should be customized for your site so that the search
52
+ results fit in well with your site. The default _partial
53
+ template that exists will render out a generic table showing all
54
+ the fields and their values for the search results. You should
55
+ customize the index.rhtml file to integrate with your site, and
56
+ you should create a _partial.rhtml for each Model that you could
57
+ have search results for.
58
+
59
+ That is, if you have an application with an Article Model, you
60
+ should create a _article.rhtml file in this view to render out
61
+ the results of your search that are of Article class.
62
+
63
+ The generic index.rhtml that is present will automatically use
64
+ the appropriate _parital.rhtml file based upon the class name if
65
+ it is present.
66
+
67
+ 2. Open Search
68
+
69
+ This view is to return results in the format of OpenSearch RSS
70
+ (http://opensearch.a9.com/spec/opensearchrss/1.0/). This
71
+ combined with the rss/opensearch Routes above and the
72
+ description.rxml file found in this view should provide you with
73
+ an OpenSearch-able site.
74
+
75
+ Again, you should create a _partial.rhtml file in this directory
76
+ for each Model you have that can return search results.
77
+
78
+ Also, the description.xml file should be configure with your
79
+ sites information.
80
+
81
+
@@ -0,0 +1,48 @@
1
+ require_dependency "search_system"
2
+
3
+ class <%= class_name %>Controller < ApplicationController
4
+ layout "<%= file_name %>" , :except => [:rss, :description]
5
+
6
+ def index
7
+ search
8
+ end
9
+
10
+ #-----------------------------------------------------------------------
11
+ # search through the index, get the ActiveRecord's that match and
12
+ # return those to the caller
13
+ #-----------------------------------------------------------------------
14
+ def search
15
+ @search_terms = @params['search_terms'].split
16
+ @count = @params['count'].to_i || -1
17
+
18
+ empty_contents = MockContents.new
19
+ simple_index = Search::Simple::Searcher.load(empty_contents,"#{File.dirname(__FILE__)}/../../db/search_cache")
20
+
21
+ # what to return to the caller
22
+ @results = Array.new
23
+
24
+ # TODO: sort by score at some point ?
25
+ search_results = simple_index.find_words(@search_terms)
26
+
27
+ if search_results.contains_matches then
28
+ search_results.results.sort.each do |result|
29
+ (classname,pk_id,column) = result.name.split(".")
30
+ classvar = eval(classname)
31
+ @results << classvar.find(pk_id)
32
+ break if @count > 0 and @results.size >= @count
33
+ end
34
+ end
35
+
36
+ @results.uniq!
37
+ end
38
+
39
+ def description
40
+ @headers["Content-Type"] = "text/xml"
41
+ render 'opensearch/description'
42
+ end
43
+ def rss
44
+ @headers["Content-Type"] = "text/xml"
45
+ search
46
+ render 'opensearch/rss'
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
3
+ <head>
4
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
5
+ <title>SITE TITLE</title>
6
+ </head>
7
+ <body>
8
+
9
+ <h1>Search SITE</h1>
10
+
11
+ <form action="/search" method="get">
12
+ <%% if @search_terms.size > 0 %>
13
+ <input type="text" name="search_terms" value="<%%= @search_terms.join(" ") %>" size="30" />
14
+ <%% else %>
15
+ <input type="text" name="search_terms" value="" size="30" />
16
+ <%% end %>
17
+ </form>
18
+
19
+ <hr />
20
+
21
+ <h2>Results</h2>
22
+ <%%= @content_for_layout %>
23
+
24
+ </body>
25
+ </html>
@@ -0,0 +1,11 @@
1
+ <title><%%= generic.class.name %></title>
2
+ <link>INSERT CODE TO GET LINK TO THIS ITEM</link>
3
+ <description>
4
+ <div class="record">
5
+ <table>
6
+ <%% generic.class.searchable_fields.each do |field| %>
7
+ <tr> <th><%%= field.to_s %></th> <td><%%= generic.send(field) %></td> </tr>
8
+ <%% end %>
9
+ </table>
10
+ </div>
11
+ </description>
@@ -0,0 +1,9 @@
1
+ xml.OpenSearchDescription(:xmlns => "http://a9.com/-/spec/opensearchrss/1.0/") do
2
+ xml.url("http://localhost:3000/opensearch/{searchTerms}/{count}")
3
+ xml.format("http://a9.com/-/spec/opensearchrss/1.0/")
4
+ xml.shortname("site shortname")
5
+ xml.longname("site longname")
6
+ xml.description("site description")
7
+ xml.syndicationright("open")
8
+ xml.adultcontent("false")
9
+ end
@@ -0,0 +1,23 @@
1
+ <?xml version="1.0"?>
2
+ <rss version="1.0" xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/">
3
+ <channel>
4
+ <title>Site title</title>
5
+ <link><%%= @request.env['REQUEST_URI'] %></link>
6
+ <description>Site description</description>
7
+ <language>en-us</language>
8
+ <copyright>&amp;copy;2003-2005, site its affiliates.</copyright>
9
+ <openSearch:totalResults><%%= @results.size %></openSearch:totalResults>
10
+ <openSearch:startIndex>1</openSearch:startIndex>
11
+ <openSearch:itemsPerPage><%%= @results.size %></openSearch:itemsPerPage>
12
+
13
+
14
+ <%% for result in @results %>
15
+ <%% begin %>
16
+ <%%= render_partial "opensearch/#{Inflector.underscore(result.class)}", result %>
17
+ <%% rescue ActionView::ActionViewError %>
18
+ <%%= render_partial "opensearch/generic", result %>
19
+ <%% end %>
20
+ <%% end %>
21
+ </channel>
22
+ </rss>
23
+
@@ -0,0 +1,7 @@
1
+ <div class="record">
2
+ <table>
3
+ <%% generic.class.searchable_fields.each do |field| %>
4
+ <tr> <th><%%= field.to_s %></th> <td><%%= generic.send(field) %></td> </tr>
5
+ <%% end %>
6
+ </table>
7
+ </div>
@@ -0,0 +1,14 @@
1
+ <%% @results.each do |result| %>
2
+ <hr />
3
+
4
+ <%%# Try to render a partial based upon the class name %>
5
+ <%%# but if that does not work, reneder a generic one %>
6
+
7
+ <%% begin %>
8
+ <%%= render_partial Inflector.underscore(result.class), result %>
9
+ <%% rescue ActionView::ActionViewError %>
10
+ <pre>No parital available for <%%= result.class %>, using generic </pre>
11
+ <%%= render_partial "generic", result %>
12
+ <%% end %>
13
+
14
+ <%% end %>
@@ -0,0 +1,20 @@
1
+ require_gem "SimpleSearch"
2
+
3
+ class ActiveRecord::Base
4
+
5
+ def self.make_searchable(list_of_field_symbols)
6
+ @searchable_fields = list_of_field_symbols
7
+ end
8
+
9
+ def self.searchable_fields
10
+ @searchable_fields
11
+ end
12
+
13
+ end
14
+
15
+ class MockContents
16
+ def latest_mtime
17
+ Time.at(0)
18
+ end
19
+ end
20
+
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "#{File.dirname(__FILE__)}/../config/environment"
4
+
5
+ require_gem "SimpleSearch"
6
+
7
+ #-----------------------------------------------------------------------
8
+ # the container for all the contents to be indexed
9
+ #-----------------------------------------------------------------------
10
+ contents = Search::Simple::Contents.new
11
+
12
+ #-----------------------------------------------------------------------
13
+ # the current time is used to invalidate the Searcher's cache so it will
14
+ # force an indexing
15
+ #-----------------------------------------------------------------------
16
+ ts = Time.now
17
+
18
+ #-----------------------------------------------------------------------
19
+ # iterate over all the files in the models directory looking for those
20
+ # Models that are descendants of ActiveRecord
21
+ #-----------------------------------------------------------------------
22
+ puts "Searching for Models..."
23
+ Dir.glob("#{File.dirname(__FILE__)}/../app/models/*.rb").each do |rbfile|
24
+
25
+ bname = File.basename(rbfile,'.rb')
26
+ classname = Inflector.camelize(bname)
27
+ classvar = eval(classname)
28
+
29
+ if classvar.respond_to?(:descends_from_active_record?) and
30
+ classvar.descends_from_active_record? then
31
+ puts "\tFound #{classname} "
32
+
33
+
34
+ #---------------------------------------------------------------
35
+ # if the class that is a dscendant from ActiveRecord also has
36
+ # the special method :searchable_fields then those fields are
37
+ # considered searchable
38
+ #---------------------------------------------------------------
39
+ if classvar.respond_to?(:searchable_fields) then
40
+
41
+ if not classvar.searchable_fields.nil?
42
+
43
+ cnames = classvar.searchable_fields.collect {|c| c.to_s}
44
+ puts "\t\t- searching columns #{cnames.join(",")}"
45
+ indexed_records = 0
46
+
47
+ #-----------------------------------------------------------
48
+ # now we interate over all the records of the particular
49
+ # type and record all the information for indexing.
50
+ #-----------------------------------------------------------
51
+ classvar.find_all.each do |ar|
52
+ record_id = "#{classname}.#{ar.send(classvar.primary_key)}"
53
+ indexed_records = indexed_records + 1
54
+
55
+ classvar.searchable_fields.each do |column|
56
+ column_id = "#{record_id}.#{column.to_s}"
57
+ data = ar.send(column)
58
+
59
+ # only index non-nil content
60
+ if not data.nil? then
61
+ contents << Search::Simple::Content.new(ar.send(column), column_id, ts)
62
+ end
63
+ end
64
+ end
65
+
66
+ puts "\t\t- indexed #{indexed_records} records"
67
+ else
68
+ puts "\t\t- no make_searchable fields"
69
+ end
70
+
71
+ else
72
+ puts "\t\t- class is not searchable"
73
+ end
74
+ end
75
+ end
76
+
77
+ #-----------------------------------------------------------------------
78
+ # this is where the actual indexing is done. There previous sections of
79
+ # code just collected the data to do the indexing.
80
+ #-----------------------------------------------------------------------
81
+ s = Search::Simple::Searcher.load(contents,"#{File.dirname(__FILE__)}/../db/search_cache")
82
+
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "#{File.dirname(__FILE__)}/../config/environment"
4
+
5
+ require_dependency "search_system"
6
+
7
+ #-----------------------------------------------------------------------
8
+ # grab the first word on the command line as the term to search for
9
+ #-----------------------------------------------------------------------
10
+ search_terms = Array.new
11
+ if ARGV.size > 0 then
12
+ search_terms << ARGV[0]
13
+ else
14
+ puts "no search term given, searching for 'ruby'"
15
+ search_terms << "ruby"
16
+ end
17
+
18
+ #-----------------------------------------------------------------------
19
+ # a mock contents so that we use the cache
20
+ #-----------------------------------------------------------------------
21
+ contents = MockContents.new
22
+ s = Search::Simple::Searcher.load(contents,"#{File.dirname(__FILE__)}/../db/search_cache")
23
+
24
+ #-----------------------------------------------------------------------
25
+ # do the search
26
+ #-----------------------------------------------------------------------
27
+ sr = s.find_words(search_terms)
28
+
29
+ if sr.contains_matches then
30
+ puts "Score\t#File"
31
+ sr.results.sort.each do |res|
32
+ puts "#{res.score}\t#{res.name}"
33
+ end
34
+ else
35
+ puts sr.warnings
36
+ puts "No matches"
37
+ end
38
+
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.10
3
+ specification_version: 1
4
+ name: search_generator
5
+ version: !ruby/object:Gem::Version
6
+ version: "0.4"
7
+ date: 2005-04-11
8
+ summary: "[Rails] Search generator."
9
+ require_paths:
10
+ - "."
11
+ email: jeremy@hinegardner.org
12
+ homepage:
13
+ rubyforge_project:
14
+ description: Generates Rails code implementing a search system using SimpleSearch for your Rails app.
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: false
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ -
22
+ - ">"
23
+ - !ruby/object:Gem::Version
24
+ version: 0.0.0
25
+ version:
26
+ platform: ruby
27
+ authors:
28
+ - Jeremy Hinegardner
29
+ files:
30
+ - USAGE
31
+ - LICENSE
32
+ - search_generator.rb
33
+ - templates/README
34
+ - templates/lib
35
+ - templates/app
36
+ - templates/script
37
+ - templates/lib/search_system.rb
38
+ - templates/app/controllers
39
+ - templates/app/views
40
+ - templates/app/controllers/controller.rb
41
+ - templates/app/views/search
42
+ - templates/app/views/opensearch
43
+ - templates/app/views/layouts
44
+ - templates/app/views/search/index.rhtml
45
+ - templates/app/views/search/_generic.rhtml
46
+ - templates/app/views/opensearch/_generic.rhtml
47
+ - templates/app/views/opensearch/rss.rhtml
48
+ - templates/app/views/opensearch/description.rxml
49
+ - templates/app/views/layouts/layout.rhtml
50
+ - templates/script/indexer
51
+ - templates/script/searcher
52
+ test_files: []
53
+ rdoc_options: []
54
+ extra_rdoc_files: []
55
+ executables: []
56
+ extensions: []
57
+ requirements: []
58
+ dependencies:
59
+ - !ruby/object:Gem::Dependency
60
+ name: rails
61
+ version_requirement:
62
+ version_requirements: !ruby/object:Gem::Version::Requirement
63
+ requirements:
64
+ -
65
+ - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 0.10.0
68
+ version:
69
+ - !ruby/object:Gem::Dependency
70
+ name: SimpleSearch
71
+ version_requirement:
72
+ version_requirements: !ruby/object:Gem::Version::Requirement
73
+ requirements:
74
+ -
75
+ - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: 0.5.0
78
+ version: