name_search 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +15 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +25 -0
- data/lib/generators/name_search/migration/migration_generator.rb +22 -0
- data/lib/generators/name_search/migration/templates/migration.rb +35 -0
- data/lib/name_search/active_relation_search.rb +9 -0
- data/lib/name_search/models/name.rb +47 -0
- data/lib/name_search/models/nick_name_family.rb +17 -0
- data/lib/name_search/models/nick_name_family_join.rb +8 -0
- data/lib/name_search/models/searchable.rb +11 -0
- data/lib/name_search/name_search_on.rb +22 -0
- data/lib/name_search/name_searchables_concerns.rb +49 -0
- data/lib/name_search/railtie.rb +10 -0
- data/lib/name_search/railties/tasks.rake +18 -0
- data/lib/name_search/search.rb +46 -0
- data/lib/name_search/search_result.rb +14 -0
- data/lib/name_search/version.rb +3 -0
- data/lib/name_search.rb +13 -0
- metadata +79 -0
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
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,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,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,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
|
data/lib/name_search.rb
ADDED
@@ -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: []
|