trample_search 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c648e258dedbc817af3e8fd1dd0e2beb759dbbda
4
+ data.tar.gz: 92b492f65baf9d040f8b9a6616f109940441b53c
5
+ SHA512:
6
+ metadata.gz: 6da88788dc47a9cf4749957aff836bfaadde11251b9a1b8d7d0bfac11129ac467f3252669a25f49e1c11a4ed5fe51a76004001869f9eb849a57e301ce05e1bb6
7
+ data.tar.gz: 087289caefa82470f2d59501e2d40b0574a37bd5a2fd377446e7009d7f3fd9fc483c809151f01e54d871b91696d7a82ccf681710df9f370aaf6904fbc9085a21
@@ -0,0 +1,12 @@
1
+ *.swp
2
+ *.swo
3
+ /.bundle/
4
+ /.yardoc
5
+ /Gemfile.lock
6
+ /_yardoc/
7
+ /coverage/
8
+ /doc/
9
+ /pkg/
10
+ /spec/reports/
11
+ /tmp/
12
+ vendor
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://artifactory.dev.bloomberg.com:8080/artifactory/api/gems/rubygems/"
2
+
3
+ # Specify your gem's dependencies in trample.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 richmolj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,46 @@
1
+ # Trample
2
+
3
+ Additional querying sugar for [searchkick](https://github.com/ankane/searchkick).
4
+
5
+ ## Why Trample?
6
+
7
+ Searchkick provides a nice query mechanism. But it doesn't provide a way to build up those queries, particularly syncing with UI input. Trample makes this simple:
8
+
9
+ ```ruby
10
+ class PeopleSearchesController < ApplicationController
11
+
12
+ def update
13
+ search = PeopleSearch.new(params[:people_search)
14
+ search.query!
15
+
16
+ render json: search
17
+ end
18
+
19
+ end
20
+ ```
21
+
22
+ Or, build up queries manually:
23
+
24
+ ```ruby
25
+ search = PeopleSearch.new
26
+ search.condition(:security_level).in(%w(low medium)) unless current_user.admin?
27
+ search.paginate(size: 10, number: 2).sort("-age")
28
+ search.query!
29
+ search.results
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ Check out the complete [documentation](http://richmolj.github.io/trample), or see usage in the [specs](https://github.com/richmolj/trample/blob/master/spec/integration/search_spec.rb).
35
+
36
+ ## Specs
37
+
38
+ Run elasticsearch on port 9250 and `bundle exec rspec`.
39
+
40
+ ## Contributing
41
+
42
+ 1. Fork it ( https://github.com/fotinakis/swagger-blocks/fork )
43
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
44
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
45
+ 4. Push to the branch (`git push origin my-new-feature`)
46
+ 5. Create a new Pull Request
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "trample"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This file was generated by Bundler.
4
+ #
5
+ # The application 'rspec' is installed as part of a gem, and
6
+ # this file is here to facilitate running it.
7
+ #
8
+
9
+ require 'pathname'
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
11
+ Pathname.new(__FILE__).realpath)
12
+
13
+ require 'rubygems'
14
+ require 'bundler/setup'
15
+
16
+ load Gem.bin_path('rspec-core', 'rspec')
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,22 @@
1
+ require 'virtus'
2
+
3
+ require "trample/version"
4
+ require "trample/aggregation"
5
+ require "trample/serializable"
6
+ require "trample/condition"
7
+ require "trample/condition_proxy"
8
+ require "trample/metadata"
9
+ require "trample/search"
10
+ require "trample/backend/searchkick"
11
+ require "trample/results"
12
+ require "trample/errors"
13
+
14
+ require "trample/swagger"
15
+
16
+ require "trample/autocomplete/formatter"
17
+
18
+ require "trample/railtie" if defined?(Rails)
19
+
20
+ module Trample
21
+ # Your code goes here...
22
+ end
@@ -0,0 +1,85 @@
1
+ module Trample
2
+ class Aggregation
3
+ include Virtus.model
4
+
5
+ class Buckets < Array
6
+ def <<(entry)
7
+ if entry.is_a?(Hash)
8
+ super(Bucket.new(entry))
9
+ else
10
+ super
11
+ end
12
+ end
13
+ end
14
+
15
+ class Bucket
16
+ include Virtus.model
17
+ attribute :key, String
18
+ attribute :label, String, default: ->(bucket,attr) { bucket['key'] }
19
+ attribute :count, Integer, default: 0
20
+ attribute :selected, Boolean, default: false
21
+ end
22
+
23
+ attribute :name, Symbol
24
+ attribute :label, String
25
+ attribute :order, Integer
26
+ attribute :buckets, Buckets[Bucket]
27
+
28
+ attr_accessor :bucket_sort
29
+
30
+ def to_query
31
+ {name => selections}
32
+ end
33
+
34
+ def selections?
35
+ !selections.empty?
36
+ end
37
+
38
+ def selections
39
+ buckets.select(&:selected?).map(&:key)
40
+ end
41
+
42
+ # Usage:
43
+ # aggregation :foo do |agg|
44
+ # agg.bucket_sort = :count
45
+ # # OR agg.bucket_sort = :alpha
46
+ # # OR add.bucket_sort = proc { |a, b| ... }
47
+ # end
48
+ def bucket_sort
49
+ @bucket_sort ||= :alpha
50
+
51
+ if @bucket_sort == :alpha
52
+ proc { |a, b| a.key.downcase <=> b.key.downcase }
53
+ elsif @bucket_sort == :count
54
+ proc { |a, b|
55
+ if a.count == b.count
56
+ a.key.downcase <=> b.key.downcase
57
+ else
58
+ b.count <=> a.count
59
+ end
60
+ }
61
+ else
62
+ @bucket_sort
63
+ end
64
+ end
65
+
66
+ def buckets
67
+ ordered = super
68
+ ordered.sort!(&bucket_sort)
69
+ ordered
70
+ end
71
+
72
+ def force(key, opts = {})
73
+ self.buckets << opts.merge(key: key)
74
+ end
75
+
76
+ def find_or_initialize_bucket(key)
77
+ bucket = buckets.find { |b| b['key'].downcase == key.downcase }
78
+ if bucket.nil?
79
+ bucket = Bucket.new(key: key)
80
+ self.buckets << bucket
81
+ end
82
+ bucket
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,53 @@
1
+ module Trample
2
+ module Autocomplete
3
+ class Formatter
4
+
5
+ def initialize(formatter = nil)
6
+ if formatter.respond_to?(:call)
7
+ @formatter = formatter
8
+ else
9
+ @formatter = hash_formatter_to_proc(formatter)
10
+ end
11
+ end
12
+
13
+ def format_all(results, options = {})
14
+ results = results.map do |r|
15
+ format_one(r)
16
+ end
17
+
18
+ if user_query = options[:user_query]
19
+ results.unshift(Hashie::Mash.new(id: user_query, key: user_query, text: "\"#{user_query}\"", user_query: true))
20
+ end
21
+
22
+ results
23
+ end
24
+
25
+ def format_one(result)
26
+ @formatter.call(result)
27
+ end
28
+
29
+ private
30
+
31
+ def default_hash
32
+ {
33
+ id: :id,
34
+ key: :id,
35
+ text: :name
36
+ }
37
+ end
38
+
39
+ def hash_formatter_to_proc(hash)
40
+ hash ||= default_hash
41
+
42
+ ->(result) {
43
+ {
44
+ id: result.send(hash[:id]),
45
+ key: result.send(hash[:key]),
46
+ text: result.send(hash[:text])
47
+ }
48
+ }
49
+ end
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,94 @@
1
+ require 'searchkick'
2
+
3
+ module Trample
4
+ module Backend
5
+ class Searchkick
6
+
7
+ def initialize(metadata, models)
8
+ @metadata = metadata
9
+ @_models = models
10
+ end
11
+
12
+ def query!(conditions, aggregations)
13
+ query = build_query(conditions, aggregations, @metadata, @_models)
14
+ results = @_models.first.search(keywords(conditions), query)
15
+ parse_response_aggs!(results.aggs, aggregations) if results.response.has_key?('aggregations')
16
+
17
+ {
18
+ total: results.total_count,
19
+ took: results.response['took'],
20
+ results: results.results
21
+ }
22
+ end
23
+
24
+ private
25
+
26
+ def keywords(conditions)
27
+ if conditions[:keywords] and conditions[:keywords].values.first != ''
28
+ conditions[:keywords].values.first
29
+ else
30
+ '*'
31
+ end
32
+ end
33
+
34
+ def build_query(conditions, aggregations, metadata, models)
35
+ clauses = build_condition_clauses(conditions, aggregations)
36
+ query = searchkick_payload(conditions[:keywords], clauses, metadata, aggregations)
37
+ query.merge!(index_name: models.map { |m| m.searchkick_index.name }) if models.length > 1
38
+ query
39
+ end
40
+
41
+ # N.B. aggs and conditions could hit same key
42
+ def build_condition_clauses(conditions, aggregations)
43
+ {}.tap do |clauses|
44
+ aggregations.each do |agg|
45
+ clauses.merge!(agg.to_query) if agg.selections?
46
+ end
47
+ conditions.each_pair do |name, condition|
48
+ next if condition.name == :keywords
49
+ merge_clause(clauses, condition.to_query) unless condition.blank?
50
+ end
51
+ end
52
+ end
53
+
54
+ def merge_clause(clauses, clause)
55
+ if clause[:or]
56
+ clauses[:or] ||= []
57
+ clauses[:or] << clause[:or]
58
+ else
59
+ clauses.merge!(clause)
60
+ end
61
+ end
62
+
63
+ def searchkick_payload(keywords, clauses, metadata, aggs)
64
+ payload = {
65
+ where: clauses,
66
+ order: _sorts(metadata),
67
+ page: metadata.pagination.current_page,
68
+ per_page: metadata.pagination.per_page,
69
+ aggs: aggs.map(&:name),
70
+ load: false
71
+ }
72
+ payload[:fields] = keywords.fields if keywords and !keywords.fields.empty?
73
+ payload
74
+ end
75
+
76
+ def _sorts(metadata)
77
+ metadata.sort.map do |s|
78
+ {s.att => s.dir}
79
+ end
80
+ end
81
+
82
+ def parse_response_aggs!(response_aggs, search_aggs)
83
+ response_aggs.each_pair do |key, payload|
84
+ agg = search_aggs.find { |a| a.name.to_sym == key.to_sym }
85
+ payload['buckets'].each do |response_bucket|
86
+ bucket = agg.find_or_initialize_bucket(response_bucket['key'])
87
+ bucket.count = response_bucket['doc_count']
88
+ end
89
+ end
90
+ end
91
+
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,203 @@
1
+ module Trample
2
+ class Condition
3
+ include Virtus.model
4
+
5
+ attribute :name, Symbol
6
+ attribute :query_name, Symbol, default: :name
7
+ attribute :values, Array
8
+ attribute :search_analyzed, Boolean, default: false
9
+ attribute :and, Boolean
10
+ attribute :not, Boolean
11
+ attribute :prefix, Boolean, default: false
12
+ attribute :any_text, Boolean, default: false
13
+ attribute :autocomplete, Boolean, default: false
14
+ attribute :from_eq
15
+ attribute :to_eq
16
+ attribute :from
17
+ attribute :to
18
+ attribute :single, Boolean, default: false
19
+ attribute :range, Boolean, default: false
20
+ attribute :fields, Array
21
+ attribute :user_query, Hash
22
+
23
+ def initialize(attrs)
24
+ attrs.merge!(single: true) if attrs[:name] == :keywords
25
+ super(attrs)
26
+ end
27
+
28
+ def blank?
29
+ values.reject { |v| v == "" || v.nil? }.empty? && !range?
30
+ end
31
+
32
+ def as_json(*opts)
33
+ if single?
34
+ values.first
35
+ elsif range?
36
+ {}.tap do |json|
37
+ json[:from_eq] = from_eq if from_eq?
38
+ json[:from] = from if from?
39
+ json[:to_eq] = to_eq if to_eq?
40
+ json[:to] = to if to?
41
+ end
42
+ else
43
+ { values: values, and: and? }
44
+ end
45
+ end
46
+
47
+ def runtime_query_name
48
+ name = query_name
49
+ return "#{name}.text_start" if prefix?
50
+ return "#{name}.text_middle" if any_text?
51
+ return "#{name}.analyzed" if search_analyzed?
52
+ return "#{name}.autocomplete" if autocomplete?
53
+ name
54
+ end
55
+
56
+ def to_query
57
+ if range?
58
+ to_range_query
59
+ else
60
+ _values = values.dup.map { |v| v.is_a?(Hash) ? v.dup : v }
61
+ user_queries = _values.select(&is_user_query)
62
+ transformed = transform_values(_values - user_queries)
63
+
64
+ user_query_clause = derive_user_query_clause(user_queries)
65
+ main_clause = derive_main_clause(transformed)
66
+
67
+ if user_query_clause.present?
68
+ { or: [ main_clause, user_query_clause ] }
69
+ else
70
+ main_clause
71
+ end
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def transform_values(entries)
78
+ entries = pluck_autocomplete_keys(entries) if has_autocomplete_keys?(entries)
79
+ entries.map(&:downcase!) if search_analyzed?
80
+ entries = entries.first if entries.length == 1
81
+ entries
82
+ end
83
+
84
+ def derive_user_query_clause(user_queries)
85
+ if user_queries.length > 0
86
+ user_queries.each { |q| q.delete(:user_query) }
87
+ condition = Condition.new(user_query.merge(values: user_queries))
88
+ condition.to_query
89
+ else
90
+ {}
91
+ end
92
+ end
93
+
94
+ def derive_main_clause(transformed)
95
+ if prefix?
96
+ to_prefix_query(transformed)
97
+ elsif has_combinator?
98
+ to_combinator_query(transformed)
99
+ elsif exclusion?
100
+ to_exclusion_query(transformed)
101
+ else
102
+ {runtime_query_name => transformed}
103
+ end
104
+ end
105
+
106
+ def pluck_user_query_values!(values)
107
+ user_queries = values.select(&is_user_query)
108
+ values.reject!(&is_user_query)
109
+ [values, user_queries]
110
+ end
111
+
112
+ def has_user_queries?(entries)
113
+ entries.any?(&is_user_query)
114
+ end
115
+
116
+ def is_user_query
117
+ ->(entry) { entry.is_a?(Hash) and !!entry[:user_query] }
118
+ end
119
+
120
+ def pluck_autocomplete_keys(entries)
121
+ entries.map { |v| v[:key] }
122
+ end
123
+
124
+ def has_autocomplete_keys?(entries)
125
+ multiple? and entries.any? { |e| e.is_a?(Hash) }
126
+ end
127
+
128
+ def has_combinator?
129
+ not attributes[:and].nil?
130
+ end
131
+
132
+ def prefix?
133
+ !!prefix
134
+ end
135
+
136
+ def exclusion?
137
+ not attributes[:not].nil?
138
+ end
139
+
140
+ def anded?
141
+ has_combinator? and !!self.and
142
+ end
143
+
144
+ def multiple?
145
+ not single?
146
+ end
147
+
148
+ def not?
149
+ !!self.not
150
+ end
151
+
152
+ def from?
153
+ !!self.from
154
+ end
155
+
156
+ def to?
157
+ !!self.to
158
+ end
159
+
160
+ def from_eq?
161
+ !!self.from_eq
162
+ end
163
+
164
+ def to_eq?
165
+ !!self.to_eq
166
+ end
167
+
168
+ def to_prefix_query(vals)
169
+ if has_combinator?
170
+ to_combinator_query(vals)
171
+ else
172
+ {runtime_query_name => vals}
173
+ end
174
+ end
175
+
176
+ def to_exclusion_query(vals)
177
+ if not?
178
+ {runtime_query_name => {not: vals}}
179
+ else
180
+ {runtime_query_name => vals}
181
+ end
182
+ end
183
+
184
+ def to_combinator_query(vals, query_name_override = nil)
185
+ if anded?
186
+ {runtime_query_name => {all: vals}}
187
+ else
188
+ {runtime_query_name => vals}
189
+ end
190
+ end
191
+
192
+ def to_range_query
193
+ hash = {}
194
+ hash.merge!(gte: from_eq) if from_eq?
195
+ hash.merge!(gt: from) if from?
196
+ hash.merge!(lte: to_eq) if to_eq?
197
+ hash.merge!(lt: to) if to?
198
+
199
+ {runtime_query_name => hash}
200
+ end
201
+
202
+ end
203
+ end