From bfeb19e5d3fdd61603d1da86008070b3933000eb Mon Sep 17 00:00:00 2001 From: Marco Milanesi Date: Mon, 14 Oct 2019 19:35:19 +0200 Subject: [PATCH] Port user schema to exact match search --- README.md | 14 ++++----- lib/bombadil.ex | 9 +++--- lib/bombadil/search.ex | 23 ++++++++------- test/bombadil_test.exs | 64 ++++++++++++++++++++++++++++++++++++++---- 4 files changed, 80 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index f1c5460..ca95767 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ iex> YourRepo.insert_or_update(Bombadil.index(SearchIndex, payload: %{"book" => {:ok, struct} # Full string provided -iex> YourRepo.all(Bombadil.search("Lord of the Rings")) +iex> YourRepo.all(Bombadil.search(SearchIndex, "Lord of the Rings")) # Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (to_tsvector('simple', payload::text) @@ plainto_tsquery('simple', 'Lord of the Rings')) [ %Bombadil.Ecto.Schema.SearchIndex{ @@ -51,7 +51,7 @@ iex> YourRepo.all(Bombadil.search("Lord of the Rings")) # One word provided (treated as case-insensitive) -iex> YourRepo.all(Bombadil.search("lord")) +iex> YourRepo.all(Bombadil.search(SearchIndex, "lord")) # Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (to_tsvector('simple', payload::text) @@ plainto_tsquery('simple', 'lord')) [ %Bombadil.Ecto.Schema.SearchIndex{ @@ -63,7 +63,7 @@ iex> YourRepo.all(Bombadil.search("lord")) # No results -iex> YourRepo.all(Bombadil.search("lordz")) +iex> YourRepo.all(Bombadil.search(SearchIndex, "lordz")) # Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (to_tsvector('simple', payload::text) @@ plainto_tsquery('simple', 'lordz')) [] ``` @@ -95,7 +95,7 @@ iex> YourRepo.all(Bombadil.fuzzy_search(SearchIndex, "lard of the ringz asdf")) ```elixir iex> YourRepo.insert_or_update(Bombadil.index(SearchIndex, payload: %{"character" => "Tom Bombadil"})) {:ok, struct} -iex> YourRepo.all(Bombadil.search([%{"book" => "rings"}])) +iex> YourRepo.all(Bombadil.search(SearchIndex, [%{"book" => "rings"}])) # Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR to_tsvector('simple', (payload->'book')::text) @@ plainto_tsquery('simple', 'rings')) [ %Bombadil.Ecto.Schema.SearchIndex{ @@ -104,7 +104,7 @@ iex> YourRepo.all(Bombadil.search([%{"book" => "rings"}])) id: 1 } ] -iex> YourRepo.all(Bombadil.search([%{"character" => "bombadil"}])) +iex> YourRepo.all(Bombadil.search(SearchIndex, [%{"character" => "bombadil"}])) # Raw SQL: SELECT s0."id", s0."payload" FROM "search_index" AS s0 WHERE (FALSE OR to_tsvector('simple', (payload->'character')::text) @@ plainto_tsquery('simple', 'bombadil')) [ %Bombadil.Ecto.Schema.SearchIndex{ @@ -170,7 +170,7 @@ iex> YourRepo.all(Bombadil.fuzzy_search(SearchIndex, "lord of the ringz", contex # Encoding to JSON ```elixir -iex> Bombadil.search("rings") |> Jason.encode!() +iex> Bombadil.search(SearchIndex, "rings") |> Jason.encode!() "[{\"payload\":{\"book\":\"Lord of the Rings\"}}]" ``` @@ -290,8 +290,6 @@ And implement indexing and search for your use case by using the # TODO -[ ] Port user schema to `Bombadil.search` - [ ] Support other fields, rather than jsonb (?) ## Thank you(s) diff --git a/lib/bombadil.ex b/lib/bombadil.ex index 53029aa..26a887a 100644 --- a/lib/bombadil.ex +++ b/lib/bombadil.ex @@ -17,7 +17,7 @@ defmodule Bombadil do alias Bombadil.Ecto.Schema.SearchIndex - iex> YourEctoRepo.all(Bombadil.search("Lord of the Rings")) + iex> YourEctoRepo.all(Bombadil.search(SearchIndex, "Lord of the Rings")) [ %Bombadil.Ecto.Schema.SearchIndex{ __meta__: #Ecto.Schema.Metadata<:loaded, "search_index">, @@ -26,8 +26,8 @@ defmodule Bombadil do } ] """ - @spec search(String.t()) :: Ecto.Query.t() - defdelegate search(query), to: Bombadil.Search + @spec search(Ecto.Schema.t(), String.t()) :: Ecto.Query.t() + defdelegate search(schema, query, opts \\ []), to: Bombadil.Search @doc """ Fuzzy search data of a string (or substring) @@ -59,6 +59,7 @@ defmodule Bombadil do YourEctoRepo.insert_or_update(Bombadil.index(SearchIndex, payload: %{"book" => "Lord of the Rings"})) """ - @spec index(Ecto.Schema.t(), Keyword.t() | Ecto.Changeset.t(), Keyword.t()) :: Ecto.Changeset.t() + @spec index(Ecto.Schema.t(), Keyword.t() | Ecto.Changeset.t(), Keyword.t()) :: + Ecto.Changeset.t() defdelegate index(schema, payload, params \\ []), to: Bombadil.Index end diff --git a/lib/bombadil/search.ex b/lib/bombadil/search.ex index cd1c6b4..e8ab7a3 100644 --- a/lib/bombadil/search.ex +++ b/lib/bombadil/search.ex @@ -3,22 +3,18 @@ defmodule Bombadil.Search do import Ecto.Query - alias Bombadil.Ecto.Schema.SearchIndex - # EXPLORE websearch # SELECT websearch_to_tsquery('english', '"supernovae stars" -crab'); # TODO: Convert to opts the options ;) - def search(search_query, opts \\ [operator: :or]) + def search(schema, search_query, opts \\ []) - def search(search_query, opts) when is_list(search_query) do - operator = Keyword.get(opts, :operator, :or) + def search(schema, search_query, opts) when is_list(search_query) do search_query = to_tuple_list(search_query) - construct_extact_match_query(search_query, operator) + construct_extact_match_query(schema, search_query, opts) end - def search(search_query, opts) when is_binary(search_query) do - operator = Keyword.get(opts, :operator, :or) - construct_extact_match_query(search_query, operator) + def search(schema, search_query, opts) when is_binary(search_query) do + construct_extact_match_query(schema, search_query, opts) end def fuzzy_search(schema, search_query, opts) @@ -31,9 +27,12 @@ defmodule Bombadil.Search do construct_fuzzy_query(schema, search_query, opts) end - defp construct_extact_match_query(search_query, operator) do - from(i in SearchIndex, - where: ^Bombadil.Criteria.prepare(search_query, operator) + defp construct_extact_match_query(schema, search_query, opts) do + context = Keyword.get(opts, :context, %{}) + + from(i in schema, + where: ^Bombadil.Criteria.prepare(search_query), + where: ^Enum.into(context, []) ) end diff --git a/test/bombadil_test.exs b/test/bombadil_test.exs index df13032..a74ec86 100644 --- a/test/bombadil_test.exs +++ b/test/bombadil_test.exs @@ -19,7 +19,7 @@ defmodule BombadilTest do ) assert [%_{payload: %{"ask" => "ciao"}, test: "I was generated by config dynamically!"}] = - TestRepo.all(Bombadil.search([%{"ask" => "ciao"}])) + TestRepo.all(Bombadil.search(SearchIndex, [%{"ask" => "ciao"}])) end end @@ -34,7 +34,9 @@ defmodule BombadilTest do ) assert [%_{payload: %{"ask" => "ciao"}}, %_{payload: %{"ask" => "ciao2"}}] = - TestRepo.all(Bombadil.search([%{"ask" => "ciao"}, %{"ask" => "ciao2"}])) + TestRepo.all( + Bombadil.search(SearchIndex, [%{"ask" => "ciao"}, %{"ask" => "ciao2"}]) + ) end test "search payload (exact match with one criteria)" do @@ -42,7 +44,7 @@ defmodule BombadilTest do TestRepo.insert_or_update(Bombadil.index(SearchIndex, payload: %{"ask" => "ciao"})) assert [%_{payload: %{"ask" => "ciao"}}] = - TestRepo.all(Bombadil.search([%{"ask" => "ciao"}])) + TestRepo.all(Bombadil.search(SearchIndex, [%{"ask" => "ciao"}])) end test "search payload (does not match)" do @@ -51,7 +53,7 @@ defmodule BombadilTest do Bombadil.index(SearchIndex, payload: %{"ask" => "hello"}) ) - assert [] = TestRepo.all(Bombadil.search([%{"ask" => "ciao"}])) + assert [] = TestRepo.all(Bombadil.search(SearchIndex, [%{"ask" => "ciao"}])) end end @@ -65,7 +67,8 @@ defmodule BombadilTest do Bombadil.index(SearchIndex, payload: %{"ask" => "ciao2"}) ) - assert [%_{payload: %{"ask" => "ciao2"}}] = TestRepo.all(Bombadil.search("ciao2")) + assert [%_{payload: %{"ask" => "ciao2"}}] = + TestRepo.all(Bombadil.search(SearchIndex, "ciao2")) end test "simple match with metadata" do @@ -77,7 +80,7 @@ defmodule BombadilTest do ) assert [%_{payload: %{"ask" => "hello world", "metadata" => [%{"meta" => "data"}]}}] = - TestRepo.all(Bombadil.search("hello world")) + TestRepo.all(Bombadil.search(SearchIndex, "hello world")) end end @@ -146,6 +149,55 @@ defmodule BombadilTest do end describe "search with a context" do + test "and exact match" do + assert {:ok, _} = + TestRepo.insert_or_update( + Bombadil.index(SearchIndex, item_id: 42, payload: %{"ask" => "hello exact match"}) + ) + + assert {:ok, _} = + TestRepo.insert_or_update( + Bombadil.index(SearchIndex, + item_id: 42, + payload: %{"ask" => "I am hiding with the same id, don't find me!"} + ) + ) + + assert {:ok, _} = + TestRepo.insert_or_update( + Bombadil.index(SearchIndex, + item_id: 24, + payload: %{"dont_look_at_me" => "hello exact match"} + ) + ) + + assert [ + %Bombadil.Ecto.Schema.SearchIndex{ + id: _id, + item_id: 42, + payload: %{"ask" => "hello exact match"}, + test: nil + } + ] = + TestRepo.all( + Bombadil.search(SearchIndex, [%{"ask" => "hello exact match"}], + context: %{item_id: 42} + ) + ) + + assert [ + %Bombadil.Ecto.Schema.SearchIndex{ + id: _id, + item_id: 42, + payload: %{"ask" => "hello exact match"}, + test: nil + } + ] = + TestRepo.all( + Bombadil.search(SearchIndex, "hello exact match", context: %{item_id: 42}) + ) + end + test "and fuzzy search" do assert {:ok, _} = TestRepo.insert_or_update(