eagle_search 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 95f2c540d8392becd307b86370b410a014938ad8
4
+ data.tar.gz: 08278f066fe793bf2d8e13bcc41830d5b19dbb6d
5
+ SHA512:
6
+ metadata.gz: 0f63b0ca08028e9fd056220efa3731aaf6a902bc5152b2ec6f73887bdb1fecfc75f4e809bfd16ce5fb1846a86799cba7d2fc549be88274d30c4a20b569e1e6ed
7
+ data.tar.gz: b460acef98b7274ae245c4c8f75f565d07d7e710d8c1726bb35e35d0e9c80bf405f3d7c8b6e317c4cd248c0e8293dbabd42d52ededec6ff2c925679827ea534d
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,54 @@
1
+ module EagleSearch
2
+ class Field
3
+ def initialize(index, column)
4
+ @index = index
5
+ @column = column
6
+ end
7
+
8
+ def mapping
9
+ mapping = { type: type }
10
+ index_settings = @index.settings
11
+
12
+ if index_settings[:unsearchable_fields] && (index_settings[:unsearchable_fields].include?(@column.name) || index_settings[:unsearchable_fields].include?(@column.name.to_sym))
13
+ mapping[:index] = "no"
14
+ else
15
+ if type == "string"
16
+ if index_settings[:exact_match_fields] && (index_settings[:exact_match_fields].include?(@column.name) || index_settings[:exact_match_fields].include?(@column.name.to_sym))
17
+ mapping[:index] = "not_analyzed"
18
+ else
19
+ mapping[:index] = "analyzed"
20
+ mapping[:analyzer] = index_settings[:language] || "english"
21
+ mapping[:fields] = {
22
+ shingle: {
23
+ type: "string",
24
+ analyzer: "eagle_search_shingle_analyzer"
25
+ }
26
+ }
27
+ end
28
+ end
29
+ end
30
+
31
+ mapping
32
+ end
33
+
34
+ private
35
+ def type
36
+ case @column.type
37
+ when :integer
38
+ if @column.limit.to_i <= 8
39
+ "integer"
40
+ else
41
+ "long"
42
+ end
43
+ when :date, :datetime
44
+ "date"
45
+ when :boolean
46
+ "boolean"
47
+ when :decimal, :float
48
+ "float"
49
+ else
50
+ "string"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,121 @@
1
+ require 'date'
2
+
3
+ module EagleSearch
4
+ class Index
5
+ attr_reader :settings, :alias_name
6
+ delegate :columns, to: :klass
7
+
8
+ def initialize(klass, settings)
9
+ @klass = klass
10
+ @settings = settings
11
+ end
12
+
13
+ def create
14
+ EagleSearch.client.indices.create index: name, body: body
15
+ end
16
+
17
+ def delete
18
+ EagleSearch.client.indices.delete index: alias_name
19
+ end
20
+
21
+ def refresh
22
+ EagleSearch.client.indices.refresh index: alias_name
23
+ end
24
+
25
+ def info
26
+ EagleSearch.client.indices.get index: alias_name
27
+ end
28
+
29
+ def name
30
+ @name ||= @settings[:index_name] || "#{ alias_name }_#{ DateTime.now.strftime('%Q') }"
31
+ end
32
+
33
+ def alias_name
34
+ @alias_name ||= @settings[:index_name] || "#{ @klass.model_name.route_key.downcase }_#{ EagleSearch.env }"
35
+ end
36
+
37
+ def type_name
38
+ if @settings[:mappings]
39
+ @settings[:mappings].keys.first.downcase
40
+ else
41
+ @klass.model_name.param_key
42
+ end
43
+ end
44
+
45
+ def reindex
46
+ client = EagleSearch.client
47
+ begin
48
+ aliases = client.indices.get_alias name: alias_name
49
+ client.indices.delete index: aliases.keys.join(",")
50
+ rescue
51
+ #do something
52
+ ensure
53
+ create
54
+ bulk = []
55
+ @klass.all.each do |record|
56
+ bulk << { index: { _index: alias_name, _type: type_name, _id: record.id } }
57
+ bulk << record.index_data
58
+ end
59
+ client.bulk body: bulk
60
+ end
61
+ end
62
+
63
+ def mappings
64
+ if @settings[:mappings]
65
+ @settings[:mappings]
66
+ else
67
+ base_mappings = {
68
+ type_name => {
69
+ properties: {}
70
+ }
71
+ }
72
+
73
+ columns.each do |column|
74
+ base_mappings[type_name][:properties][column.name] = EagleSearch::Field.new(self, column).mapping
75
+ end
76
+
77
+ base_mappings
78
+ end
79
+ end
80
+
81
+ private
82
+ def body
83
+ body = {
84
+ mappings: mappings,
85
+ aliases: { alias_name => {} },
86
+ settings: {
87
+ analysis: analysis_settings
88
+ }
89
+ }
90
+ body[:settings][:number_of_shards] = 1 if EagleSearch.env == "test" || EagleSearch.env == "development"
91
+ body
92
+ end
93
+
94
+ def analysis_settings
95
+ {
96
+ filter: {
97
+ eagle_search_shingle_filter: {
98
+ type: "shingle",
99
+ min_shingle_size: 2,
100
+ max_shingle_size: 2,
101
+ output_unigrams: false
102
+ }
103
+ },
104
+ analyzer: {
105
+ eagle_search_shingle_analyzer: {
106
+ type: "custom",
107
+ tokenizer: "standard",
108
+ filter: [
109
+ "lowercase",
110
+ "eagle_search_shingle_filter"
111
+ ]
112
+ }
113
+ }
114
+ }
115
+ end
116
+
117
+ def klass
118
+ @klass
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,55 @@
1
+ module EagleSearch
2
+ class Interpreter::Filter
3
+ attr_reader :payload
4
+
5
+ LOGICAL_OPERATORS = { and: :must, not: :must_not, or: :should }
6
+
7
+ def initialize(filters)
8
+ @filters = filters
9
+ end
10
+
11
+ def payload
12
+ @payload ||= generate_payload(@filters)
13
+ end
14
+
15
+ private
16
+ def generate_payload(filters)
17
+ payload = {}
18
+
19
+ filters.each do |key, value|
20
+ key = key.to_sym
21
+
22
+ if LOGICAL_OPERATORS.include?(key)
23
+ payload = { bool: { LOGICAL_OPERATORS[key] => [] } }
24
+
25
+ if value.is_a?(Array)
26
+ value.each { |filter| payload[:bool][LOGICAL_OPERATORS[key]] << generate_payload(filter) }
27
+ else
28
+ value.each { |field, field_value| payload[:bool][LOGICAL_OPERATORS[key]] << generate_payload({ field => field_value }) }
29
+ end
30
+ else
31
+ payload = elasticsearch_filter_hash(key, value)
32
+ end
33
+ end
34
+
35
+ payload
36
+ end
37
+
38
+ def elasticsearch_filter_hash(field, field_value)
39
+ case field_value
40
+ when Array
41
+ { terms: { field => field_value } }
42
+ when Hash
43
+ if field_value.keys.any? { |key| %i(lt gt lte gte).include?(key.to_sym) }
44
+ { range: { field => field_value } }
45
+ end
46
+ when Range
47
+ { range: { field => { gte: field_value.min, lte: field_value.max } } }
48
+ when Regexp
49
+ { regexp: { field => field_value.source } }
50
+ else
51
+ { term: { field => field_value } }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,112 @@
1
+ module EagleSearch
2
+ class Interpreter::Query
3
+
4
+ def initialize(index, query, options)
5
+ @index = index
6
+ @query = query
7
+ @options = options
8
+ end
9
+
10
+ def payload
11
+ case @query
12
+ when String
13
+ if @query == "*"
14
+ { match_all: {} }
15
+ else
16
+ query_payload
17
+ end
18
+ end
19
+ end
20
+
21
+ private
22
+ def properties
23
+ @index.mappings[@index.type_name][:properties]
24
+ end
25
+
26
+ def analyzed_properties
27
+ properties.select { |field_name, field_hash| field_hash[:type] == "string" && field_hash[:index] == "analyzed" }
28
+ end
29
+
30
+ def not_analyzed_properties
31
+ properties.select { |field_name, field_hash| field_hash[:type] == "string" && field_hash[:index] == "not_analyzed" }
32
+ end
33
+
34
+ def query_payload
35
+ @query_payload = {}
36
+ build_multi_match_query
37
+ build_match_queries
38
+ build_term_queries
39
+ @query_payload
40
+ end
41
+
42
+ def build_multi_match_query
43
+ if analyzed_properties
44
+ @query_payload = {
45
+ bool: {
46
+ should: []
47
+ }
48
+ }
49
+
50
+ @query_payload[:bool][:should] << {
51
+ multi_match: {
52
+ query: @query,
53
+ fields: @options[:fields] || analyzed_properties.keys,
54
+ tie_breaker: 0.3
55
+ }
56
+ }
57
+ end
58
+ end
59
+
60
+ def build_match_queries
61
+ return unless analyzed_properties
62
+
63
+ match_queries = []
64
+ analyzed_properties.keys.each do |field_name|
65
+ match_queries << {
66
+ match: {
67
+ "#{ field_name }.shingle" => {
68
+ query: @query,
69
+ boost: 3
70
+ }
71
+ }
72
+ }
73
+ end
74
+
75
+ payload = {
76
+ bool: {
77
+ should: match_queries
78
+ }
79
+ }
80
+
81
+ @query_payload[:bool] ? @query_payload[:bool][:should] << payload : @query_payload[:bool][:should] = payload
82
+ end
83
+
84
+ def build_term_queries
85
+ return unless not_analyzed_properties
86
+
87
+ term_queries = []
88
+ not_analyzed_properties.keys.each do |field_name|
89
+ term_queries << {
90
+ term: {
91
+ field_name => {
92
+ value: @query,
93
+ boost: 5
94
+ }
95
+ }
96
+ }
97
+ end
98
+
99
+ payload = {
100
+ bool: {
101
+ should: term_queries
102
+ }
103
+ }
104
+
105
+ if @query_payload[:bool]
106
+ @query_payload[:bool][:should][1][:bool][:should] << payload
107
+ else
108
+ @query_payload = payload
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,56 @@
1
+ module EagleSearch
2
+ class Interpreter
3
+ def initialize(index, query, options)
4
+ @index = index
5
+ @query = query
6
+ @options = options
7
+ @options[:page] = @options[:page].to_i if @options[:page]
8
+ @options[:per_page] = @options[:per_page].to_i if @options[:per_page]
9
+ end
10
+
11
+ def payload
12
+ return @options[:custom_payload] if @options[:custom_payload]
13
+
14
+ payload = {
15
+ query: {
16
+ filtered: {
17
+ query: query_payload,
18
+ filter: filter_payload
19
+ }
20
+ }
21
+ }
22
+
23
+ payload.merge!({ sort: @options[:sort] }) if @options[:sort]
24
+
25
+ # from
26
+ if @options[:page] && @options[:page] > 1
27
+ from = (@options[:page] - 1) * (@options[:per_page] || 10)
28
+ payload.merge!({ from: from })
29
+ end
30
+
31
+ #size
32
+ payload.merge!({ size: @options[:per_page] }) if @options[:per_page]
33
+
34
+ payload
35
+ end
36
+
37
+ private
38
+ def query_payload
39
+ if @options[:custom_query]
40
+ @options[:custom_query]
41
+ else
42
+ EagleSearch::Interpreter::Query.new(@index, @query, @options).payload
43
+ end
44
+ end
45
+
46
+ def filter_payload
47
+ if @options[:filters]
48
+ EagleSearch::Interpreter::Filter.new(@options[:filters]).payload
49
+ elsif @options[:custom_filters]
50
+ @options[:custom_filters]
51
+ else
52
+ {}
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,62 @@
1
+ module EagleSearch
2
+ class Model
3
+ module ClassMethods
4
+ def eagle_search(settings = {})
5
+ @index = EagleSearch::Index.new(self, settings)
6
+ end
7
+
8
+ def eagle_search_index
9
+ @index
10
+ end
11
+
12
+ def create_index
13
+ eagle_search_index.create
14
+ end
15
+
16
+ def delete_index
17
+ eagle_search_index.delete
18
+ end
19
+
20
+ def refresh_index
21
+ eagle_search_index.refresh
22
+ end
23
+
24
+ def index_info
25
+ eagle_search_index.info
26
+ end
27
+
28
+ def search(term, options = {})
29
+ interpreter = EagleSearch::Interpreter.new(@index, term, options)
30
+ search_response = EagleSearch.client.search index: eagle_search_index.alias_name, body: interpreter.payload
31
+ EagleSearch::Response.new(self, search_response, options)
32
+ end
33
+
34
+ def reindex
35
+ eagle_search_index.reindex
36
+ end
37
+ end
38
+
39
+ module InstanceMethods
40
+ def reindex
41
+ index = self.class.eagle_search_index
42
+ reindex_option = index.settings[:reindex]
43
+
44
+ if reindex_option.nil? || reindex_option
45
+ begin
46
+ index.info
47
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound
48
+ index.create
49
+ ensure
50
+ index.refresh
51
+ EagleSearch.client.index(
52
+ index: index.alias_name,
53
+ type: index.type_name,
54
+ id: id,
55
+ body: index_data
56
+ )
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,44 @@
1
+ module EagleSearch
2
+ class Response
3
+ def initialize(klass, response, options)
4
+ @klass = klass
5
+ @response = response
6
+ @options = options
7
+ end
8
+
9
+ def records
10
+ ids = hits.map { |hit| hit["_id"] }
11
+ #avoids n+1
12
+ @klass.includes(@options[:includes]) if @options[:includes]
13
+ @klass.where(@klass.primary_key => ids)
14
+ end
15
+
16
+ def each
17
+ if block_given?
18
+ records.each { |e| yield(e) }
19
+ else
20
+ records.to_enum
21
+ end
22
+ end
23
+
24
+ def total_hits
25
+ @response["hits"]["total"]
26
+ end
27
+
28
+ def hits
29
+ @response["hits"]["hits"]
30
+ end
31
+
32
+ def current_page
33
+ @options[:page] || 1
34
+ end
35
+
36
+ def total_pages
37
+ (total_hits / limit_value.to_f).ceil
38
+ end
39
+
40
+ def limit_value
41
+ @options[:per_page] || 25
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,3 @@
1
+ module EagleSearch
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,25 @@
1
+ require "eagle_search/version"
2
+ require "eagle_search/model"
3
+ require "eagle_search/index"
4
+ require "eagle_search/field"
5
+ require "eagle_search/response"
6
+ require "eagle_search/interpreter"
7
+ require "eagle_search/interpreter/query"
8
+ require "eagle_search/interpreter/filter"
9
+ require "elasticsearch"
10
+
11
+ module EagleSearch
12
+ def self.included(base)
13
+ base.extend(EagleSearch::Model::ClassMethods)
14
+ base.include(EagleSearch::Model::InstanceMethods)
15
+ base.after_commit :reindex, on: [:create, :update]
16
+ end
17
+
18
+ def self.client
19
+ @client ||= Elasticsearch::Client.new
20
+ end
21
+
22
+ def self.env
23
+ @env ||= ENV['RAILS_ENV'] || "development"
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: eagle_search
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Igor Belo
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-11-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: elasticsearch
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.10'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.10'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.3'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.3'
97
+ description: Rails Model integration for Elasticsearch.
98
+ email:
99
+ - igorcoura@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - Rakefile
105
+ - lib/eagle_search.rb
106
+ - lib/eagle_search/field.rb
107
+ - lib/eagle_search/index.rb
108
+ - lib/eagle_search/interpreter.rb
109
+ - lib/eagle_search/interpreter/filter.rb
110
+ - lib/eagle_search/interpreter/query.rb
111
+ - lib/eagle_search/model.rb
112
+ - lib/eagle_search/response.rb
113
+ - lib/eagle_search/version.rb
114
+ homepage: https://github.com/igorbelo/eagle_search
115
+ licenses:
116
+ - MIT
117
+ metadata: {}
118
+ post_install_message:
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubyforge_project:
134
+ rubygems_version: 2.4.6
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: Rails Model integration for Elasticsearch.
138
+ test_files: []