search_generator 0.4

Sign up to get free protection for your applications and to get access to all the features.
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: