asari 0.5.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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: