Current section
Files
Jump to
Current section
Files
lib/algolia.ex
defmodule Algolia do
@moduledoc """
Elixir implementation of Algolia search API, using Hackney for http requests
"""
@application_id Application.fetch_env!(:algolia, :application_id)
@api_key Application.fetch_env!(:algolia, :api_key)
defp host(:read, 0),
do: "#{@application_id}-dsn.algolia.net"
defp host(:write, 0),
do: "#{@application_id}.algolia.net"
defp host(_read_or_write, curr_retry) when curr_retry <= 3,
do: "#{@application_id}-#{curr_retry}.algolianet.com"
@doc """
Multiple queries
"""
def multi(queries, opts \\ [strategy: :none]) do
strategy = opts[:strategy]
params = case strategy do
:none -> "?strategy=none"
:stop_if_enough_matches -> "?strategy=stopIfEnoughMatches"
_ -> ""
end
path = "*/queries" <> params
body = Poison.encode! format_multi(queries)
send_request(:read, :post, path, body)
end
defp format_multi(queries) do
requests = Enum.map queries, fn(query) ->
index_name = query[:index_name] || query["index_name"]
if !index_name,
do: raise ArgumentError, message: "Missing index_name for one of the multiple queries"
params =
query
|> Map.delete(:index_name)
|> Map.delete("index_name")
|> format_multi_params
%{indexName: index_name, params: params }
end
%{ requests: requests }
end
def format_multi_params(query) do
query
|> Stream.map(fn {k, v} ->
"#{k}=#{v}"
end)
|> Enum.join("&")
|> URI.encode
end
@doc """
Search a single index
"""
def search(index, query, opts) do
path = "#{index}?query=#{query}" <> opts_to_query_params(opts)
send_request(:read, :get, path)
end
def search(index, query), do: search(index, query, [])
defp opts_to_query_params([]), do: ""
defp opts_to_query_params(opts) do
opts
|> Stream.map(fn {key, value} ->
"&#{key}=#{value}"
end)
|> Enum.join
|> URI.encode
end
defp send_request(_, _, _, _, 4),
do: {:error, "Unable to connect to Algolia"}
defp send_request(read_or_write, method, path),
do: send_request(read_or_write, method, path, "", 0)
defp send_request(read_or_write, method, path, body),
do: send_request(read_or_write, method, path, body, 0)
defp send_request(read_or_write, method, path, body, curr_retry) do
url =
"https://"
|> Path.join(host(read_or_write, curr_retry))
|> Path.join("/1/indexes")
|> Path.join(path)
headers = [
"X-Algolia-API-Key": @api_key,
"X-Algolia-Application-Id": @application_id
]
:hackney.request(method, url, headers, body, [
:with_body,
path_encode_fun: &(&1),
connect_timeout: 3_000 * (curr_retry + 1),
recv_timeout: 30_000 * (curr_retry + 1),
])
|> case do
{:ok, 200, _headers, body} ->
{:ok, body |> Poison.decode!}
{:ok, code, _, body} ->
{:error, code, body}
_ ->
send_request(read_or_write, method, path, body, curr_retry + 1)
end
end
@doc """
Get an object in an index by objectID
"""
def get_object(index, object_id) do
path = "#{index}/#{object_id}"
send_request(:read, :get, path)
|> inject_index_into_response(index)
end
@doc """
Save a single object, with objectID specified
"""
def save_object(index, object, object_id) when is_bitstring(object_id) do
body = object |> Poison.encode!
path = "#{index}/#{object_id}"
send_request(:write, :put, path, body)
|> inject_index_into_response(index)
end
@doc """
Save a single object, without objectID specified, must have objectID as
a field
"""
def save_object(index, object, opts \\ [id_attribute: :objectID]) do
id_attribute = opts[:id_attribute]
object_id = object[id_attribute] || object[to_string id_attribute]
if !object_id do
raise "Object must have an objectID"
end
save_object(index, object, object_id)
end
@doc """
Save multiple objects
"""
def save_objects(index, objects, opts \\ [id_attribute: :objectID]) when is_list(objects) do
id_attribute = opts[:id_attribute]
objects
|> add_object_ids(id_attribute: id_attribute)
|> build_batch_request("updateObject", with_object_id: true)
|> send_batch_request(index)
end
@doc """
Partially updates an object, takes option upsert: true or false
"""
def partial_update_object(index, object, object_id, opts \\ [upsert?: true]) do
body = object |> Poison.encode!
params = if opts[:upsert?] do
""
else
"?createIfNotExists=false"
end
path = "#{index}/#{object_id}/partial" <> URI.encode(params)
send_request(:write, :post, path, body)
|> inject_index_into_response(index)
end
@doc """
Partially updates multiple objects
"""
def partial_update_objects(index, objects, opts \\ [upsert?: true, id_attribute: :objectID]) do
id_attribute = opts[:id_attribute] || :objectID
upsert = case opts[:upsert?] do
false -> false
_ -> true
end
action = if upsert, do: "partialUpdateObject", else: "partialUpdateObjectNoCreate"
objects
|> add_object_ids(id_attribute: id_attribute)
|> build_batch_request(action, with_object_id: true)
|> send_batch_request(index)
end
# No need to add any objectID by default
defp add_object_ids(objects, id_attribute: :objectID), do: objects
defp add_object_ids(objects, id_attribute: "objectID"), do: objects
defp add_object_ids(objects, id_attribute: attribute) do
Enum.map(objects, fn(object) ->
object_id = object[attribute] || object[to_string attribute]
if !object_id do
raise ArgumentError, message: "id attribute `#{attribute}` doesn't exist"
end
add_object_id(object, object_id)
end)
end
defp add_object_id(object, object_id) do
Map.put(object, :objectID, object_id)
end
defp get_object_id(object) do
case object[:objectID] || object["objectID"] do
nil -> {:error, "Not objectID found"}
object_id -> {:ok, object_id}
end
end
defp get_object_id!(object) do
case get_object_id(object) do
{:error, _} ->
raise ArgumentError, message: "objectID doesn't exist"
{:ok, object_id} -> object_id
end
end
defp send_batch_request(requests, index) do
path = "/#{index}/batch"
body = requests |> Poison.encode!
send_request(:write, :post, path, body)
|> inject_index_into_response(index)
end
defp build_batch_request(objects, action, with_object_id: with_object_id) do
requests = Enum.map objects, fn(object) ->
if with_object_id do
object_id = get_object_id!(object)
%{action: action, body: object, objectID: object_id}
else
%{action: action, body: object}
end
end
%{ requests: requests }
end
@doc """
Delete a object by its objectID
"""
def delete_object(index, object_id) do
path = "#{index}/#{object_id}"
send_request(:write, :delete, path)
|> inject_index_into_response(index)
end
@doc """
Delete multiple objects
"""
def delete_objects(index, object_ids) do
object_ids
|> Enum.map(fn (id) ->
%{objectID: id}
end)
|> build_batch_request("deleteObject", with_object_id: true)
|> send_batch_request(index)
end
@doc """
List all indexes
"""
def list_indexes do
send_request(:read, :get, "")
end
@doc """
Clears all content of an index
"""
def clear_index(index) do
path = "#{index}/clear"
send_request(:write, :post, path)
|> inject_index_into_response(index)
end
@doc """
Set the settings of a index
"""
def set_settings(index, settings) do
body = settings |> Poison.encode!
send_request(:write, :put, "/#{index}/settings", body)
|> inject_index_into_response(index)
end
@doc """
Get the settings of a index
"""
def get_settings(index) do
send_request(:read, :get, "/#{index}/settings")
|> inject_index_into_response(index)
end
@doc """
Moves an index to new one
"""
def move_index(src_index, dst_index) do
body = %{ operation: "move", destination: dst_index } |> Poison.encode!
send_request(:write, :post, "/#{src_index}/operation", body)
|> inject_index_into_response(src_index)
end
@doc """
Copies an index to a new one
"""
def copy_index(src_index, dst_index) do
body = %{ operation: "copy", destination: dst_index } |> Poison.encode!
send_request(:write, :post, "/#{src_index}/operation", body)
|> inject_index_into_response(src_index)
end
## Helps piping a response into wait_task, as it requires the index
defp inject_index_into_response({:ok, body}, index) do
{:ok, Map.put(body, "indexName", index)}
end
defp inject_index_into_response(response, index), do: response
@doc """
Wait for a task for an index to complete
returns :ok when it's done
"""
def wait_task(index, task_id, time_before_retry \\ 1000) do
case send_request(:write, :get, "#{index}/task/#{task_id}") do
{:ok, %{"status" => "published"}} -> :ok
{:ok, %{"status" => "notPublished"}} ->
:timer.sleep(time_before_retry)
wait_task(index, task_id, time_before_retry)
other -> other
end
end
@doc """
Convinient version of wait_task/4, accepts a response to be waited on
directly. This enables piping a operation directly into wait_task
"""
def wait(response = {:ok, %{"indexName" => index, "taskID" => task_id}}, time_before_retry) do
with :ok <- wait_task(index, task_id, time_before_retry), do: response
end
def wait(response = {:ok, _}), do: wait(response, 1000)
def wait(response = {:error, _}), do: response
def wait(response), do: response
end