lib/elastix/search.ex

defmodule Elastix.Search do
  @moduledoc """
  The search APIs are used to query indices.

  [Elastic documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search.html)
  """
  import Elastix.HTTP, only: [prepare_url: 2]
  alias Elastix.{HTTP, JSON}

  @doc """
  Makes a request to the `_search` or the `_msearch` endpoint depending on the type of
  `data`.

  When passing a map for data, it'll make a simple search, but you can pass a list of
  header and body params to make a [multi search](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-multi-search.html).

  ## Examples

      iex> Elastix.Search.search("http://localhost:9200", "twitter", ["tweet"], %{query: %{term: %{user: "kimchy"}}})
      {:ok, %HTTPoison.Response{...}}

      iex> Elastix.Search.search("http://localhost:9200", ["twitter", "other-index"], ["tweet"], %{query: %{term: %{user: "kimchy"}}})
      {:ok, %HTTPoison.Response{...}}
  """
  @spec search(
          elastic_url :: String.t(),
          index :: String.t() | list,
          types :: list,
          data :: map | list
        ) :: HTTP.resp()
  def search(elastic_url, index, types, data) when is_list(data),
    do: search(elastic_url, index, types, data, [])

  def search(elastic_url, index, types, data),
    do: search(elastic_url, index, types, data, [])

  @doc """
  Same as `search/4` but allows to specify query params and options for
  [`HTTPoison.request/5`](https://hexdocs.pm/httpoison/HTTPoison.html#request/5).
  """
  @spec search(
          elastic_url :: String.t(),
          index :: String.t() | list,
          types :: list,
          data :: map | list,
          query_params :: Keyword.t(),
          options :: Keyword.t()
        ) :: HTTP.resp()
  def search(elastic_url, index, types, data, query_params, options \\ [])

  def search(elastic_url, index, types, data, query_params, options)
      when is_list(index) do
    search(elastic_url, Enum.join(index, ","), types, data, query_params, options)
  end

  def search(elastic_url, index, types, data, query_params, options)
      when is_list(data) do
    data =
      Enum.reduce(data, [], fn d, acc -> ["\n", JSON.encode!(d) | acc] end)
      |> Enum.reverse()
      |> IO.iodata_to_binary()

    prepare_url(elastic_url, make_path(index, types, query_params, "_msearch"))
    |> HTTP.post(data, [], options)
  end

  def search(elastic_url, index, types, data, query_params, options) do
    prepare_url(elastic_url, make_path(index, types, query_params))
    |> HTTP.post(JSON.encode!(data), [], options)
  end

  @doc """
  Uses the [Scroll API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html)
  to allow scrolling through a list of results.

  ## Examples

      iex> Elastix.Search.scroll("http://localhost:9200", %{query: %{term: %{user: "kimchy"}}})
      {:ok, %HTTPoison.Response{...}}
  """
  @spec scroll(elastic_url :: String.t(), data :: map, options :: Keyword.t()) ::
          HTTP.resp()
  def scroll(elastic_url, data, options \\ []) do
    prepare_url(elastic_url, "_search/scroll")
    |> HTTP.post(JSON.encode!(data), [], options)
  end

  @doc """
  Returns the number of results for a query using the
  [Count API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-count.html).

  ## Examples

      iex> Elastix.Search.count("http://localhost:9200", "twitter", ["tweet"], %{query: %{term: %{user: "kimchy"}}})
      {:ok, %HTTPoison.Response{...}}
  """
  @spec count(elastic_url :: String.t(), index :: String.t(), types :: list, data :: map) ::
          HTTP.resp()
  def count(elastic_url, index, types, data),
    do: count(elastic_url, index, types, data, [])

  @doc """
  Same as `count/4` but allows to specify query params and options for
  [`HTTPoison.request/5`](https://hexdocs.pm/httpoison/HTTPoison.html#request/5).
  """
  @spec count(
          elastic_url :: String.t(),
          index :: String.t(),
          types :: list,
          data :: map,
          query_params :: Keyword.t(),
          options :: Keyword.t()
        ) :: HTTP.resp()
  def count(elastic_url, index, types, data, query_params, options \\ []) do
    (elastic_url <> make_path(index, types, query_params, "_count"))
    |> HTTP.post(JSON.encode!(data), [], options)
  end

  @doc false
  def make_path(index, types, query_params, api_type \\ "_search") do
    path_root = "/#{index}"

    path =
      case types do
        [] -> path_root
        _ -> path_root <> "/" <> Enum.join(types, ",")
      end

    full_path = "#{path}/#{api_type}"

    case query_params do
      [] -> full_path
      _ -> HTTP.append_query_string(full_path, query_params)
    end
  end
end