Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

ESQuery

elastic_query_builder.livemd

ESQuery

Mix.install([
  {:snap, "~> 0.9.0"},
  {:faker, "~> 0.18.0"},
  {:jason, "~> 1.4"}
])

ExUnit.configure(exclude: [:skip])
ExUnit.start(autorun: false)

Section

defmodule ESQuery do
  defmodule BoolQuery do
    defstruct must: [], filter: [], should: [], must_not: []
  end

  alias ESQuery.BoolQuery

  defmacro __using__(_) do
    quote do
      alias ESQuery.BoolQuery

      import ESQuery,
        only: [
          boost: 2,
          minimum_should_match: 2,
          filter: 2,
          must: 2,
          must_not: 2,
          should: 2,
          match: 1,
          term: 1,
          terms: 2,
          terms: 3,
          range: 1,
          generate: 1,
          query: 2,
          match_all: 0
        ]
    end
  end

  def generate(%BoolQuery{} = query, type \\ :bool) do
    query =
      query
      |> Map.from_struct()
      |> Enum.reject(fn {_k, v} -> v == [] end)
      |> Enum.map(fn
        {k, [element]} -> {k, element}
        elements -> elements
      end)
      |> Map.new()

    %{query: %{type => query}}
    |> Jason.encode!()
    |> Jason.decode!()
  end

  def boost(query, val) do
    Map.put(query, :boost, val)
  end

  def minimum_should_match(query, val) do
    Map.put(query, :minimum_should_match, val)
  end

  def must(%BoolQuery{} = query, val) do
    %{query | must: query.must ++ [val]}
  end

  def must_not(%BoolQuery{} = query, val) do
    %{query | must_not: query.must_not ++ [val]}
  end

  def should(%BoolQuery{} = query, val) do
    %{query | should: query.should ++ [val]}
  end

  def filter(%BoolQuery{} = query, val) do
    %{query | filter: query.filter ++ [val]}
  end

  def match([{key, value}]) do
    %{match: %{key => value}}
  end

  def term([{key, value}]) do
    %{term: %{key => value}}
  end

  def terms([{key, value}]) when is_list(value) do
    %{terms: %{key => value}}
  end

  def terms(key, value, opts) when is_list(value) and is_list(opts) do
    %{terms: %{key => value} |> Map.merge(Map.new(opts))}
  end

  def range([{key, kw}]) do
    %{range: %{key => Map.new(kw)}}
  end

  def match_all() do
    %{match_all: %{}}
  end

  def query(val, opts) do
    opts
    |> Kernel.++(query: val)
    |> Map.new()
  end
end
ExUnit.start()

defmodule ESQueryTest do
  use ExUnit.Case, async: true

  use ESQuery

  test "bool filter example" do
    query =
      %ESQuery.BoolQuery{}
      |> must(match(title: "Search"))
      |> must(match(content: "Elasticsearch"))
      |> filter(term(status: "published"))
      |> filter(range(publish_date: [gte: ~D"2015-01-01"]))
      |> generate()

    assert query ==
             """
             {
               "query": { 
                 "bool": { 
                   "must": [
                     { "match": { "title":   "Search"        }},
                     { "match": { "content": "Elasticsearch" }}
                   ],
                   "filter": [ 
                     { "term":  { "status": "published" }},
                     { "range": { "publish_date": { "gte": "2015-01-01" }}}
                   ]
                 }
               }
             }
             """
             |> Jason.decode!()
  end

  test "bool query full" do
    query =
      %ESQuery.BoolQuery{}
      |> must(term("user.id": "kimchy"))
      |> must_not(range(age: [gte: 10, lte: 20]))
      |> filter(term(tags: "production"))
      |> should(term(tags: "env1"))
      |> should(term(tags: "deployed"))
      |> boost(1.0)
      |> minimum_should_match(1)
      |> generate()

    assert query ==
             """
             {
             "query": {
               "bool" : {
                 "must" : {
                   "term" : { "user.id" : "kimchy" }
                 },
                 "filter": {
                   "term" : { "tags" : "production" }
                 },
                 "must_not" : {
                   "range" : {
                     "age" : { "gte" : 10, "lte" : 20 }
                   }
                 },
                 "should" : [
                   { "term" : { "tags" : "env1" } },
                   { "term" : { "tags" : "deployed" } }
                 ],
                 "minimum_should_match" : 1,
                 "boost" : 1.0
               }
             }
             }
             """
             |> Jason.decode!()
  end

  test "bool query named" do
    query =
      %ESQuery.BoolQuery{}
      |> filter(terms("name.last", ["banon", "kimchy"], _name: "test"))
      |> should(match("name.first": query("shay", _name: "first")))
      |> should(match("name.last": query("banon", _name: "last")))
      |> generate()

    assert query ==
             """
             {
               "query": {
                 "bool": {
                   "should": [
                     { "match": { "name.first": { "query": "shay", "_name": "first" } } },
                     { "match": { "name.last": { "query": "banon", "_name": "last" } } }
                   ],
                   "filter": {
                     "terms": {
                       "name.last": [ "banon", "kimchy" ],
                       "_name": "test"
                     }
                   }
                 }
               }
             }
             """
             |> Jason.decode!()
  end
end

ExUnit.run()