elasticfusion 1.0.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 +7 -0
- data/.codeclimate.yml +5 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +67 -0
- data/.travis.yml +22 -0
- data/Gemfile +3 -0
- data/LICENSE +116 -0
- data/README.md +40 -0
- data/Rakefile +11 -0
- data/elasticfusion.gemspec +31 -0
- data/lib/elasticfusion.rb +7 -0
- data/lib/elasticfusion/definition.rb +25 -0
- data/lib/elasticfusion/hooks.rb +14 -0
- data/lib/elasticfusion/jobs/reindex_job.rb +12 -0
- data/lib/elasticfusion/loader.rb +9 -0
- data/lib/elasticfusion/model/indexing.rb +37 -0
- data/lib/elasticfusion/model/searching.rb +16 -0
- data/lib/elasticfusion/model/settings.rb +63 -0
- data/lib/elasticfusion/search/builder.rb +60 -0
- data/lib/elasticfusion/search/errors.rb +32 -0
- data/lib/elasticfusion/search/peeker.rb +63 -0
- data/lib/elasticfusion/search/query/ast.rb +17 -0
- data/lib/elasticfusion/search/query/lexer.rb +90 -0
- data/lib/elasticfusion/search/query/parser.rb +147 -0
- data/lib/elasticfusion/search/query/value_sanitizer.rb +45 -0
- data/lib/elasticfusion/search/query/visitor.rb +22 -0
- data/lib/elasticfusion/search/query/visitors/elasticsearch.rb +52 -0
- data/lib/elasticfusion/search/query/visitors/polyadic_tree.rb +59 -0
- data/lib/elasticfusion/search/wrapper.rb +56 -0
- data/lib/elasticfusion/version.rb +4 -0
- metadata +213 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'chronic'
|
3
|
+
|
4
|
+
module Elasticfusion
|
5
|
+
module Search
|
6
|
+
module Query
|
7
|
+
class ValueSanitizer
|
8
|
+
def initialize(mapping)
|
9
|
+
@mapping = mapping
|
10
|
+
end
|
11
|
+
|
12
|
+
def value(value, field:)
|
13
|
+
case @mapping[field.to_sym][:type]
|
14
|
+
when 'keyword'
|
15
|
+
value
|
16
|
+
when 'integer'
|
17
|
+
es_integer(value, field: field)
|
18
|
+
when 'date'
|
19
|
+
es_date(value, field: field)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def es_integer(string, field:)
|
26
|
+
if string.match? /\A[+-]?\d+\z/
|
27
|
+
string
|
28
|
+
else
|
29
|
+
raise InvalidFieldValueError.new(field, string)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def es_date(string, field:)
|
34
|
+
parsed = Chronic.parse(string)
|
35
|
+
|
36
|
+
if parsed.nil?
|
37
|
+
raise InvalidFieldValueError.new(field, string)
|
38
|
+
else
|
39
|
+
parsed.iso8601
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Elasticfusion
|
3
|
+
module Search
|
4
|
+
module Query
|
5
|
+
class Visitor
|
6
|
+
def accept(node)
|
7
|
+
visit(node)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Roughly based on https://github.com/rails/arel/blob/7-1-stable/lib/arel/visitors/visitor.rb.
|
11
|
+
|
12
|
+
def visit(node)
|
13
|
+
send Visitor.visitor_method(node), node
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.visitor_method(node)
|
17
|
+
(@visitor_methods ||= {})[node.class] ||= "visit_#{node.class.name.demodulize}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'elasticfusion/search/query/visitors/polyadic_tree'
|
3
|
+
require 'elasticfusion/search/query/value_sanitizer'
|
4
|
+
|
5
|
+
module Elasticfusion
|
6
|
+
module Search
|
7
|
+
module Query
|
8
|
+
module Visitors
|
9
|
+
class Elasticsearch < PolyadicTree
|
10
|
+
def initialize(keyword_field, mapping)
|
11
|
+
@keyword_field = keyword_field
|
12
|
+
@sanitizer = ValueSanitizer.new(mapping)
|
13
|
+
end
|
14
|
+
|
15
|
+
OPERATORS = { and: :must,
|
16
|
+
or: :should }.freeze
|
17
|
+
|
18
|
+
def visit_PolyadicExpression(node)
|
19
|
+
operator = OPERATORS[node.op]
|
20
|
+
operands = node.children.map { |n| visit(n) }
|
21
|
+
|
22
|
+
{ bool: { operator => operands } }
|
23
|
+
end
|
24
|
+
|
25
|
+
def visit_NegatedClause(node)
|
26
|
+
clause = if node.body.respond_to?(:op) && node.body.op == :and
|
27
|
+
node.body.children.map { |n| visit(n) }
|
28
|
+
else
|
29
|
+
[visit(node.body)]
|
30
|
+
end
|
31
|
+
|
32
|
+
{ bool: { must_not: clause } }
|
33
|
+
end
|
34
|
+
|
35
|
+
def visit_FieldTerm(node)
|
36
|
+
value = @sanitizer.value(node.value, field: node.field)
|
37
|
+
|
38
|
+
if node.qualifier
|
39
|
+
{ range: { node.field.to_sym => { node.qualifier => value } } }
|
40
|
+
else
|
41
|
+
{ term: { node.field.to_sym => value } }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def visit_Term(node)
|
46
|
+
{ term: { @keyword_field => node.body } }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'elasticfusion/search/query/visitor'
|
3
|
+
require 'elasticfusion/search/query/ast'
|
4
|
+
|
5
|
+
module Elasticfusion
|
6
|
+
module Search
|
7
|
+
module Query
|
8
|
+
module Visitors
|
9
|
+
# +PolyadicTree+ is a base class for visitors that accept
|
10
|
+
# more than two operands for logical expressions.
|
11
|
+
# It converts a binary tree into multi-way tree, replacing all
|
12
|
+
# +Expression+ nodes with +PolyadicExpression+ nodes.
|
13
|
+
#
|
14
|
+
# Given an AST:
|
15
|
+
#
|
16
|
+
# and
|
17
|
+
# / \
|
18
|
+
# A and
|
19
|
+
# / \
|
20
|
+
# B and
|
21
|
+
# / \
|
22
|
+
# C or
|
23
|
+
# / \
|
24
|
+
# D E
|
25
|
+
#
|
26
|
+
# A polyadic representation would be:
|
27
|
+
#
|
28
|
+
# and
|
29
|
+
# / / \ \
|
30
|
+
# A B C or
|
31
|
+
# / \
|
32
|
+
# D E
|
33
|
+
|
34
|
+
class PolyadicTree < Visitor
|
35
|
+
def accept(node)
|
36
|
+
super(rewrite(node))
|
37
|
+
end
|
38
|
+
|
39
|
+
def rewrite(node, parent: nil)
|
40
|
+
case node
|
41
|
+
when NegatedClause
|
42
|
+
node.body = rewrite(node.body)
|
43
|
+
when Expression
|
44
|
+
flattened = [rewrite(node.left, parent: node),
|
45
|
+
rewrite(node.right, parent: node)].flatten
|
46
|
+
|
47
|
+
if parent && node.op && node.op == parent.op
|
48
|
+
return flattened
|
49
|
+
else
|
50
|
+
return PolyadicExpression.new(node.op, flattened)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
node
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'elasticfusion/search/builder'
|
3
|
+
require 'elasticfusion/search/peeker'
|
4
|
+
require 'elasticfusion/search/errors'
|
5
|
+
require 'elasticfusion/search/query/parser'
|
6
|
+
require 'elasticfusion/search/query/visitors/elasticsearch'
|
7
|
+
|
8
|
+
# An instance of this class represents a single search.
|
9
|
+
# It encapsulates all custom search features (advanced query parsing,
|
10
|
+
# query building, etc.)
|
11
|
+
module Elasticfusion
|
12
|
+
module Search
|
13
|
+
class Wrapper
|
14
|
+
def initialize(model, query, &block)
|
15
|
+
@search_runner = model.method(:search)
|
16
|
+
|
17
|
+
@searchable_mapping = model.elasticfusion[:searchable_mapping]
|
18
|
+
@searchable_fields = model.elasticfusion[:searchable_fields]
|
19
|
+
@keyword_field = model.elasticfusion[:keyword_field]
|
20
|
+
|
21
|
+
@builder = Search::Builder.new(model.elasticfusion)
|
22
|
+
@builder.instance_eval(&block) if block_given?
|
23
|
+
|
24
|
+
# The subset of queries that is currently supported can be executed
|
25
|
+
# in the filter context, which does not compute _score and can be cached.
|
26
|
+
# It cannot be used for relevance sorting, though.
|
27
|
+
@builder.filter parse_query(query) if query.present?
|
28
|
+
end
|
29
|
+
|
30
|
+
delegate :next_record, :previous_record, to: :peeker
|
31
|
+
|
32
|
+
def perform(request = elasticsearch_request)
|
33
|
+
@search_runner.call(request)
|
34
|
+
end
|
35
|
+
|
36
|
+
def elasticsearch_request
|
37
|
+
{ query: { bool: { must: @builder.queries,
|
38
|
+
filter: @builder.filters } },
|
39
|
+
sort: @builder.sorts }
|
40
|
+
end
|
41
|
+
|
42
|
+
def peeker
|
43
|
+
Peeker.new(self)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def parse_query(query)
|
49
|
+
ast = Query::Parser.new(query, @searchable_fields).ast
|
50
|
+
visitor = Query::Visitors::Elasticsearch.new(@keyword_field,
|
51
|
+
@searchable_mapping)
|
52
|
+
visitor.accept(ast)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
metadata
ADDED
@@ -0,0 +1,213 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: elasticfusion
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- little-bobby-tables
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-04-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: chronic
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: elasticsearch
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: elasticsearch-rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: elasticsearch-model
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: openssl
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: sqlite3
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: minitest-reporters
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: simplecov
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: codeclimate-test-reporter
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
description:
|
154
|
+
email:
|
155
|
+
- little-bobby-tables@users.noreply.github.com
|
156
|
+
executables: []
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files: []
|
159
|
+
files:
|
160
|
+
- ".codeclimate.yml"
|
161
|
+
- ".gitignore"
|
162
|
+
- ".rubocop.yml"
|
163
|
+
- ".travis.yml"
|
164
|
+
- Gemfile
|
165
|
+
- LICENSE
|
166
|
+
- README.md
|
167
|
+
- Rakefile
|
168
|
+
- elasticfusion.gemspec
|
169
|
+
- lib/elasticfusion.rb
|
170
|
+
- lib/elasticfusion/definition.rb
|
171
|
+
- lib/elasticfusion/hooks.rb
|
172
|
+
- lib/elasticfusion/jobs/reindex_job.rb
|
173
|
+
- lib/elasticfusion/loader.rb
|
174
|
+
- lib/elasticfusion/model/indexing.rb
|
175
|
+
- lib/elasticfusion/model/searching.rb
|
176
|
+
- lib/elasticfusion/model/settings.rb
|
177
|
+
- lib/elasticfusion/search/builder.rb
|
178
|
+
- lib/elasticfusion/search/errors.rb
|
179
|
+
- lib/elasticfusion/search/peeker.rb
|
180
|
+
- lib/elasticfusion/search/query/ast.rb
|
181
|
+
- lib/elasticfusion/search/query/lexer.rb
|
182
|
+
- lib/elasticfusion/search/query/parser.rb
|
183
|
+
- lib/elasticfusion/search/query/value_sanitizer.rb
|
184
|
+
- lib/elasticfusion/search/query/visitor.rb
|
185
|
+
- lib/elasticfusion/search/query/visitors/elasticsearch.rb
|
186
|
+
- lib/elasticfusion/search/query/visitors/polyadic_tree.rb
|
187
|
+
- lib/elasticfusion/search/wrapper.rb
|
188
|
+
- lib/elasticfusion/version.rb
|
189
|
+
homepage: https://github.com/little-bobby-tables/elasticfusion
|
190
|
+
licenses:
|
191
|
+
- CC0-1.0
|
192
|
+
metadata: {}
|
193
|
+
post_install_message:
|
194
|
+
rdoc_options: []
|
195
|
+
require_paths:
|
196
|
+
- lib
|
197
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0'
|
202
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
203
|
+
requirements:
|
204
|
+
- - ">="
|
205
|
+
- !ruby/object:Gem::Version
|
206
|
+
version: '0'
|
207
|
+
requirements: []
|
208
|
+
rubyforge_project:
|
209
|
+
rubygems_version: 2.6.11
|
210
|
+
signing_key:
|
211
|
+
specification_version: 4
|
212
|
+
summary: elasticsearch-rails extensions
|
213
|
+
test_files: []
|