routing function in Document protocol

This commit is contained in:
Cary Dunn 2018-08-01 18:32:40 -07:00
parent 8e00d8dabe
commit ed8a3a07e3
12 changed files with 154 additions and 54 deletions

View file

@ -1,5 +1,15 @@
# Change Log
## [v0.5.0](https://github.com/infinitered/elasticsearch-elixir/tree/v0.5.0) (2018-08-01)
[Full Changelog](https://github.com/infinitered/elasticsearch-elixir/compare/v0.4.1...v0.5.0)
**Closed issues:**
**Merged pull requests:**
- Add support for routing keys in Document protocol. Add breaking change
documentation.
## [v0.4.1](https://github.com/infinitered/elasticsearch-elixir/tree/v0.4.1) (2018-06-26)
[Full Changelog](https://github.com/infinitered/elasticsearch-elixir/compare/v0.4.0...v0.4.1)
@ -90,4 +100,4 @@
\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*

View file

@ -141,6 +141,7 @@ protocol.
```elixir
defimpl Elasticsearch.Document, for: MyApp.Post do
def id(post), do: post.id
def routing(_), do: false
def encode(post) do
%{
title: post.title,

View file

@ -0,0 +1,55 @@
# Upgrading from 0.4.x to 0.5.x
Version `0.5.0` added the `routing` meta-field to be specified in the Document
protocol.
## Rationale
The `routing`/`_routing` meta-field used to force a document to be hashed to a
particular shard which is required for `join` field datatypes but also used to
gain more control over which shard your documents are routed to.
## Changes
**BREAKING**: `routing` function is now required to be specified in the
`Elasticsearch.Document` protocol. You may specify it to return `false` to
use default routing (document `id`).
## How to Update Your App
Add a `routing/1` function to your `Elasticsearch.Document` implementation.
# BEFORE
defimpl Elasticsearch.Document, for: MyApp.Post do
def id(post), do: post.id
def encode(post) do
%{
title: post.title,
author: post.author
}
end
end
# AFTER (using default routing)
defimpl Elasticsearch.Document, for: MyApp.Post do
def id(post), do: post.id
def routing(_), do: false
def encode(post) do
%{
title: post.title,
author: post.author
}
end
end
# AFTER (using routing)
defimpl Elasticsearch.Document, for: MyApp.Post do
def id(post), do: post.id
def routing(post), do: post.account_id
def encode(post) do
%{
title: post.title,
author: post.author
}
end
end

View file

@ -9,7 +9,6 @@ defmodule Elasticsearch do
alias Elasticsearch.{
Document,
DocumentMeta,
Cluster,
Cluster.Config
}
@ -127,12 +126,22 @@ defmodule Elasticsearch do
defp document_url(document, index) do
url = "/#{index}/_doc/#{Document.id(document)}"
case DocumentMeta.routing(document) do
nil -> url
routing -> "#{url}?routing=#{routing}"
if routing = Document.routing(document) do
document_url_with_routing(url, routing)
else
url
end
end
defp document_url_with_routing(url, routing) do
url <>
if url =~ ~r/\?/ do
"&"
else
"?"
end <> URI.encode_query(%{routing: routing})
end
@doc """
Waits for a given Elasticsearch cluster to be available.

View file

@ -6,8 +6,7 @@ defmodule Elasticsearch.Index.Bulk do
alias Elasticsearch.{
Cluster,
DataStream,
Document,
DocumentMeta
Document
}
require Logger
@ -71,10 +70,12 @@ defmodule Elasticsearch.Index.Bulk do
"_id" => Document.id(struct)
}
attrs = case DocumentMeta.routing(struct) do
nil -> attrs
routing -> Map.put(attrs, "_routing", routing)
end
attrs =
if routing = Document.routing(struct) do
Map.put(attrs, "_routing", routing)
else
attrs
end
config.json_library.encode!(%{type => attrs})
end

View file

@ -40,4 +40,25 @@ defprotocol Elasticsearch.Document do
"""
@spec encode(any) :: map
def encode(item)
@doc """
Returns the Elasticsearch `_routing` for the item. Elasticsearch
default if this value is not provided is to use the `_id`.
Setting this value to `false` or `nil` will omit sending the
meta-field with your requests and use default routing behaviour.
Routing allows you to control which shard the document should
be directed to which is necessary for `join` fields.
## Example
Specify a routing key to control the destination shard, like so:
def routing(item), do: item.parent_id
or omit routing and use default Elasticsearch functionality:
def routing(_), do: false
"""
@spec routing(any) :: any
def routing(item)
end

View file

@ -1,27 +0,0 @@
defprotocol Elasticsearch.DocumentMeta do
@fallback_to_any true
@moduledoc """
A protocol for converting a struct into an Elasticsearch meta-fields.
## Example
defimpl Elasticsearch.DocumentMeta, for: MyStruct do
def routing(struct), do: struct.id
end
"""
@doc """
Returns the Elasticsearch `_routing` for the item. Elasticsearch
default if this value is not provided is to use the `_id`.
## Example
def routing(item), do: item.id
"""
@spec routing(any) :: any
def routing(item)
end
defimpl Elasticsearch.DocumentMeta, for: Any do
def routing(_), do: nil
end

View file

@ -81,7 +81,8 @@ defmodule Elasticsearch.Index.BulkTest do
describe ".encode!/3" do
test "respects _routing meta-field" do
assert Bulk.encode!(Cluster, %Comment{id: "my-id", post_id: "123"}, "my-index") =~ "\"_routing\":\"123\""
assert Bulk.encode!(Cluster, %Comment{id: "my-id", post_id: "123"}, "my-index") =~
"\"_routing\":\"123\""
end
end
end

View file

@ -18,4 +18,31 @@ defmodule ElasticsearchTest do
Elasticsearch.delete(Cluster, "/nonexistent")
end)
end
describe ".put_document/3" do
test "routing meta-field is included if specified in Document" do
assert :ok =
Elasticsearch.Index.create_from_file(
Cluster,
"posts-routing",
"test/support/settings/posts.json"
)
assert {:ok, _} =
Elasticsearch.put_document(
Cluster,
%Post{id: 1, title: "Example Post", author: "John Smith"},
"posts-routing"
)
# If a routing key is not provided, this will throw an {:error, _}
# Elasticsearch.Exception: [routing] is missing for join field [doctype]
assert {:ok, _} =
Elasticsearch.put_document(
Cluster,
%Comment{id: 2, body: "Example Comment", author: "Jane Smith", post_id: 1},
"posts-routing"
)
end
end
end

View file

@ -14,6 +14,7 @@ defimpl Elasticsearch.Document, for: Comment do
def id(comment), do: comment.id
def type(_item), do: "comment"
def parent(_item), do: false
def routing(comment), do: comment.post_id
def encode(comment) do
%{
@ -26,7 +27,3 @@ defimpl Elasticsearch.Document, for: Comment do
}
end
end
defimpl Elasticsearch.DocumentMeta, for: Comment do
def routing(comment), do: comment.post_id
end

View file

@ -48,23 +48,27 @@ defmodule Elasticsearch.DataCase do
def random_post_id do
case Elasticsearch.Test.Repo.one(
from p in Post,
order_by: fragment("RANDOM()"),
limit: 1
) do
from(
p in Post,
order_by: fragment("RANDOM()"),
limit: 1
)
) do
nil -> nil
post -> post.id
end
end
def populate_comments_table(quantity \\ 10) do
comments = 0..quantity |> Enum.map(fn _ ->
%{
body: "Example Comment",
author: "Jane Doe",
post_id: random_post_id()
}
end)
comments =
0..quantity
|> Enum.map(fn _ ->
%{
body: "Example Comment",
author: "Jane Doe",
post_id: random_post_id()
}
end)
Elasticsearch.Test.Repo.insert_all("comments", comments)
end

View file

@ -13,6 +13,7 @@ defimpl Elasticsearch.Document, for: Post do
def id(post), do: post.id
def type(_item), do: "post"
def parent(_item), do: false
def routing(_item), do: false
def encode(post) do
%{