algoliasearch 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.
@@ -0,0 +1,163 @@
1
+ require 'algolia/protocol'
2
+ require 'algolia/error'
3
+ require 'json'
4
+
5
+ module Algolia
6
+
7
+ # A class which encapsulates the HTTPS communication with the Algolia
8
+ # API server. Uses the Curb (Curl) library for low-level HTTP communication.
9
+ class Client
10
+ attr_accessor :hosts
11
+ attr_accessor :ssl
12
+ attr_accessor :application_id
13
+ attr_accessor :api_key
14
+
15
+ def initialize(data = {})
16
+ @ssl = data[:ssl].nil? ? true : data[:ssl]
17
+ @application_id = data[:application_id]
18
+ @api_key = data[:api_key]
19
+ @gzip = data[:gzip].nil? ? false : data[:gzip]
20
+ rhosts = (data[:hosts] || 1.upto(3).map { |i| "#{@application_id}-#{i}.algolia.io" }).shuffle
21
+
22
+ @hosts = []
23
+ rhosts.each do |host|
24
+ hinfo = {}
25
+ hinfo["base_url"] = "http#{@ssl ? 's' : ''}://#{host}"
26
+ hinfo["host"] = host
27
+ hinfo["session"] = Curl::Easy.new(@base_url) do |s|
28
+ s.headers[Protocol::HEADER_API_KEY] = @api_key
29
+ s.headers[Protocol::HEADER_APP_ID] = @application_id
30
+ s.headers["Content-Type"] = "application/json; charset=utf-8"
31
+ s.headers["Accept"] = "Accept-Encoding: gzip,deflate" if @gzip
32
+ s.headers["User-Agent"] = "Algolia for Ruby"
33
+ s.verbose = true if data[:debug]
34
+ end
35
+ @hosts << hinfo
36
+ end
37
+ end
38
+
39
+ # Perform an HTTP request for the given uri and method
40
+ # with common basic response handling. Will raise a
41
+ # AlgoliaProtocolError if the response has an error status code,
42
+ # and will return the parsed JSON body on success, if there is one.
43
+ def request(uri, method, data = nil)
44
+ @hosts.each do |host|
45
+ begin
46
+ session = host["session"]
47
+ session.url = host["base_url"] + uri
48
+ case method
49
+ when :GET
50
+ session.http_get
51
+ when :POST
52
+ session.post_body = data
53
+ session.http_post
54
+ when :PUT
55
+ session.http_put(data)
56
+ when :DELETE
57
+ session.http_delete
58
+ end
59
+ if session.response_code >= 400 || session.response_code < 200
60
+ raise AlgoliaProtocolError.new(session.response_code, "#{method} #{session.url}: #{session.body_str}")
61
+ end
62
+ return JSON.parse(session.body_str)
63
+ rescue AlgoliaProtocolError => e
64
+ if e.code != Protocol::ERROR_TIMEOUT and e.code != Protocol::ERROR_UNAVAILABLE
65
+ raise
66
+ end
67
+ rescue Curl::Err::CurlError => e
68
+ end
69
+ end
70
+ raise AlgoliaProtocolError.new(0, "Cannot reach any hosts")
71
+ end
72
+
73
+ def get(uri)
74
+ request(uri, :GET)
75
+ end
76
+
77
+ def post(uri, body)
78
+ request(uri, :POST, body)
79
+ end
80
+
81
+ def put(uri, body)
82
+ request(uri, :PUT, body)
83
+ end
84
+
85
+ def delete(uri)
86
+ request(uri, :DELETE)
87
+ end
88
+
89
+ end
90
+
91
+ # Module methods
92
+ # ------------------------------------------------------------
93
+
94
+ # A singleton client
95
+ # Always use Algolia.client to retrieve the client object.
96
+ @@client = nil
97
+
98
+ # Initialize the singleton instance of Client which is used
99
+ # by all API methods.
100
+ def Algolia.init(options = {})
101
+ defaulted = { :api_id => ENV["ALGOLIA_API_ID"], :api_key => ENV["ALGOLIA_REST_API_KEY"] }
102
+ defaulted.merge!(options)
103
+
104
+ @@client = Client.new(defaulted)
105
+ end
106
+
107
+ #
108
+ # List all existing indexes
109
+ # return an Answer object with answer in the form
110
+ # {"items": [{ "name": "contacts", "createdAt": "2013-01-18T15:33:13.556Z"},
111
+ # {"name": "notes", "createdAt": "2013-01-18T15:33:13.556Z"}]}
112
+ #
113
+ def Algolia.list_indexes
114
+ Algolia.client.get(Protocol.indexes_uri)
115
+ end
116
+
117
+ # List all existing user keys with their associated ACLs
118
+ def Algolia.list_user_keys
119
+ Algolia.client.get(Protocol.keys_uri)
120
+ end
121
+
122
+ # Get ACL of a user key
123
+ def Algolia.get_user_key(key)
124
+ Algolia.client.get(Protocol.key_uri(key))
125
+ end
126
+
127
+ #
128
+ # Create a new user key
129
+ #
130
+ # @param acls the list of ACL for this key. Defined by an array of strings that
131
+ # can contains the following values:
132
+ # - search: allow to search (https and http)
133
+ # - addObject: allows to add a new object in the index (https only)
134
+ # - updateObject : allows to change content of an existing object (https only)
135
+ # - deleteObject : allows to delete an existing object (https only)
136
+ # - deleteIndex : allows to delete index content (https only)
137
+ # - settings : allows to get index settings (https only)
138
+ # - editSettings : allows to change index settings (https only)
139
+ # @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key)
140
+ #
141
+ def Algolia.add_user_key(acls, validity = 0)
142
+ Algolia.client.post(Protocol.keys_uri, {"acl" => acls, "validity" => validity}.to_json)
143
+ end
144
+
145
+ # Delete an existing user key
146
+ def Algolia.delete_user_key(key)
147
+ Algolia.client.delete(Protocol.key_uri(key))
148
+ end
149
+
150
+ # Used mostly for testing. Lets you delete the api key global vars.
151
+ def Algolia.destroy
152
+ @@client = nil
153
+ self
154
+ end
155
+
156
+ def Algolia.client
157
+ if !@@client
158
+ raise AlgoliaError, "API not initialized"
159
+ end
160
+ @@client
161
+ end
162
+
163
+ end
@@ -0,0 +1,23 @@
1
+ module Algolia
2
+
3
+ # Base exception class for errors thrown by the Algolia
4
+ # client library. AlgoliaError will be raised by any
5
+ # network operation if Algolia.init() has not been called.
6
+ class AlgoliaError < StandardError #Exception ... why? A:http://www.skorks.com/2009/09/ruby-exceptions-and-exception-handling/
7
+ end
8
+
9
+ # An exception class raised when the REST API returns an error.
10
+ # The error code and message will be parsed out of the HTTP response,
11
+ # which is also included in the response attribute.
12
+ class AlgoliaProtocolError < AlgoliaError
13
+ attr_accessor :code
14
+ attr_accessor :message
15
+
16
+ def initialize(code, message)
17
+ self.code = code
18
+ self.message = message
19
+ super("#{self.code}: #{self.message}")
20
+ end
21
+ end
22
+
23
+ end
@@ -0,0 +1,295 @@
1
+ require 'algolia/client'
2
+ require 'algolia/error'
3
+
4
+ module Algolia
5
+
6
+ class Index
7
+ attr_accessor :name
8
+
9
+ def initialize(name)
10
+ self.name = name
11
+ end
12
+
13
+ # Delete an index
14
+ #
15
+ # return an object of the form array(:deletedAt => "2013-01-18T15:33:13.556Z")
16
+ def delete
17
+ Algolia.client.delete(Protocol.index_uri(name))
18
+ end
19
+
20
+ # Add an object in this index
21
+ #
22
+ # @param content contains the object to add inside the index.
23
+ # The object is represented by an associative array
24
+ # @param objectID (optional) an objectID you want to attribute to this object
25
+ # (if the attribute already exist the old object will be overwrite)
26
+ def add_object(obj, objectID = nil)
27
+ if objectID == nil
28
+ Algolia.client.post(Protocol.index_uri(name), obj.to_json)
29
+ else
30
+ Algolia.client.put(Protocol.object_uri(name, objectID), obj.to_json)
31
+ end
32
+ end
33
+
34
+ # Add an object in this index and wait end of indexing
35
+ #
36
+ # @param content contains the object to add inside the index.
37
+ # The object is represented by an associative array
38
+ # @param objectID (optional) an objectID you want to attribute to this object
39
+ # (if the attribute already exist the old object will be overwrite)
40
+ def add_object!(obj, objectID = nil)
41
+ res = add_object(obj, objectID)
42
+ wait_task(res["taskID"])
43
+ return res
44
+ end
45
+
46
+ # Add several objects in this index
47
+ #
48
+ # @param content contains the object to add inside the index.
49
+ # The object is represented by an associative array
50
+ # @param objectID (optional) an objectID you want to attribute to this object
51
+ # (if the attribute already exist the old object will be overwrite)
52
+ def add_objects(objs)
53
+ requests = []
54
+ objs.each do |obj|
55
+ requests.push({"action" => "addObject", "body" => obj})
56
+ end
57
+ request = {"requests" => requests};
58
+ Algolia.client.post(Protocol.batch_uri(name), request.to_json)
59
+ end
60
+
61
+ # Add several objects in this index and wait end of indexing
62
+ #
63
+ # @param content contains the object to add inside the index.
64
+ # The object is represented by an associative array
65
+ # @param objectID (optional) an objectID you want to attribute to this object
66
+ # (if the attribute already exist the old object will be overwrite)
67
+ def add_objects!(obj)
68
+ res = add_objects(obj)
69
+ wait_task(res["taskID"])
70
+ return res
71
+ end
72
+
73
+ # Search inside the index
74
+ #
75
+ # @param query the full text query
76
+ # @param args (optional) if set, contains an associative array with query parameters:
77
+ # - attributes: a string that contains attribute names to retrieve separated by a comma.
78
+ # By default all attributes are retrieved.
79
+ # - attributesToHighlight: a string that contains attribute names to highlight separated by a comma.
80
+ # By default indexed attributes are highlighted.
81
+ # - attributesToSnippet: a string that contains the names of attributes to snippet alongside the number
82
+ # of words to return (syntax is 'attributeName:nbWords').
83
+ # Attributes are separated by a comma (Example: "attributesToSnippet=name:10,content:10").
84
+ # By default no snippet is computed.
85
+ # - minWordSizeForApprox1: the minimum number of characters in a query word to accept one typo in this word.
86
+ # Defaults to 3.
87
+ # - minWordSizeForApprox2: the minimum number of characters in a query word to accept two typos in this word.
88
+ # Defaults to 7.
89
+ # - getRankingInfo: if set to 1, the result hits will contain ranking information in
90
+ # _rankingInfo attribute
91
+ # - page: (pagination parameter) page to retrieve (zero base). Defaults to 0.
92
+ # - hitsPerPage: (pagination parameter) number of hits per page. Defaults to 10.
93
+ # - aroundLatLng let you search for entries around a given latitude/longitude (two float separated
94
+ # by a ',' for example aroundLatLng=47.316669,5.016670).
95
+ # You can specify the maximum distance in meters with aroundRadius parameter (in meters).
96
+ # At indexing, geoloc of an object should be set with _geoloc attribute containing lat and lng attributes (for example {"_geoloc":{"lat":48.853409, "lng":2.348800}})
97
+ # - insideBoundingBox let you search entries inside a given area defined by the two extreme points of
98
+ # a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat, p2Lng.
99
+ # For example insideBoundingBox=47.3165,4.9665,47.3424,5.0201).
100
+ # At indexing, geoloc of an object should be set with _geoloc attribute containing lat and lng attributes (for example {"_geoloc":{"lat":48.853409, "lng":2.348800}})
101
+ # - queryType: select how the query words are interpreted:
102
+ # - prefixAll: all query words are interpreted as prefixes (default behavior).
103
+ # - prefixLast: only the last word is interpreted as a prefix. This option is recommended if you have a lot of content to speedup the processing.
104
+ # - prefixNone: no query word is interpreted as a prefix. This option is not recommended.
105
+ # - tags filter the query by a set of tags. You can AND tags by separating them by commas. To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 OR tag3).
106
+ # At indexing, tags should be added in the _tags attribute of objects (for example {"_tags":["tag1","tag2"]} )
107
+ #
108
+ def search(query, params = {})
109
+ Algolia.client.get(Protocol.search_uri(name, query, params))
110
+ end
111
+
112
+ #
113
+ # Get an object from this index
114
+ #
115
+ # @param objectID the unique identifier of the object to retrieve
116
+ # @param attributesToRetrieve (optional) if set, contains the list of attributes to retrieve as a string separated by ","
117
+ #
118
+ def get_object(objectID, attributesToRetrieve = nil)
119
+ if attributesToRetrieve == nil
120
+ Algolia.client.get(Protocol.object_uri(name, objectID, nil))
121
+ else
122
+ Algolia.client.get(Protocol.object_uri(name, objectID, {"attributes" => attributesToRetrieve}))
123
+ end
124
+ end
125
+
126
+ # Wait the publication of a task on the server.
127
+ # All server task are asynchronous and you can check with this method that the task is published.
128
+ #
129
+ # @param taskID the id of the task returned by server
130
+ # @param timeBeforeRetry the time in milliseconds before retry (default = 100ms)
131
+ #
132
+ def wait_task(taskID, timeBeforeRetry = 100)
133
+ loop do
134
+ status = Algolia.client.get(Protocol.task_uri(name, taskID))["status"]
135
+ if status == "published"
136
+ return
137
+ end
138
+ sleep(timeBeforeRetry / 1000)
139
+ end
140
+ end
141
+
142
+ # Override the content of object
143
+ #
144
+ # @param object contains the object to save, the object must contains an objectID attribute
145
+ #
146
+ def save_object(obj)
147
+ Algolia.client.put(Protocol.object_uri(name, obj["objectID"]), obj.to_json)
148
+ end
149
+
150
+ # Override the content of object and wait indexing
151
+ #
152
+ # @param object contains the object to save, the object must contains an objectID attribute
153
+ #
154
+ def save_object!(obj)
155
+ res = save_object(obj)
156
+ wait_task(res["taskID"])
157
+ return res
158
+ end
159
+
160
+ # Override the content of several objects
161
+ #
162
+ # @param object contains the object to save, the object must contains an objectID attribute
163
+ #
164
+ def save_objects(objs)
165
+ requests = []
166
+ objs.each do |obj|
167
+ requests.push({"action" => "updateObject", "objectID" => obj["objectID"], "body" => obj})
168
+ end
169
+ request = {"requests" => requests};
170
+ Algolia.client.post(Protocol.batch_uri(name), request.to_json)
171
+ end
172
+
173
+ # Override the content of several objects and wait indexing
174
+ #
175
+ # @param object contains the javascript object to save, the object must contains an objectID attribute
176
+ #
177
+ def save_objects!(objs)
178
+ res = save_objects(objs)
179
+ wait_task(res["taskID"])
180
+ return res
181
+ end
182
+
183
+ #
184
+ # Update partially an object (only update attributes passed in argument)
185
+ #
186
+ # @param obj contains the javascript attributes to override, the
187
+ # object must contains an objectID attribute
188
+ #
189
+ def partial_update_object(obj)
190
+ Algolia.client.post(Protocol.partial_object_uri(name, obj["objectID"]), obj.to_json)
191
+ end
192
+
193
+ #
194
+ # Update partially an object (only update attributes passed in argument) and wait indexing
195
+ #
196
+ # @param obj contains the javascript attributes to override, the
197
+ # object must contains an objectID attribute
198
+ #
199
+ def partial_update_object!(obj)
200
+ res = partial_update_object(obj)
201
+ wait_task(res["taskID"])
202
+ return res
203
+ end
204
+
205
+ #
206
+ # Delete an object from the index
207
+ #
208
+ # @param objectID the unique identifier of object to delete
209
+ #
210
+ def delete_object(objectID)
211
+ Algolia.client.delete(Protocol.object_uri(name, objectID))
212
+ end
213
+
214
+ #
215
+ # Set settings for this index
216
+ #
217
+ # @param settigns the settings object that can contains :
218
+ # - minWordSizeForApprox1 (integer) the minimum number of characters to accept one typo (default = 3)
219
+ # - minWordSizeForApprox2: (integer) the minimum number of characters to accept two typos (default = 7)
220
+ # - hitsPerPage: (integer) the number of hits per page (default = 10)
221
+ # - attributesToRetrieve: (array of strings) default list of attributes to retrieve for objects
222
+ # - attributesToHighlight: (array of strings) default list of attributes to highlight
223
+ # - attributesToSnippet: (array of strings) default list of attributes to snippet alongside the number
224
+ # of words to return (syntax is 'attributeName:nbWords').
225
+ # By default no snippet is computed.
226
+ # - attributesToIndex: (array of strings) the list of fields you want to index.
227
+ # By default all textual attributes of your objects are indexed, but you should update it to get optimal
228
+ # results. This parameter has two important uses:
229
+ # - Limit the attributes to index.
230
+ # For example if you store a binary image in base64, you want to store it in the index but you
231
+ # don't want to use the base64 string for search.
232
+ # - Control part of the ranking (see the ranking parameter for full explanation).
233
+ # Matches in attributes at the beginning of the list will be considered more important than matches
234
+ # in attributes further down the list.
235
+ # - ranking: (array of strings) controls the way results are sorted.
236
+ # We have four available criteria:
237
+ # - typo (sort according to number of typos),
238
+ # - geo: (sort according to decreassing distance when performing a geo-location based search),
239
+ # - proximity: sort according to the proximity of query words in hits,
240
+ # - attribute: sort according to the order of attributes defined by **attributesToIndex**,
241
+ # - exact: sort according to the number of words that are matched identical to query word (and not as a prefix),
242
+ # - custom which is user defined
243
+ # (the standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"])
244
+ # - queryType: select how the query words are interpreted:
245
+ # - prefixAll: all query words are interpreted as prefixes (default behavior).
246
+ # - prefixLast: only the last word is interpreted as a prefix. This option is recommended if you have a lot of content to speedup the processing.
247
+ # - prefixNone: no query word is interpreted as a prefix. This option is not recommended.
248
+ # - customRanking: (array of strings) lets you specify part of the ranking.
249
+ # The syntax of this condition is an array of strings containing attributes prefixed
250
+ # by asc (ascending order) or desc (descending order) operator.
251
+ #
252
+ def set_settings(new_settings)
253
+ Algolia.client.put(Protocol.settings_uri(name), new_settings.to_json)
254
+ end
255
+
256
+ # Get settings of this index
257
+ def get_settings
258
+ Algolia.client.get(Protocol.settings_uri(name))
259
+ end
260
+
261
+ # List all existing user keys with their associated ACLs
262
+ def list_user_keys
263
+ Algolia.client.get(Protocol.index_keys_uri(name))
264
+ end
265
+
266
+ # Get ACL of a user key
267
+ def get_user_key(key)
268
+ Algolia.client.get(Protocol.index_key_uri(name, key))
269
+ end
270
+
271
+ #
272
+ # Create a new user key
273
+ #
274
+ # @param acls the list of ACL for this key. Defined by an array of strings that
275
+ # can contains the following values:
276
+ # - search: allow to search (https and http)
277
+ # - addObject: allows to add a new object in the index (https only)
278
+ # - updateObject : allows to change content of an existing object (https only)
279
+ # - deleteObject : allows to delete an existing object (https only)
280
+ # - deleteIndex : allows to delete index content (https only)
281
+ # - settings : allows to get index settings (https only)
282
+ # - editSettings : allows to change index settings (https only)
283
+ # @param validity the number of seconds after which the key will be automatically removed (0 means no time limit for this key)
284
+ #
285
+ def add_user_key(acls, validity = 0)
286
+ Algolia.client.post(Protocol.index_keys_uri(name), {"acl" => acls, "validity" => validity}.to_json)
287
+ end
288
+
289
+ # Delete an existing user key
290
+ def delete_user_key(key)
291
+ Algolia.client.delete(Protocol.index_key_uri(name, key))
292
+ end
293
+
294
+ end
295
+ end