asari 0.5.4 → 0.6.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.
- data/README.md +117 -3
- data/lib/asari.rb +4 -3
- data/lib/asari/collection.rb +34 -1
- data/lib/asari/version.rb +1 -1
- data/spec/active_record_spec.rb +3 -3
- data/spec/collection_spec.rb +30 -0
- data/spec/search_spec.rb +17 -5
- data/spec_helper.rb +5 -2
- metadata +7 -5
data/README.md
CHANGED
@@ -1,4 +1,118 @@
|
|
1
|
-
|
2
|
-
=====
|
1
|
+
# Asari
|
3
2
|
|
4
|
-
|
3
|
+
## Description
|
4
|
+
|
5
|
+
Asari is a Ruby wrapper for AWS CloudSearch, with optional ActiveRecord support
|
6
|
+
for easy integration with your Rails apps.
|
7
|
+
|
8
|
+
#### Why Asari?
|
9
|
+
|
10
|
+
"Asari" is Japanese for "rummaging search." Seemed appropriate.
|
11
|
+
|
12
|
+
## Usage
|
13
|
+
|
14
|
+
#### Basic Usage
|
15
|
+
|
16
|
+
asari = Asari.new("my-search-domain-asdfkljwe4") # CloudSearch search domain
|
17
|
+
asari.add_item("1", { :name => "Tommy Morgan", :email => "tommy@wellbredgrapefruit.com"})
|
18
|
+
asari.search("tommy") #=> ["1"] - a list of document IDs
|
19
|
+
|
20
|
+
#### Pagination
|
21
|
+
|
22
|
+
Asari defaults to a page size of 10 (because that's CloudSearch's default), but
|
23
|
+
it allows you to specify pagination parameters with any search:
|
24
|
+
|
25
|
+
asari.search("tommy", :page_size => 30, :page => 10)
|
26
|
+
|
27
|
+
The results you get back from Asari#search aren't actually Array objects,
|
28
|
+
either: they're Asari::Collection objects, which are (currently) API-compatible
|
29
|
+
with will\_paginate:
|
30
|
+
|
31
|
+
results = asari.search("tommy", :page_size => 30, :page => 10)
|
32
|
+
results.total_entries #=> 5000
|
33
|
+
results.total_pages #=> 167
|
34
|
+
results.current_page #=> 10
|
35
|
+
results.offset #=> 300
|
36
|
+
results.page_size #=> 30
|
37
|
+
|
38
|
+
#### ActiveRecord
|
39
|
+
|
40
|
+
If you require 'asari/active\_record' in your project, you have access to the
|
41
|
+
ActiveRecord module for Asari. You can take advantage of that module like so:
|
42
|
+
|
43
|
+
class User < ActiveRecord::Base
|
44
|
+
include Asari::ActiveRecord
|
45
|
+
|
46
|
+
#... other stuff...
|
47
|
+
|
48
|
+
asari_index("search-domain-for-users", [:name, :email, :twitter_handle, :favorite_sweater])
|
49
|
+
end
|
50
|
+
|
51
|
+
This will automatically set up before\_destroy, after\_create, and after\_update
|
52
|
+
hooks for your AR model to keep the data in sync with your CloudSearch index -
|
53
|
+
the second argument to asari\_index is the list of fields to maintain in the
|
54
|
+
index, and can represent any function on your AR object. You can then interact
|
55
|
+
with your AR objects as follows:
|
56
|
+
|
57
|
+
# Klass.asari_find returns a list of model objects in an
|
58
|
+
# Asari::Collection...
|
59
|
+
User.asari_find("tommy") #=> [<User:...>, <User:...>, <User:...>]
|
60
|
+
|
61
|
+
# or with a specific instance, if you need to manually do some index
|
62
|
+
# management...
|
63
|
+
@user.asari_add_to_index
|
64
|
+
@user.asari_update_in_index
|
65
|
+
@user.asari_remove_from_index
|
66
|
+
|
67
|
+
Because index updates are done as part of the AR lifecycle by default, you also
|
68
|
+
might want to have control over how Asari handles index update errors - it's
|
69
|
+
kind of problematic, if, say, users can't sign up on your site because
|
70
|
+
CloudSearch isn't available at the moment. By default Asari just raises these
|
71
|
+
exceptions when they occur, but you can define a special handler if you want
|
72
|
+
using the asari\_on\_error method:
|
73
|
+
|
74
|
+
class User < ActiveRecord::Base
|
75
|
+
include Asari::ActiveRecord
|
76
|
+
|
77
|
+
asari_index(... )
|
78
|
+
|
79
|
+
def self.asari_on_error(exception)
|
80
|
+
Airbrake.notify(...)
|
81
|
+
true
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
In the above example we decide that, instead of raising exceptions every time,
|
86
|
+
we're going to log exception data to Airbrake so that we can review it later and
|
87
|
+
then return true so that the AR lifecycle continues normally.
|
88
|
+
|
89
|
+
## Get it
|
90
|
+
|
91
|
+
It's a gem named asari. Install it and make it available however you prefer.
|
92
|
+
|
93
|
+
Asari is developed on ruby 1.9.3, and the ActiveRecord portion has been tested
|
94
|
+
with Rails 3.2. I don't know off-hand of any reasons that it shouldn't work in
|
95
|
+
other environments, but be aware that it hasn't (yet) been tested.
|
96
|
+
|
97
|
+
## Contributions
|
98
|
+
|
99
|
+
If Asari interests you and you think you might want to contribute, hit me up on
|
100
|
+
Github. You can also just fork it and make some changes, but there's a better
|
101
|
+
chance that your work won't be duplicated or rendered obsolete if you check in
|
102
|
+
on the current development status first.
|
103
|
+
|
104
|
+
Gem requirements/etc. should be handled by Bundler.
|
105
|
+
|
106
|
+
## License
|
107
|
+
Copyright (C) 2012 by Tommy Morgan
|
108
|
+
|
109
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
110
|
+
a copy of this software and associated documentation files (the
|
111
|
+
"Software"), to deal in the Software without restriction, including
|
112
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
113
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
114
|
+
permit persons to whom the Software is furnished to do so, subject to
|
115
|
+
the following conditions:
|
116
|
+
|
117
|
+
The above copyright notice and this permission notice shall be
|
118
|
+
included in all copies or substantial portions of the Software.
|
data/lib/asari.rb
CHANGED
@@ -5,6 +5,7 @@ require "asari/exceptions"
|
|
5
5
|
|
6
6
|
require "httparty"
|
7
7
|
|
8
|
+
require "ostruct"
|
8
9
|
require "json"
|
9
10
|
require "cgi"
|
10
11
|
|
@@ -45,14 +46,14 @@ class Asari
|
|
45
46
|
#
|
46
47
|
# @asari.search("fritters") #=> ["13","28"]
|
47
48
|
#
|
48
|
-
# Returns: An
|
49
|
-
# specified search term. If no results are found, an empty
|
49
|
+
# Returns: An Asari::Collection containing all document IDs in the system that match the
|
50
|
+
# specified search term. If no results are found, an empty Asari::Collection is
|
50
51
|
# returned.
|
51
52
|
#
|
52
53
|
# Raises: SearchException if there's an issue communicating the request to
|
53
54
|
# the server.
|
54
55
|
def search(term, options = {})
|
55
|
-
return
|
56
|
+
return Asari::Collection.sandbox_fake if self.class.mode == :sandbox
|
56
57
|
|
57
58
|
page_size = options[:page_size].nil? ? 10 : options[:page_size].to_i
|
58
59
|
|
data/lib/asari/collection.rb
CHANGED
@@ -1,10 +1,25 @@
|
|
1
1
|
class Asari
|
2
|
+
# Public: The Asari::Collection object represents a page of data returned from
|
3
|
+
# CloudSearch. It very closely delegates to an array containing the intended
|
4
|
+
# results, but provides a few extra methods containing metadata about the
|
5
|
+
# current pagination state: current_page, page_size, total_entries, offset, and
|
6
|
+
# total_pages.
|
7
|
+
#
|
8
|
+
# Asari::Collection is compatible with will_paginate collections, and the two
|
9
|
+
# can be used interchangeably for the purposes of pagination.
|
10
|
+
#
|
2
11
|
class Collection < BasicObject
|
3
12
|
attr_reader :current_page
|
4
13
|
attr_reader :page_size
|
5
14
|
attr_reader :total_entries
|
6
15
|
attr_reader :total_pages
|
7
16
|
|
17
|
+
# Internal: method for returning a sandbox-friendly empty search result.
|
18
|
+
#
|
19
|
+
def self.sandbox_fake
|
20
|
+
Collection.new(::OpenStruct.new(:parsed_response => {"hits" => { "found" => 0, "start" => 0, "hit" => []}}), 10)
|
21
|
+
end
|
22
|
+
|
8
23
|
# Internal: This object should really only ever be instantiated from within
|
9
24
|
# Asari code. The Asari Collection knows how to build itself from an
|
10
25
|
# HTTParty::Response object representing a search query result from
|
@@ -18,7 +33,11 @@ class Asari
|
|
18
33
|
resp = httparty_response.parsed_response
|
19
34
|
@total_entries = resp["hits"]["found"]
|
20
35
|
@page_size = page_size
|
21
|
-
|
36
|
+
|
37
|
+
complete_pages = (@total_entries / @page_size)
|
38
|
+
@total_pages = (@total_entries % @page_size > 0) ? complete_pages + 1 : complete_pages
|
39
|
+
# There's always one page, even for no results
|
40
|
+
@total_pages = 1 if @total_pages == 0
|
22
41
|
|
23
42
|
start = resp["hits"]["start"]
|
24
43
|
@current_page = (start / page_size) + 1
|
@@ -30,12 +49,26 @@ class Asari
|
|
30
49
|
(@current_page - 1) * @page_size
|
31
50
|
end
|
32
51
|
|
52
|
+
# Public: replace the current data collection with a new data collection,
|
53
|
+
# without losing pagination information. Useful for mapping results, etc.
|
54
|
+
#
|
55
|
+
# Examples:
|
56
|
+
#
|
57
|
+
# results = @asari.find("test") #=> ["1", "3", "10", "28"]
|
58
|
+
# results.replace(results.map { |id| User.find(id)}) #=> [<User...>,<User...>,<User...>]
|
59
|
+
#
|
60
|
+
# Returns: self. #replace is a chainable method.
|
61
|
+
#
|
33
62
|
def replace(array)
|
34
63
|
@data = array
|
35
64
|
|
36
65
|
self
|
37
66
|
end
|
38
67
|
|
68
|
+
def class
|
69
|
+
Asari::Collection
|
70
|
+
end
|
71
|
+
|
39
72
|
def method_missing(method, *args, &block)
|
40
73
|
@data.send(method, *args, &block)
|
41
74
|
end
|
data/lib/asari/version.rb
CHANGED
data/spec/active_record_spec.rb
CHANGED
@@ -36,13 +36,13 @@ describe Asari do
|
|
36
36
|
end
|
37
37
|
|
38
38
|
it "will allow you to search for items with the index" do
|
39
|
-
@asari.should_receive(:search).with("fritters").and_return(["1"])
|
39
|
+
@asari.should_receive(:search).with("fritters", {}).and_return(["1"])
|
40
40
|
|
41
41
|
ActiveRecordFake.asari_find("fritters")
|
42
42
|
end
|
43
43
|
|
44
44
|
it "will return a list of model objects when you search" do
|
45
|
-
@asari.should_receive(:search).with("fritters").and_return(["1"])
|
45
|
+
@asari.should_receive(:search).with("fritters", {}).and_return(["1"])
|
46
46
|
|
47
47
|
results = ActiveRecordFake.asari_find("fritters")
|
48
48
|
expect(results.class).to eq(Array)
|
@@ -50,7 +50,7 @@ describe Asari do
|
|
50
50
|
end
|
51
51
|
|
52
52
|
it "will return an empty list when you search for a term that isn't in the index" do
|
53
|
-
@asari.should_receive(:search).with("veggie burgers").and_return([])
|
53
|
+
@asari.should_receive(:search).with("veggie burgers", {}).and_return([])
|
54
54
|
|
55
55
|
results = ActiveRecordFake.asari_find("veggie burgers")
|
56
56
|
expect(results.class).to eq(Array)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe Asari do
|
4
|
+
describe Asari::Collection do
|
5
|
+
before :each do
|
6
|
+
response = OpenStruct.new(:parsed_response => { "hits" => { "found" => 10, "start" => 0, "hit" => ["1","2"]}})
|
7
|
+
@collection = Asari::Collection.new(response, 2)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "calculates the page_size correctly" do
|
11
|
+
expect(@collection.page_size).to eq(2)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "calculates the total_entries correctly" do
|
15
|
+
expect(@collection.total_entries).to eq(10)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "calculates the total_pages correctly" do
|
19
|
+
expect(@collection.total_pages).to eq(5)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "calculates the current_page correctly" do
|
23
|
+
expect(@collection.current_page).to eq(1)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "calculates the offset correctly" do
|
27
|
+
expect(@collection.offset).to eq(0)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/spec/search_spec.rb
CHANGED
@@ -9,12 +9,12 @@ describe Asari do
|
|
9
9
|
end
|
10
10
|
|
11
11
|
it "allows you to search." do
|
12
|
-
HTTParty.should_receive(:get).with("http://search-testdomain.us-east-1.cloudsearch.amazonaws.com/2011-02-01/search?q=testsearch")
|
12
|
+
HTTParty.should_receive(:get).with("http://search-testdomain.us-east-1.cloudsearch.amazonaws.com/2011-02-01/search?q=testsearch&size=10")
|
13
13
|
@asari.search("testsearch")
|
14
14
|
end
|
15
15
|
|
16
16
|
it "escapes dangerous characters in search terms." do
|
17
|
-
HTTParty.should_receive(:get).with("http://search-testdomain.us-east-1.cloudsearch.amazonaws.com/2011-02-01/search?q=testsearch%21")
|
17
|
+
HTTParty.should_receive(:get).with("http://search-testdomain.us-east-1.cloudsearch.amazonaws.com/2011-02-01/search?q=testsearch%21&size=10")
|
18
18
|
@asari.search("testsearch!")
|
19
19
|
end
|
20
20
|
|
@@ -24,17 +24,29 @@ describe Asari do
|
|
24
24
|
end
|
25
25
|
|
26
26
|
it "honors the page option" do
|
27
|
-
HTTParty.should_receive(:get).with("http://search-testdomain.us-east-1.cloudsearch.amazonaws.com/2011-02-01/search?q=testsearch&size=20&start=
|
27
|
+
HTTParty.should_receive(:get).with("http://search-testdomain.us-east-1.cloudsearch.amazonaws.com/2011-02-01/search?q=testsearch&size=20&start=40")
|
28
28
|
@asari.search("testsearch", :page_size => 20, :page => 3)
|
29
29
|
end
|
30
30
|
|
31
31
|
it "returns a list of document IDs for search results." do
|
32
|
-
|
32
|
+
result = @asari.search("testsearch")
|
33
|
+
|
34
|
+
expect(result.size).to eq(2)
|
35
|
+
expect(result[0]).to eq("123")
|
36
|
+
expect(result[1]).to eq("456")
|
37
|
+
expect(result.total_pages).to eq(1)
|
38
|
+
expect(result.current_page).to eq(1)
|
39
|
+
expect(result.page_size).to eq(10)
|
40
|
+
expect(result.total_entries).to eq(2)
|
33
41
|
end
|
34
42
|
|
35
43
|
it "returns an empty list when no search results are found." do
|
36
44
|
HTTParty.stub(:get).and_return(fake_empty_response)
|
37
|
-
|
45
|
+
result = @asari.search("testsearch")
|
46
|
+
expect(result.size).to eq(0)
|
47
|
+
expect(result.total_pages).to eq(1)
|
48
|
+
expect(result.current_page).to eq(1)
|
49
|
+
expect(result.total_entries).to eq(0)
|
38
50
|
end
|
39
51
|
|
40
52
|
it "raises an exception if the service errors out." do
|
data/spec_helper.rb
CHANGED
@@ -8,12 +8,15 @@ Asari.mode = :production
|
|
8
8
|
RSpec.configuration.expect_with(:rspec) { |c| c.syntax = :expect }
|
9
9
|
|
10
10
|
def fake_response
|
11
|
-
OpenStruct.new(:parsed_response => { "hits" => {
|
11
|
+
OpenStruct.new(:parsed_response => { "hits" => {
|
12
|
+
"found" => 2,
|
13
|
+
"start" => 0,
|
14
|
+
"hit" => [{"id" => "123"}, {"id" => "456"}]}},
|
12
15
|
:response => OpenStruct.new(:code => "200"))
|
13
16
|
end
|
14
17
|
|
15
18
|
def fake_empty_response
|
16
|
-
OpenStruct.new(:parsed_response => { "hits" => {"hit" => []}},
|
19
|
+
OpenStruct.new(:parsed_response => { "hits" => { "found" => 0, "start" => 0, "hit" => []}},
|
17
20
|
:response => OpenStruct.new(:code => "200"))
|
18
21
|
end
|
19
22
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: asari
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -13,7 +13,7 @@ date: 2012-07-12 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: httparty
|
16
|
-
requirement: &
|
16
|
+
requirement: &70109076129460 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70109076129460
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rspec
|
27
|
-
requirement: &
|
27
|
+
requirement: &70109076128860 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,7 +32,7 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70109076128860
|
36
36
|
description: Asari s a Ruby interface for AWS CloudSearch
|
37
37
|
email:
|
38
38
|
- tommy@wellbredgrapefruit.com
|
@@ -52,6 +52,7 @@ files:
|
|
52
52
|
- lib/asari/version.rb
|
53
53
|
- spec/active_record_spec.rb
|
54
54
|
- spec/asari_spec.rb
|
55
|
+
- spec/collection_spec.rb
|
55
56
|
- spec/documents_spec.rb
|
56
57
|
- spec/search_spec.rb
|
57
58
|
- spec_helper.rb
|
@@ -82,6 +83,7 @@ summary: Asari is a Ruby interface for AWS CloudSearch.
|
|
82
83
|
test_files:
|
83
84
|
- spec/active_record_spec.rb
|
84
85
|
- spec/asari_spec.rb
|
86
|
+
- spec/collection_spec.rb
|
85
87
|
- spec/documents_spec.rb
|
86
88
|
- spec/search_spec.rb
|
87
89
|
has_rdoc:
|