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 CHANGED
@@ -1,4 +1,118 @@
1
- asari
2
- =====
1
+ # Asari
3
2
 
4
- a Ruby wrapper for AWS CloudSearch.
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 Array of all document IDs in the system that match the
49
- # specified search term. If no results are found, an empty Array is
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 [] if self.class.mode == :sandbox
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
 
@@ -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
- @total_pages = (@total_entries / @page_size) + 1
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
@@ -1,3 +1,3 @@
1
1
  class Asari
2
- VERSION = "0.5.4"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -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=41")
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
- expect(@asari.search("testsearch")).to eq(["123","456"])
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
- expect(@asari.search("testsearch")).to eq([])
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" => {"hit" => [{"id" => "123"}, {"id" => "456"}]}},
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.5.4
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: &70352597799280 !ruby/object:Gem::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: *70352597799280
24
+ version_requirements: *70109076129460
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &70352597819640 !ruby/object:Gem::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: *70352597819640
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: