name_search 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'rails', '3.0.7'
4
+ gem 'capybara', '>= 0.4.0'
5
+ gem 'sqlite3'
6
+
7
+ group :development, :test do
8
+ gem 'factory_girl'
9
+ gem 'ruby-debug19'
10
+ end
11
+
12
+ group :test do
13
+ gem 'rspec-rails', '>= 2.0.0.beta'
14
+ gem 'ruby-debug19'
15
+ end
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2011 Paul Yoder
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ = Name Search
2
+
3
+ Search for names while taking into consideration nick names and word ordering.
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ # encoding: UTF-8
2
+ require 'rubygems'
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+
9
+ require 'rake'
10
+ require 'rdoc/task'
11
+ require 'rake/testtask'
12
+
13
+ require 'rspec/core'
14
+ require 'rspec/core/rake_task'
15
+ RSpec::Core::RakeTask.new(:spec)
16
+
17
+ task :default => :spec
18
+
19
+ RDoc::Task.new do |rdoc|
20
+ rdoc.rdoc_dir = 'rdoc'
21
+ rdoc.title = 'NameSearch'
22
+ rdoc.options << '--line-numbers' << '--inline-source'
23
+ rdoc.rdoc_files.include('README.rdoc')
24
+ rdoc.rdoc_files.include('lib/**/*.rb')
25
+ end
@@ -0,0 +1,22 @@
1
+ require 'rails/generators/migration'
2
+ require 'rails/generators/active_record'
3
+
4
+ module NameSearch
5
+ class MigrationGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+
8
+ desc 'Generates migration for name_search models'
9
+
10
+ def self.source_root
11
+ @source_root ||= File.join(File.dirname(__FILE__), 'templates')
12
+ end
13
+
14
+ def self.next_migration_number(dirname)
15
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
16
+ end
17
+
18
+ def create_migration_file
19
+ migration_template 'migration.rb', 'db/migrate/create_name_search_tables.rb'
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,35 @@
1
+ class CreateNameSearchTables < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :name_search_names do |t|
4
+ t.string :value
5
+ end
6
+ add_index :name_search_names, :value
7
+
8
+ create_table :name_search_nick_name_family_joins do |t|
9
+ t.integer :name_id
10
+ t.integer :nick_name_family_id
11
+ end
12
+ add_index :name_search_nick_name_family_joins, :name_id
13
+ add_index :name_search_nick_name_family_joins, :nick_name_family_id
14
+
15
+ create_table :name_search_nick_name_families do |t|
16
+ end
17
+
18
+ create_table :name_search_searchables do |t|
19
+ t.integer :name_id
20
+ t.integer :searchable_id
21
+ t.string :searchable_type
22
+ end
23
+ add_index :name_search_searchables, :name_id
24
+ add_index :name_search_searchables,
25
+ [:searchable_id, :searchable_type],
26
+ :name => 'index_name_search_searchable'
27
+ end
28
+
29
+ def self.down
30
+ drop_table :name_search_names
31
+ drop_table :name_search_nick_name_family_joins
32
+ drop_table :name_search_nick_name_families
33
+ drop_table :name_search_searchables
34
+ end
35
+ end
@@ -0,0 +1,9 @@
1
+ module NameSearch
2
+ module ActiveRelationSearch
3
+ def name_search(name, options = {})
4
+ NameSearch::Search.new(self, name, options)
5
+ end
6
+ end
7
+ end
8
+
9
+ ActiveRecord::Relation.send :include, NameSearch::ActiveRelationSearch
@@ -0,0 +1,47 @@
1
+ module NameSearch
2
+ class Name < ActiveRecord::Base
3
+ set_table_name :name_search_names
4
+ before_create :downcase_value
5
+
6
+ has_many :nick_name_family_joins
7
+ has_many :nick_name_families, :through => :nick_name_family_joins
8
+
9
+ def nick_names()
10
+ nick_name_families.map(&:names).flatten
11
+ end
12
+
13
+ def nick_name_values()
14
+ nick_names.map(&:value)
15
+ end
16
+
17
+ validates :value, :uniqueness => true
18
+
19
+ cattr_accessor :excluded_values
20
+ @@excluded_values = %w( and or )
21
+
22
+ def self.find(*args)
23
+ return Name.where(:value => args.first).first if args.first.kind_of?(String)
24
+ super
25
+ end
26
+
27
+ def self.scrub_and_split_name(name)
28
+ scrubbed = name.downcase.gsub(/[^a-z0-9 -]/, '')
29
+ split = scrubbed.split(/[ -]/).uniq
30
+ split - @@excluded_values
31
+ end
32
+
33
+ private
34
+
35
+ def self.get_family_for_nick_names(names)
36
+ family = Name.where(:value => names.map(&:downcase)).
37
+ where('nick_name_family_id IS NOT NULL').
38
+ first.
39
+ try(:nick_name_family)
40
+ family ||= NickNameFamily.create
41
+ end
42
+
43
+ def downcase_value
44
+ value.downcase!
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,17 @@
1
+ module NameSearch
2
+ class NickNameFamily < ActiveRecord::Base
3
+ set_table_name :name_search_nick_name_families
4
+
5
+ has_many :nick_name_family_joins
6
+ has_many :names, :through => :nick_name_family_joins
7
+
8
+ def self.create_family(*nick_names)
9
+ family = NickNameFamily.create
10
+ scrubbed_names = nick_names.map{|x| Name.scrub_and_split_name(x)}.flatten
11
+ scrubbed_names.each do |name|
12
+ family.names << Name.find_or_create_by_value(name)
13
+ end
14
+ family
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ module NameSearch
2
+ class NickNameFamilyJoin < ActiveRecord::Base
3
+ set_table_name :name_search_nick_name_family_joins
4
+
5
+ belongs_to :name
6
+ belongs_to :nick_name_family
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ module NameSearch
2
+ class Searchable < ActiveRecord::Base
3
+ set_table_name :name_search_searchables
4
+
5
+ belongs_to :name
6
+ belongs_to :searchable, :polymorphic => true
7
+
8
+ validates :name, :presence => true
9
+ validates :searchable, :presence => true
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ module NameSearch
2
+ module NameSearchOn
3
+ def name_search_on(*attributes)
4
+ write_inheritable_attribute(:name_search_attributes, attributes)
5
+ class_inheritable_reader(:name_search_attributes)
6
+
7
+ def name_search(name, options = {})
8
+ NameSearch::Search.new(self, name, options)
9
+ end
10
+
11
+ class_eval do
12
+ include NameSearchablesConcerns
13
+
14
+ after_save :sync_name_searchables
15
+ has_many :name_searchables, :as => :searchable, :dependent => :destroy,
16
+ :include => :name, :class_name => 'NameSearch::Searchable'
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ ActiveRecord::Base.send :extend, NameSearch::NameSearchOn
@@ -0,0 +1,49 @@
1
+ module NameSearch
2
+ module NameSearchablesConcerns
3
+ def sync_name_searchables
4
+ return unless name_search_attributes_changed?
5
+ update_name_searchables
6
+ end
7
+
8
+ def update_name_searchables
9
+ names = name_search_attributes_names
10
+ create_new_name_searchables(names)
11
+ destroy_orphaned_name_searchables(names)
12
+ end
13
+
14
+ def name_searchable_values(force_reload = false)
15
+ name_searchables(force_reload).map{|x| x.name.value}
16
+ end
17
+
18
+ def name_search_attributes_names()
19
+ names = []
20
+ attributes = self.class.name_search_attributes
21
+ attributes.each do |att|
22
+ value = self.send(att)
23
+ names.concat(NameSearch::Name.scrub_and_split_name(value))
24
+ end
25
+ names
26
+ end
27
+
28
+ def create_new_name_searchables(names)
29
+ names_to_add = names - name_searchable_values
30
+ names_to_add.each do |name|
31
+ name_searchables.create :name => NameSearch::Name.find_or_create_by_value(name)
32
+ end
33
+ end
34
+
35
+ #Destroys name_searchable values that used to exist but don't exist any longer
36
+ #Example: 'Jen York' changes to 'Jen Yoder', then 'york' would be destroyed
37
+ def destroy_orphaned_name_searchables(names)
38
+ orphaned_names = name_searchable_values - names
39
+ orphaned_names.each do |orphan_name|
40
+ searchable = name_searchables.select{|x| x.name.value == orphan_name}.first
41
+ searchable.destroy if searchable.present?
42
+ end
43
+ end
44
+
45
+ def name_search_attributes_changed?()
46
+ (changed & self.class.name_search_attributes.map(&:to_s)).length > 0
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,10 @@
1
+ require 'name_search'
2
+ require 'rails'
3
+
4
+ module NameSearch
5
+ class Railtie < Rails::Railtie
6
+ rake_tasks do
7
+ load 'name_search/railties/tasks.rake'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ namespace :name_search do
2
+ desc 'runs the update_name_searchables() method on all records of the class'
3
+ task :update_name_searchables, [:model, :page_size] => :environment do |t, args|
4
+ klass = args[:model].camelize.constantize
5
+ page_size = args[:page_size].try(:to_i) || 100
6
+ page = 0
7
+
8
+ record_count = klass.count
9
+ records = klass.offset(page * page_size).limit(page_size)
10
+ while records.length > 0
11
+ records.each{|x| x.update_name_searchables}
12
+ page += 1
13
+ puts "Updated #{page * page_size} of #{record_count}"
14
+ records = klass.offset(page * page_size).limit(page_size)
15
+ end
16
+ puts 'finished'
17
+ end
18
+ end
@@ -0,0 +1,46 @@
1
+ module NameSearch
2
+ class Search < Array
3
+ def initialize(klass_or_query, name, options = {})
4
+ name_values = Name.scrub_and_split_name(name)
5
+ names = get_names(name_values)
6
+ nick_names = (options[:match_mode] == :exact) ?
7
+ [] :
8
+ get_nick_names(names)
9
+
10
+ results = matched_models(klass_or_query, names + nick_names).
11
+ map{|x| SearchResult.new(x, name_values, nick_names.map(&:value)) }.
12
+ sort{|a,b| b.match_score <=> a.match_score }
13
+
14
+ if options.has_key?(:matches_at_least)
15
+ results = results.delete_if{|x| x.matched_names.length < options[:matches_at_least] }
16
+ end
17
+
18
+ self.concat(results)
19
+ end
20
+
21
+ private
22
+
23
+ def matched_models(klass_or_query, names_to_search)
24
+ klass_or_query.includes(:name_searchables).
25
+ where(:name_search_searchables => {
26
+ :name_id => names_to_search.map(&:id) }).
27
+ all.
28
+ uniq
29
+ end
30
+
31
+ def get_names(name_values)
32
+ Name.where(:value => name_values)
33
+ end
34
+
35
+ def get_nick_names(names)
36
+ all_names = Name.joins(:nick_name_families).
37
+ where("name_search_nick_name_families.id IN (#{
38
+ NickNameFamily.joins(:names).
39
+ where(:name_search_names => { :value => names.map(&:value) }).
40
+ select('name_search_nick_name_families.id').
41
+ to_sql
42
+ })").
43
+ where('name_search_names.id NOT IN (?)', names.map(&:id))
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,14 @@
1
+ module NameSearch
2
+ class SearchResult
3
+ attr_reader :model, :matched_names, :exact_name_matches, :nick_name_matches, :match_score
4
+
5
+ def initialize(model, searched_names, searched_nick_names)
6
+ @model = model
7
+ @exact_name_matches = model.name_searchable_values & searched_names
8
+ @nick_name_matches = model.name_searchable_values & searched_nick_names
9
+ @matched_names = @exact_name_matches + @nick_name_matches
10
+ @match_score = (@exact_name_matches.length * 4) +
11
+ (@nick_name_matches.length * 3)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module NameSearch
2
+ VERSION = '0.8.0'
3
+ end
@@ -0,0 +1,13 @@
1
+ module NameSearch
2
+ require 'name_search/models/name.rb'
3
+ require 'name_search/models/nick_name_family.rb'
4
+ require 'name_search/models/nick_name_family_join.rb'
5
+ require 'name_search/models/searchable.rb'
6
+ require 'name_search/active_relation_search.rb'
7
+ require 'name_search/name_search_on.rb'
8
+ require 'name_search/name_searchables_concerns.rb'
9
+ require 'name_search/railtie.rb' if defined?(Rails) && Rails::VERSION::MAJOR == 3
10
+ require 'name_search/search.rb'
11
+ require 'name_search/search_result.rb'
12
+ require 'name_search/version.rb'
13
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: name_search
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Paul Yoder
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-07-23 00:00:00.000000000 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rails
17
+ requirement: &21939800 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: 3.0.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *21939800
26
+ description: Search for people's names while taking into consideration nick names
27
+ and word ordering.
28
+ email:
29
+ - paulyoder@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/name_search.rb
35
+ - lib/generators/name_search/migration/migration_generator.rb
36
+ - lib/generators/name_search/migration/templates/migration.rb
37
+ - lib/name_search/active_relation_search.rb
38
+ - lib/name_search/railties/tasks.rake
39
+ - lib/name_search/name_search_on.rb
40
+ - lib/name_search/models/searchable.rb
41
+ - lib/name_search/models/name.rb
42
+ - lib/name_search/models/nick_name_family.rb
43
+ - lib/name_search/models/nick_name_family_join.rb
44
+ - lib/name_search/search.rb
45
+ - lib/name_search/railtie.rb
46
+ - lib/name_search/version.rb
47
+ - lib/name_search/search_result.rb
48
+ - lib/name_search/name_searchables_concerns.rb
49
+ - MIT-LICENSE
50
+ - Rakefile
51
+ - Gemfile
52
+ - README.rdoc
53
+ has_rdoc: true
54
+ homepage: https://github.com/paulyoder/name_search
55
+ licenses: []
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ! '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubyforge_project:
74
+ rubygems_version: 1.6.2
75
+ signing_key:
76
+ specification_version: 3
77
+ summary: Search for people's names while taking into consideration nick names and
78
+ word ordering.
79
+ test_files: []