defmodule ExAws.CloudSearch.Document do @moduledoc """ Operations to add and remove documents from a CloudSearch domain index. """ alias ExAws.Operation.CloudSearch, as: Operation @typedoc """ Field values may be strings (`text`, `literal`, `latlon`, `date`), lists of strings (`text-array`, `literal-array`, `date-array`), numbers (`int`, `double`), or lists of numbers (`int-array`, `double-array`). Empty or `nil` values are not permitted. """ @type field_value :: String.t() | list(String.t()) | number | list(number) @typedoc """ Fields are maps of field names (which must be strings matching the pattern `[a-zA-Z0-9][a-zA-Z0-9_]{0,63}`) with field values. At least one field/value pair must be provided. """ @type fields :: %{required(String.t() | atom) => field_value} @type add :: %{type: :add, id: String.t(), fields: fields} @type delete :: %{type: :delete, id: String.t()} @type document :: add | delete @type batch :: list(document) @type request :: %Operation{ params: batch, request_type: :doc } @doc """ Adds the provided document(s) to the index. Note that nothing in this function prevents a request from being too large to process in CloudSearch (batches may be at most 5Mb, with each document being at most 1Mb). Documents may be added in several ways: add(%Band{id: 3, name: "Grimes"}) add(%{id: 3, name: "Grimes"}) add(%{"id" => 3, "name" => "Grimes"}) add(%{3 => %{"name" => "Grimes"}}) add({3, %{"name" => "Grimes"}}) add([%Band{id: 3, name: "Grimes"}]) add([%{id: 3, name: "Grimes"}]) add([%{"id" => 3, "name" => "Grimes"}]) add([%{3 => %{"name" => "Grimes"}}]) add([{3, %{name: "Grimes"}}]) However the document is provided, it must have an ID and the list of fields. The list of fields *must* match the document as defined in the index field configuration in CloudSearch (or the document upload will fail at least for that document). The ID will be converted to a string with `to_string/1`. Fields may not be `nil` or empty (`""` or `[]`), so those will be removed. """ def add(%_mod{} = document) do add(add_normalize(document)) end def add(%{id: _} = document) do add(add_normalize(document)) end def add(%{"id" => _} = document) do add(add_normalize(document)) end def add(documents) when is_map(documents) do add(Enum.into(documents, [])) end def add({_id, _fields} = document) do add([document]) end def add([]) do raise(ExAws.Error, "Must provide documents to index.") end def add(documents) when is_list(documents) do documents |> Enum.map(&add_normalize/1) |> Enum.map(&to_add_request/1) |> request() end @doc """ Removes the provided document(s) by ID from the index. remove(%Band{id: 3}) remove(%{id: 3}) remove(%{"id" => 3}) remove(3) remove([%Band{id: 3}]) remove([%{id: 3}]) remove([%{"id" => 3}]) remove([3]) """ def remove(%_mod{} = document) do remove(remove_normalize(document)) end def remove(%{id: _} = document) do remove(remove_normalize(document)) end def remove(%{"id" => _} = document) do remove(remove_normalize(document)) end def remove([]) do raise(ExAws.Error, "Must provide documents to remove.") end def remove(documents) when is_list(documents) do documents |> Enum.flat_map(&remove_normalize/1) |> Enum.map(&to_remove_request/1) |> request() end def remove(document) do remove(remove_normalize(document)) end defp add_normalize(%_mod{id: _} = document) do add_normalize(Map.from_struct(document)) end defp add_normalize(%mod{}) do raise ExAws.Error, "Cannot add a document directly from struct #{mod}; it does not have an id field." end defp add_normalize(%{id: id} = document) do add_normalize({id, Map.delete(document, :id)}) end defp add_normalize(%{"id" => id} = document) do add_normalize({id, Map.delete(document, "id")}) end defp add_normalize(%{}) do raise ExAws.Error, "Cannot add a document from a map that does not have an id field." end defp add_normalize({id, fields}) when is_map(fields) do if Enum.empty?(fields) do raise ExAws.Error, "At least one field must be specified for document #{id}." else {to_string(id), fields} end end defp add_normalize({id, _}) do raise ExAws.Error, "Cannot add document #{id} because it does not have a map for fields." end defp remove_normalize(%_mod{id: id}) do [id] end defp remove_normalize(%{id: id}) do [id] end defp remove_normalize(%{"id" => id}) do [id] end defp remove_normalize(%mod{}) do raise( ExAws.Error, "Cannot remove a document of struct #{mod}; it does not have an id field." ) end defp remove_normalize(%{}) do raise( ExAws.Error, "Cannot find an id field in the provided map to remove this document." ) end defp remove_normalize(id) do List.wrap(id) end defp to_add_request({id, fields}) do %{type: :add, id: to_string(id), fields: add_normalize_fields(fields)} end defp to_remove_request(id) do %{type: :delete, id: to_string(id)} end defp add_normalize_fields(%{} = fields) do fields |> Enum.map(&add_normalize_field/1) |> Enum.filter(&elem(&1, 1)) |> Enum.into(%{}) end defp add_normalize_field({key, %Date{} = value}) do {key, "#{Date.to_iso8601(value)}T00:00:00Z"} end defp add_normalize_field({key, %Time{}}) do raise ExAws.Error, "Cannot add a Time as a document field for #{key}." end defp add_normalize_field({key, %DateTime{} = value}) do {key, DateTime.to_iso8601(value)} end defp add_normalize_field({key, []}) do {key, nil} end defp add_normalize_field({key, ""}) do {key, nil} end defp add_normalize_field(pair) do pair end defp request(batch) do %Operation{ path: "/documents/batch", data: List.wrap(batch), request_type: :doc } end end