elasticsearch-facetedsearch 0.0.4

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: 1e6625292febfa91f3b4c88c2664eab607d2edf3
4
+ data.tar.gz: 23929ddb68d398a41a4dc49fb55f6f8fd5d1504b
5
+ SHA512:
6
+ metadata.gz: 8614e10f04da9baf552148827245a2bd93a1e66974155a844209ca97717381a2e5effa84cf3a2465c828a02357abfae6d392a6a8d9ae5a6ff38f646cc8be0dd3
7
+ data.tar.gz: 3e9b1ef954bb8d7931549eff70b8e81fd3520a40211524b9aea094ce630bbae197ab4079fc03257c8a56234b2cd3f05af8853922cb2331a7ed472c8eef0b8a7b
@@ -0,0 +1,18 @@
1
+ # 0.0.4 - hotfix
2
+
3
+ - Pagination was occationally missing a page due to a non-float
4
+
5
+ # 0.0.3 - hotfix
6
+
7
+ - Added spec due to improper merge on 0.0.2 tag
8
+ - Fixed bug with bad hash merge on :facet_filter parameter
9
+
10
+ # 0.0.2
11
+
12
+ - Allow facets to keep counts regardless of filters applied
13
+ - Updating readme to reflect this capability
14
+
15
+ # 0.0.1
16
+
17
+ - Initial release
18
+ - Contains full test suite
@@ -0,0 +1,216 @@
1
+ # Elasticsearch::FacetedSearch
2
+
3
+ [![Code Climate](https://codeclimate.com/github/spodlecki/elasticsearch-facetedsearch/badges/gpa.svg)](https://codeclimate.com/github/spodlecki/elasticsearch-facetedsearch)
4
+ [![Test Coverage](https://codeclimate.com/github/spodlecki/elasticsearch-facetedsearch/badges/coverage.svg)](https://codeclimate.com/github/spodlecki/elasticsearch-facetedsearch/coverage)
5
+ [![Build Status](https://travis-ci.org/spodlecki/elasticsearch-facetedsearch.svg)](https://travis-ci.org/spodlecki/elasticsearch-facetedsearch)
6
+
7
+ Quickly add faceted searching to your Rails app. This gem is opinionated as to how faceted searching works. **Filters are applied to the counts** so the counts themselves will change while different filters are applied.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'elasticsearch-facetedsearch'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Create `config/initializers/elasticsearch.rb`. We normally namespace our indexed like below.
20
+
21
+ ELASTICSEARCH_INDEX = [
22
+ Rails.env.development? && `whoami`.strip,
23
+ Rails.env,
24
+ Rails.application.class.to_s.split("::").first.downcase
25
+ ].reject(&:blank?).join('_')
26
+
27
+ # Optional concepts to help with indexing / connection
28
+ ELASTICSEARCH_MODELS = []
29
+ ELASTICSEARCH_SERVER = 'http://lalaland.com:9200'
30
+
31
+ Elasticsearch::Model.client = Elasticsearch::Client.new({
32
+ log: false,
33
+ host: ELASTICSEARCH_SERVER,
34
+ retry_on_failure: 5,
35
+ reload_connections: true
36
+ })
37
+
38
+ ## Dependencies
39
+
40
+ - [Elasticsearch::Model](https://github.com/elastic/elasticsearch-rails/tree/master/elasticsearch-model)
41
+ - [Rails > 3.2.8](http://rubyonrails.org/)
42
+ - [Elasticsearch Server > 1.0.1](http://www.elastic.co)
43
+
44
+ ## Usage
45
+
46
+ ### Controller
47
+
48
+ # Good idea to prefilter the params with strong_params
49
+ #
50
+ def search
51
+ @search = FruitFacetsSearch.new(params)
52
+
53
+ # Fetch results
54
+ # @search.results
55
+
56
+ # Fetch facets
57
+ # @search.facets
58
+ end
59
+
60
+ ### Facet Search Class
61
+
62
+ class FruitFacetsSearch
63
+ include Elasticsearch::FacetedSearch::FacetBase
64
+
65
+ # ... include other facet classes (examples below) ...
66
+ include Elasticsearch::FacetedSearch::FacetColor
67
+
68
+ # *required
69
+ # The type to search for (Elasticsearch Type)
70
+ # Can be an Array or String
71
+ def type
72
+ 'fruit'
73
+ end
74
+
75
+ # Use this to add a query search or something
76
+ # Probably best to cache the results
77
+ def query
78
+ @query ||= super
79
+ end
80
+
81
+ # Apply additional pre-filters
82
+ # If overwriting this method, ensure to call super, and ensure to cache the results
83
+ # => require color to be 'blue'
84
+ def filter_query
85
+ @filter_query ||= begin
86
+ fq = super
87
+ fq << { term: { color: 'blue' } }
88
+ fq
89
+ end
90
+ end
91
+
92
+ # Force specific limit or allow changable #s
93
+ #
94
+ def limit
95
+ 33
96
+ end
97
+
98
+ # Whitelisted collection of sortable options
99
+ def sorts
100
+ [
101
+ {
102
+ label: "Relevant",
103
+ value: "relevant",
104
+ search: [
105
+ "_score"
106
+ ],
107
+ default: true
108
+ }
109
+ ]
110
+ end
111
+
112
+ # Want to always keep facet counts the same regardless of filters applied?
113
+ # pass true to keep counts scoped to search, & false to remove filters entirely
114
+ #
115
+ def build_facets
116
+ super(false)
117
+ end
118
+ end
119
+
120
+ ### Facet Creation Class
121
+
122
+ module Elasticsearch
123
+ module FacetedSearch
124
+ module FacetColor
125
+ extend ActiveSupport::Concern
126
+
127
+ included do
128
+ # Adds the facet to the class.facets collection
129
+ #
130
+ # Available types:
131
+ # => facet_multivalue
132
+ # => facet_multivalue_and(:ident, 'elasticsearch_field', 'Human String')
133
+ # - Allows multiple values, but filters with :and execution
134
+ # => facet_multivalue_or(:ident, 'elasticsearch_field', 'Human String')
135
+ # - Allows multiple values, but filters with :or execution
136
+ # => facet_exclusive_or(:ident, 'elasticsearch_field', 'Human String')
137
+ # - Allows single value only
138
+ #
139
+ facet_multivalue_or(:color, 'color_field', 'Skin Color')
140
+ end
141
+
142
+ # *required
143
+ # Should we apply the filter for this facet?
144
+ # __Replace 'color' with the :ident value of your facet key
145
+ def filter_color?
146
+ valid_color?
147
+ end
148
+
149
+ # *required
150
+ # Returns the array of selected values
151
+ # __Replace 'color' with the :ident value of your facet key
152
+ # __You should really take this time to whitelist the values and remove any noise. Elasticsearch can be picky if you're searching a number field and pass it an alpha character
153
+ def color_value
154
+ return unless valid_color?
155
+ search_params[:color].split( operator_for(:color) )
156
+ end
157
+
158
+ # (optional)
159
+ # By default, Elasticsearch only returns terms that are pertaining to the specific search. If a result was filtered out, that term would not show up.
160
+ # Normally this isn't optimal... create this method and return an array of hashes with id and term keys.
161
+ #
162
+ # __Replace 'color' with the :ident value of your facet key
163
+ def color_collection
164
+ @color_collection ||= begin
165
+ Color.all.map{|x| {id: x.id, term: x.name}}
166
+ end
167
+ end
168
+
169
+ # (concept)
170
+ # Use these type of helper methods to validate the information given to you by users
171
+ #
172
+ def valid_color?
173
+ search_params[:color].present? && !!(search_params[:color] =~ /[\|[0-9]+]/)
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ ### Using Sortable
180
+
181
+ The only requirement to change sort options is to apply a `:sort` http param
182
+
183
+ ### Pagniation
184
+
185
+ Pagination is supported, but only tested with Kaminari.
186
+
187
+ ### HTML
188
+
189
+ **Facets**
190
+
191
+ %ul
192
+ -@search.facets.each do |group|
193
+ %li.title=group.title
194
+ -group.items.each do |item|
195
+ - # Assuming you have some dynamic urls, you can use the url_for and merge in the params
196
+ %li=item.link_to("#{item.term} (#{item.count})", url_for(params.merge(item.params_for)))
197
+
198
+ **Results**
199
+
200
+ %ul
201
+ -@search.results.each do |item|
202
+ - # item is now direct reference to the elastic search _source
203
+ - # item also contains item._type that displays the Elasticsearch type field
204
+ %li=item.id
205
+
206
+ ## Contributing
207
+
208
+ 1. Fork it
209
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
210
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
211
+ 4. Push to the branch (`git push origin my-new-feature`)
212
+ 5. Create new Pull Request
213
+
214
+ ## TODO
215
+
216
+ - Setup facet generator
@@ -0,0 +1,8 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new('spec')
4
+
5
+ # If you want to make this the default task
6
+ task :default => :spec
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1,15 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+ require 'elasticsearch/model'
4
+ require "elasticsearch/faceted_search/version"
5
+ require "elasticsearch/faceted_search/pagination"
6
+ require "elasticsearch/faceted_search/sortable"
7
+ require "elasticsearch/faceted_search/facet_base"
8
+
9
+ require "elasticsearch/faceted_search/facet_group"
10
+ require "elasticsearch/faceted_search/facet_item"
11
+
12
+ module Elasticsearch
13
+ module FacetedSearch
14
+ end
15
+ end
@@ -0,0 +1,184 @@
1
+ require 'ostruct'
2
+ require 'elasticsearch/model'
3
+
4
+ module Elasticsearch
5
+ module FacetedSearch
6
+ module FacetBase
7
+ include Pagination
8
+ include Sortable
9
+
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ extend ClassMethods
14
+ attr_accessor :params
15
+
16
+ def initialize(p)
17
+ self.params = p
18
+ end
19
+
20
+ def results
21
+ @results ||= search['hits']['hits'].map{|x| Hashie::Mash.new(x['_source'].merge('_type' => x['_type'])) }
22
+ end
23
+
24
+ def facets
25
+ @facets ||= search['facets'].map{|key, values| FacetGroup.new(self, key, values) }
26
+ end
27
+
28
+ def search
29
+ @search ||= Elasticsearch::Model.client.search({
30
+ index: ELASTICSEARCH_INDEX,
31
+ type: type,
32
+ body: query
33
+ })
34
+ end
35
+ end
36
+
37
+ ## self <<
38
+ module ClassMethods
39
+
40
+ def facets
41
+ @facets || []
42
+ end
43
+
44
+ def facet(name, field, type, title)
45
+ @facets ||= {}
46
+ @facets.merge!({
47
+ name.to_sym => {
48
+ field: field,
49
+ type: type,
50
+ title: title
51
+ }
52
+ })
53
+ end
54
+
55
+ def facet_multivalue(name, field, title = nil)
56
+ facet(name, field, 'multivalue', title)
57
+ end
58
+
59
+ def facet_multivalue_and(name, field, title = nil)
60
+ facet(name, field, 'multivalue_and', title)
61
+ end
62
+
63
+ def facet_multivalue_or(name, field, title = nil)
64
+ facet(name, field, 'multivalue_or', title)
65
+ end
66
+
67
+ def facet_exclusive_or(name, field, title = nil)
68
+ facet(name, field, 'exclusive_or', title)
69
+ end
70
+ end
71
+ # / self
72
+
73
+ ##############
74
+ # Instance
75
+ #
76
+ def class_facets
77
+ self.class.facets.dup
78
+ end
79
+
80
+ def query
81
+ q = {
82
+ size: limit,
83
+ from: ([current_page.to_i, 1].max - 1) * limit,
84
+ sort: current_sort_for_search
85
+ }
86
+
87
+ # Filters
88
+ q.merge!({
89
+ :filter => {
90
+ :and => filter_query
91
+ }
92
+ }) unless filter_query.blank?
93
+
94
+ # Facets
95
+ q.merge!({
96
+ :facets => facet_query
97
+ }) unless facet_query.blank?
98
+
99
+ q.reject{|k,v| v.blank? }
100
+ end
101
+
102
+ def facet_query
103
+ @facet_query ||= build_facets
104
+ end
105
+
106
+ def filter_query
107
+ @filter_query ||= build_filters
108
+ end
109
+
110
+ def search_params
111
+ params
112
+ end
113
+
114
+ protected
115
+
116
+ def operator_mappings(i=nil)
117
+ FacetGroup::OPERATOR_MAPPING.fetch(i, nil)
118
+ end
119
+
120
+ def operator_for(key)
121
+ operator_mappings(
122
+ execution_type(
123
+ class_facets[key][:type]
124
+ )
125
+ )
126
+ end
127
+
128
+ def execution_type(type)
129
+ case type
130
+ when 'multivalue'
131
+ # TODO: Based off params
132
+ when 'multivalue_or'
133
+ :or
134
+ when 'multivalue_and'
135
+ :and
136
+ when 'exclusive_or'
137
+ nil
138
+ end
139
+ end
140
+
141
+ def facet_size_allowed
142
+ 70
143
+ end
144
+
145
+ private
146
+
147
+ def build_facets(filter_counts=true)
148
+ h = {}
149
+ filtered_facets = filter_counts && filter_query.present? ? {facet_filter: { :and => filter_query }} : {}
150
+
151
+ class_facets.each do |k,v|
152
+ h.merge!({
153
+ k => {
154
+ terms: {
155
+ field: v[:field],
156
+ size: facet_size_allowed
157
+ }
158
+ }.merge(filtered_facets).reject{|k,v| v.blank?}
159
+ })
160
+ end
161
+
162
+ h
163
+ end
164
+
165
+ # Whitelist and filter
166
+ def build_filters
167
+ class_facets.map do |type, info|
168
+ if respond_to?(:"filter_#{type}?") and public_send("filter_#{type}?")
169
+ {
170
+ terms: {
171
+ info[:field] => values_for(type),
172
+ :execution => execution_type(info[:type])
173
+ }.reject{|k,v| v.blank?}
174
+ }
175
+ end
176
+ end.compact
177
+ end
178
+
179
+ def values_for(facet_type)
180
+ Array(public_send("#{facet_type}_value"))
181
+ end
182
+ end
183
+ end
184
+ end