sunspot_stat 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/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +64 -0
- data/Rakefile +2 -0
- data/lib/dsl/field_query.rb +22 -0
- data/lib/query/field_stat.rb +31 -0
- data/lib/query/stat_query.rb +12 -0
- data/lib/search/stat_facet.rb +50 -0
- data/lib/search/stat_row.rb +28 -0
- data/lib/search/stat_search.rb +68 -0
- data/lib/sunspot_stat/version.rb +3 -0
- data/lib/sunspot_stats.rb +15 -0
- data/spec/api/query/spec_helper.rb +1 -0
- data/spec/api/query/stats_spec.rb +16 -0
- data/spec/api/search/spec_helper.rb +1 -0
- data/spec/api/search/stats_spec.rb +13 -0
- data/spec/api/spec_helper.rb +3 -0
- data/spec/ext.rb +11 -0
- data/spec/helpers/indexer_helper.rb +17 -0
- data/spec/helpers/integration_helper.rb +8 -0
- data/spec/helpers/mock_session_helper.rb +13 -0
- data/spec/helpers/query_helper.rb +26 -0
- data/spec/helpers/search_helper.rb +55 -0
- data/spec/mocks/adapters.rb +32 -0
- data/spec/mocks/connection.rb +126 -0
- data/spec/mocks/content.rb +12 -0
- data/spec/mocks/mock_adapter.rb +30 -0
- data/spec/mocks/mock_record.rb +52 -0
- data/spec/mocks/super_class.rb +2 -0
- data/spec/spec_helper.rb +41 -0
- data/sunspot_stat.gemspec +25 -0
- data/sunspot_stats-0.0.7.gem +0 -0
- metadata +113 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 40d727eb2de555417b4261763ac814f7187c0af8ca7a76e516a5352167c58890
|
|
4
|
+
data.tar.gz: 2bb4e8d14181b8c4bf2fbe40e18bec2f5b2a3c9d5508d1b055bd04b580930838
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c92b7a2fd614dacda57177b1f1a134fc449088c307d2f9eaa72a94e30ff7de25c6d98ae897fe4f1131b7e8521a36314f0d287d47b430116a9a6cd030e7022b68
|
|
7
|
+
data.tar.gz: fd689b14bf56bca7c227fa64583c3227382ce6624e40bb1caf35a86023b7f7484f4ec44c661745b88810689f75bfd5887a15ca645919ff2a01967a4494e8d8fa
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2012 Duccio Giovannelli
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Sunspot Stats
|
|
2
|
+
|
|
3
|
+
Sunspot is a Ruby library for expressive, powerful interaction with the Solr search engine. I use the latest version [3.4.1](http://rubygems.org/gems/sunspot/versions/2.1.0) but i need the statsComponent in order to get the sum (min, max, count, sumOfSquares, mean, stddev) on a given indexed field.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
gem 'sunspot_stat'
|
|
10
|
+
|
|
11
|
+
And then execute:
|
|
12
|
+
|
|
13
|
+
$ bundle
|
|
14
|
+
|
|
15
|
+
Or install it yourself as:
|
|
16
|
+
|
|
17
|
+
$ gem install sunspot_stat
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
The statComponent let you to have *min*, *max*, *count*, *missing*, *sum*, *sumOfsquare*, *mean* and *stdev*.
|
|
22
|
+
|
|
23
|
+
### An usage example
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
Model.search do
|
|
27
|
+
#Filters
|
|
28
|
+
...
|
|
29
|
+
#Facets
|
|
30
|
+
...
|
|
31
|
+
#Stats
|
|
32
|
+
stat(:your_field, :facet => :your_facet_filed, :type => "min")
|
|
33
|
+
...
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**:facet** and **:type** are optional the default type is the "sum" (you can pass: *min*, *max*, *count*, *missing*, *sum*, *sumOfsquare*, *mean*, *stdev* as string)
|
|
38
|
+
|
|
39
|
+
Remember to stats on a non multi valued field and update to the solr3.6, because the solr3.5 has a bug with stats.
|
|
40
|
+
|
|
41
|
+
the Sunspot::SearchStatRow has the method *stat_field* and the *value*.
|
|
42
|
+
|
|
43
|
+
If you enable faceting on stats the stat_field is the facet and the value is the type defined.
|
|
44
|
+
|
|
45
|
+
[Upgrade solr in sunspot](https://github.com/sunspot/sunspot/wiki/Upgrading-sunspot_solr-Solr-Instance)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
### StatsComponent Notes
|
|
49
|
+
|
|
50
|
+
The facet field can be selectively applied. That is if you want stats on field "A" and "B", you can facet a on "X" and B on "Y" using &stats.field=A&f.A.stats.facet=X&stats.field=B&f.B.stats.facet=Y
|
|
51
|
+
|
|
52
|
+
*Warning*, as implemented, all facet results are returned, be careful what fields you ask for!
|
|
53
|
+
|
|
54
|
+
Multi-valued fields and facets may be slow.
|
|
55
|
+
|
|
56
|
+
[StatsComponent](http://wiki.apache.org/solr/StatsComponent)
|
|
57
|
+
|
|
58
|
+
## Contributing
|
|
59
|
+
|
|
60
|
+
1. Fork it
|
|
61
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
62
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
|
63
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
64
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Sunspot
|
|
2
|
+
module DSL
|
|
3
|
+
#
|
|
4
|
+
# Provides an API for areas of the query DSL that operate on specific
|
|
5
|
+
# fields. This functionality is provided by the query DSL and the dynamic
|
|
6
|
+
# query DSL.
|
|
7
|
+
#
|
|
8
|
+
class FieldQuery < Scope
|
|
9
|
+
def initialize(search, query, setup) #:nodoc:
|
|
10
|
+
@search, @query = search, query
|
|
11
|
+
super(query.scope, setup)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def stat(field_name, options = {})
|
|
15
|
+
field = @setup.field(field_name)
|
|
16
|
+
options[:facet] = @setup.field(options[:facet]) if !options[:facet].nil?
|
|
17
|
+
stat = @query.add_stat(Sunspot::Query::FieldStat.new(field, options))
|
|
18
|
+
@search.add_field_stat(field, options)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Sunspot
|
|
2
|
+
module Query
|
|
3
|
+
#
|
|
4
|
+
# A FieldStat stats by the unique values of a given field.
|
|
5
|
+
#
|
|
6
|
+
class FieldStat
|
|
7
|
+
|
|
8
|
+
def initialize(field, options = {})
|
|
9
|
+
if field.multiple?
|
|
10
|
+
raise(ArgumentError, "#{field.name} cannot be used for stats because it is a multiple-value field")
|
|
11
|
+
end
|
|
12
|
+
@field = field
|
|
13
|
+
@options = options
|
|
14
|
+
@sort = SortComposite.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add_sort(sort)
|
|
18
|
+
@sort << sort
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_params
|
|
22
|
+
params = {
|
|
23
|
+
:stats => 'true',
|
|
24
|
+
:"stats.field" => @field.indexed_name
|
|
25
|
+
}
|
|
26
|
+
params.merge!({:"stats.facet" => @options[:facet].indexed_name}) if !@options[:facet].nil?
|
|
27
|
+
params
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Sunspot
|
|
2
|
+
module Search
|
|
3
|
+
class StatFacet
|
|
4
|
+
|
|
5
|
+
def initialize(field, search, options)
|
|
6
|
+
@field, @search, @options = field, search, options
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def field_name
|
|
10
|
+
@field.name
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def rows
|
|
14
|
+
#sort options :count or :stat_field
|
|
15
|
+
@options[:sort] ||= :count
|
|
16
|
+
@options[:type] ||= "sum"
|
|
17
|
+
@options[:limit] ||= -1
|
|
18
|
+
@sort = false
|
|
19
|
+
@rows ||=
|
|
20
|
+
begin
|
|
21
|
+
if @search.stat_response.present? && @search.stat_response['stats_fields'].present?
|
|
22
|
+
if @options[:facet].present?
|
|
23
|
+
stat = @search.stat_response['stats_fields'][@field.indexed_name]
|
|
24
|
+
data = stat.nil? ? [] : stat['facets'][@options[:facet].indexed_name]
|
|
25
|
+
@sort = true
|
|
26
|
+
else
|
|
27
|
+
data = @search.stat_response['stats_fields'].to_a
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
rows = []
|
|
32
|
+
|
|
33
|
+
data.collect do |stat, value|
|
|
34
|
+
rows << StatRow.new(stat, value[@options[:type]], value, self) if value.present?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if @options[:sort] == :count
|
|
38
|
+
rows.sort! { |lrow, rrow| rrow.value <=> lrow.value }
|
|
39
|
+
else
|
|
40
|
+
rows.sort! { |lrow, rrow| lrow.stat_field <=> rrow.stat_field }
|
|
41
|
+
end if @sort
|
|
42
|
+
rows.empty? ? [] : rows[0..@options[:limit]]
|
|
43
|
+
rescue Exception => e
|
|
44
|
+
puts "Sunspot Stats error: #{e.message} \n\n #{e.backtrace}"
|
|
45
|
+
return []
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Sunspot
|
|
2
|
+
module Search
|
|
3
|
+
class StatRow
|
|
4
|
+
attr_reader :stat_field, :value, :all_values
|
|
5
|
+
attr_writer :instance #:nodoc:
|
|
6
|
+
|
|
7
|
+
def initialize(stat_field, value, all_values, stat) #:nodoc:
|
|
8
|
+
@stat_field, @value, @all_values, @stat = stat_field, value, all_values, stat
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
#
|
|
12
|
+
# Return the instance referenced by this stat row. Only valid for field
|
|
13
|
+
# stats whose fields are defined with the :references key.
|
|
14
|
+
#
|
|
15
|
+
|
|
16
|
+
def instance
|
|
17
|
+
if !defined?(@instance)
|
|
18
|
+
@stat.populate_instances
|
|
19
|
+
end
|
|
20
|
+
@instance
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def inspect
|
|
24
|
+
"<Sunspot::Search::StatRow:#{stat_field.inspect} (#{value} - #{all_values})>"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require 'sunspot/search/paginated_collection'
|
|
2
|
+
require 'sunspot/search/hit_enumerable'
|
|
3
|
+
|
|
4
|
+
module Sunspot
|
|
5
|
+
module Search #:nodoc:
|
|
6
|
+
|
|
7
|
+
#
|
|
8
|
+
# This class encapsulates the results of a Solr search. It provides access
|
|
9
|
+
# to search results, total result count, facets, and pagination information.
|
|
10
|
+
# Instances of Search are returned by the Sunspot.search and
|
|
11
|
+
# Sunspot.new_search methods.
|
|
12
|
+
#
|
|
13
|
+
class AbstractSearch
|
|
14
|
+
#
|
|
15
|
+
# Retrieve all facet objects defined for this search, in order they were
|
|
16
|
+
# defined. To retrieve an individual facet by name, use #facet()
|
|
17
|
+
#
|
|
18
|
+
attr_reader :stats
|
|
19
|
+
|
|
20
|
+
def initialize(connection, setup, query, configuration) #:nodoc:
|
|
21
|
+
@connection, @setup, @query = connection, setup, query
|
|
22
|
+
@query.paginate(1, configuration.pagination.default_per_page)
|
|
23
|
+
|
|
24
|
+
@stats = []
|
|
25
|
+
@stats_by_name = {}
|
|
26
|
+
|
|
27
|
+
@facets = []
|
|
28
|
+
@facets_by_name = {}
|
|
29
|
+
|
|
30
|
+
@groups_by_name = {}
|
|
31
|
+
@groups = []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def stat(name)
|
|
36
|
+
if name
|
|
37
|
+
@stats_by_name[name.to_sym]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def stat_response #:nodoc:
|
|
42
|
+
@solr_result['stats']["stats_fields"].each_pair do |k, value|
|
|
43
|
+
if value && value.key?("facets")
|
|
44
|
+
value["facets"].each_pair do |k1, value1|
|
|
45
|
+
value1.each_pair do |k2, value2|
|
|
46
|
+
if @solr_result['stats']['stats_fields'][k]['facets'][k1][k2]['mean'].to_s == 'NaN'
|
|
47
|
+
@solr_result['stats']['stats_fields'][k]['facets'][k1][k2]['mean'] = 0.0
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
@solr_result['stats']||[]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def add_field_stat(field, options = {}) #:nodoc:
|
|
57
|
+
name = (options[:name] || field.name)
|
|
58
|
+
add_stat(name, StatFacet.new(field, self, options))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def add_stat(name, stat)
|
|
62
|
+
@stats << stat
|
|
63
|
+
@stats_by_name[name.to_sym] = stat
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require "sunspot_stat/version"
|
|
2
|
+
|
|
3
|
+
%w(field_query).each do |file|
|
|
4
|
+
require File.join(File.dirname(__FILE__), "dsl", file)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
%w(stat_search stat_row stat_facet).each do |file|
|
|
9
|
+
require File.join(File.dirname(__FILE__), "search", file)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
%w(stat_query field_stat).each do |file|
|
|
13
|
+
require(File.join(File.dirname(__FILE__), 'query', file))
|
|
14
|
+
end
|
|
15
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require File.expand_path('spec_helper', File.join(File.dirname(__FILE__), '..'))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require File.expand_path('spec_helper', File.dirname(__FILE__))
|
|
2
|
+
|
|
3
|
+
describe "stats component" do
|
|
4
|
+
|
|
5
|
+
it "sends stats parameters to solr" do
|
|
6
|
+
|
|
7
|
+
session.search Content do
|
|
8
|
+
stat :visibility, :facet => :published_at
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
connection.should have_last_search_including(:stats, "true")
|
|
12
|
+
connection.should have_last_search_including(:"stats.field", "visibility_f")
|
|
13
|
+
connection.should have_last_search_including(:"stats.facet", "published_at_d")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require File.expand_path('spec_helper', File.join(File.dirname(__FILE__), '..'))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require File.expand_path('spec_helper', File.dirname(__FILE__))
|
|
2
|
+
|
|
3
|
+
describe 'stats', :type => :search do
|
|
4
|
+
it 'returns field name for facet' do
|
|
5
|
+
#stub_stat(:visibility)
|
|
6
|
+
result = session.search Content do
|
|
7
|
+
stat :visibility, :facet => :published_at
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
result.stat(:visibility).field_name.should == :visibility
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
end
|
data/spec/ext.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module IndexerHelper
|
|
2
|
+
def post(attrs = {})
|
|
3
|
+
@post ||= Post.new(attrs)
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def last_add
|
|
7
|
+
@connection.adds.last
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def value_in_last_document_for(field_name)
|
|
11
|
+
@connection.adds.last.last.field_by_name(field_name).value
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def values_in_last_document_for(field_name)
|
|
15
|
+
@connection.adds.last.last.fields_by_name(field_name).map { |field| field.value }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module QueryHelper
|
|
2
|
+
def get_filter_tag(boolean_query)
|
|
3
|
+
connection.searches.last[:fq].each do |fq|
|
|
4
|
+
if match = fq.match(/^\{!tag=(.+)\}#{Regexp.escape(boolean_query)}$/)
|
|
5
|
+
return match[1]
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def subqueries(param)
|
|
12
|
+
q = connection.searches.last[:q]
|
|
13
|
+
subqueries = []
|
|
14
|
+
subqueries = q.scan(%r(_query_:"\{!dismax (.*?)\}(.*?)"))
|
|
15
|
+
subqueries.map do |subquery|
|
|
16
|
+
params = {}
|
|
17
|
+
subquery[0].scan(%r((\S+?)='(.+?)')) do |key, value|
|
|
18
|
+
params[key.to_sym] = value
|
|
19
|
+
end
|
|
20
|
+
unless subquery[1].empty?
|
|
21
|
+
params[:v] = subquery[1]
|
|
22
|
+
end
|
|
23
|
+
params
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module SearchHelper
|
|
2
|
+
def stub_nil_results
|
|
3
|
+
connection.response = { 'response' => nil }
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def stub_full_results(*results)
|
|
7
|
+
count =
|
|
8
|
+
if results.last.is_a?(Integer) then results.pop
|
|
9
|
+
else results.length
|
|
10
|
+
end
|
|
11
|
+
docs = results.map do |result|
|
|
12
|
+
instance = result.delete('instance')
|
|
13
|
+
result.merge('id' => "#{instance.class.name} #{instance.id}")
|
|
14
|
+
end
|
|
15
|
+
response = {
|
|
16
|
+
'response' => {
|
|
17
|
+
'docs' => docs,
|
|
18
|
+
'numFound' => count
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
connection.response = response
|
|
22
|
+
response
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def stub_results(*results)
|
|
26
|
+
stub_full_results(
|
|
27
|
+
*results.map do |result|
|
|
28
|
+
if result.is_a?(Integer)
|
|
29
|
+
result
|
|
30
|
+
else
|
|
31
|
+
{ 'instance' => result }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def stub_stat(name, values)
|
|
38
|
+
connection.response = {
|
|
39
|
+
'facet_counts' => {
|
|
40
|
+
'facet_fields' => {
|
|
41
|
+
name.to_s => values.to_a.sort_by { |value, count| -count }.flatten
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def stat_field_name(result, field_name)
|
|
48
|
+
result.stat(field_name).rows.map { |row| row.field_name }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def values(result, field_name)
|
|
52
|
+
result.stat(field_name).rows.map { |row| row.value }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class AbstractModel
|
|
2
|
+
end
|
|
3
|
+
|
|
4
|
+
class Model < AbstractModel
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class AbstractModelInstanceAdapter < Sunspot::Adapters::InstanceAdapter
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class AbstractModelDataAccessor < Sunspot::Adapters::DataAccessor
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
Sunspot::Adapters::InstanceAdapter.register(AbstractModelInstanceAdapter, AbstractModel)
|
|
14
|
+
Sunspot::Adapters::DataAccessor.register(AbstractModelDataAccessor, AbstractModel)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
module MixInModel
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class MixModel
|
|
21
|
+
include MixInModel
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class MixInModelInstanceAdapter < Sunspot::Adapters::InstanceAdapter
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class MixInModelDataAccessor < Sunspot::Adapters::DataAccessor
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Sunspot::Adapters::InstanceAdapter.register(MixInModelInstanceAdapter, MixInModel)
|
|
31
|
+
Sunspot::Adapters::DataAccessor.register(MixInModelDataAccessor, MixInModel)
|
|
32
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
module Mock
|
|
2
|
+
class ConnectionFactory
|
|
3
|
+
def connect(opts)
|
|
4
|
+
if @instance
|
|
5
|
+
raise('Factory can only create an instance once!')
|
|
6
|
+
else
|
|
7
|
+
@instance = Connection.new(opts)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def new(url = nil)
|
|
12
|
+
if @instance
|
|
13
|
+
raise('Factory can only create an instance once!')
|
|
14
|
+
else
|
|
15
|
+
@instance = Connection.new(url)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def instance
|
|
20
|
+
@instance ||= Connection.new
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Connection
|
|
25
|
+
attr_reader :adds, :commits, :optims, :searches, :message, :opts, :deletes_by_query
|
|
26
|
+
attr_accessor :response
|
|
27
|
+
attr_writer :expected_handler
|
|
28
|
+
undef_method :select # annoyingly defined on Object
|
|
29
|
+
|
|
30
|
+
def initialize(opts = {})
|
|
31
|
+
@opts = opts
|
|
32
|
+
@message = OpenStruct.new
|
|
33
|
+
@adds, @deletes, @deletes_by_query, @commits, @optims, @searches = Array.new(6) { [] }
|
|
34
|
+
@expected_handler = :select
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def add(documents)
|
|
38
|
+
@adds << Array(documents)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def delete_by_id(ids)
|
|
42
|
+
@deletes << Array(ids)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def delete_by_query(query)
|
|
46
|
+
@deletes_by_query << query
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def commit
|
|
50
|
+
@commits << Time.now
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def optimize
|
|
54
|
+
@optims << Time.now
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def post(path, params)
|
|
58
|
+
unless path == "#{@expected_handler}"
|
|
59
|
+
raise ArgumentError, "Expected request to #{@expected_handler} request handler"
|
|
60
|
+
end
|
|
61
|
+
@searches << @last_search = params[:data]
|
|
62
|
+
@response || {}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def method_missing(method, *args, &block)
|
|
66
|
+
get("#{method}", *args)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def has_add_with?(*documents)
|
|
70
|
+
@adds.any? do |add|
|
|
71
|
+
documents.all? do |document|
|
|
72
|
+
add.any? do |added|
|
|
73
|
+
if document.is_a?(Hash)
|
|
74
|
+
document.all? do |field, value|
|
|
75
|
+
added.fields_by_name(field).map do |field|
|
|
76
|
+
field.value.to_s
|
|
77
|
+
end == Array(value).map { |v| v.to_s }
|
|
78
|
+
end
|
|
79
|
+
else
|
|
80
|
+
!added.fields_by_name(document).empty?
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def has_delete?(*ids)
|
|
88
|
+
@deletes.any? do |delete|
|
|
89
|
+
delete & ids == ids
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def has_delete_by_query?(query)
|
|
94
|
+
@deletes_by_query.include?(query)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def has_last_search_with?(params)
|
|
98
|
+
with?(@last_search, params) if @last_search
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def has_last_search_including?(key, *values)
|
|
102
|
+
return unless @last_search
|
|
103
|
+
if @last_search.has_key?(key)
|
|
104
|
+
if @last_search[key].is_a?(Array)
|
|
105
|
+
(@last_search[key] & values).length == values.length
|
|
106
|
+
elsif values.length == 1
|
|
107
|
+
@last_search[key] == values.first
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def with?(request, params)
|
|
115
|
+
if params.respond_to?(:all?)
|
|
116
|
+
params.all? do |key, value|
|
|
117
|
+
if request.has_key?(key)
|
|
118
|
+
request[key] == value
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
request.has_key?(params)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'content')
|
|
2
|
+
|
|
3
|
+
module MockAdapter
|
|
4
|
+
class InstanceAdapter < Sunspot::Adapters::InstanceAdapter
|
|
5
|
+
def id
|
|
6
|
+
@instance.id
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class DataAccessor < Sunspot::Adapters::DataAccessor
|
|
11
|
+
def load(id)
|
|
12
|
+
@clazz.get(id.to_i)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def load_all(ids)
|
|
16
|
+
all = @clazz.get_all(ids.map { |id| id.to_i })
|
|
17
|
+
if @custom_title
|
|
18
|
+
all.each { |item| item.title = @custom_title }
|
|
19
|
+
end
|
|
20
|
+
all
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def custom_title=(custom_title)
|
|
24
|
+
@custom_title = custom_title
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Sunspot::Adapters::DataAccessor.register(MockAdapter::DataAccessor, MockRecord)
|
|
30
|
+
Sunspot::Adapters::InstanceAdapter.register(MockAdapter::InstanceAdapter, MockRecord)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
class MockRecord
|
|
2
|
+
IDS = Hash.new { |h, k| h[k] = 0 }
|
|
3
|
+
QUERY_COUNTS = Hash.new { |h, k| h[k] = 0 }
|
|
4
|
+
INSTANCES = Hash.new { |h, k| h[k] = {} }
|
|
5
|
+
|
|
6
|
+
attr_reader :id
|
|
7
|
+
|
|
8
|
+
class <<self
|
|
9
|
+
def reset!
|
|
10
|
+
IDS[name.to_sym] = 0
|
|
11
|
+
INSTANCES[name.to_sym] = {}
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(attrs = {})
|
|
16
|
+
@id = attrs.delete(:id) || IDS[self.class.name.to_sym] += 1
|
|
17
|
+
INSTANCES[self.class.name.to_sym][@id] = self
|
|
18
|
+
attrs.each_pair do |name, value|
|
|
19
|
+
send(:"#{name}=", value)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.inherited(base)
|
|
24
|
+
base.extend(ClassMethods)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module ClassMethods
|
|
28
|
+
def get(id)
|
|
29
|
+
QUERY_COUNTS[self.name.to_sym] += 1
|
|
30
|
+
get_instance(id)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def get_all(ids)
|
|
34
|
+
QUERY_COUNTS[self.name.to_sym] += 1
|
|
35
|
+
ids.map { |id| get_instance(id) }.compact.sort_by { |instance| instance.id }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def query_count
|
|
39
|
+
QUERY_COUNTS[self.name.to_sym]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def get_instance(id)
|
|
45
|
+
INSTANCES[self.name.to_sym][id]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def destroy
|
|
50
|
+
INSTANCES[self.class.name.to_sym].delete(@id)
|
|
51
|
+
end
|
|
52
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
require 'ostruct'
|
|
4
|
+
require 'sunspot'
|
|
5
|
+
require 'sunspot_stat'
|
|
6
|
+
|
|
7
|
+
require File.join(File.dirname(__FILE__), 'mocks', 'mock_record.rb')
|
|
8
|
+
Dir.glob(File.join(File.dirname(__FILE__), 'mocks', '**', '*.rb')).each do |file|
|
|
9
|
+
require file unless File.basename(file) == 'mock_record.rb'
|
|
10
|
+
end
|
|
11
|
+
Dir.glob(File.join(File.dirname(__FILE__), "helpers", "*.rb")).each do |file|
|
|
12
|
+
require file
|
|
13
|
+
end
|
|
14
|
+
require File.join(File.dirname(__FILE__), 'ext')
|
|
15
|
+
|
|
16
|
+
RSpec.configure do |config|
|
|
17
|
+
# Mock session available to all spec/api tests
|
|
18
|
+
config.include MockSessionHelper,
|
|
19
|
+
:type => :api,
|
|
20
|
+
:example_group => {:file_path => /spec[\\\/]api/}
|
|
21
|
+
|
|
22
|
+
# Real Solr instance is available to integration tests
|
|
23
|
+
config.include IntegrationHelper,
|
|
24
|
+
:type => :integration,
|
|
25
|
+
:example_group => {:file_path => /spec[\\\/]integration/}
|
|
26
|
+
|
|
27
|
+
# Nested under spec/api
|
|
28
|
+
[:indexer, :query, :search].each do |spec_type|
|
|
29
|
+
helper_name = "#{spec_type}_helper"
|
|
30
|
+
|
|
31
|
+
config.include Sunspot::Util.full_const_get(Sunspot::Util.camel_case(helper_name)),
|
|
32
|
+
:type => spec_type,
|
|
33
|
+
:example_group => {:file_path => /spec[\\\/]api[\\\/]#{spec_type}/}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def without_class(clazz)
|
|
38
|
+
Object.class_eval { remove_const(clazz.name.to_sym) }
|
|
39
|
+
yield
|
|
40
|
+
Object.class_eval { const_set(clazz.name.to_sym, clazz) }
|
|
41
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
require File.expand_path('../lib/sunspot_stat/version', __FILE__)
|
|
3
|
+
|
|
4
|
+
Gem::Specification.new do |gem|
|
|
5
|
+
gem.authors = ["nitin rakesh"]
|
|
6
|
+
gem.email = ["nitinrak.25s@gmail.com"]
|
|
7
|
+
gem.description = <<-TEXT
|
|
8
|
+
Sunspot is a library providing a powerful, all-ruby API for the Solr search engine. This gem extend sunspot adding the
|
|
9
|
+
statsComponent feature, which returns simple statistics for indexed numeric fields within the DocSet.
|
|
10
|
+
TEXT
|
|
11
|
+
gem.summary = "Added the statsComponent to sunspot"
|
|
12
|
+
gem.homepage = "https://github.com/nitinrakesh-332/sunspot_stat"
|
|
13
|
+
gem.files = `git ls-files`.split($\)
|
|
14
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
|
15
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
16
|
+
gem.name = "sunspot_stat"
|
|
17
|
+
gem.require_paths = ["lib"]
|
|
18
|
+
gem.version = SunspotStat::VERSION
|
|
19
|
+
|
|
20
|
+
gem.add_development_dependency "rspec"
|
|
21
|
+
|
|
22
|
+
gem.rdoc_options << '--webcvs=http://https://github.com/nitinrakesh-332/sunspot_stat/tree/master/%s' <<
|
|
23
|
+
'--title' << 'Sunspot Stat - StatsComponent for sunspot - API Documentation' <<
|
|
24
|
+
'--main' << 'README.rdoc'
|
|
25
|
+
end
|
|
Binary file
|
metadata
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sunspot_stat
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- nitin rakesh
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-02-12 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rspec
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
description: " Sunspot is a library providing a powerful, all-ruby API for the
|
|
28
|
+
Solr search engine. This gem extend sunspot adding the \n statsComponent feature,
|
|
29
|
+
which returns simple statistics for indexed numeric fields within the DocSet.\n"
|
|
30
|
+
email:
|
|
31
|
+
- nitinrak.25s@gmail.com
|
|
32
|
+
executables: []
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- Gemfile
|
|
37
|
+
- LICENSE
|
|
38
|
+
- README.md
|
|
39
|
+
- Rakefile
|
|
40
|
+
- lib/dsl/field_query.rb
|
|
41
|
+
- lib/query/field_stat.rb
|
|
42
|
+
- lib/query/stat_query.rb
|
|
43
|
+
- lib/search/stat_facet.rb
|
|
44
|
+
- lib/search/stat_row.rb
|
|
45
|
+
- lib/search/stat_search.rb
|
|
46
|
+
- lib/sunspot_stat/version.rb
|
|
47
|
+
- lib/sunspot_stats.rb
|
|
48
|
+
- spec/api/query/spec_helper.rb
|
|
49
|
+
- spec/api/query/stats_spec.rb
|
|
50
|
+
- spec/api/search/spec_helper.rb
|
|
51
|
+
- spec/api/search/stats_spec.rb
|
|
52
|
+
- spec/api/spec_helper.rb
|
|
53
|
+
- spec/ext.rb
|
|
54
|
+
- spec/helpers/indexer_helper.rb
|
|
55
|
+
- spec/helpers/integration_helper.rb
|
|
56
|
+
- spec/helpers/mock_session_helper.rb
|
|
57
|
+
- spec/helpers/query_helper.rb
|
|
58
|
+
- spec/helpers/search_helper.rb
|
|
59
|
+
- spec/mocks/adapters.rb
|
|
60
|
+
- spec/mocks/connection.rb
|
|
61
|
+
- spec/mocks/content.rb
|
|
62
|
+
- spec/mocks/mock_adapter.rb
|
|
63
|
+
- spec/mocks/mock_record.rb
|
|
64
|
+
- spec/mocks/super_class.rb
|
|
65
|
+
- spec/spec_helper.rb
|
|
66
|
+
- sunspot_stat.gemspec
|
|
67
|
+
- sunspot_stats-0.0.7.gem
|
|
68
|
+
homepage: https://github.com/nitinrakesh-332/sunspot_stat
|
|
69
|
+
licenses: []
|
|
70
|
+
metadata: {}
|
|
71
|
+
post_install_message:
|
|
72
|
+
rdoc_options:
|
|
73
|
+
- "--webcvs=http://https://github.com/nitinrakesh-332/sunspot_stat/tree/master/%s"
|
|
74
|
+
- "--title"
|
|
75
|
+
- Sunspot Stat - StatsComponent for sunspot - API Documentation
|
|
76
|
+
- "--main"
|
|
77
|
+
- README.rdoc
|
|
78
|
+
require_paths:
|
|
79
|
+
- lib
|
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '0'
|
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
requirements: []
|
|
91
|
+
rubygems_version: 3.5.3
|
|
92
|
+
signing_key:
|
|
93
|
+
specification_version: 4
|
|
94
|
+
summary: Added the statsComponent to sunspot
|
|
95
|
+
test_files:
|
|
96
|
+
- spec/api/query/spec_helper.rb
|
|
97
|
+
- spec/api/query/stats_spec.rb
|
|
98
|
+
- spec/api/search/spec_helper.rb
|
|
99
|
+
- spec/api/search/stats_spec.rb
|
|
100
|
+
- spec/api/spec_helper.rb
|
|
101
|
+
- spec/ext.rb
|
|
102
|
+
- spec/helpers/indexer_helper.rb
|
|
103
|
+
- spec/helpers/integration_helper.rb
|
|
104
|
+
- spec/helpers/mock_session_helper.rb
|
|
105
|
+
- spec/helpers/query_helper.rb
|
|
106
|
+
- spec/helpers/search_helper.rb
|
|
107
|
+
- spec/mocks/adapters.rb
|
|
108
|
+
- spec/mocks/connection.rb
|
|
109
|
+
- spec/mocks/content.rb
|
|
110
|
+
- spec/mocks/mock_adapter.rb
|
|
111
|
+
- spec/mocks/mock_record.rb
|
|
112
|
+
- spec/mocks/super_class.rb
|
|
113
|
+
- spec/spec_helper.rb
|