defmodule ExTypesense.Document do
@moduledoc since: "0.1.0"
@moduledoc """
Module for CRUD operations for documents. Refer to this [doc guide](https://typesense.org/docs/latest/api/documents.html).
"""
alias ExTypesense.HttpClient
import Ecto.Query, warn: false
@root_path "/"
@collections_path @root_path <> "collections"
@documents_path "documents"
@import_path "import"
@type response :: :ok | {:ok, map()} | {:error, map()}
@doc """
Get a document from a collection.
## Examples
iex> schema = %{
...> name: "posts",
...> fields: [
...> %{name: "title", type: "string"}
...> ],
...> }
...> ExTypesense.create_collection(schema)
...> post = %{
...> id: "444",
...> collection_name: "posts",
...> title: "the quick brown fox"
...> }
iex> ExTypesense.create_document(post)
iex> ExTypesense.get_document("posts", 444)
{:ok,
%{
"id" => "444",
"collection_name" => "posts",
"title" => "the quick brown fox",
}
}
"""
@doc since: "0.1.0"
@spec get_document(module() | String.t(), integer()) :: response()
def get_document(module_name, document_id)
when is_atom(module_name) and is_integer(document_id) do
collection_name = module_name.__schema__(:source)
do_get_document(collection_name, document_id)
end
def get_document(collection_name, document_id)
when is_binary(collection_name) and is_integer(document_id) do
do_get_document(collection_name, document_id)
end
@spec do_get_document(String.t() | module(), integer()) :: response()
defp do_get_document(collection_name, document_id) do
path =
[
@collections_path,
collection_name,
@documents_path,
to_string(document_id)
]
|> Path.join()
HttpClient.run(:get, path)
end
@doc """
Indexes multiple documents via maps.
**Note**: when using maps as documents, you should pass a key named `collection_name`
and with the lists of documents named `documents` (example shown below).
## Examples
iex> schema = %{
...> name: "posts",
...> fields: [
...> %{name: "title", type: "string"}
...> ],
...> }
...> ExTypesense.create_collection(schema)
...> posts = %{
...> collection_name: "posts",
...> documents: [
...> %{title: "the quick brown fox"},
...> %{title: "jumps over the lazy dog"}
...> ]
...> }
iex> ExTypesense.index_multiple_documents(posts)
{:ok, [%{"success" => true}, %{"success" => true}]}
"""
@doc since: "0.1.0"
@spec index_multiple_documents(list(struct()) | map()) :: response()
def index_multiple_documents([struct | _] = list_of_structs)
when is_struct(struct) do
collection_name = struct.__struct__.__schema__(:source)
do_index_multiple_documents(collection_name, "create", list_of_structs)
end
def index_multiple_documents(%{collection_name: collection_name, documents: documents} = map)
when is_map(map) do
do_index_multiple_documents(collection_name, "create", documents)
end
@doc """
Updates multiple documents via maps.
**Note**: when using maps as documents, you should pass a key named `collection_name`
and with the lists of documents named `documents` (example shown below). Also add the `id`
for each documents.
## Examples
iex> schema = %{
...> name: "posts",
...> fields: [
...> %{name: "title", type: "string"}
...> ],
...> }
iex> ExTypesense.create_collection(schema)
iex> posts = %{
...> collection_name: "posts",
...> documents: [
...> %{id: "5", title: "the quick brown fox"},
...> %{id: "6", title: "jumps over the lazy dog"}
...> ]
...> }
iex> {:ok, _} = ExTypesense.index_multiple_documents(posts)
iex> updated_posts = %{
...> collection_name: "posts",
...> documents: [
...> %{id: "5", title: "the quick"},
...> %{id: "6", title: "jumps over"}
...> ]
...> }
iex> ExTypesense.update_multiple_documents(updated_posts)
{:ok, [%{"success" => true}, %{"success" => true}]}
"""
@doc since: "0.3.0"
@spec update_multiple_documents(list(struct()) | map()) :: response()
def update_multiple_documents([struct | _] = list_of_structs) when is_struct(struct) do
collection_name = struct.__struct__.__schema__(:source)
do_index_multiple_documents(collection_name, "update", list_of_structs)
end
def update_multiple_documents(%{collection_name: collection_name, documents: documents} = map)
when is_map(map) do
do_index_multiple_documents(collection_name, "update", documents)
end
@doc """
Upserts multiple documents via maps. Same with `update_multiple_documents/1` with some
difference: creates one if not existed, otherwise updates it.
**Note**: when using maps as documents, you should pass a key named `collection_name`
and with the lists of documents named `documents` (example shown below). When `id` is added,
it will update, otherwise creates a new document.
for each documents.
## Examples
iex> schema = %{
...> name: "posts",
...> fields: [
...> %{name: "title", type: "string"}
...> ],
...> }
iex> ExTypesense.create_collection(schema)
iex> posts = %{
...> collection_name: "posts",
...> documents: [
...> %{id: "0", title: "the quick"},
...> %{id: "1", title: "jumps over"}
...> ]
...> }
iex> ExTypesense.upsert_multiple_documents(posts)
{:ok, [%{"success" => true}, %{"success" => true}]}
"""
@doc since: "0.3.0"
@spec upsert_multiple_documents(map()) :: response()
def upsert_multiple_documents(%{collection_name: collection_name, documents: documents} = map)
when is_map(map) do
do_index_multiple_documents(collection_name, "upsert", documents)
end
@spec do_index_multiple_documents(String.t(), String.t(), [struct()] | [map()]) :: response()
defp do_index_multiple_documents(collection_name, action, documents) do
payload =
documents
|> Stream.map(&Jason.encode!/1)
|> Enum.join("\n")
path = Path.join([@collections_path, collection_name, @documents_path, @import_path])
uri = %URI{path: path, query: "action=#{action}"}
HttpClient.httpc_run(uri, :post, payload, 'text/plain')
end
@doc """
Indexes a single document using struct or map. When using struct,
the pk maps to document's id as string.
**Note**: when using maps as documents, you should pass a key named "collection_name".
## Examples
iex> schema = %{
...> name: "posts",
...> fields: [
...> %{name: "title", type: "string"}
...> ],
...> }
iex> ExTypesense.create_collection(schema)
iex> post =
...> %{
...> id: "34",
...> collection_name: "posts",
...> post_id: 34,
...> title: "the quick brown fox",
...> description: "jumps over the lazy dog"
...> }
iex> ExTypesense.create_document(post)
{:ok,
%{
"id" => "34",
"collection_name" => "posts",
"post_id" => 34,
"title" => "the quick brown fox",
"description" => "jumps over the lazy dog"
}
}
"""
@doc since: "0.3.0"
@spec create_document(struct() | map() | [struct()] | [map()]) :: response()
def create_document(struct) when is_struct(struct) do
collection_name = struct.__struct__.__schema__(:source)
path = Path.join([@collections_path, collection_name, @documents_path])
payload = Map.put(struct, :id, to_string(struct.id)) |> Jason.encode!()
do_index_document(path, :post, "create", payload)
end
def create_document(document) when is_map(document) do
collection_name = Map.get(document, :collection_name)
path = Path.join([@collections_path, collection_name, @documents_path])
do_index_document(path, :post, "create", Jason.encode!(document))
end
@doc """
Updates a single document using struct or map.
**Note**: when using maps as documents, you should pass a key named "collection_name".
## Examples
iex> schema = %{
...> name: "posts",
...> fields: [
...> %{name: "title", type: "string"}
...> ],
...> }
iex> ExTypesense.create_collection(schema)
iex> post =
...> %{
...> id: "94",
...> collection_name: "posts",
...> post_id: 94,
...> title: "the quick brown fox"
...> }
iex> ExTypesense.create_document(post)
iex> updated_post =
...> %{
...> id: "94",
...> collection_name: "posts",
...> post_id: 94,
...> title: "test"
...> }
iex> ExTypesense.update_document(updated_post)
{:ok,
%{
"id" => "94",
"collection_name" => "posts",
"post_id" => 94,
"title" => "test"
}
}
"""
@doc since: "0.3.0"
@spec update_document(struct() | map()) :: response()
def update_document(struct) when is_struct(struct) do
collection_name = struct.__struct__.__schema__(:source)
path =
Path.join([@collections_path, collection_name, @documents_path, Jason.encode!(struct.id)])
do_index_document(path, :patch, "update", Jason.encode!(struct))
end
def update_document(document) when is_map(document) do
id = String.to_integer(document.id)
collection_name = Map.get(document, :collection_name)
path = Path.join([@collections_path, collection_name, @documents_path, Jason.encode!(id)])
do_index_document(path, :patch, "update", Jason.encode!(document))
end
@doc """
Upserts a single document using struct or map.
**Note**: when using maps as documents, you should pass a key named "collection_name".
"""
@doc since: "0.3.0"
@spec upsert_document(map() | struct()) :: response()
def upsert_document(struct) when is_struct(struct) do
id = to_string(struct.id)
collection_name = struct.__struct__.__schema__(:source)
document = Map.put(struct, :id, id) |> Jason.encode!()
path = Path.join([@collections_path, collection_name, @documents_path])
do_index_document(path, :post, "upsert", document)
end
def upsert_document(document) when is_map(document) do
collection_name = Map.get(document, :collection_name)
id = document.id
document =
if is_integer(id) do
document |> Map.put(:id, Jason.encode!(id)) |> Jason.encode!()
else
document |> Jason.encode!()
end
path = Path.join([@collections_path, collection_name, @documents_path])
do_index_document(path, :post, "upsert", document)
end
@spec do_index_document(String.t(), atom(), String.t(), String.t()) :: response()
defp do_index_document(path, method, action, document) do
uri = %URI{path: path, query: "action=#{action}"}
HttpClient.httpc_run(uri, method, document)
end
@doc """
Deletes a document by struct.
"""
@spec delete_document(struct()) :: response()
def delete_document(struct) when is_struct(struct) do
document_id = struct.id
collection_name = struct.__struct__.__schema__(:source)
do_delete_document(collection_name, document_id)
end
@doc """
Deletes a document by id.
## Examples
iex> schema = %{
...> name: "posts",
...> fields: [
...> %{name: "title", type: "string"}
...> ],
...> }
...> ExTypesense.create_collection(schema)
iex> post =
...> %{
...> id: "12",
...> collection_name: "posts",
...> post_id: 22,
...> title: "the quick brown fox"
...> }
iex> ExTypesense.create_document(post)
iex> ExTypesense.delete_document("posts", 12)
{:ok,
%{
"id" => "12",
"post_id" => 22,
"title" => "the quick brown fox",
"collection_name" => "posts"
}
}
"""
@doc since: "0.3.0"
@spec delete_document(String.t(), integer()) :: response()
def delete_document(collection_name, document_id)
when is_binary(collection_name) and is_integer(document_id) do
do_delete_document(collection_name, document_id)
end
defp do_delete_document(collection_name, document_id) do
path =
Path.join([
@collections_path,
collection_name,
@documents_path,
Jason.encode!(document_id)
])
HttpClient.run(:delete, path)
end
end