Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

add basic search functionality

  • Loading branch information...
commit f96f03b58f22aea724f9d8d37579edb6f1c34de7 1 parent 42e7e50
@mislav authored
View
4 Gemfile
@@ -12,9 +12,11 @@ end
gem 'sass', '> 3.2.0.alpha'
gem 'coffee-script'
-# gem 'therubyracer', :group => :development
gem 'therubyracer-heroku', '~> 0.8.1.pre3', :group => :production
gem 'uglifier'
gem 'nokogiri'
gem 'erubis'
+
+gem 'dm-postgres-adapter'
+gem 'dm-migrations'
View
17 Gemfile.lock
@@ -4,11 +4,26 @@ GEM
activesupport (3.2.3)
i18n (~> 0.6)
multi_json (~> 1.0)
+ addressable (2.2.8)
coffee-script (2.2.0)
coffee-script-source
execjs
coffee-script-source (1.3.1)
daemons (1.1.8)
+ data_objects (0.10.8)
+ addressable (~> 2.1)
+ dm-core (1.2.0)
+ addressable (~> 2.2.6)
+ dm-do-adapter (1.2.0)
+ data_objects (~> 0.10.6)
+ dm-core (~> 1.2.0)
+ dm-migrations (1.2.0)
+ dm-core (~> 1.2.0)
+ dm-postgres-adapter (1.2.0)
+ dm-do-adapter (~> 1.2.0)
+ do_postgres (~> 0.10.6)
+ do_postgres (0.10.8)
+ data_objects (= 0.10.8)
erubis (2.7.0)
eventmachine (0.12.10)
execjs (1.3.0)
@@ -44,6 +59,8 @@ PLATFORMS
DEPENDENCIES
activesupport
coffee-script
+ dm-migrations
+ dm-postgres-adapter
erubis
i18n
nokogiri
View
39 Rakefile
@@ -0,0 +1,39 @@
+task :environment do
+ require 'bundler'
+ Bundler.setup
+ require_relative 'app'
+end
+
+namespace :db do
+ task :rebuild => :environment do
+ DataMapper.auto_migrate!
+ end
+
+ task :migrate => :environment do
+ DataMapper.auto_upgrade!
+ end
+end
+
+task :import_index => ['tmp/rfc-index.xml', :environment] do |task|
+ require 'nokogiri'
+ require 'active_support/core_ext/object/try'
+
+ DataMapper.logger.set_log($stderr, :warn)
+
+ index = Nokogiri File.open(task.prerequisites.first)
+
+ index.search('rfc-entry').each do |xml_entry|
+ entry = RfcEntry.new
+ entry.document_id = xml_entry.at('./doc-id').text
+ entry.title = xml_entry.at('./title').text
+ entry.abstract = xml_entry.at('./abstract').try(:inner_html)
+ entry.keywords = xml_entry.search('./keywords/*').map(&:text)
+ entry.save!
+ end
+end
+
+file 'tmp/rfc-index.xml' do |task|
+ mkdir_p 'tmp'
+ index_url = 'ftp://ftp.rfc-editor.org/in-notes/rfc-index.xml'
+ sh 'curl', '-#', index_url, '-o', task.name
+end
View
38 app.rb
@@ -16,6 +16,38 @@
configure :development do
set :logging, false
+ ENV['DATABASE_URL'] ||= 'postgres://localhost/rfc'
+end
+
+require 'dm-migrations'
+require_relative 'searchable'
+
+configure :development do
+ DataMapper::Logger.new($stderr, :debug)
+end
+
+configure do
+ DataMapper.setup(:default, ENV['DATABASE_URL'])
+end
+
+class RfcEntry
+ include DataMapper::Resource
+ extend Searchable
+
+ property :document_id, String, length: 10, key: true
+ property :title, String, length: 255
+ property :abstract, Text, length: 2200
+ property :keywords, Text, length: 500
+
+ def keywords=(value)
+ if Array === value
+ super(value.empty?? nil : value.join(', '))
+ else
+ super
+ end
+ end
+
+ searchable [:title, :abstract, :keywords]
end
get "/" do
@@ -24,6 +56,12 @@
erb :index, {}, title: "Pretty RFCs"
end
+get "/search" do
+ @query = params[:q]
+ @results = RfcEntry.search @query, limit: 50
+ erb :search, {}, title: "RFC search"
+end
+
get "/oauth" do
expires 3600, :public
doc = RFC::Document.new File.open('draft-ietf-oauth-v2-25.xml')
View
82 searchable.rb
@@ -0,0 +1,82 @@
+require 'active_support/core_ext/hash/except'
+
+## Author: Xavier Shay
+#
+# Mix this module into a DataMapper::Resource to get fast, indexed full
+# text searching.
+#
+# class Post
+# include DataMapper::Resource
+# include Searchable
+#
+# property :title, String
+# property :body, Text
+#
+# searchable [:title, :body]
+# searchable [:title], :index => :title_only
+# end
+#
+# Post.search("hello")
+# Post.search("hello", :index => :title_only)
+module Searchable
+ def searchable(columns, opts = {})
+ opts[:index] ||= 'search'
+ __searches[opts[:index]] = columns
+ end
+
+ def search(q, opts = {})
+ opts[:index] ||= 'search'
+ finder = all(opts.except(:index, :conditions).merge(:conditions => [
+ "#{opts[:index]}_vector @@ plainto_tsquery('english', ?)", q]))
+ finder &= all(opts[:conditions]) if opts[:conditions]
+ finder
+ end
+
+ def auto_migrate_up!(repository_name)
+ super
+
+ __searches.each do |name, columns|
+ [
+ create_alter_table_sql(repository_name, name),
+ create_index_sql(repository_name, name),
+ create_trigger_sql(repository_name, name, columns)
+ ].each do |sql|
+ repository(repository_name).adapter.execute sql
+ end
+ end
+ end
+
+ private
+
+ def create_alter_table_sql(repository_name, name)
+ <<-EOS
+ ALTER TABLE #{storage_name(repository_name)}
+ ADD COLUMN #{name}_vector tsvector NOT NULL
+ EOS
+ end
+
+ def create_index_sql(repository_name, name)
+ <<-EOS
+ CREATE INDEX #{storage_name(repository_name)}_#{name}_vector_idx
+ ON #{storage_name(repository_name)} USING gin(#{name}_vector)
+ EOS
+ end
+
+ def create_trigger_sql(repository_name, name, columns)
+ <<-EOS
+ CREATE TRIGGER #{storage_name(repository_name)}_#{name}_vector_refresh
+ BEFORE INSERT OR UPDATE ON #{storage_name(repository_name)}
+ FOR EACH ROW EXECUTE PROCEDURE
+ tsvector_update_trigger(#{name}_vector, 'pg_catalog.english',
+ #{column_sql(columns)});
+ EOS
+ end
+
+ def __searches
+ @__searches ||= {}
+ end
+
+ def column_sql(columns)
+ columns.map {|column| send(column).field }.join(", ")
+ end
+end
View
5 views/index.erb
@@ -2,6 +2,11 @@
<h1>The RFC is now <em>diamonds</em></h1>
+ <form action=/search>
+ <input type=search name=q placeholder="Search RFC titles">
+ <input type=submit value="Search">
+ </form>
+
<div class=well>
<p>
Sample RFC: <a href=/oauth>OAuth 2.0</a>
View
19 views/search.erb
@@ -0,0 +1,19 @@
+<div class=container>
+
+ <h1>Search for <%= @query.inspect %></h1>
+
+ <% if @results.any? %>
+ <ol>
+ <% for rfc in @results %>
+ <li>
+ <h2><%= rfc.document_id %>: <%= rfc.title %></h2>
+ <%= rfc.abstract %>
+ </li>
+ <% end %>
+ </ol>
+
+ <% else %>
+ <p>No results.</p>
+ <% end %>
+
+</div>
Please sign in to comment.
Something went wrong with that request. Please try again.