diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..00c9c11 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# Test +export API_MOCK_SERVER_PORT=8002 +export SLACK_MOCK_API_PORT=8002 +export POSTGRES_HOST=localhost +export POSTGRES_PORT=5432 +export DB_NAME=algoliax_test +export DB_USERNAME=postgres +export DB_PASSWORD=postgres + +# Run +export ALGOLIA_API_KEY= +export ALGOLIA_APPLICATION_ID= diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..4a7beba --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @WTTJ/mission-platform-backend diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2255699 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: mix + directory: "/" + schedule: + interval: daily + time: "06:00" + timezone: "Europe/Paris" + open-pull-requests-limit: 100 + rebase-strategy: "disabled" diff --git a/.github/workflows/publish_public_package.yml b/.github/workflows/publish_public_package.yml new file mode 100644 index 0000000..e494bfc --- /dev/null +++ b/.github/workflows/publish_public_package.yml @@ -0,0 +1,44 @@ +name: Publish package to Hex.pm + +on: + release: + types: [released] + +jobs: + publish: + runs-on: ubuntu-latest + if: "!github.event.release.prerelease" + + steps: + - uses: actions/checkout@v4 + + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: '1.17.2' + otp-version: '26.2.5.2' + + - name: Install dependencies + run: | + mix deps.get + + - name: Compile + run: | + mix compile + + - name: Check version consistency + run: | + MIX_VERSION=$(mix run -e 'IO.puts Mix.Project.config[:version]') + RELEASE_VERSION=${{ github.event.release.tag_name }} + RELEASE_VERSION=${RELEASE_VERSION#v} + + if [ "$MIX_VERSION" != "$RELEASE_VERSION" ]; then + echo "Version mismatch: mix.exs version ($MIX_VERSION) != release version ($RELEASE_VERSION)" + exit 1 + fi + + - name: Publish to Hex.pm + env: + HEX_API_KEY: ${{ secrets.HEXPM_PUBLIC_PACKAGES_KEY }} + run: | + mix hex.publish --yes diff --git a/.gitignore b/.gitignore index 651c58e..0c6e6dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ +# IDE +.idea/ +.vscode/ + +# Env +.env + # The directory Mix will write compiled artifacts to. /_build/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dfdd70..a8e7a30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,167 @@ # Changelog -## DEV +## v0.10.0 - 2025-03-10 + +### New + +Added the `synonyms` settings to the `Algoliax.Indexer`. +It allows you to automatically setup synonyms for your indexes when calling `MyIndexer.configure_index()` + +If not specified or set to `nil`, the synonyms will not be configured. +Otherwise, the following keywords are expected, with each key having a default value: + +- `synonyms`: a list of synonym groups, as expected by Algolia. Default `[]` +- `replace_existing_synonyms`: Whether to replace existing synonyms. Default `true` +- `forward_to_replicas`: Whether to forward synonyms to replicas. Default `true` + +You can also provide an arity-1 function (that takes the `index_name`) that returns the same keyword list. + +If using `forward_to_replicas: true`, make sure not to specify synonyms on the replicas themselves to avoid conflicts/overwrites. + +```elixir + defmodule Global do + use Algoliax.Indexer, + index_name: :people, + object_id: :reference, + schemas: People, + algolia: [ + attribute_for_faceting: ["age"], + custom_ranking: ["desc(updated_at)"] + ] + synonyms: [ + synonyms: [ + %{ + objectID: "synonym1", + type: "synonym", + synonyms: ["pants", "trousers", "slacks"], + ... + }, + %{ + objectID: "synonym2", + ... + } + ], + forward_to_replicas: false, + replace_existing_synonyms: false + ] + end +``` + +### Other changes + +- Updated the list of supported algolia settings. + +## v0.9.1 - 2024-12-12 + +### New + +The `Algoliax.Indexer` now supports dynamic definition for the `:algolia` settings. It can supports 2 types of configurations: + +- (Existing) Keyword list +- (New) Name of a 0-arity function that returns a keyword list + +```elixir +defmodule People do + use Algoliax.Indexer, + index_name: :people, + object_id: :reference, + schemas: [People], + algolia: :runtime_algolia + + def runtime_algolia do + [ + attribute_for_faceting: ["age"], + custom_ranking: ["desc(updated_at)"] + ] + end +end +``` + +Also added 2 new exceptions for the `:algolia` configuration: `InvalidAlgoliaSettingsFunctionError` and `InvalidAlgoliaSettingsConfigurationError` + +### Other changes + +Existing direct calls to `Algoliax.Settings.replica_settings/2` will still work but will not benefit from +the dynamic `:algolia` configuration. Please use `Algoliax.Settings.replica_settings/3` instead. + +## v0.9.0 - 2024-11-26 + +`ALGOLIA_API_KEY` and `ALGOLIA_APPLICATION_ID` aren't read anymore from system env variables +inside the code. Only application config is now used (as documented). + +If you used these env vars, you should now read them inside the config: + +```elixir +config :algoliax, + api_key: System.get_env("ALGOLIA_API_KEY"), + application_id: System.get_env("ALGOLIA_APPLICATION_ID) +``` + +## v0.8.3 - 2024-10-18 + +New `if` option for replicas which decides if they should be updated or not. + +- **If not provided**, the replica will be updated (so no impact on existing configurations) +- Must be `nil|true|false` or the name (atom) of a arity-0 func which returns a boolean +- If provided, the replica will be updated only if the value is `true` or the function returns `true` + +Useful for Algolia's A/B testing which requires replicas and only deploy them in production. + +## v0.8.2 - 2024-09-16 + +- Upgrading all dependencies +- Added `dependabot` to the repository + +## v0.8.1 - 2024-09-11 + +#### New + +- Added new **optional** settings `default_filters` to be applied automatically when calling `reindex` without query + or `reindex_atomic`. Defaults to `%{}` which was the previous behavior. + +```elixir +defmodule BlondeBeerIndexer do + use Algoliax.Indexer, + index_name: :blonde_beers, + object_id: :name, + schemas: [Beer], + default_filters: %{where: [kind: "blonde"]} # <--- can be a map +end + +defmodule BeerIndexer do + use Algoliax.Indexer, + index_name: :various_beers, + object_id: :name, + schemas: [Beer1, Beer2, Beer3], + default_filters: :get_filters # <--- can be a function + + def get_filters do + %{ + Beer1 => %{where: [kind: "blonde"]}, # <--- custom filter for Beer1 + :where => [kind: "brune"] # <--- Will be used for Beer2 and Beer3 + } + end +end +``` + +#### Contributing + +- New `CONTRIBUTING.md` file +- Simplified the `config/test.exs` file +- Provide a `.env.example` file to help contributors to setup their environment + +## v0.8.0 - 2024-09-11 + +#### Breaking changes + +- Errors for `get_object` and `get_objects` are now of arity 4 and returns the original request as last attribute + +#### New + +- Added possibility to have multiple indexes for a model (applies for replicas as well) +- Added `build_object/2` with the index name as second parameter (useful for translations for example) + +## v0.7.2 - 2023-09-18 #### Breaking changes @@ -10,7 +171,9 @@ #### New -- Add wait_task/1 (https://hexdocs.pm/algoliax/Algoliax.html#wait_task/1) +- Add wait_task/1 () + +## v0.7.0 - 2022-03-29 #### Breaking changes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..74e5de4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# Contributing + +## Install + +If you are not using `asdf`, you will need to manually install the program versions listed in `.tool-versions`. +Otherwise, simply run `asdf install` and then: + +```shell +mix deps.get +mix deps.compile +mix compile +``` + +## Run tests + +Before running `mix test`, ensure you have setup your environment variables. +Look at the `.env.example` file for the required variables. +Then run `mix test` to run the tests. + +## Quality + +- Run `mix format` to format the code. +- Run `mix credo` to run the linter. + +## CI/CD + +We use CircleCI to: + +- Run the code_analysis (format/credo) +- Check for vulnerabilities +- Run the tests + +See the `.circleci/config.yml` file for more details. diff --git a/README.md b/README.md index ba01d51..e225520 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The package can be installed by adding `:algoliax` to your list of dependencies ```elixir def deps do [ - {:algoliax, "~> 0.7.0"} + {:algoliax, "~> 0.10.0"} ] end ``` @@ -89,6 +89,22 @@ defmodule People do end ``` +- `build_object/2` does the same but provides the current index name as a second parameter. Can be useful when indexing the same model on multiple indexes (ie. for translations). + +```elixir +defmodule Article do + ... + + @impl Algoliax + def build_object(author, "article_index_" <> locale) do + %{ + author: article.author, + content: article.content[locale] + } + end +end +``` + #### Index name at runtime It's possible to define an index name at runtime, useful if `index_name` depends on environment or comes from an environment variable. @@ -108,6 +124,21 @@ defmodule People do end ``` +#### Multiple indexes + +It's possible to define multiple indexes for a same model. + +To achieve this, just specify an array of index names, or simply return an array in your `index_name/0` runtime function + +```elixir +defmodule Article do + use Algoliax.Indexer, + index_name: [:algoliax_article_fr, :algoliax_article_en], + object_id: :reference, + algolia: [...] +end +``` + #### Index functions ```elixir @@ -238,8 +269,42 @@ use Algoliax.Indexer, ] ``` +If the main index holds multiple indexes (e.g for an index per language usecase), replicas need to hold the same amount of names. +The order is important to be associated to the correct main index. + +```elixir +use Algoliax.Indexer, + index_name: [:algoliax_article_en, :algoliax_article_fr], + object_id: :reference, + repo: MyApp.Repo, + algolia: [ + attributes_for_faceting: ["published_at"], + searchable_attributes: ["content"], + ], + replicas: [ + [index_name: [:algoliax_article_by_publication_asc_en, :algoliax_article_by_publication_asc_fr], inherit: true, algolia: [ranking: ["asc(published_at)"]]], + [index_name: [:algoliax_article_by_publication_desc_en, :algoliax_article_by_publication_desc_fr], inherit: false, algolia: [ranking: ["desc(published_at)"]]] + ] +``` + +## Configure index name at runtime + +To support code for multiple environments, you can also define the index name at runtime. To achieve this, create a function within your indexer module and reference it using its atom in the Indexer configuration. + +```elixir +defmodule People do + use Algoliax.Indexer, + index_name: :runtime_index_name, + #.... + + def runtime_index_name do + System.get_env("INDEX_NAME") + end +end +``` + ## Copyright and License -Copyright (c) 2020 CORUSCANT (welcome to the jungle) - https://www.welcometothejungle.com +Copyright (c) 2020 CORUSCANT (welcome to the jungle) - This library is licensed under the [BSD-2-Clause](./LICENSE.md). diff --git a/config/test.exs b/config/test.exs index cbbf176..58cddbf 100644 --- a/config/test.exs +++ b/config/test.exs @@ -7,7 +7,14 @@ config :algoliax, ecto_repos: [Algoliax.Repo] config :algoliax, Algoliax.Repo, - database: "algoliax_test", - pool: Ecto.Adapters.SQL.Sandbox + pool: Ecto.Adapters.SQL.Sandbox, + hostname: System.get_env("POSTGRES_HOST", "localhost"), + port: System.get_env("POSTGRES_PORT", "5432"), + database: System.get_env("DB_NAME", "algoliax_test"), + username: System.get_env("DB_USERNAME", "postgres"), + password: System.get_env("DB_PASSWORD", "postgres") + +config :algoliax, + mock_api_port: System.get_env("ALGOLIA_MOCK_API_PORT", "8002") config :logger, level: :warning diff --git a/lib/algoliax.ex b/lib/algoliax.ex index b736b25..fb9ee0c 100644 --- a/lib/algoliax.ex +++ b/lib/algoliax.ex @@ -102,7 +102,12 @@ defmodule Algoliax do MyApp.People.save_object(%MyApp.People{id: 1}) |> Algoliax.wait_task() """ - def wait_task({:ok, response}), do: wait_task(response) + def wait_task({:ok, %Algoliax.Response{} = response}), do: wait_task(response) + + def wait_task({:ok, responses}) when is_list(responses) do + responses + |> Enum.flat_map(fn %Algoliax.Responses{responses: tasks} -> wait_task(tasks) end) + end def wait_task(tasks) when is_list(tasks) do tasks @@ -122,6 +127,18 @@ defmodule Algoliax do {:ok, %Algoliax.Response{response: %{"status" => "published"}}} -> {:ok, response} + {:ok, responses} when is_list(responses) -> + responses + |> Enum.all?(fn %Algoliax.Response{} = response -> response["status"] == "published" end) + |> case do + true -> + {:ok, responses} + + false -> + :timer.sleep(min(100 * retry, 1000)) + do_wait_task(response, retry) + end + _ -> :timer.sleep(min(100 * retry, 1000)) do_wait_task(response, retry) diff --git a/lib/algoliax/client.ex b/lib/algoliax/client.ex index 72f60b2..795573e 100644 --- a/lib/algoliax/client.ex +++ b/lib/algoliax/client.ex @@ -13,7 +13,8 @@ defmodule Algoliax.Client do def request(%{action: action, url_params: url_params} = request, retry) do body = Map.get(request, :body) - {method, url} = Routes.url(action, url_params, retry) + query_params = Map.get(request, :query_params) + {method, url} = Routes.url(action, url_params, query_params, retry) log(action, method, url, body) method @@ -26,7 +27,7 @@ defmodule Algoliax.Client do build_response(response, request) {:ok, code, _, response} when code in 300..499 -> - handle_error(code, response, action) + handle_error(code, response, action, request) error -> Logger.debug("#{inspect(error)}") @@ -34,11 +35,11 @@ defmodule Algoliax.Client do end end - defp handle_error(404, response, action) when action in [:get_settings, :get_object] do - {:error, 404, response} + defp handle_error(404, response, action, request) when action in [:get_settings, :get_object] do + {:error, 404, response, request} end - defp handle_error(code, response, _action) do + defp handle_error(code, response, _action, _request) do error = case Jason.decode(response) do {:ok, response} -> Map.get(response, "message") diff --git a/lib/algoliax/config.ex b/lib/algoliax/config.ex index 64d7f43..d7d0821 100644 --- a/lib/algoliax/config.ex +++ b/lib/algoliax/config.ex @@ -2,11 +2,11 @@ defmodule Algoliax.Config do @moduledoc false def api_key do - System.get_env("ALGOLIA_API_KEY") || Application.get_env(:algoliax, :api_key) + Application.get_env(:algoliax, :api_key) end def application_id do - System.get_env("ALGOLIA_APPLICATION_ID") || Application.get_env(:algoliax, :application_id) + Application.get_env(:algoliax, :application_id) end def cursor_field do diff --git a/lib/algoliax/exceptions.ex b/lib/algoliax/exceptions.ex index aac9779..93c7158 100644 --- a/lib/algoliax/exceptions.ex +++ b/lib/algoliax/exceptions.ex @@ -9,6 +9,11 @@ defmodule Algoliax.MissingRepoError do defexception [:message] + @impl true + def exception(index_name) when is_list(index_name) do + %__MODULE__{message: "No repo configured for indexes #{Enum.join(index_name, ", ")}"} + end + @impl true def exception(index_name) do %__MODULE__{message: "No repo configured for index #{index_name}"} @@ -26,6 +31,73 @@ defmodule Algoliax.MissingIndexNameError do end end +defmodule Algoliax.InvalidAlgoliaSettingsFunctionError do + @moduledoc "Raise when dynamic `:algolia` settings are invalid" + + defexception [:message] + + @impl true + def exception(%{function_name: function_name}) do + %__MODULE__{ + message: "Expected #{function_name} to be a 0-arity function that returns a list" + } + end +end + +defmodule Algoliax.InvalidAlgoliaSettingsConfigurationError do + @moduledoc "Raise when the `:algolia` settings are unsupported" + + defexception [:message] + + @impl true + def exception(_) do + %__MODULE__{ + message: + "Settings must either be a keyword list or the name of a 0-arity function that returns a list" + } + end +end + +defmodule Algoliax.InvalidSynonymsSettingsFunctionError do + @moduledoc "Raise when dynamic `:synonyms` settings are invalid" + + defexception [:message] + + @impl true + def exception(%{function_name: function_name}) do + %__MODULE__{ + message: "Expected #{function_name} to be a 1-arity function that returns a keyword list" + } + end +end + +defmodule Algoliax.InvalidSynonymsSettingsConfigurationError do + @moduledoc "Raise when the `:synonyms` settings are not defined properly" + + defexception [:message] + + @impl true + def exception(_) do + %__MODULE__{ + message: + "Synonyms settings must either be a keyword list or the name of a 1-arity function that returns a keyword list" + } + end +end + +defmodule Algoliax.InvalidReplicaConfigurationError do + @moduledoc "Raise when a replica has an invalid configuration" + + defexception [:message] + + @impl true + def exception(%{index_name: index_name, error: error}) do + %__MODULE__{ + message: "Invalid configuration for replica #{index_name}: #{error}" + } + end +end + defmodule Algoliax.AlgoliaApiError do @moduledoc "Raise Algolia API error" diff --git a/lib/algoliax/indexer.ex b/lib/algoliax/indexer.ex index 22228fd..d662793 100644 --- a/lib/algoliax/indexer.ex +++ b/lib/algoliax/indexer.ex @@ -3,12 +3,14 @@ defmodule Algoliax.Indexer do ### Usage - - `:index_name`: specificy the index where the object will be added on. **Required** + - `:index_name`: specificy the index or a list of indexes where the object will be added on. **Required** - `:object_id`: specify the attribute used to as algolia objectID. Default `:id`. - `:repo`: Specify an Ecto repo to be use to fecth records. Default `nil` - `:cursor_field`: specify the column to be used to order and go through a given table. Default `:id` - `:schemas`: Specify which schemas used to populate index, Default: `[__CALLER__]` - - `:algolia`: Any valid Algolia settings, using snake case or camel case. Ex: Algolia `attributeForFaceting` can be configured with `:attribute_for_faceting` + - `:default_filters`: Specify default filters to be used when reindex without providing a query. Must be a map or a function name (that returns a map). Default: `%{}`. + - `:algolia`: Any valid Algolia settings (using snake case or camel case, ie `attributeForFaceting` can be configured with `:attribute_for_faceting`) or the name of 0-arity function that returns those settings. + - `:synonyms`: Custom configuration for the synonym API, allowing you to register your synonyms. Can be nil, a keyword list, or an arity-1 function that returns either. Default: `nil` On first call to Algolia, we check that the settings on Algolia are up to date. @@ -98,6 +100,110 @@ defmodule Algoliax.Indexer do end + ### Default filters + + `:default_filters` allows you to define a list of filters that will be automatically applied when performing `reindex` without query, or `reindex_atomic`. + If not provided, it defaults to `%{}`, meaning it will not apply any filter and fetch the entire repo for all schemas. + You can provide either a map or a function name that returns a map. + + defmodule Global do + use Algoliax.Indexer, + index_name: :people, + object_id: :reference, + schemas: [People, Animal], + default_filters: %{where: [age: 18]}, + algolia: [ + attribute_for_faceting: ["age"], + custom_ranking: ["desc(updated_at)"] + ] + end + + The map must be a valid Ecto query but can be customized/nested by schema: + + defmodule Global do + use Algoliax.Indexer, + index_name: :people, + object_id: :reference, + schemas: [People, Animal], + default_filters: :get_filters, + algolia: [ + attribute_for_faceting: ["age"], + custom_ranking: ["desc(updated_at)"] + ] + + def get_filters do + %{ + People => where: [age: 18], # <-- Custom filter for People + :where => [kind: "cat"] # <-- Default filter for other schemas + end + end + + ### Configure index name and algolia settings at runtime + + To support code for multiple environments, you can also define things like index_name or algolia settings at runtime. + To achieve this, create a function within your indexer module and reference it using its atom in the Indexer configuration. + + ```elixir + defmodule People do + use Algoliax.Indexer, + index_name: :runtime_index_name, + #.... + algolia: :runtime_algolia + + def runtime_index_name do + System.get_env("INDEX_NAME") + end + + def runtime_algolia do + [attribute_for_faceting: ["age"]] + end + end + + ### Configure synonyms + + To automatically setup synonyms, you can provide a configuration under the `:synonyms` key. + When calling `configure_index`, the synonyms will be set up in Algolia. + It will be performed on both the main index and its replicas. + + If not specified or set to `nil`, the synonyms will not be configured. + Otherwise, the following keywords are expected, with each key having a default value: + - `synonyms`: a list of synonym groups. Default `[]` + - `replace_existing_synonyms`: Whether to replace existing synonyms. Default `true` + - `forward_to_replicas`: Whether to forward synonyms to replicas. Default `true` + You can also provide an arity-1 function (that takes the index_name) that returns the same keyword list. + + If using `forward_to_replicas: true`, make sure not to specify synonyms on the replicas themselves + to avoid conflicts/overwrites. + + ```elixir + defmodule Global do + use Algoliax.Indexer, + index_name: :people, + object_id: :reference, + schemas: People, + algolia: [ + attribute_for_faceting: ["age"], + custom_ranking: ["desc(updated_at)"] + ] + synonyms: [ + synonyms: [ + %{ + objectID: "synonym1", + type: "synonym", + synonyms: ["pants", "trousers", "slacks"], + ... + }, + %{ + objectID: "synonym2", + ... + } + ], + forward_to_replicas: false, + replace_existing_synonyms: false + ] + end + + See https://www.algolia.com/doc/rest-api/search/#tag/Synonyms/operation/saveSynonyms """ alias Algoliax.Resources.{Index, Object, Search} @@ -155,10 +261,15 @@ defmodule Algoliax.Indexer do "processingTimeMS" => 1, "query" => "john" }} + + iex> PeopleWithMultipleIndexes.search("John") + {:ok, [%{index_name: "people", responses: [%{"exhaustiveNbHits" => true, ...}, ...]}, ...]} """ @callback search(query :: binary(), params :: map()) :: - {:ok, Algoliax.Response.t()} | {:not_indexable, model :: map()} + {:ok, Algoliax.Response.t()} + | {:ok, list(Algoliax.Responses.t())} + | {:not_indexable, model :: map()} @doc """ Search for facet values @@ -182,9 +293,14 @@ defmodule Algoliax.Indexer do ], "processingTimeMS" => 1 }} + + iex> PeopleWithMultipleIndexes.search_facet("age") + {:ok, [%{index_name: "people", responses: [%{"exhaustiveNbHits" => true, ...}, ...]}, ...]} """ @callback search_facet(facet_name :: binary(), facet_query :: binary(), params :: map()) :: - {:ok, Algoliax.Response.t()} | {:not_indexable, model :: map()} + {:ok, Algoliax.Response.t()} + | {:ok, list(Algoliax.Responses.t())} + | {:not_indexable, model :: map()} @doc """ Add/update object. The object is added/updated to algolia with the object_id configured. @@ -193,9 +309,14 @@ defmodule Algoliax.Indexer do people = %People{reference: 10, last_name: "Doe", first_name: "John", age: 20}, People.save_object(people) + + iex> PeopleWithMultipleIndexes.save_object(people) + {:ok, [%{index_name: "people", responses: [%{"exhaustiveNbHits" => true, ...}, ...]}, ...]} """ @callback save_object(object :: map() | struct()) :: - {:ok, Algoliax.Response.t()} | {:not_indexable, model :: map()} + {:ok, Algoliax.Response.t()} + | {:ok, list(Algoliax.Responses.t())} + | {:not_indexable, model :: map()} @doc """ Save multiple object at once @@ -215,7 +336,7 @@ defmodule Algoliax.Indexer do People.save_objects(peoples, force_delete: true) """ @callback save_objects(models :: list(map()) | list(struct()), opts :: Keyword.t()) :: - {:ok, Algoliax.Response.t()} | {:error, map()} + {:ok, Algoliax.Response.t()} | {:ok, list(Algoliax.Responses.t())} | {:error, map()} @doc """ Fetch object from algolia. By passing the model, the object is retrieved using the object_id configured @@ -226,7 +347,7 @@ defmodule Algoliax.Indexer do People.get_object(people) """ @callback get_object(model :: map() | struct()) :: - {:ok, Algoliax.Response.t()} | {:error, map()} + {:ok, Algoliax.Response.t()} | {:ok, list(Algoliax.Responses.t())} | {:error, map()} @doc """ Delete object from algolia. By passing the model, the object is retrieved using the object_id configured @@ -237,7 +358,7 @@ defmodule Algoliax.Indexer do People.delete_object(people) """ @callback delete_object(model :: map() | struct()) :: - {:ok, Algoliax.Response.t()} | {:error, map()} + {:ok, Algoliax.Response.t()} | {:ok, list(Algoliax.Responses.t())} | {:error, map()} @doc """ Delete objects from algolia. By passing a matching filter query, the records are retrieved and deleted.[Filters](https://www.algolia.com/doc/api-reference/api-parameters/filters/) @@ -246,7 +367,7 @@ defmodule Algoliax.Indexer do People.delete_by("age > 18") """ @callback delete_by(matching_filter :: String.t()) :: - {:ok, Algoliax.Response.t()} | {:error, map()} + {:ok, Algoliax.Response.t()} | {:ok, list(Algoliax.Responses.t())} | {:error, map()} if Code.ensure_loaded?(Ecto) do @doc """ @@ -273,7 +394,7 @@ defmodule Algoliax.Indexer do > NOTE: filters as Map supports only `:where` and equality """ @callback reindex(query :: Ecto.Query.t(), opts :: Keyword.t()) :: - {:ok, [Algoliax.Response.t()]} + {:ok, [Algoliax.Response.t()]} | {:ok, list(Algoliax.Responses.t())} @doc """ Reindex all objects ([Ecto](https://hexdocs.pm/ecto/Ecto.html) specific) @@ -286,16 +407,18 @@ defmodule Algoliax.Indexer do - `:force_delete`: delete objects where `to_be_indexed?` is `false` """ - @callback reindex(opts :: Keyword.t()) :: {:ok, [Algoliax.Response.t()]} + @callback reindex(opts :: Keyword.t()) :: + {:ok, [Algoliax.Response.t()]} | {:ok, list(Algoliax.Responses.t())} @doc """ Reindex atomically ([Ecto](https://hexdocs.pm/ecto/Ecto.html) specific) """ - @callback reindex_atomic() :: {:ok, :completed} + @callback reindex_atomic() :: {:ok, :completed} | list({:ok, :completed}) end @doc """ Build the object sent to algolia. By default the object contains only `objectID` set by Algoliax.Indexer + build_object/2 provides the index name for the ongoing build ## Example @impl Algoliax.Indexer @@ -306,8 +429,18 @@ defmodule Algoliax.Indexer do first_name: person.first_name } end + + @impl Algoliax.Indexer + def build_object(person, _index_name) do + %{ + age: person.age, + last_name: person.last_name, + first_name: person.first_name + } + end """ @callback build_object(model :: map()) :: map() + @callback build_object(model :: map(), index :: String.t()) :: map() @doc """ Check if current object must be indexed or not. By default it's always true. To override this behaviour override this function in your model @@ -346,17 +479,17 @@ defmodule Algoliax.Indexer do @doc """ Get index settings from Algolia """ - @callback get_settings() :: {:ok, map()} | {:error, map()} + @callback get_settings() :: {:ok, map()} | {:ok, list(map())} | {:error, map()} @doc """ Configure index """ - @callback configure_index() :: {:ok, map()} | {:error, map()} + @callback configure_index() :: {:ok, map()} | {:ok, list(map())} | {:error, map()} @doc """ Delete index """ - @callback delete_index() :: {:ok, map()} | {:error, map()} + @callback delete_index() :: {:ok, map()} | {:ok, list(map())} | {:error, map()} defmacro __using__(settings) do quote do @@ -444,6 +577,11 @@ defmodule Algoliax.Indexer do %{} end + @impl Algoliax.Indexer + def build_object(_, _) do + %{} + end + @impl Algoliax.Indexer def to_be_indexed?(_) do true @@ -454,7 +592,7 @@ defmodule Algoliax.Indexer do :default end - defoverridable(to_be_indexed?: 1, build_object: 1, get_object_id: 1) + defoverridable(to_be_indexed?: 1, build_object: 1, build_object: 2, get_object_id: 1) end end end diff --git a/lib/algoliax/resources/index.ex b/lib/algoliax/resources/index.ex index 786145a..ef9d618 100644 --- a/lib/algoliax/resources/index.ex +++ b/lib/algoliax/resources/index.ex @@ -1,45 +1,51 @@ defmodule Algoliax.Resources.Index do @moduledoc false - import Algoliax.Utils, only: [index_name: 2, algolia_settings: 1] + import Algoliax.Utils, only: [index_name: 2, algolia_settings: 2, render_response: 1] import Algoliax.Client, only: [request: 1] alias Algoliax.{Settings, SettingsStore} - def ensure_settings(module, index_name, settings) do + def ensure_settings(module, index_name, settings, replica_index) do case SettingsStore.get_settings(index_name) do nil -> - request_configure_index(index_name, settings_to_algolia_settings(module, settings)) + request_configure_index( + index_name, + settings_to_algolia_settings(module, settings, replica_index) + ) + algolia_remote_settings = request_get_settings(index_name) SettingsStore.set_settings(index_name, algolia_remote_settings) - replicas_names(module, settings) + replicas_names(module, settings, replica_index) _ -> true end end - def replicas_names(module, settings) do - settings - |> replicas_settings() + def replicas_names(module, settings, replica_index) do + module + |> replicas_settings(settings) |> Enum.map(fn replica_settings -> index_name(module, replica_settings) + |> Enum.at(replica_index) end) end - def replicas_settings(settings) do - replicas = Keyword.get(settings, :replicas, []) - - Enum.map(replicas, fn replica -> + def replicas_settings(module, settings) do + settings + |> Keyword.get(:replicas, []) + |> Enum.filter(fn replica -> should_be_updated?(module, replica) end) + |> Enum.map(fn replica -> case Keyword.get(replica, :inherit, true) do true -> - replica_algolia_settings = algolia_settings(replica) - primary_algolia_setttings = algolia_settings(settings) + replica_algolia_settings = algolia_settings(module, replica) + primary_algolia_settings = algolia_settings(module, settings) Keyword.put( replica, :algolia, - Keyword.merge(primary_algolia_setttings, replica_algolia_settings) + Keyword.merge(primary_algolia_settings, replica_algolia_settings) ) false -> @@ -48,29 +54,86 @@ defmodule Algoliax.Resources.Index do end) end + defp should_be_updated?(module, replica) do + index_name = Keyword.get(replica, :index_name, nil) + + error_message = + "`if` must be `nil|true|false` or be the name of a 0-arity func which returns a boolean." + + value = Keyword.get(replica, :if, nil) + + cond do + # No config, defaults to true + is_nil(value) -> + true + + # Boolean, use this value + value == true || value == false -> + value + + # Name of a 0-arity func + is_atom(value) -> + if module.__info__(:functions) |> Keyword.get(value) == 0 do + apply(module, value, []) == true + else + raise Algoliax.InvalidReplicaConfigurationError, %{ + index_name: index_name, + error: error_message + } + end + + # Any other value, raise an error + true -> + raise Algoliax.InvalidReplicaConfigurationError, %{ + index_name: index_name, + error: error_message + } + end + end + def get_settings(module, settings) do - index_name = index_name(module, settings) - algolia_remote_settings = request_get_settings(index_name) - SettingsStore.set_settings(index_name, algolia_remote_settings) - algolia_remote_settings + index_name(module, settings) + |> Enum.map(fn index_name -> + algolia_remote_settings = request_get_settings(index_name) + SettingsStore.set_settings(index_name, algolia_remote_settings) + algolia_remote_settings + end) + |> render_response() end def configure_index(module, settings) do - index_name = index_name(module, settings) - r = request_configure_index(index_name, settings_to_algolia_settings(module, settings)) - configure_replicas(module, settings) - - r + index_name(module, settings) + |> Enum.with_index() + |> Enum.map(fn {index_name, replica_index} -> + r = + request_configure_index( + index_name, + settings_to_algolia_settings(module, settings, replica_index) + ) + + configure_synonyms(module, settings, index_name) + configure_replicas(module, settings) + r + end) + |> render_response() end def configure_replicas(module, settings) do - settings - |> replicas_settings() + module + |> replicas_settings(settings) |> Enum.map(fn replica_settings -> configure_index(module, replica_settings) end) end + defp configure_synonyms(module, settings, index_name) do + synonyms_settings = Settings.synonyms_settings(module, settings, index_name) + + unless is_nil(synonyms_settings) do + request_configure_synonyms(index_name, Settings.map_synonyms_settings(synonyms_settings)) + end + end + defp request_configure_index(index_name, settings) do request(%{ action: :configure_index, @@ -79,6 +142,15 @@ defmodule Algoliax.Resources.Index do }) end + defp request_configure_synonyms(index_name, {synonyms, query_params} = _synonym_settings) do + request(%{ + action: :configure_synonyms, + url_params: [index_name: index_name], + query_params: query_params, + body: synonyms + }) + end + defp request_get_settings(index_name) do request(%{ action: :get_settings, @@ -87,17 +159,21 @@ defmodule Algoliax.Resources.Index do end def delete_index(module, settings) do - request(%{action: :delete_index, url_params: [index_name: index_name(module, settings)]}) + index_name(module, settings) + |> Enum.map(fn index_name -> + request(%{action: :delete_index, url_params: [index_name: index_name]}) + end) + |> render_response() end - defp settings_to_algolia_settings(module, settings) do - settings - |> algolia_settings() + defp settings_to_algolia_settings(module, settings, replica_index) do + module + |> algolia_settings(settings) |> Settings.map_algolia_settings() - |> add_replicas_to_algolia_settings(module, settings) + |> add_replicas_to_algolia_settings(module, settings, replica_index) end - defp add_replicas_to_algolia_settings(algolia_settings, module, settings) do - algolia_settings |> Map.put(:replicas, replicas_names(module, settings)) + defp add_replicas_to_algolia_settings(algolia_settings, module, settings, replica_index) do + algolia_settings |> Map.put(:replicas, replicas_names(module, settings, replica_index)) end end diff --git a/lib/algoliax/resources/object.ex b/lib/algoliax/resources/object.ex index e984e35..ead9ea7 100644 --- a/lib/algoliax/resources/object.ex +++ b/lib/algoliax/resources/object.ex @@ -1,51 +1,76 @@ defmodule Algoliax.Resources.Object do @moduledoc false - import Algoliax.Utils, only: [index_name: 2, object_id_attribute: 1] + import Algoliax.Utils, only: [index_name: 2, object_id_attribute: 1, render_response: 1] import Algoliax.Client, only: [request: 1] alias Algoliax.TemporaryIndexer def get_object(module, settings, model) do - request(%{ - action: :get_object, - url_params: [ - index_name: index_name(module, settings), - object_id: get_object_id(module, settings, model) - ] - }) + index_name(module, settings) + |> Enum.map(fn index_name -> + request(%{ + action: :get_object, + url_params: [ + index_name: index_name, + object_id: get_object_id(module, settings, model) + ] + }) + end) + |> render_response() end def save_objects(module, settings, models, opts) do objects = - Enum.map(models, fn model -> - action = get_action(module, model, opts) - - if action do - build_batch_object(module, settings, model, action) - end + index_name(module, settings) + |> Enum.reduce(%{}, fn index_name, acc -> + objects = build_batch_objects(index_name, module, models, settings, opts) + Map.put(acc, index_name, objects) end) - |> Enum.reject(&is_nil/1) + |> Enum.reject(fn {_index_name, objects} -> Enum.empty?(objects) end) if Enum.any?(objects) do call_indexer(:save_objects, module, settings, models, opts) - request(%{ - action: :save_objects, - url_params: [index_name: index_name(module, settings)], - body: %{requests: objects} - }) + objects + |> Enum.map(fn {index_name, objects} -> + request(%{ + action: :save_objects, + url_params: [index_name: index_name], + body: %{requests: objects} + }) + end) + |> render_response() end end + defp build_batch_objects(index_name, module, models, settings, opts) do + Enum.map(models, fn model -> + action = get_action(module, model, opts) + + if action do + build_batch_object(module, settings, model, action, index_name) + end + end) + |> Enum.reject(&is_nil/1) + end + def save_object(module, settings, model) do + index_name(module, settings) + |> Enum.map(fn index_name -> + save_object(module, settings, model, index_name) + end) + |> render_response() + end + + defp save_object(module, settings, model, index_name) do if apply(module, :to_be_indexed?, [model]) do - object = build_object(module, settings, model) + object = build_object(module, settings, model, index_name) call_indexer(:save_object, module, settings, model) request(%{ action: :save_object, - url_params: [index_name: index_name(module, settings), object_id: object.objectID], + url_params: [index_name: index_name, object_id: object.objectID], body: object }) else @@ -56,13 +81,17 @@ defmodule Algoliax.Resources.Object do def delete_object(module, settings, model) do call_indexer(:delete_object, module, settings, model) - request(%{ - action: :delete_object, - url_params: [ - index_name: index_name(module, settings), - object_id: get_object_id(module, settings, model) - ] - }) + index_name(module, settings) + |> Enum.map(fn index_name -> + request(%{ + action: :delete_object, + url_params: [ + index_name: index_name, + object_id: get_object_id(module, settings, model) + ] + }) + end) + |> render_response() end def delete_by(module, settings, matching_filter) do @@ -77,31 +106,38 @@ defmodule Algoliax.Resources.Object do %{params: "filters=#{matching_filter}"} end - request(%{ - action: :delete_by, - url_params: [ - index_name: index_name(module, settings) - ], - body: body - }) + index_name(module, settings) + |> Enum.map(fn index_name -> + request(%{ + action: :delete_by, + url_params: [ + index_name: index_name + ], + body: body + }) + end) + |> render_response() end - defp build_batch_object(module, settings, model, "deleteObject" = action) do + defp build_batch_object(module, settings, model, "deleteObject" = action, _index_name) do %{ action: action, body: %{objectID: get_object_id(module, settings, model)} } end - defp build_batch_object(module, settings, model, action) do + defp build_batch_object(module, settings, model, action, index_name) do %{ action: action, - body: build_object(module, settings, model) + body: build_object(module, settings, model, index_name) } end - defp build_object(module, settings, model) do - apply(module, :build_object, [model]) + defp build_object(module, settings, model, index_name) do + case apply(module, :build_object, [model, index_name]) do + object when object == %{} -> apply(module, :build_object, [model]) + object -> object + end |> Map.put(:objectID, get_object_id(module, settings, model)) end diff --git a/lib/algoliax/resources/object_ecto.ex b/lib/algoliax/resources/object_ecto.ex index 3798463..943cb3b 100644 --- a/lib/algoliax/resources/object_ecto.ex +++ b/lib/algoliax/resources/object_ecto.ex @@ -4,19 +4,17 @@ if Code.ensure_loaded?(Ecto) do import Ecto.Query import Algoliax.Client, only: [request: 1] + import Algoliax.Utils, only: [index_name: 2, schemas: 2, default_filters: 2] alias Algoliax.Resources.Object def reindex(module, settings, %Ecto.Query{} = query, opts) do repo = Algoliax.UtilsEcto.repo(settings) - acc = - Algoliax.UtilsEcto.find_in_batches(repo, query, 0, settings, fn batch -> - Object.save_objects(module, settings, batch, opts) - end) - |> Enum.reject(&is_nil/1) - - {:ok, acc} + Algoliax.UtilsEcto.find_in_batches(repo, query, 0, settings, fn batch -> + Object.save_objects(module, settings, batch, opts) + end) + |> render_reindex() end def reindex(module, settings, nil, opts) do @@ -26,39 +24,51 @@ if Code.ensure_loaded?(Ecto) do def reindex(module, settings, query_filters, opts) when is_map(query_filters) do repo = Algoliax.UtilsEcto.repo(settings) - acc = - module - |> fetch_schemas(settings) - |> Enum.reduce([], fn {mod, preloads}, acc -> - where_filters = Map.get(query_filters, :where, []) - - query = - from(m in mod) - |> where(^where_filters) - |> preload(^preloads) - - Algoliax.UtilsEcto.find_in_batches( - repo, - query, - 0, - settings, - fn batch -> - Object.save_objects(module, settings, batch, opts) - end, - acc - ) - end) - |> Enum.reject(&is_nil/1) - - {:ok, acc} + # Use the default filters if none are provided + filters = + if map_size(query_filters) == 0 do + default_filters(module, settings) + else + query_filters + end + + module + |> fetch_schemas(settings) + |> Enum.reduce([], fn {schema, preloads}, acc -> + where_filters = extract_where_filters_for_schema(filters, schema) + + query = + from(m in schema) + |> where(^where_filters) + |> preload(^preloads) + + Algoliax.UtilsEcto.find_in_batches( + repo, + query, + 0, + settings, + fn batch -> + Object.save_objects(module, settings, batch, opts) + end, + acc + ) + end) + |> render_reindex() end def reindex(_, _, _, _) do {:error, :invalid_query} end + # Defaults to the root `:where` key if the `schema => :where` key does not exist + defp extract_where_filters_for_schema(filters, schema) when is_map(filters) do + root_where_filters = Map.get(filters, :where, []) + schema_filters = Map.get(filters, schema, %{}) + Map.get(schema_filters, :where, root_where_filters) + end + defp fetch_schemas(module, settings) do - Algoliax.Utils.schemas(module, settings) + schemas(module, settings) |> Enum.map(fn m when is_tuple(m) -> m m -> {m, []} @@ -68,32 +78,65 @@ if Code.ensure_loaded?(Ecto) do # sobelow_skip ["DOS.BinToAtom"] def reindex_atomic(module, settings) do Algoliax.UtilsEcto.repo(settings) - index_name = Algoliax.Utils.index_name(module, settings) - tmp_index_name = :"#{index_name}.tmp" - - tmp_settings = - settings |> Keyword.put(:index_name, tmp_index_name) |> Keyword.delete(:replicas) - - Algoliax.SettingsStore.start_reindexing(index_name) - - try do - reindex(module, tmp_settings, nil, []) - - request(%{ - action: :move_index, - url_params: [index_name: tmp_index_name], - body: %{ - operation: "move", - destination: "#{index_name}" - } - }) - - {:ok, :completed} - after - Algoliax.Resources.Index.delete_index(module, tmp_settings) - Algoliax.SettingsStore.delete_settings(tmp_index_name) - Algoliax.SettingsStore.stop_reindexing(index_name) - end + + index_name(module, settings) + |> Enum.map(fn index_name -> + tmp_index_name = :"#{index_name}.tmp" + + tmp_settings = + settings |> Keyword.put(:index_name, tmp_index_name) |> Keyword.delete(:replicas) + + Algoliax.SettingsStore.start_reindexing(index_name) + + try do + reindex(module, tmp_settings, nil, []) + + request(%{ + action: :move_index, + url_params: [index_name: tmp_index_name], + body: %{ + operation: "move", + destination: "#{index_name}" + } + }) + + {:ok, :completed} + after + Algoliax.Resources.Index.delete_index(module, tmp_settings) + Algoliax.SettingsStore.delete_settings(tmp_index_name) + Algoliax.SettingsStore.stop_reindexing(index_name) + end + end) + |> render_reindex_atomic() end + + defp render_reindex(responses) do + results = + responses + |> Enum.reject(&is_nil/1) + |> case do + [] -> + [] + + [{:ok, %Algoliax.Response{}} | _] = single_index_responses -> + single_index_responses + + [{:ok, [%Algoliax.Responses{} | _]} | _] = multiple_index_responses -> + multiple_index_responses + |> Enum.reduce([], fn {:ok, responses}, acc -> acc ++ responses end) + |> Enum.group_by(& &1.index_name) + |> Enum.map(fn {index_name, list} -> + %Algoliax.Responses{ + index_name: index_name, + responses: Enum.flat_map(list, & &1.responses) + } + end) + end + + {:ok, results} + end + + defp render_reindex_atomic([response]), do: response + defp render_reindex_atomic([_ | _] = responses), do: responses end end diff --git a/lib/algoliax/resources/search.ex b/lib/algoliax/resources/search.ex index 4674102..6b9fbda 100644 --- a/lib/algoliax/resources/search.ex +++ b/lib/algoliax/resources/search.ex @@ -1,41 +1,45 @@ defmodule Algoliax.Resources.Search do @moduledoc false - import Algoliax.Utils, only: [index_name: 2, camelize: 1] + import Algoliax.Utils, only: [index_name: 2, camelize: 1, render_response: 1] import Algoliax.Client, only: [request: 1] def search(module, settings, query, params) do - index_name = index_name(module, settings) + index_name(module, settings) + |> Enum.map(fn index_name -> + body = + %{ + query: query + } + |> Map.merge(camelize(params)) - body = - %{ - query: query - } - |> Map.merge(camelize(params)) - - request(%{ - action: :search, - url_params: [index_name: index_name], - body: body - }) + request(%{ + action: :search, + url_params: [index_name: index_name], + body: body + }) + end) + |> render_response() end def search_facet(module, settings, facet_name, facet_query, params) do - index_name = index_name(module, settings) - - body = - case facet_query do - nil -> - %{} + index_name(module, settings) + |> Enum.map(fn index_name -> + body = + case facet_query do + nil -> + %{} - _ -> - %{facetQuery: facet_query} - end - |> Map.merge(camelize(params)) + _ -> + %{facetQuery: facet_query} + end + |> Map.merge(camelize(params)) - request(%{ - action: :search_facet, - url_params: [index_name: index_name, facet_name: facet_name], - body: body - }) + request(%{ + action: :search_facet, + url_params: [index_name: index_name, facet_name: facet_name], + body: body + }) + end) + |> render_response() end end diff --git a/lib/algoliax/responses.ex b/lib/algoliax/responses.ex new file mode 100644 index 0000000..e97e59d --- /dev/null +++ b/lib/algoliax/responses.ex @@ -0,0 +1,12 @@ +defmodule Algoliax.Responses do + @moduledoc """ + Algolia API responses + """ + + @type t :: %__MODULE__{ + index_name: String.t(), + responses: list(Algoliax.Response) + } + + defstruct [:index_name, :responses] +end diff --git a/lib/algoliax/routes.ex b/lib/algoliax/routes.ex index 77e6593..73d3936 100644 --- a/lib/algoliax/routes.ex +++ b/lib/algoliax/routes.ex @@ -9,6 +9,7 @@ defmodule Algoliax.Routes do move_index: {"/{index_name}/operation", :post}, get_settings: {"/{index_name}/settings", :get}, configure_index: {"/{index_name}/settings", :put}, + configure_synonyms: {"/{index_name}/synonyms/batch", :post}, save_objects: {"/{index_name}/batch", :post}, get_object: {"/{index_name}/{object_id}", :get}, save_object: {"/{index_name}/{object_id}", :put}, @@ -17,7 +18,7 @@ defmodule Algoliax.Routes do delete_by: {"/{index_name}/deleteByQuery", :post} } - def url(action, url_params, retry \\ 0) do + def url(action, url_params, query_params \\ nil, retry \\ 0) do {action_path, method} = @paths |> Map.get(action) @@ -25,6 +26,7 @@ defmodule Algoliax.Routes do url = action_path |> build_path(url_params) + |> add_query_params(query_params) |> build_url(method, retry) {method, url} @@ -39,6 +41,14 @@ defmodule Algoliax.Routes do end) end + defp add_query_params(path, query_params) do + case query_params do + nil -> path + %{} when map_size(query_params) == 0 -> path + _ -> path <> "?" <> URI.encode_query(query_params) + end + end + defp build_url(path, :get, 0) do url_read() |> String.replace(~r/{{application_id}}/, Config.application_id()) @@ -60,17 +70,17 @@ defmodule Algoliax.Routes do if Mix.env() == :test do defp url_read do - port = System.get_env("SLACK_MOCK_API_PORT", "8002") + port = Application.get_env(:algoliax, :mock_api_port) "http://localhost:#{port}/{{application_id}}/read" end defp url_write do - port = System.get_env("SLACK_MOCK_API_PORT", "8002") + port = Application.get_env(:algoliax, :mock_api_port) "http://localhost:#{port}/{{application_id}}/write" end defp url_retry do - port = System.get_env("SLACK_MOCK_API_PORT", "8002") + port = Application.get_env(:algoliax, :mock_api_port) "http://localhost:#{port}/{{application_id}}/retry/{{retry}}" end else diff --git a/lib/algoliax/settings.ex b/lib/algoliax/settings.ex index 25fe799..b59a4eb 100644 --- a/lib/algoliax/settings.ex +++ b/lib/algoliax/settings.ex @@ -1,25 +1,34 @@ defmodule Algoliax.Settings do @moduledoc false - import Algoliax.Utils, only: [camelize: 1, algolia_settings: 1] + alias Algoliax.Utils + # https://www.algolia.com/doc/api-reference/settings-api-parameters/ @algolia_settings [ + # Attributes :searchable_attributes, :attributes_for_faceting, :unretrievable_attributes, :attributes_to_retrieve, + # Ranking + :mode, :ranking, :custom_ranking, + :relevancy_strictness, + # Faceting :max_values_per_facet, :sort_facet_values_by, + # Highlighting/Snippeting :attributes_to_highlight, :attributes_to_snippet, :highlight_pre_tag, :highlight_post_tag, :snippet_ellipsis_text, :restrict_highlight_and_snippet_arrays, + # Pagination :hits_per_page, :pagination_limited_to, + # Typos :min_word_sizefor1_typo, :min_word_sizefor2_typos, :typo_tolerance, @@ -27,13 +36,22 @@ defmodule Algoliax.Settings do :disable_typo_tolerance_on_attributes, :disable_typo_tolerance_on_words, :separators_to_index, + # Languages :ignore_plurals, + :attributes_to_transliterate, :remove_stop_words, :camel_case_attributes, :decompounded_attributes, :keep_diacritics_on_characters, + :custom_normalization, :query_languages, + :index_languages, + :decompound_query, + # Rules :enable_rules, + # Personalization + :enable_personalization, + # Query strategy :query_type, :remove_words_if_no_results, :advanced_syntax, @@ -42,28 +60,41 @@ defmodule Algoliax.Settings do :disable_exact_on_attributes, :exact_on_single_word_query, :alternatives_as_exact, + :advanced_syntax_features, + # Performance :numeric_attributes_for_filtering, :allow_compression_of_integer_array, - :numeric_attributes_to_index, + # Advanced :attribute_for_distinct, :distinct, :replace_synonyms_in_highlight, :min_proximity, :response_fields, :max_facet_hits, - :synonyms, - :placeholders, - :alt_corrections + :attribute_criteria_computed_by_min_proximity, + :user_data, + :rendering_content ] + @default_synonyms_settings [ + synonyms: [], + forward_to_replicas: true, + replace_existing_synonyms: true + ] + + @synonyms_settings Keyword.keys(@default_synonyms_settings) + def settings do @algolia_settings end - def replica_settings(settings, replica_settings) do + def replica_settings(settings, replica_settings), + do: replica_settings(%{}, settings, replica_settings) + + def replica_settings(module, settings, replica_settings) do replica_settings = case Keyword.get(replica_settings, :inherit, true) do - true -> replica_settings ++ algolia_settings(settings) + true -> replica_settings ++ Utils.algolia_settings(module, settings) false -> replica_settings end @@ -71,9 +102,33 @@ defmodule Algoliax.Settings do end def map_algolia_settings(algolia_settings) do - @algolia_settings - |> Enum.into(%{}, fn setting -> - {camelize(setting), Keyword.get(algolia_settings, setting)} + Enum.into(@algolia_settings, %{}, fn setting -> + {Utils.camelize(setting), Keyword.get(algolia_settings, setting)} + end) + end + + def synonyms_settings(module, settings, index_name) do + case Utils.synonyms_settings(module, settings, index_name) do + nil -> + nil + + synonyms_settings -> + @default_synonyms_settings + |> Keyword.merge(synonyms_settings) + |> Keyword.take(@synonyms_settings) + end + end + + def map_synonyms_settings(synonyms_settings) do + @default_synonyms_settings + |> Enum.into(%{}, fn {key, value} -> + {Utils.camelize(key), Keyword.get(synonyms_settings, key, value)} + end) + |> then(fn settings -> + { + Map.get(settings, "synonyms", []), + Map.drop(settings, ["synonyms"]) + } end) end end diff --git a/lib/algoliax/temporary_indexer.ex b/lib/algoliax/temporary_indexer.ex index f3f63e9..64b6845 100644 --- a/lib/algoliax/temporary_indexer.ex +++ b/lib/algoliax/temporary_indexer.ex @@ -3,7 +3,7 @@ defmodule Algoliax.TemporaryIndexer do Execute save_object(s) on temporary index to keep it synchronized with main index """ - import Algoliax.Utils, only: [index_name: 2] + import Algoliax.Utils, only: [index_name: 2, render_response: 1] alias Algoliax.SettingsStore alias Algoliax.Resources.Object @@ -16,14 +16,16 @@ defmodule Algoliax.TemporaryIndexer do defp do_run(action, module, settings, models, opts) do opts = Keyword.delete(opts, :temporary_only) - index_name = index_name(module, settings) + index_name(module, settings) + |> Enum.map(fn index_name -> + if SettingsStore.reindexing?(index_name) do + tmp_index_name = :"#{index_name}.tmp" + tmp_settings = SettingsStore.get_settings(tmp_index_name) - if SettingsStore.reindexing?(index_name) do - tmp_index_name = :"#{index_name}.tmp" - tmp_settings = SettingsStore.get_settings(tmp_index_name) - - execute(action, module, tmp_settings, models, opts) - end + execute(action, module, tmp_settings, models, opts) + end + end) + |> render_response() end defp execute(:save_objects, module, settings, models, opts) do @@ -38,7 +40,7 @@ defmodule Algoliax.TemporaryIndexer do Object.delete_object(module, settings, models) end - defp execute(:delete_by, module, settings, matching_filter) do + defp execute(:delete_by, module, settings, matching_filter, _) do Object.delete_by(module, settings, matching_filter) end end diff --git a/lib/algoliax/utils.ex b/lib/algoliax/utils.ex index 50d0666..634d98c 100644 --- a/lib/algoliax/utils.ex +++ b/lib/algoliax/utils.ex @@ -4,32 +4,96 @@ defmodule Algoliax.Utils do alias Algoliax.Resources.Index def index_name(module, settings) do - index_name_opt = Keyword.get(settings, :index_name) + indexes = + case Keyword.get(settings, :index_name) do + nil -> + raise Algoliax.MissingIndexNameError - if index_name_opt do - index_name = - if module.__info__(:functions) - |> Keyword.get(index_name_opt) == 0 do - apply(module, index_name_opt, []) + atom when is_atom(atom) -> + if module.__info__(:functions) |> Keyword.get(atom) == 0 do + apply(module, atom, []) + |> to_list() + else + [atom] + end + + list when is_list(list) -> + list + end + + indexes + |> Enum.with_index() + |> Enum.each(fn {index, i} -> Index.ensure_settings(module, index, settings, i) end) + + indexes + end + + def algolia_settings(module, settings) do + case Keyword.get(settings, :algolia, []) do + # Could be a 0-arity function that returns a list + atom when is_atom(atom) -> + with 0 <- Keyword.get(module.__info__(:functions), atom), + result when is_list(result) <- apply(module, atom, []) do + result else - index_name_opt + _any -> + raise Algoliax.InvalidAlgoliaSettingsFunctionError, %{ + function_name: inspect(atom) + } end - Index.ensure_settings(module, index_name, settings) - index_name - else - raise Algoliax.MissingIndexNameError + # Could be a list + list when is_list(list) -> + list + + # Refuse anything else + _other -> + raise Algoliax.InvalidAlgoliaSettingsConfigurationError end end - def algolia_settings(settings) do - Keyword.get(settings, :algolia, []) + def synonyms_settings(module, settings, index_name) do + case Keyword.get(settings, :synonyms, nil) do + # Could be nil + nil -> + nil + + # Could be a 1-arity function that returns a keyword list or nil + atom when is_atom(atom) -> + with 1 <- Keyword.get(module.__info__(:functions), atom), + result when is_list(result) or is_nil(result) <- apply(module, atom, [index_name]) do + result + else + _any -> + raise Algoliax.InvalidSynonymsSettingsFunctionError, %{ + function_name: inspect(atom) + } + end + + # Could be an hardcoded list + list when is_list(list) -> + list + + # Refuse anything else + _other -> + raise Algoliax.InvalidSynonymsSettingsConfigurationError + end end def object_id_attribute(settings) do Keyword.get(settings, :object_id, :id) end + def default_filters(module, settings) do + case Keyword.get(settings, :default_filters, %{}) do + fn_name when is_atom(fn_name) -> + apply(module, fn_name, []) + + default_filters -> + default_filters + end + end + def schemas(module, settings) do with fn_name when is_atom(fn_name) <- Keyword.get(settings, :schemas, [module]) do apply(module, fn_name, []) @@ -47,4 +111,28 @@ defmodule Algoliax.Utils do |> Atom.to_string() |> Inflex.camelize(:lower) end + + def render_response([response]), do: response + + def render_response([_ | _] = responses) do + results = + responses + |> List.flatten() + |> Enum.reject(&(match?({:not_indexable, _model}, &1) or is_nil(&1))) + |> Enum.group_by(fn + {:ok, %Algoliax.Response{params: params}} -> params[:index_name] + {:error, _, _, %{url_params: params}} -> params[:index_name] + end) + |> Enum.map(fn {index_name, results} -> + %Algoliax.Responses{ + index_name: index_name, + responses: results + } + end) + + {:ok, results} + end + + defp to_list(indexes) when is_list(indexes), do: indexes + defp to_list(index), do: [index] end diff --git a/mix.exs b/mix.exs index 0cf1c35..a1de8ec 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule Algoliax.MixProject do use Mix.Project @source_url "https://github.com/WTTJ/algoliax" - @version "0.8.0" + @version "0.10.0" def project do [ @@ -38,7 +38,7 @@ defmodule Algoliax.MixProject do {:ecto, "~> 3.9", optional: true}, {:ecto_sql, "~> 3.9", only: [:dev, :test]}, {:postgrex, ">= 0.0.0", only: [:dev, :test]}, - {:inflex, "~> 2.0.0"}, + {:inflex, "~> 2.1.0"}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:faker, "~> 0.12", only: :test}, {:plug_cowboy, "~> 2.6", only: :test}, diff --git a/mix.lock b/mix.lock index 31f4566..f9e1b00 100644 --- a/mix.lock +++ b/mix.lock @@ -1,47 +1,47 @@ %{ - "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, - "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, - "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, + "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, - "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, - "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, - "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"}, - "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"}, + "credo": {:hex, :credo, "1.7.13", "126a0697df6b7b71cd18c81bc92335297839a806b6f62b61d417500d1070ff4e", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "47641e6d2bbff1e241e87695b29f617f1a8f912adea34296fb10ecc3d7e9e84f"}, + "db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"}, + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.21", "7299db854f6d63730c15c8a781862889bb0fbf4432d7c306b3e63ce825d64baa", [:mix], [], "hexpm", "60664e1bdf7a02d8cbec2ac1d5b6fe0a68cf1d749ba955990d647346fac421e4"}, - "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"}, - "ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"}, - "ex_doc": {:hex, :ex_doc, "0.28.2", "e031c7d1a9fc40959da7bf89e2dc269ddc5de631f9bd0e326cbddf7d8085a9da", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "51ee866993ffbd0e41c084a7677c570d0fc50cb85c6b5e76f8d936d9587fa719"}, - "faker": {:hex, :faker, "0.12.0", "796cbac868c86c2df6f273ea4cdf2e271860863820e479e04a374b7ee6c376b6", [:mix], [], "hexpm", "ab8259d15122205e1970b2b2149a9664d17de77da5354d21229b6af88a3b6a71"}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, + "ecto": {:hex, :ecto, "3.13.4", "27834b45d58075d4a414833d9581e8b7bb18a8d9f264a21e42f653d500dbeeb5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5ad7d1505685dfa7aaf86b133d54f5ad6c42df0b4553741a1ff48796736e88b2"}, + "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, + "ex_doc": {:hex, :ex_doc, "0.39.1", "e19d356a1ba1e8f8cfc79ce1c3f83884b6abfcb79329d435d4bbb3e97ccc286e", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "8abf0ed3e3ca87c0847dfc4168ceab5bedfe881692f1b7c45f4a11b232806865"}, + "faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"}, + "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, + "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, "iconv": {:hex, :iconv, "1.0.10", "fc7de75c0a1fbc1e4ed0c68637ae7464a5dd9eee81710fff26321922d144ecea", [:rebar3], [{:p1_utils, "1.0.13", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "inflex": {:hex, :inflex, "2.0.0", "db69d542b8fdb23ac667f9bc0c2395a3983fa2da6ae2efa7ab5dc541928f7a75", [:mix], [], "hexpm", "c018852409bd48b03ad96ed53594186bc074bdd1519043a0ad1fa5697aac4399"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "jungle_inflexor": {:hex, :jungle_inflexor, "0.1.3", "6ef17fb80176349f8499cafbf913848b5ec96bd12869e876d0d2db4fad9f40c6", [:mix], [{:ex_doc, "~> 0.18.3", [hex: :ex_doc, repo: "hexpm", optional: false]}, {:iconv, "~> 1.0.7", [hex: :iconv, repo: "hexpm", optional: false]}], "hexpm"}, - "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "mix_audit": {:hex, :mix_audit, "2.1.0", "3c0dafb29114dffcdb508164a3d35311a9ac2c5baeba6495c9cd5315c25902b9", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "14c57a23e0a5f652c1e7f6e8dab93f166f66d63bd0c85f97278f5972b14e2be0"}, - "mix_test_watch": {:hex, :mix_test_watch, "1.0.2", "34900184cbbbc6b6ed616ed3a8ea9b791f9fd2088419352a6d3200525637f785", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "47ac558d8b06f684773972c6d04fcc15590abdb97aeb7666da19fcbfdc441a07"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, + "mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"}, + "mix_test_watch": {:hex, :mix_test_watch, "1.4.0", "d88bcc4fbe3198871266e9d2f00cd8ae350938efbb11d3fa1da091586345adbb", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "2b4693e17c8ead2ef56d4f48a0329891e8c2d0d73752c0f09272a2b17dc38d1b"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "p1_utils": {:hex, :p1_utils, "1.0.13", "176577cafb54a8c2fdc0a7fc62b9a21ab0f66588f4062792cd9e65f3e500bfdb", [:rebar3], [], "hexpm"}, - "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, - "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, - "postgrex": {:hex, :postgrex, "0.17.3", "c92cda8de2033a7585dae8c61b1d420a1a1322421df84da9a82a6764580c503d", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "946cf46935a4fdca7a81448be76ba3503cff082df42c6ec1ff16a4bdfbfb098d"}, - "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, - "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, + "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, + "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"}, + "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, + "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, - "yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"}, + "yaml_elixir": {:hex, :yaml_elixir, "2.11.0", "9e9ccd134e861c66b84825a3542a1c22ba33f338d82c07282f4f1f52d847bd50", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "53cc28357ee7eb952344995787f4bb8cc3cecbf189652236e9b163e8ce1bc242"}, } diff --git a/priv/repo/migrations/20230525101211_create_people_with_association_multiple_indexes.exs b/priv/repo/migrations/20230525101211_create_people_with_association_multiple_indexes.exs new file mode 100644 index 0000000..a6f5f5d --- /dev/null +++ b/priv/repo/migrations/20230525101211_create_people_with_association_multiple_indexes.exs @@ -0,0 +1,22 @@ +defmodule Algoliax.Repo.Migrations.CreatePeopleWithAssociationMultipleIndexes do + use Ecto.Migration + + def change do + create table(:people_with_associations_multiple_indexes) do + add(:reference, :uuid) + add(:first_name, :string) + add(:last_name, :string) + add(:age, :integer) + add(:gender, :string) + + timestamps() + end + + create table(:flowers) do + add(:kind, :string) + add(:people_with_association_multiple_indexes_id, references(:people_with_associations_multiple_indexes)) + + timestamps() + end + end +end diff --git a/test/algoliax/replica_test.exs b/test/algoliax/replica_test.exs index 332c800..201a45f 100644 --- a/test/algoliax/replica_test.exs +++ b/test/algoliax/replica_test.exs @@ -2,9 +2,15 @@ defmodule AlgoliaxTest.ReplicaTest do use Algoliax.RequestCase alias Algoliax.Schemas.PeopleWithReplicas + alias Algoliax.Schemas.PeopleWithReplicasAndCustomSynonyms + alias Algoliax.Schemas.PeopleWithReplicasMultipleIndexes + alias Algoliax.Schemas.PeopleWithReplicasMultipleIndexes + alias Algoliax.Schemas.PeopleWithInvalidReplicas setup do Algoliax.SettingsStore.set_settings(:algoliax_people_replicas, %{}) + Algoliax.SettingsStore.set_settings(:algoliax_people_replicas_en, %{}) + Algoliax.SettingsStore.set_settings(:algoliax_people_replicas_fr, %{}) Algoliax.SettingsStore.set_settings(:algoliax_people_replicas_asc, %{}) Algoliax.SettingsStore.set_settings(:algoliax_people_replicas_desc, %{}) :ok @@ -41,6 +47,174 @@ defmodule AlgoliaxTest.ReplicaTest do "ranking" => ["desc(age)"] } }) + + assert_request("POST", %{ + path: ~r/algoliax_people_replicas\/synonyms\/batch/, + body: %{ + "_json" => [ + %{ + "objectID" => "synonym1", + "synonyms" => ["synonym1", "synonym2"], + "type" => "synonym" + } + ] + } + }) + end + + test "configure_index/0 with different synonyms" do + assert {:ok, res} = PeopleWithReplicasAndCustomSynonyms.configure_index() + assert %Algoliax.Response{response: %{"taskID" => _, "updatedAt" => _}} = res + + assert_request("PUT", %{ + path: ~r/algoliax_people_replicas_synonyms/, + body: %{ + "searchableAttributes" => ["full_name"], + "attributesForFaceting" => ["age"], + "replicas" => ["algoliax_people_replicas_synonyms_asc"] + } + }) + + assert_request("PUT", %{ + path: ~r/algoliax_people_replicas_synonyms_asc/, + body: %{ + "searchableAttributes" => ["age"], + "attributesForFaceting" => ["age"], + "ranking" => ["asc(age)"] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_replicas_synonyms\/synonyms\/batch/, + body: %{ + "_json" => [ + %{ + "objectID" => "synonym1", + "synonyms" => ["synonym1", "synonym2"], + "type" => "synonym" + } + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_replicas_synonyms_asc\/synonyms\/batch/, + body: %{ + "_json" => [ + %{ + "objectID" => "synonym2", + "synonyms" => ["synonym3", "synonym4"], + "type" => "synonym" + } + ] + } + }) + end + + test "configure_index/0 with multiple indexes" do + assert {:ok, [res, res2]} = PeopleWithReplicasMultipleIndexes.configure_index() + + assert %Algoliax.Responses{ + index_name: :algoliax_people_replicas_en, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"taskID" => _, "updatedAt" => _}, + params: [index_name: :algoliax_people_replicas_en] + }} + ] + } = res + + assert %Algoliax.Responses{ + index_name: :algoliax_people_replicas_fr, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"taskID" => _, "updatedAt" => _}, + params: [index_name: :algoliax_people_replicas_fr] + }} + ] + } = res2 + + assert_request("PUT", %{ + path: ~r/algoliax_people_replicas_en/, + body: %{ + "searchableAttributes" => ["full_name"], + "attributesForFaceting" => ["age"], + "replicas" => [ + "algoliax_people_replicas_asc_en", + "algoliax_people_replicas_desc_en", + "algoliax_people_replicas_not_skipped_en" + ] + } + }) + + assert_request("PUT", %{ + path: ~r/algoliax_people_replicas_fr/, + body: %{ + "searchableAttributes" => ["full_name"], + "attributesForFaceting" => ["age"], + "replicas" => [ + "algoliax_people_replicas_asc_fr", + "algoliax_people_replicas_desc_fr", + "algoliax_people_replicas_not_skipped_fr" + ] + } + }) + + assert_request("PUT", %{ + path: ~r/algoliax_people_replicas_asc_en/, + body: %{ + "searchableAttributes" => ["age"], + "attributesForFaceting" => ["age"], + "ranking" => ["asc(age)"] + } + }) + + assert_request("PUT", %{ + path: ~r/algoliax_people_replicas_asc_fr/, + body: %{ + "searchableAttributes" => ["age"], + "attributesForFaceting" => ["age"], + "ranking" => ["asc(age)"] + } + }) + + assert_request("PUT", %{ + path: ~r/algoliax_people_replicas_desc_en/, + body: %{ + "searchableAttributes" => nil, + "attributesForFaceting" => nil, + "ranking" => ["desc(age)"] + } + }) + + assert_request("PUT", %{ + path: ~r/algoliax_people_replicas_desc_fr/, + body: %{ + "searchableAttributes" => nil, + "attributesForFaceting" => nil, + "ranking" => ["desc(age)"] + } + }) + + assert_request("PUT", %{ + path: ~r/algoliax_people_replicas_not_skipped_en/, + body: %{ + "searchableAttributes" => ["age"], + "attributesForFaceting" => ["age"], + "ranking" => ["asc(age)"] + } + }) + + assert_request("PUT", %{ + path: ~r/algoliax_people_replicas_not_skipped_fr/, + body: %{ + "searchableAttributes" => ["age"], + "attributesForFaceting" => ["age"], + "ranking" => ["asc(age)"] + } + }) end test "save_object/1" do @@ -72,6 +246,67 @@ defmodule AlgoliaxTest.ReplicaTest do }) end + test "save_object/1 with multiple indexes" do + reference = :rand.uniform(1_000_000) |> to_string() + + person = %PeopleWithReplicasMultipleIndexes{ + reference: reference, + last_name: "Doe", + first_name: "John", + age: 77 + } + + assert {:ok, [res, res2]} = PeopleWithReplicasMultipleIndexes.save_object(person) + + assert %Algoliax.Responses{ + index_name: :algoliax_people_replicas_en, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"taskID" => _, "updatedAt" => _, "objectID" => ^reference}, + params: [index_name: :algoliax_people_replicas_en, object_id: ^reference] + }} + ] + } = res + + assert %Algoliax.Responses{ + index_name: :algoliax_people_replicas_fr, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"taskID" => _, "updatedAt" => _, "objectID" => ^reference}, + params: [index_name: :algoliax_people_replicas_fr, object_id: ^reference] + }} + ] + } = res2 + + assert_request("PUT", %{ + path: ~r/algoliax_people_replicas_en/, + body: %{ + "age" => 77, + "first_name" => "John", + "full_name" => "John Doe", + "last_name" => "Doe", + "nickname" => "john", + "objectID" => reference, + "updated_at" => 1_546_300_800 + } + }) + + assert_request("PUT", %{ + path: ~r/algoliax_people_replicas_fr/, + body: %{ + "age" => 77, + "first_name" => "John", + "full_name" => "John Doe", + "last_name" => "Doe", + "nickname" => "john", + "objectID" => reference, + "updated_at" => 1_546_300_800 + } + }) + end + test "save_objects/1" do reference1 = :rand.uniform(1_000_000) |> to_string() reference2 = :rand.uniform(1_000_000) |> to_string() @@ -100,7 +335,77 @@ defmodule AlgoliaxTest.ReplicaTest do }) end - test "get_object/1" do + test "save_objects/1 with multiple indexes" do + reference1 = :rand.uniform(1_000_000) |> to_string() + reference2 = :rand.uniform(1_000_000) |> to_string() + + people = [ + %PeopleWithReplicasMultipleIndexes{ + reference: reference1, + last_name: "Doe", + first_name: "John", + age: 77 + }, + %PeopleWithReplicasMultipleIndexes{ + reference: reference2, + last_name: "al", + first_name: "bert", + age: 35 + } + ] + + assert {:ok, [res, res2]} = PeopleWithReplicasMultipleIndexes.save_objects(people) + + assert %Algoliax.Responses{ + index_name: :algoliax_people_replicas_en, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{ + "taskID" => _, + "objectIDs" => [^reference1, ^reference2] + }, + params: [index_name: :algoliax_people_replicas_en] + }} + ] + } = res + + assert %Algoliax.Responses{ + index_name: :algoliax_people_replicas_fr, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{ + "taskID" => _, + "objectIDs" => [^reference1, ^reference2] + }, + params: [index_name: :algoliax_people_replicas_fr] + }} + ] + } = res2 + + assert_request("POST", %{ + path: ~r/algoliax_people_replicas_en/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => reference1}}, + %{"action" => "updateObject", "body" => %{"objectID" => reference2}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_replicas_fr/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => reference1}}, + %{"action" => "updateObject", "body" => %{"objectID" => reference2}} + ] + } + }) + end + + test "get_object/1 " do person = %PeopleWithReplicas{ reference: "known", last_name: "Doe", @@ -113,6 +418,42 @@ defmodule AlgoliaxTest.ReplicaTest do assert_request("GET", %{body: %{}}) end + test "get_object/1 with multiple indexes" do + person = %PeopleWithReplicasMultipleIndexes{ + reference: "known", + last_name: "Doe", + first_name: "John", + age: 77 + } + + assert {:ok, [res, res2]} = PeopleWithReplicasMultipleIndexes.get_object(person) + + assert %Algoliax.Responses{ + index_name: :algoliax_people_replicas_en, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"objectID" => "known"}, + params: [index_name: :algoliax_people_replicas_en, object_id: "known"] + }} + ] + } = res + + assert %Algoliax.Responses{ + index_name: :algoliax_people_replicas_fr, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"objectID" => "known"}, + params: [index_name: :algoliax_people_replicas_fr, object_id: "known"] + }} + ] + } = res2 + + assert_request("GET", %{path: ~r/algoliax_people_replicas_en/, body: %{}}) + assert_request("GET", %{path: ~r/algoliax_people_replicas_fr/, body: %{}}) + end + test "get_object/1 w/ unknown" do person = %PeopleWithReplicas{ reference: "unknown", @@ -121,7 +462,31 @@ defmodule AlgoliaxTest.ReplicaTest do age: 77 } - assert {:error, 404, _} = PeopleWithReplicas.get_object(person) + assert {:error, 404, _, _} = PeopleWithReplicas.get_object(person) + end + + test "get_object/1 w/ unknown & multiple indexes" do + person = %PeopleWithReplicasMultipleIndexes{ + reference: "unknown", + last_name: "Doe", + first_name: "John", + age: 77 + } + + assert {:ok, + [ + %Algoliax.Responses{ + index_name: :algoliax_people_replicas_en, + responses: [{:error, 404, "{}", _}] + }, + %Algoliax.Responses{ + index_name: :algoliax_people_replicas_fr, + responses: [{:error, 404, "{}", _}] + } + ]} = PeopleWithReplicasMultipleIndexes.get_object(person) + + assert_request("GET", %{path: ~r/algoliax_people_replicas_en/, body: %{}}) + assert_request("GET", %{path: ~r/algoliax_people_replicas_fr/, body: %{}}) end test "delete_object/1" do @@ -136,25 +501,129 @@ defmodule AlgoliaxTest.ReplicaTest do assert_request("DELETE", %{body: %{}}) end + test "delete_object/1 with multiple indexes" do + person = %PeopleWithReplicasMultipleIndexes{ + reference: "unknown", + last_name: "Doe", + first_name: "John", + age: 77 + } + + assert {:ok, + [ + %Algoliax.Responses{index_name: :algoliax_people_replicas_en}, + %Algoliax.Responses{index_name: :algoliax_people_replicas_fr} + ]} = PeopleWithReplicasMultipleIndexes.delete_object(person) + + assert_request("DELETE", %{path: ~r/algoliax_people_replicas_en/, body: %{}}) + assert_request("DELETE", %{path: ~r/algoliax_people_replicas_fr/, body: %{}}) + end + test "delete_index/0" do assert {:ok, _} = PeopleWithReplicas.delete_index() assert_request("DELETE", %{body: %{}}) end + test "delete_index/0 with multiple indexes" do + assert {:ok, + [ + %Algoliax.Responses{index_name: :algoliax_people_replicas_en}, + %Algoliax.Responses{index_name: :algoliax_people_replicas_fr} + ]} = PeopleWithReplicasMultipleIndexes.delete_index() + + assert_request("DELETE", %{path: ~r/algoliax_people_replicas_en/, body: %{}}) + assert_request("DELETE", %{path: ~r/algoliax_people_replicas_fr/, body: %{}}) + end + test "get_settings/0" do assert {:ok, res} = PeopleWithReplicas.get_settings() assert %Algoliax.Response{response: %{"searchableAttributes" => ["test"]}} = res assert_request("GET", %{body: %{}}) end + test "get_settings/0 with multiple indexes" do + assert {:ok, [res, res2]} = PeopleWithReplicasMultipleIndexes.get_settings() + + assert %Algoliax.Responses{ + index_name: :algoliax_people_replicas_en, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"searchableAttributes" => ["test"]}, + params: [index_name: :algoliax_people_replicas_en] + }} + ] + } = res + + assert %Algoliax.Responses{ + index_name: :algoliax_people_replicas_fr, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"searchableAttributes" => ["test"]}, + params: [index_name: :algoliax_people_replicas_fr] + }} + ] + } = res2 + + assert_request("GET", %{path: ~r/algoliax_people_replicas_en/, body: %{}}) + assert_request("GET", %{path: ~r/algoliax_people_replicas_fr/, body: %{}}) + end + test "search/2" do assert {:ok, _} = PeopleWithReplicas.search("john", %{hitsPerPage: 10}) assert_request("POST", %{body: %{"query" => "john", "hitsPerPage" => 10}}) end + test "search/2 with multiple indexes" do + assert {:ok, + [ + %Algoliax.Responses{index_name: :algoliax_people_replicas_en}, + %Algoliax.Responses{index_name: :algoliax_people_replicas_fr} + ]} = PeopleWithReplicasMultipleIndexes.search("john", %{hitsPerPage: 10}) + + assert_request("POST", %{ + path: ~r/algoliax_people_replicas_en/, + body: %{"query" => "john", "hitsPerPage" => 10} + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_replicas_fr/, + body: %{"query" => "john", "hitsPerPage" => 10} + }) + end + test "search_facet/2" do assert {:ok, _} = PeopleWithReplicas.search_facet("age", "2") assert_request("POST", %{body: %{"facetQuery" => "2"}}) end + + test "search_facet/2 with multiple indexes" do + assert {:ok, + [ + %Algoliax.Responses{index_name: :algoliax_people_replicas_en}, + %Algoliax.Responses{index_name: :algoliax_people_replicas_fr} + ]} = PeopleWithReplicasMultipleIndexes.search_facet("age", "2") + + assert_request("POST", %{ + path: ~r/algoliax_people_replicas_en/, + body: %{"facetQuery" => "2"} + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_replicas_fr/, + body: %{"facetQuery" => "2"} + }) + end + + test "should raise error on invalid replica config" do + assert_raise( + Algoliax.InvalidReplicaConfigurationError, + "Invalid configuration for replica algoliax_people_replicas_asc: `if` must be `nil|true|false` or be the name of a 0-arity func which returns a boolean.", + fn -> + PeopleWithInvalidReplicas.configure_index() + end + ) + end end end diff --git a/test/algoliax/routes_test.exs b/test/algoliax/routes_test.exs index 839f15e..8d254a5 100644 --- a/test/algoliax/routes_test.exs +++ b/test/algoliax/routes_test.exs @@ -29,6 +29,21 @@ defmodule Algoliax.RoutesTest do {:put, "http://localhost:8002/APPLICATION_ID/write/algolia_index/settings"} end + test "url configure_synonyms" do + assert Routes.url(:configure_synonyms, index_name: @index_name) == + {:post, "http://localhost:8002/APPLICATION_ID/write/algolia_index/synonyms/batch"} + end + + test "url configure_synonyms with query params" do + assert Routes.url( + :configure_synonyms, + [index_name: @index_name], + %{"some_param" => "value", "other_param" => "value"} + ) == + {:post, + "http://localhost:8002/APPLICATION_ID/write/algolia_index/synonyms/batch?other_param=value&some_param=value"} + end + test "url save_objects" do assert Routes.url(:save_objects, index_name: @index_name) == {:post, "http://localhost:8002/APPLICATION_ID/write/algolia_index/batch"} @@ -57,42 +72,42 @@ defmodule Algoliax.RoutesTest do describe "First retry" do test "url delete_index" do - assert Routes.url(:delete_index, [index_name: @index_name], 1) == + assert Routes.url(:delete_index, [index_name: @index_name], nil, 1) == {:delete, "http://localhost:8002/APPLICATION_ID/retry/1/algolia_index"} end test "url get_settings" do - assert Routes.url(:get_settings, [index_name: @index_name], 1) == + assert Routes.url(:get_settings, [index_name: @index_name], nil, 1) == {:get, "http://localhost:8002/APPLICATION_ID/retry/1/algolia_index/settings"} end test "url configure_index" do - assert Routes.url(:configure_index, [index_name: @index_name], 1) == + assert Routes.url(:configure_index, [index_name: @index_name], nil, 1) == {:put, "http://localhost:8002/APPLICATION_ID/retry/1/algolia_index/settings"} end test "url save_objects" do - assert Routes.url(:save_objects, [index_name: @index_name], 1) == + assert Routes.url(:save_objects, [index_name: @index_name], nil, 1) == {:post, "http://localhost:8002/APPLICATION_ID/retry/1/algolia_index/batch"} end test "url get_object" do - assert Routes.url(:get_object, [index_name: @index_name, object_id: 10], 1) == + assert Routes.url(:get_object, [index_name: @index_name, object_id: 10], nil, 1) == {:get, "http://localhost:8002/APPLICATION_ID/retry/1/algolia_index/10"} end test "url save_object" do - assert Routes.url(:save_object, [index_name: @index_name, object_id: 10], 1) == + assert Routes.url(:save_object, [index_name: @index_name, object_id: 10], nil, 1) == {:put, "http://localhost:8002/APPLICATION_ID/retry/1/algolia_index/10"} end test "url delete_object" do - assert Routes.url(:delete_object, [index_name: @index_name, object_id: 10], 1) == + assert Routes.url(:delete_object, [index_name: @index_name, object_id: 10], nil, 1) == {:delete, "http://localhost:8002/APPLICATION_ID/retry/1/algolia_index/10"} end test "url delete_by" do - assert Routes.url(:delete_by, [index_name: @index_name], 1) == + assert Routes.url(:delete_by, [index_name: @index_name], nil, 1) == {:post, "http://localhost:8002/APPLICATION_ID/retry/1/algolia_index/deleteByQuery"} end end diff --git a/test/algoliax/schema_test.exs b/test/algoliax/schema_test.exs index 0e0125b..703e05d 100644 --- a/test/algoliax/schema_test.exs +++ b/test/algoliax/schema_test.exs @@ -1,10 +1,3 @@ -# defmodule Algoliax.SchemaTest do -# use Algoliax.RequestCase - -# import Ecto.Query -# alias Algoliax.PeopleEcto -# end - defmodule AlgoliaxTest.Schema do use Algoliax.RequestCase import Ecto.Query @@ -14,12 +7,21 @@ defmodule AlgoliaxTest.Schema do alias Algoliax.Schemas.{ Animal, Beer, + BeerWithFilters, + BeerWithSchemaFilters, + Flower, PeopleEcto, + PeopleEctoMultipleIndexes, PeopleEctoFail, + PeopleEctoFailMultipleIndexes, PeopleWithoutIdEcto, + PeopleWithoutIdEctoMultipleIndexes, PeopleWithSchemas, + PeopleWithSchemasMultipleIndexes, PeopleWithAssociation, - PeopleWithCustomObjectId + PeopleWithAssociationMultipleIndexes, + PeopleWithCustomObjectId, + PeopleWithCustomObjectIdMultipleIndexes } @ref1 Ecto.UUID.generate() @@ -30,9 +32,19 @@ defmodule AlgoliaxTest.Schema do Algoliax.SettingsStore.set_settings(:algoliax_people, %{}) Algoliax.SettingsStore.set_settings(:"algoliax_people.tmp", %{}) + Algoliax.SettingsStore.set_settings(:algoliax_people_en, %{}) + Algoliax.SettingsStore.set_settings(:"algoliax_people_en.tmp", %{}) + Algoliax.SettingsStore.set_settings(:algoliax_people_fr, %{}) + Algoliax.SettingsStore.set_settings(:"algoliax_people_fr.tmp", %{}) + Algoliax.SettingsStore.set_settings(:algoliax_people_fail, %{}) Algoliax.SettingsStore.set_settings(:"algoliax_people_fail.tmp", %{}) + Algoliax.SettingsStore.set_settings(:algoliax_people_fail_en, %{}) + Algoliax.SettingsStore.set_settings(:"algoliax_people_fail_en.tmp", %{}) + Algoliax.SettingsStore.set_settings(:algoliax_people_fail_fr, %{}) + Algoliax.SettingsStore.set_settings(:"algoliax_people_fail_fr.tmp", %{}) + Algoliax.SettingsStore.set_settings(:algoliax_people_without_id, %{}) Algoliax.SettingsStore.set_settings(:"algoliax_people_without_id.tmp", %{}) @@ -120,6 +132,33 @@ defmodule AlgoliaxTest.Schema do first_name: "Dark", age: 9, animals: [%Animal{kind: "dog"}] + }, + %PeopleWithAssociationMultipleIndexes{ + reference: @ref1, + last_name: "Doe", + first_name: "John", + age: 77, + flowers: [%Flower{kind: "rose"}, %Flower{kind: "lily"}] + }, + %PeopleWithAssociationMultipleIndexes{ + reference: @ref1, + last_name: "Einstein", + first_name: "Alber", + age: 22, + flowers: [%Flower{kind: "rose"}, %Flower{kind: "lily"}, %Flower{kind: "orchid"}] + }, + %PeopleWithAssociationMultipleIndexes{ + reference: @ref2, + last_name: "al", + first_name: "bert", + age: 35 + }, + %PeopleWithAssociationMultipleIndexes{ + reference: @ref3, + last_name: "Vador", + first_name: "Dark", + age: 9, + flowers: [%Flower{kind: "orchid"}] } ] |> Enum.each(fn p -> @@ -131,276 +170,318 @@ defmodule AlgoliaxTest.Schema do :ok end - test "reindex" do - assert {:ok, [{:ok, %Algoliax.Response{}}, {:ok, %Algoliax.Response{}}]} = - PeopleEcto.reindex() - - assert_request("POST", %{ - body: %{ - "requests" => [ - %{ - "action" => "updateObject", - "body" => %{ - "objectID" => @ref1, - "last_name" => "Doe", - "first_name" => "John", - "age" => 77 - } - } - ] - } - }) - - assert_request("POST", %{ - body: %{ - "requests" => [ - %{ - "action" => "updateObject", - "body" => %{ - "objectID" => @ref2, - "last_name" => "al", - "first_name" => "bert", - "age" => 35 - } - } - ] - } - }) - end + describe "reindex" do + test "reindex" do + assert {:ok, [{:ok, %Algoliax.Response{}}, {:ok, %Algoliax.Response{}}]} = + PeopleEcto.reindex() - test "reindex with force delete" do - assert {:ok, - [ - {:ok, %Algoliax.Response{}}, - {:ok, %Algoliax.Response{}}, - {:ok, %Algoliax.Response{}} - ]} = PeopleEcto.reindex(force_delete: true) - - assert_request("POST", %{ - body: %{ - "requests" => [ - %{"action" => "updateObject", "body" => %{"objectID" => @ref1}} - ] - } - }) + assert_request("POST", %{ + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "objectID" => @ref1, + "last_name" => "Doe", + "first_name" => "John", + "age" => 77 + } + } + ] + } + }) - assert_request("POST", %{ - body: %{ - "requests" => [ - %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} - ] - } - }) + assert_request("POST", %{ + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "objectID" => @ref2, + "last_name" => "al", + "first_name" => "bert", + "age" => 35 + } + } + ] + } + }) + end - assert_request("POST", %{ - body: %{ - "requests" => [ - %{"action" => "deleteObject", "body" => %{"objectID" => @ref3}} - ] - } - }) - end + test "with multiple indexes" do + assert {:ok, + [ + %Algoliax.Responses{ + index_name: :algoliax_people_en, + responses: [{:ok, %Algoliax.Response{}}, {:ok, %Algoliax.Response{}}] + }, + %Algoliax.Responses{ + index_name: :algoliax_people_fr, + responses: [{:ok, %Algoliax.Response{}}, {:ok, %Algoliax.Response{}}] + } + ]} = PeopleEctoMultipleIndexes.reindex() - test "reindex with query" do - query = - from(p in PeopleEcto, - where: p.age == 35 - ) + assert_request("POST", %{ + path: ~r/algoliax_people_en/, + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "objectID" => @ref1, + "last_name" => "Doe", + "first_name" => "John", + "age" => 77 + } + } + ] + } + }) - assert {:ok, [{:ok, %Algoliax.Response{}}]} = PeopleEcto.reindex(query) + assert_request("POST", %{ + path: ~r/algoliax_people_en/, + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "objectID" => @ref2, + "last_name" => "al", + "first_name" => "bert", + "age" => 35 + } + } + ] + } + }) - assert_request("POST", %{ - body: %{ - "requests" => [ - %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} - ] - } - }) - end + assert_request("POST", %{ + path: ~r/algoliax_people_fr/, + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "objectID" => @ref1, + "last_name" => "Doe", + "first_name" => "John", + "age" => 77 + } + } + ] + } + }) - test "reindex with query and force delete" do - query = - from(p in PeopleEcto, - where: p.age == 35 or p.first_name == "Dark" - ) - - assert {:ok, - [ - {:ok, %Algoliax.Response{}}, - {:ok, %Algoliax.Response{}} - ]} = PeopleEcto.reindex(query, force_delete: true) - - assert_request("POST", %{ - body: %{ - "requests" => [ - %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} - ] - } - }) + assert_request("POST", %{ + path: ~r/algoliax_people_fr/, + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "objectID" => @ref2, + "last_name" => "al", + "first_name" => "bert", + "age" => 35 + } + } + ] + } + }) + end - assert_request("POST", %{ - body: %{ - "requests" => [ - %{"action" => "deleteObject", "body" => %{"objectID" => @ref3}} - ] - } - }) - end + test "with force delete" do + assert {:ok, + [ + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}} + ]} = PeopleEcto.reindex(force_delete: true) - test "reindex atomic" do - assert {:ok, :completed} = PeopleEcto.reindex_atomic() + assert_request("POST", %{ + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref1}} + ] + } + }) - assert_request("POST", %{ - body: %{ - "requests" => [ - %{"action" => "updateObject", "body" => %{"objectID" => @ref1}} - ] - } - }) + assert_request("POST", %{ + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} + ] + } + }) - assert_request("POST", %{ - body: %{ - "requests" => [ - %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} - ] - } - }) + assert_request("POST", %{ + body: %{ + "requests" => [ + %{"action" => "deleteObject", "body" => %{"objectID" => @ref3}} + ] + } + }) + end - assert_request("POST", %{ - path: ~r/algoliax_people\.tmp/, - body: %{ - "destination" => "algoliax_people", - "operation" => "move" - } - }) - end + test "with multiple indexes and with force delete" do + assert {:ok, + [ + %Algoliax.Responses{ + index_name: :algoliax_people_en, + responses: [ + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}} + ] + }, + %Algoliax.Responses{ + index_name: :algoliax_people_fr, + responses: [ + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}} + ] + } + ]} = PeopleEctoMultipleIndexes.reindex(force_delete: true) - test "reindex atomic with fail" do - assert_raise Postgrex.Error, fn -> - PeopleEctoFail.reindex_atomic() - end + assert_request("POST", %{ + path: ~r/algoliax_people_en/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref1}} + ] + } + }) - assert_request("DELETE", %{path: ~r/algoliax_people_fail\.tmp/, body: %{}}) - refute Algoliax.SettingsStore.reindexing?(:algoliax_people_fail) - end + assert_request("POST", %{ + path: ~r/algoliax_people_en/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} + ] + } + }) - test "reindex without an id column" do - assert {:ok, - [ - {:ok, %Algoliax.Response{}}, - {:ok, %Algoliax.Response{}}, - {:ok, %Algoliax.Response{}} - ]} = PeopleWithoutIdEcto.reindex() - - assert_request("POST", %{ - body: %{ - "requests" => [ - %{"action" => "updateObject", "body" => %{"objectID" => @ref1}} - ] - } - }) + assert_request("POST", %{ + path: ~r/algoliax_people_en/, + body: %{ + "requests" => [ + %{"action" => "deleteObject", "body" => %{"objectID" => @ref3}} + ] + } + }) - assert_request("POST", %{ - body: %{ - "requests" => [ - %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} - ] - } - }) + assert_request("POST", %{ + path: ~r/algoliax_people_fr/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref1}} + ] + } + }) - assert_request("POST", %{ - body: %{ - "requests" => [ - %{"action" => "updateObject", "body" => %{"objectID" => @ref3}} - ] - } - }) - end + assert_request("POST", %{ + path: ~r/algoliax_people_fr/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} + ] + } + }) - test "save_object/1 without attribute(s)" do - assert {:ok, _} = PeopleWithSchemas.save_object(%Beer{kind: "brune", name: "chimay", id: 1}) + assert_request("POST", %{ + path: ~r/algoliax_people_fr/, + body: %{ + "requests" => [ + %{"action" => "deleteObject", "body" => %{"objectID" => @ref3}} + ] + } + }) + end - assert_request("PUT", %{ - body: %{ - "name" => "chimay", - "objectID" => 1 - } - }) - end + test "with query" do + query = + from(p in PeopleEcto, + where: p.age == 35 + ) - test "reindex/1 with schemas" do - assert PeopleWithSchemas.reindex() + assert {:ok, [{:ok, %Algoliax.Response{}}]} = PeopleEcto.reindex(query) - assert_request("POST", %{ - body: %{ - "requests" => [ - %{"action" => "updateObject", "body" => %{"name" => "chimay", "objectID" => 1}} - ] - } - }) + assert_request("POST", %{ + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} + ] + } + }) + end - assert_request("POST", %{ - body: %{ - "requests" => [ - %{"action" => "updateObject", "body" => %{"name" => "jupiler", "objectID" => 2}} - ] - } - }) - end + test "with nothing as no result" do + query = + from(p in PeopleEcto, + where: p.age == 999 + ) - test "reindex/1 with schemas and query" do - query = - from(b in Beer, - where: b.name == "chimay" - ) + assert {:ok, []} = PeopleEcto.reindex(query) + end - assert {:ok, _} = PeopleWithSchemas.reindex(query) + test "with multiple indexes and with query" do + query = + from(p in PeopleEctoMultipleIndexes, + where: p.age == 35 + ) - assert_request("POST", %{ - body: %{ - "requests" => [ - %{"action" => "updateObject", "body" => %{"name" => "chimay", "objectID" => 1}} - ] - } - }) - end + assert {:ok, + [ + %Algoliax.Responses{ + index_name: :algoliax_people_en, + responses: [ + {:ok, %Algoliax.Response{}} + ] + }, + %Algoliax.Responses{ + index_name: :algoliax_people_fr, + responses: [ + {:ok, %Algoliax.Response{}} + ] + } + ]} = PeopleEctoMultipleIndexes.reindex(query) - test "reindex/1 with schemas and query as keyword list" do - query = %{where: [name: "heineken"]} - assert {:ok, _} = PeopleWithSchemas.reindex(query) + assert_request("POST", %{ + path: ~r/algoliax_people_en/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} + ] + } + }) - assert_request("POST", %{ - body: %{ - "requests" => [ - %{"action" => "updateObject", "body" => %{"name" => "heineken", "objectID" => 3}} - ] - } - }) - end + assert_request("POST", %{ + path: ~r/algoliax_people_fr/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} + ] + } + }) + end - test "reindex/1 with association" do - assert {:ok, _} = PeopleWithAssociation.reindex() - end + test "with query and force delete" do + query = + from(p in PeopleEcto, + where: p.age == 35 or p.first_name == "Dark" + ) - describe "indexer w/ custom object id" do - test "reindex" do assert {:ok, [ - {:ok, %Algoliax.Response{}}, {:ok, %Algoliax.Response{}}, {:ok, %Algoliax.Response{}} - ]} = PeopleWithCustomObjectId.reindex() + ]} = PeopleEcto.reindex(query, force_delete: true) assert_request("POST", %{ body: %{ "requests" => [ - %{ - "action" => "updateObject", - "body" => %{ - "objectID" => "people-" <> @ref1, - "last_name" => "Doe" - } - } + %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} ] } }) @@ -408,11 +489,805 @@ defmodule AlgoliaxTest.Schema do assert_request("POST", %{ body: %{ "requests" => [ - %{ - "action" => "updateObject", - "body" => %{ - "objectID" => "people-" <> @ref2, - "last_name" => "al" + %{"action" => "deleteObject", "body" => %{"objectID" => @ref3}} + ] + } + }) + end + + test "multiple indexes with query and force delete" do + query = + from(p in PeopleEctoMultipleIndexes, + where: p.age == 35 or p.first_name == "Dark" + ) + + assert {:ok, + [ + %Algoliax.Responses{ + index_name: :algoliax_people_en, + responses: [ + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}} + ] + }, + %Algoliax.Responses{ + index_name: :algoliax_people_fr, + responses: [ + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}} + ] + } + ]} = PeopleEctoMultipleIndexes.reindex(query, force_delete: true) + + assert_request("POST", %{ + path: ~r/algoliax_people_en/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_fr/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_en/, + body: %{ + "requests" => [ + %{"action" => "deleteObject", "body" => %{"objectID" => @ref3}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_fr/, + body: %{ + "requests" => [ + %{"action" => "deleteObject", "body" => %{"objectID" => @ref3}} + ] + } + }) + end + end + + describe "reindex atomic" do + test "reindex atomic" do + assert {:ok, :completed} = PeopleEcto.reindex_atomic() + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref1}} + ] + } + }) + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people\.tmp/, + body: %{"destination" => "algoliax_people", "operation" => "move"} + }) + end + + test "with multiple indexes" do + assert [{:ok, :completed}, {:ok, :completed}] = PeopleEctoMultipleIndexes.reindex_atomic() + + assert_request("POST", %{ + path: ~r/algoliax_people_en/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref1}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_fr/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref1}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_en/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_fr/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_en\.tmp/, + body: %{"destination" => "algoliax_people_en", "operation" => "move"} + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_fr\.tmp/, + body: %{"destination" => "algoliax_people_fr", "operation" => "move"} + }) + end + + test "with fail" do + assert_raise Postgrex.Error, fn -> + PeopleEctoFail.reindex_atomic() + end + + assert_request("DELETE", %{path: ~r/algoliax_people_fail\.tmp/, body: %{}}) + refute Algoliax.SettingsStore.reindexing?(:algoliax_people_fail) + end + + test "with multiple indexes atomic with fail" do + assert_raise Postgrex.Error, fn -> + PeopleEctoFailMultipleIndexes.reindex_atomic() + end + + assert_request("DELETE", %{path: ~r/algoliax_people_fail_en\.tmp/, body: %{}}) + refute Algoliax.SettingsStore.reindexing?(:algoliax_people_fail_en) + refute Algoliax.SettingsStore.reindexing?(:algoliax_people_fail_fr) + end + end + + describe "reindex without an id column" do + test "reindex" do + assert {:ok, + [ + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}} + ]} = PeopleWithoutIdEcto.reindex() + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref1}} + ] + } + }) + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} + ] + } + }) + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref3}} + ] + } + }) + end + + test "with mutiple indexes" do + assert {:ok, + [ + %Algoliax.Responses{ + index_name: :algoliax_people_without_id_en, + responses: [ + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}} + ] + }, + %Algoliax.Responses{ + index_name: :algoliax_people_without_id_fr, + responses: [ + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}} + ] + } + ]} = PeopleWithoutIdEctoMultipleIndexes.reindex() + + assert_request("POST", %{ + path: ~r/algoliax_people_without_id_en/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref1}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_without_id_en/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_without_id_en/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref3}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_without_id_fr/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref1}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_without_id_fr/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref2}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_without_id_fr/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => @ref3}} + ] + } + }) + end + end + + describe "save_objects" do + test "without attribute(s)" do + assert {:ok, _} = PeopleWithSchemas.save_object(%Beer{kind: "brune", name: "chimay", id: 1}) + + assert_request("PUT", %{ + body: %{"name" => "chimay", "objectID" => 1} + }) + end + + test "without attribute(s) and with multiple indexes" do + assert {:ok, [%Algoliax.Responses{}, %Algoliax.Responses{}]} = + PeopleWithSchemasMultipleIndexes.save_object(%Beer{ + kind: "brune", + name: "chimay", + id: 1 + }) + + assert_request("PUT", %{ + path: ~r/algoliax_with_schemas_en/, + body: %{"name" => "chimay", "objectID" => 1} + }) + + assert_request("PUT", %{ + path: ~r/algoliax_with_schemas_fr/, + body: %{"name" => "chimay", "objectID" => 1} + }) + end + end + + describe "reindex with schemas" do + test "reindex" do + assert PeopleWithSchemas.reindex() + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"name" => "chimay", "objectID" => 1}} + ] + } + }) + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"name" => "jupiler", "objectID" => 2}} + ] + } + }) + end + + test "with multiple indexes" do + assert PeopleWithSchemasMultipleIndexes.reindex() + + assert_request("POST", %{ + path: ~r/algoliax_with_schemas_en/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"name" => "chimay", "objectID" => 1}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_with_schemas_en/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"name" => "jupiler", "objectID" => 2}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_with_schemas_fr/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"name" => "chimay", "objectID" => 1}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_with_schemas_fr/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"name" => "jupiler", "objectID" => 2}} + ] + } + }) + end + + test "with query" do + query = + from(b in Beer, + where: b.name == "chimay" + ) + + assert {:ok, _} = PeopleWithSchemas.reindex(query) + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"name" => "chimay", "objectID" => 1}} + ] + } + }) + end + + test "with query and multiple indexes" do + query = + from(b in Beer, + where: b.name == "chimay" + ) + + assert {:ok, + [ + %Algoliax.Responses{ + index_name: :algoliax_with_schemas_en, + responses: [ + {:ok, %Algoliax.Response{}} + ] + }, + %Algoliax.Responses{ + index_name: :algoliax_with_schemas_fr, + responses: [ + {:ok, %Algoliax.Response{}} + ] + } + ]} = PeopleWithSchemasMultipleIndexes.reindex(query) + + assert_request("POST", %{ + path: ~r/algoliax_with_schemas_en/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"name" => "chimay", "objectID" => 1}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_with_schemas_fr/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"name" => "chimay", "objectID" => 1}} + ] + } + }) + end + + test "with query as keyword list" do + query = %{where: [name: "heineken"]} + assert {:ok, _} = PeopleWithSchemas.reindex(query) + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"name" => "heineken", "objectID" => 3}} + ] + } + }) + end + + test "with query as keyword list and multiple indexes" do + query = %{where: [name: "heineken"]} + + assert {:ok, + [ + %Algoliax.Responses{ + index_name: :algoliax_with_schemas_en, + responses: [ + {:ok, %Algoliax.Response{}} + ] + }, + %Algoliax.Responses{ + index_name: :algoliax_with_schemas_fr, + responses: [ + {:ok, %Algoliax.Response{}} + ] + } + ]} = PeopleWithSchemasMultipleIndexes.reindex(query) + + assert_request("POST", %{ + path: ~r/algoliax_with_schemas_en/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"name" => "heineken", "objectID" => 3}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_with_schemas_fr/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"name" => "heineken", "objectID" => 3}} + ] + } + }) + end + end + + describe "reindex with association" do + test "reindex" do + assert {:ok, _} = PeopleWithAssociation.reindex() + end + + test "with multiple indexes" do + assert {:ok, _} = PeopleWithAssociationMultipleIndexes.reindex() + end + end + + describe "indexer w/ custom object id" do + test "reindex" do + assert {:ok, + [ + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}} + ]} = PeopleWithCustomObjectId.reindex() + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "objectID" => "people-" <> @ref1, + "last_name" => "Doe" + } + } + ] + } + }) + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "objectID" => "people-" <> @ref2, + "last_name" => "al" + } + } + ] + } + }) + end + + test "reindex with multiple indexes" do + assert {:ok, + [ + %Algoliax.Responses{ + index_name: :algoliax_people_with_custom_object_id_en, + responses: [ + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}} + ] + }, + %Algoliax.Responses{ + index_name: :algoliax_people_with_custom_object_id_fr, + responses: [ + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}}, + {:ok, %Algoliax.Response{}} + ] + } + ]} = PeopleWithCustomObjectIdMultipleIndexes.reindex() + + assert_request("POST", %{ + path: ~r/algoliax_people_with_custom_object_id_en/, + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "objectID" => "people-" <> @ref1, + "last_name" => "Doe" + } + } + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_with_custom_object_id_fr/, + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "objectID" => "people-" <> @ref1, + "last_name" => "Doe" + } + } + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_with_custom_object_id_en/, + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "objectID" => "people-" <> @ref2, + "last_name" => "al" + } + } + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_with_custom_object_id_fr/, + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "objectID" => "people-" <> @ref2, + "last_name" => "al" + } + } + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_with_custom_object_id_en/, + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "objectID" => "people-" <> @ref3, + "last_name" => "Vador" + } + } + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_with_custom_object_id_fr/, + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "objectID" => "people-" <> @ref3, + "last_name" => "Vador" + } + } + ] + } + }) + end + end + + describe "reindex with default_filters" do + test "reindex with default filters" do + # Expect 2 blondes + assert {:ok, [{:ok, %Algoliax.Response{}}, {:ok, %Algoliax.Response{}}]} = + BeerWithFilters.reindex() + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "kind" => "blonde", + "name" => "heineken", + "objectID" => 3 + } + } + ] + } + }) + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "kind" => "blonde", + "name" => "jupiler", + "objectID" => 2 + } + } + ] + } + }) + end + + test "reindex with default filters per schemas" do + # Expect 1 brune + assert {:ok, [{:ok, %Algoliax.Response{}}]} = BeerWithSchemaFilters.reindex() + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "kind" => "brune", + "name" => "chimay", + "objectID" => 1 + } + } + ] + } + }) + end + + test "reindex_atomic with default filters" do + # Expect 2 blondes + assert {:ok, :completed} = BeerWithFilters.reindex_atomic() + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "kind" => "blonde", + "name" => "heineken", + "objectID" => 3 + } + } + ] + } + }) + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "kind" => "blonde", + "name" => "jupiler", + "objectID" => 2 + } + } + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_beer_with_filters\.tmp/, + body: %{ + "destination" => "algoliax_beer_with_filters", + "operation" => "move" + } + }) + end + + test "reindex_atomic with default filters per schemas" do + # Expect 1 brune + assert {:ok, :completed} = BeerWithSchemaFilters.reindex_atomic() + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "kind" => "brune", + "name" => "chimay", + "objectID" => 1 + } + } + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_beer_with_schema_filters\.tmp/, + body: %{ + "destination" => "algoliax_beer_with_schema_filters", + "operation" => "move" + } + }) + end + + test "reindex ignore default filters if query is provided" do + # Expect 1 brune beer (as opposed to the default "2 blonde beers") + query = from(b in Beer, where: b.kind == "brune") + assert {:ok, [{:ok, %Algoliax.Response{}}]} = BeerWithFilters.reindex(query) + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "kind" => "brune", + "name" => "chimay", + "objectID" => 1 + } + } + ] + } + }) + end + + test "reindex ignore default filters if query (keyword list) is provided" do + # Expect 1 blonde beer (as opposed to the default "1 brune beer") + query = %{where: [id: 2]} + assert {:ok, [{:ok, %Algoliax.Response{}}]} = BeerWithSchemaFilters.reindex(query) + + assert_request("POST", %{ + body: %{ + "requests" => [ + %{ + "action" => "updateObject", + "body" => %{ + "kind" => "blonde", + "name" => "jupiler", + "objectID" => 2 } } ] diff --git a/test/algoliax/settings_test.exs b/test/algoliax/settings_test.exs index 64bcc7a..8c8e534 100644 --- a/test/algoliax/settings_test.exs +++ b/test/algoliax/settings_test.exs @@ -1,14 +1,37 @@ defmodule Algoliax.SettingsTest do use ExUnit.Case, async: true + defmodule ModuleWithAlgoliaSettingsFunc do + def algolia_settings do + [ + attributes_for_faceting: ["location"], + searchable_attributes: ["first_name"] + ] + end + end + + defmodule SynonymsWithFunc do + use Algoliax.Indexer, + index_name: :algoliax_people, + algolia: [], + object_id: :reference, + synonyms: :get_synonyms + + def get_synonyms("list"), do: [forward_to_replicas: false] + def get_synonyms("nil"), do: nil + def get_synonyms(_), do: [extra_keys: "should be removed"] + end + test "settings/0" do assert Algoliax.Settings.settings() == [ :searchable_attributes, :attributes_for_faceting, :unretrievable_attributes, :attributes_to_retrieve, + :mode, :ranking, :custom_ranking, + :relevancy_strictness, :max_values_per_facet, :sort_facet_values_by, :attributes_to_highlight, @@ -27,12 +50,17 @@ defmodule Algoliax.SettingsTest do :disable_typo_tolerance_on_words, :separators_to_index, :ignore_plurals, + :attributes_to_transliterate, :remove_stop_words, :camel_case_attributes, :decompounded_attributes, :keep_diacritics_on_characters, + :custom_normalization, :query_languages, + :index_languages, + :decompound_query, :enable_rules, + :enable_personalization, :query_type, :remove_words_if_no_results, :advanced_syntax, @@ -41,18 +69,18 @@ defmodule Algoliax.SettingsTest do :disable_exact_on_attributes, :exact_on_single_word_query, :alternatives_as_exact, + :advanced_syntax_features, :numeric_attributes_for_filtering, :allow_compression_of_integer_array, - :numeric_attributes_to_index, :attribute_for_distinct, :distinct, :replace_synonyms_in_highlight, :min_proximity, :response_fields, :max_facet_hits, - :synonyms, - :placeholders, - :alt_corrections + :attribute_criteria_computed_by_min_proximity, + :user_data, + :rendering_content ] end @@ -133,5 +161,33 @@ defmodule Algoliax.SettingsTest do assert result["searchableAttributes"] == nil assert result["ranking"] == ["asc(age)"] end + + test "with algolia settings func" do + replica_settings = [ + name: :algoliax_people_by_age_asc, + attributes_for_faceting: ["age"], + ranking: ["asc(age)"], + inherit: true + ] + + settings = [ + index_name: :algoliax_people, + object_id: :reference, + repo: MyApp.Repo, + algolia: :algolia_settings, + replicas: [replica_settings] + ] + + assert result = + Algoliax.Settings.replica_settings( + ModuleWithAlgoliaSettingsFunc, + settings, + replica_settings + ) + + assert result["attributesForFaceting"] == ["age"] + assert result["searchableAttributes"] == ["first_name"] + assert result["ranking"] == ["asc(age)"] + end end end diff --git a/test/algoliax/struct_test.exs b/test/algoliax/struct_test.exs index f0999b6..4a7e975 100644 --- a/test/algoliax/struct_test.exs +++ b/test/algoliax/struct_test.exs @@ -1,7 +1,12 @@ defmodule AlgoliaxTest.StructTest do use Algoliax.RequestCase - alias Algoliax.Schemas.{PeopleStruct, PeopleStructRuntimeIndexName} + alias Algoliax.Schemas.{ + PeopleStruct, + PeopleStructMultipleIndexes, + PeopleStructRuntimeMultipleIndexes, + PeopleStructRuntimeIndexName + } setup do Algoliax.SettingsStore.set_settings(:algoliax_people_struct, %{}) @@ -100,7 +105,7 @@ defmodule AlgoliaxTest.StructTest do test "get_object/1 w/ unknown" do person = %PeopleStruct{reference: "unknown", last_name: "Doe", first_name: "John", age: 77} - assert {:error, 404, _} = PeopleStruct.get_object(person) + assert {:error, 404, _, _} = PeopleStruct.get_object(person) end test "delete_object/1" do @@ -156,6 +161,424 @@ defmodule AlgoliaxTest.StructTest do end end + describe "struct with multiple indexes" do + test "configure_index/0" do + assert {:ok, [res, res2]} = PeopleStructMultipleIndexes.configure_index() + + assert %Algoliax.Responses{ + index_name: :algoliax_people_struct_en, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"taskID" => _, "updatedAt" => _}, + params: [index_name: :algoliax_people_struct_en] + }} + ] + } = res + + assert %Algoliax.Responses{ + index_name: :algoliax_people_struct_fr, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"taskID" => _, "updatedAt" => _}, + params: [index_name: :algoliax_people_struct_fr] + }} + ] + } = res2 + + assert_request("PUT", %{ + path: ~r/algoliax_people_struct_en\/settings/, + body: %{ + "searchableAttributes" => ["full_name"], + "attributesForFaceting" => ["age"] + } + }) + + assert_request("PUT", %{ + path: ~r/algoliax_people_struct_fr\/settings/, + body: %{ + "searchableAttributes" => ["full_name"], + "attributesForFaceting" => ["age"] + } + }) + end + + test "save_object/1" do + reference = :rand.uniform(1_000_000) |> to_string() + + person = %PeopleStructMultipleIndexes{ + reference: reference, + last_name: "Doe", + first_name: "John", + age: 77 + } + + assert {:ok, [res, res2]} = PeopleStructMultipleIndexes.save_object(person) + + assert %Algoliax.Responses{ + index_name: :algoliax_people_struct_en, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"objectID" => ^reference, "taskID" => _, "updatedAt" => _}, + params: [index_name: :algoliax_people_struct_en, object_id: ^reference] + }} + ] + } = res + + assert %Algoliax.Responses{ + index_name: :algoliax_people_struct_fr, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"objectID" => ^reference, "taskID" => _, "updatedAt" => _}, + params: [index_name: :algoliax_people_struct_fr, object_id: ^reference] + }} + ] + } = res2 + + assert_request("PUT", %{ + path: ~r/algoliax_people_struct_en/, + body: %{ + "age" => 77, + "first_name" => "John", + "full_name" => "John Doe", + "last_name" => "Doe", + "nickname" => "john", + "objectID" => reference, + "updated_at" => 1_546_300_800 + } + }) + + assert_request("PUT", %{ + path: ~r/algoliax_people_struct_fr/, + body: %{ + "age" => 77, + "first_name" => "John", + "full_name" => "John Doe", + "last_name" => "Doe", + "nickname" => "john", + "objectID" => reference, + "updated_at" => 1_546_300_800 + } + }) + end + + test "save_objects/1" do + reference1 = :rand.uniform(1_000_000) |> to_string() + reference2 = :rand.uniform(1_000_000) |> to_string() + + people = [ + %PeopleStructMultipleIndexes{ + reference: reference1, + last_name: "Doe", + first_name: "John", + age: 77 + }, + %PeopleStructMultipleIndexes{ + reference: reference2, + last_name: "al", + first_name: "bert", + age: 35 + } + ] + + assert {:ok, [res, res2]} = PeopleStructMultipleIndexes.save_objects(people) + + assert %Algoliax.Responses{ + index_name: :algoliax_people_struct_en, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"taskID" => _, "objectIDs" => [^reference1]}, + params: [index_name: :algoliax_people_struct_en] + }} + ] + } = res + + assert %Algoliax.Responses{ + index_name: :algoliax_people_struct_fr, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"taskID" => _, "objectIDs" => [^reference1]}, + params: [index_name: :algoliax_people_struct_fr] + }} + ] + } = res2 + + assert_request("POST", %{ + path: ~r/algoliax_people_struct_en/, + body: %{ + "requests" => [%{"action" => "updateObject", "body" => %{"objectID" => reference1}}] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_struct_fr/, + body: %{ + "requests" => [%{"action" => "updateObject", "body" => %{"objectID" => reference1}}] + } + }) + end + + test "save_objects/1 w/ force_delete: true" do + reference1 = :rand.uniform(1_000_000) |> to_string() + reference2 = :rand.uniform(1_000_000) |> to_string() + + people = [ + %PeopleStructMultipleIndexes{ + reference: reference1, + last_name: "Doe", + first_name: "John", + age: 77 + }, + %PeopleStructMultipleIndexes{ + reference: reference2, + last_name: "al", + first_name: "bert", + age: 35 + } + ] + + assert {:ok, [res, res2]} = + PeopleStructMultipleIndexes.save_objects(people, force_delete: true) + + assert %Algoliax.Responses{ + index_name: :algoliax_people_struct_en, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"taskID" => _, "objectIDs" => [^reference1, ^reference2]}, + params: [index_name: :algoliax_people_struct_en] + }} + ] + } = res + + assert %Algoliax.Responses{ + index_name: :algoliax_people_struct_fr, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"taskID" => _, "objectIDs" => [^reference1, ^reference2]}, + params: [index_name: :algoliax_people_struct_fr] + }} + ] + } = res2 + + assert_request("POST", %{ + path: ~r/algoliax_people_struct_en/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => reference1}}, + %{"action" => "deleteObject", "body" => %{"objectID" => reference2}} + ] + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_struct_fr/, + body: %{ + "requests" => [ + %{"action" => "updateObject", "body" => %{"objectID" => reference1}}, + %{"action" => "deleteObject", "body" => %{"objectID" => reference2}} + ] + } + }) + end + + test "get_object/1" do + person = %PeopleStructMultipleIndexes{ + reference: "known", + last_name: "Doe", + first_name: "John", + age: 77 + } + + assert {:ok, [res, res2]} = PeopleStructMultipleIndexes.get_object(person) + + assert %Algoliax.Responses{ + index_name: :algoliax_people_struct_en, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"objectID" => "known"}, + params: [index_name: :algoliax_people_struct_en, object_id: "known"] + }} + ] + } = res + + assert %Algoliax.Responses{ + index_name: :algoliax_people_struct_fr, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"objectID" => "known"}, + params: [index_name: :algoliax_people_struct_fr, object_id: "known"] + }} + ] + } = res2 + + assert_request("GET", %{path: ~r/algoliax_people_struct_en/, body: %{}}) + assert_request("GET", %{path: ~r/algoliax_people_struct_fr/, body: %{}}) + end + + test "get_object/1 w/ unknown" do + person = %PeopleStructMultipleIndexes{ + reference: "unknown", + last_name: "Doe", + first_name: "John", + age: 77 + } + + assert {:ok, + [ + %Algoliax.Responses{ + index_name: :algoliax_people_struct_en, + responses: [{:error, 404, "{}", _}] + }, + %Algoliax.Responses{ + index_name: :algoliax_people_struct_fr, + responses: [{:error, 404, "{}", _}] + } + ]} = PeopleStructMultipleIndexes.get_object(person) + end + + test "delete_object/1" do + person = %PeopleStructMultipleIndexes{ + reference: "unknown", + last_name: "Doe", + first_name: "John", + age: 77 + } + + assert {:ok, + [ + %Algoliax.Responses{index_name: :algoliax_people_struct_en}, + %Algoliax.Responses{index_name: :algoliax_people_struct_fr} + ]} = PeopleStructMultipleIndexes.delete_object(person) + + assert_request("DELETE", %{path: ~r/algoliax_people_struct_en/, body: %{}}) + assert_request("DELETE", %{path: ~r/algoliax_people_struct_fr/, body: %{}}) + end + + test "reindex/0" do + assert_raise(Algoliax.MissingRepoError, fn -> PeopleStructMultipleIndexes.reindex() end) + end + + test "delete_index/0" do + assert {:ok, + [ + %Algoliax.Responses{index_name: :algoliax_people_struct_en}, + %Algoliax.Responses{index_name: :algoliax_people_struct_fr} + ]} = PeopleStructMultipleIndexes.delete_index() + + assert_request("DELETE", %{path: ~r/algoliax_people_struct_en/, body: %{}}) + assert_request("DELETE", %{path: ~r/algoliax_people_struct_fr/, body: %{}}) + end + + test "get_settings/0" do + assert {:ok, [res, res2]} = PeopleStructMultipleIndexes.get_settings() + + assert %Algoliax.Responses{ + index_name: :algoliax_people_struct_en, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"searchableAttributes" => ["test"]}, + params: [index_name: :algoliax_people_struct_en] + }} + ] + } = res + + assert %Algoliax.Responses{ + index_name: :algoliax_people_struct_fr, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"searchableAttributes" => ["test"]}, + params: [index_name: :algoliax_people_struct_fr] + }} + ] + } = res2 + + assert_request("GET", %{path: ~r/algoliax_people_struct_fr/, body: %{}}) + assert_request("GET", %{path: ~r/algoliax_people_struct_en/, body: %{}}) + end + + test "search/2" do + assert assert {:ok, + [ + %Algoliax.Responses{index_name: :algoliax_people_struct_en}, + %Algoliax.Responses{index_name: :algoliax_people_struct_fr} + ]} = PeopleStructMultipleIndexes.search("john", %{hitsPerPage: 10}) + + assert_request("POST", %{ + path: ~r/algoliax_people_struct_en/, + body: %{ + "query" => "john", + "hitsPerPage" => 10 + } + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_struct_fr/, + body: %{ + "query" => "john", + "hitsPerPage" => 10 + } + }) + end + + test "search_facet/2" do + assert {:ok, + [ + %Algoliax.Responses{index_name: :algoliax_people_struct_en}, + %Algoliax.Responses{index_name: :algoliax_people_struct_fr} + ]} = PeopleStructMultipleIndexes.search_facet("age", "2") + + assert_request("POST", %{path: ~r/algoliax_people_struct_en/, body: %{"facetQuery" => "2"}}) + assert_request("POST", %{path: ~r/algoliax_people_struct_fr/, body: %{"facetQuery" => "2"}}) + end + + test "delete_by/1" do + assert {:ok, [res, res2]} = PeopleStructMultipleIndexes.delete_by("age > 18") + + assert %Algoliax.Responses{ + index_name: :algoliax_people_struct_en, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"taskID" => _, "updatedAt" => _}, + params: [index_name: :algoliax_people_struct_en] + }} + ] + } = res + + assert %Algoliax.Responses{ + index_name: :algoliax_people_struct_fr, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"taskID" => _, "updatedAt" => _}, + params: [index_name: :algoliax_people_struct_fr] + }} + ] + } = res2 + + assert_request("POST", %{ + path: ~r/algoliax_people_struct_en/, + body: %{"params" => "filters=age > 18"} + }) + + assert_request("POST", %{ + path: ~r/algoliax_people_struct_fr/, + body: %{"params" => "filters=age > 18"} + }) + end + end + describe "runtime index name" do test "get_object/1" do person = %PeopleStructRuntimeIndexName{ @@ -173,6 +596,48 @@ defmodule AlgoliaxTest.StructTest do end end + describe "runtime multiple indexes" do + test "get_object/1" do + person = %PeopleStructRuntimeMultipleIndexes{ + reference: "known", + last_name: "Doe", + first_name: "John", + age: 77 + } + + assert {:ok, [res, res2]} = PeopleStructRuntimeMultipleIndexes.get_object(person) + + assert %Algoliax.Responses{ + index_name: :people_runtime_index_name_en, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"objectID" => "known"}, + params: [index_name: :people_runtime_index_name_en, object_id: "known"] + }} + ] + } = res + + assert %Algoliax.Responses{ + index_name: :people_runtime_index_name_fr, + responses: [ + {:ok, + %Algoliax.Response{ + response: %{"objectID" => "known"}, + params: [index_name: :people_runtime_index_name_fr, object_id: "known"] + }} + ] + } = res2 + + assert_request("PUT", %{path: ~r/people_runtime_index_name_en\/settings/, body: %{}}) + assert_request("GET", %{path: ~r/people_runtime_index_name_en\/settings/, body: %{}}) + assert_request("GET", %{path: ~r/people_runtime_index_name_en\/known/, body: %{}}) + assert_request("PUT", %{path: ~r/people_runtime_index_name_fr\/settings/, body: %{}}) + assert_request("GET", %{path: ~r/people_runtime_index_name_fr\/settings/, body: %{}}) + assert_request("GET", %{path: ~r/people_runtime_index_name_fr\/known/, body: %{}}) + end + end + describe "wait for task" do reference = :rand.uniform(1_000_000) |> to_string() person = %PeopleStruct{reference: reference, last_name: "Doe", first_name: "John", age: 77} @@ -188,4 +653,38 @@ defmodule AlgoliaxTest.StructTest do assert_request("GET", %{path: ~r/algoliax_people_struct\/task\/#{task_id}/, body: %{}}) assert_request("GET", %{path: ~r/algoliax_people_struct\/task\/#{task_id}/, body: %{}}) end + + describe "wait for task with multiple indexes" do + reference = :rand.uniform(1_000_000) |> to_string() + + person = %PeopleStructMultipleIndexes{ + reference: reference, + last_name: "Doe", + first_name: "John", + age: 77 + } + + assert [{:ok, res}, {:ok, res2}] = + PeopleStructMultipleIndexes.save_object(person) |> Algoliax.wait_task() + + assert %Algoliax.Response{ + response: %{"objectID" => ^reference, "taskID" => task_id, "updatedAt" => _}, + params: [index_name: :algoliax_people_struct_en, object_id: ^reference] + } = res + + assert %Algoliax.Response{ + response: %{"objectID" => ^reference, "taskID" => task_id2, "updatedAt" => _}, + params: [index_name: :algoliax_people_struct_fr, object_id: ^reference] + } = res2 + + # Assert that there are 4 calls to check task status per index + assert_request("GET", %{path: ~r/algoliax_people_struct_en\/task\/#{task_id}/, body: %{}}) + assert_request("GET", %{path: ~r/algoliax_people_struct_en\/task\/#{task_id}/, body: %{}}) + assert_request("GET", %{path: ~r/algoliax_people_struct_en\/task\/#{task_id}/, body: %{}}) + assert_request("GET", %{path: ~r/algoliax_people_struct_en\/task\/#{task_id}/, body: %{}}) + assert_request("GET", %{path: ~r/algoliax_people_struct_fr\/task\/#{task_id2}/, body: %{}}) + assert_request("GET", %{path: ~r/algoliax_people_struct_fr\/task\/#{task_id2}/, body: %{}}) + assert_request("GET", %{path: ~r/algoliax_people_struct_fr\/task\/#{task_id2}/, body: %{}}) + assert_request("GET", %{path: ~r/algoliax_people_struct_fr\/task\/#{task_id2}/, body: %{}}) + end end diff --git a/test/algoliax/utils_test.exs b/test/algoliax/utils_test.exs index 990dcaa..ca1b7c8 100644 --- a/test/algoliax/utils_test.exs +++ b/test/algoliax/utils_test.exs @@ -4,18 +4,22 @@ defmodule Algoliax.UtilsTest do defmodule NoRepo do use Algoliax.Indexer, index_name: :algoliax_people, - attributes_for_faceting: ["age"], - searchable_attributes: ["full_name"], - custom_ranking: ["desc(updated_at)"], + algolia: [ + attributes_for_faceting: ["age"], + searchable_attributes: ["full_name"], + custom_ranking: ["desc(updated_at)"] + ], object_id: :reference end defmodule IndexNameFromFunction do use Algoliax.Indexer, index_name: :algoliax_people, - attributes_for_faceting: ["age"], - searchable_attributes: ["full_name"], - custom_ranking: ["desc(updated_at)"], + algolia: [ + attributes_for_faceting: ["age"], + searchable_attributes: ["full_name"], + custom_ranking: ["desc(updated_at)"] + ], object_id: :reference def algoliax_people do @@ -23,14 +27,110 @@ defmodule Algoliax.UtilsTest do end end + defmodule MultipleIndexNames do + use Algoliax.Indexer, + index_name: [:algoliax_people_en, :algoliax_people_fr], + algolia: [ + attributes_for_faceting: ["age"], + searchable_attributes: ["full_name"], + custom_ranking: ["desc(updated_at)"] + ], + object_id: :reference + end + + defmodule MultipleIndexNameFromFunction do + use Algoliax.Indexer, + index_name: :algoliax_people, + algolia: [ + attributes_for_faceting: ["age"], + searchable_attributes: ["full_name"], + custom_ranking: ["desc(updated_at)"] + ], + object_id: :reference + + def algoliax_people do + [:algoliax_people_from_function_en, :algoliax_people_from_function_fr] + end + end + defmodule NoIndexName do use Algoliax.Indexer, - attributes_for_faceting: ["age"], - searchable_attributes: ["full_name"], - custom_ranking: ["desc(updated_at)"], + algolia: [ + attributes_for_faceting: ["age"], + searchable_attributes: ["full_name"], + custom_ranking: ["desc(updated_at)"] + ], + object_id: :reference + end + + defmodule NoDefaultFilters do + use Algoliax.Indexer, + index_name: :algoliax_people, + algolia: [ + attributes_for_faceting: ["age"], + searchable_attributes: ["full_name"], + custom_ranking: ["desc(updated_at)"] + ], object_id: :reference end + defmodule DefaultFiltersInSettings do + use Algoliax.Indexer, + index_name: :algoliax_people, + algolia: [ + attributes_for_faceting: ["age"], + searchable_attributes: ["full_name"], + custom_ranking: ["desc(updated_at)"] + ], + object_id: :reference, + default_filters: %{where: [age: 42]} + end + + defmodule DefaultFiltersWithFunction do + use Algoliax.Indexer, + index_name: :algoliax_people, + algolia: [ + attributes_for_faceting: ["age"], + searchable_attributes: ["full_name"], + custom_ranking: ["desc(updated_at)"] + ], + object_id: :reference, + default_filters: :default_filters + + def default_filters do + %{where: [age: 43]} + end + end + + defmodule SynonymsWithFunc do + use Algoliax.Indexer, + index_name: :algoliax_people, + algolia: [], + object_id: :reference, + synonyms: :get_synonyms + + def get_synonyms("list"), do: [] + def get_synonyms("nil"), do: nil + def get_synonyms(_), do: "invalid" + end + + defmodule AlgoliaSettingsFunction do + def valid_func do + [ + attributes_for_faceting: ["age2"], + searchable_attributes: ["full_name2"] + ] + end + + def invalid_return_func do + :invalid + end + + def invalid_arity_func(arg) do + arg + end + end + describe "Raise exception if trying Ecto specific methods" do test "Algoliax.MissingRepoError" do assert_raise(Algoliax.MissingRepoError, fn -> @@ -62,15 +162,149 @@ defmodule Algoliax.UtilsTest do end end - describe "should get correct index_name" do - test "if there is a function" do + describe "index_name/2" do + test "with a function" do assert Algoliax.Utils.index_name(IndexNameFromFunction, index_name: :algoliax_people) == - :algoliax_people_from_function + [:algoliax_people_from_function] end - test "if there is not function" do + test "without a function" do assert Algoliax.Utils.index_name(NoRepo, index_name: :algoliax_people) == - :algoliax_people + [:algoliax_people] + end + + test "multiple indexes with a function" do + assert Algoliax.Utils.index_name(MultipleIndexNameFromFunction, + index_name: :algoliax_people + ) == + [:algoliax_people_from_function_en, :algoliax_people_from_function_fr] + end + + test "multiple indexes without a function" do + assert Algoliax.Utils.index_name(MultipleIndexNames, + index_name: [:algoliax_people_en, :algoliax_people_fr] + ) == + [:algoliax_people_en, :algoliax_people_fr] + end + end + + describe "algolia_settings/2" do + test "with a nothing" do + assert Algoliax.Utils.algolia_settings(%{}, []) == [] + end + + test "with a list" do + assert Algoliax.Utils.algolia_settings(%{}, + algolia: [ + attributes_for_faceting: ["age"], + searchable_attributes: ["full_name"] + ] + ) == [ + attributes_for_faceting: ["age"], + searchable_attributes: ["full_name"] + ] + end + + test "with a function" do + assert Algoliax.Utils.algolia_settings(AlgoliaSettingsFunction, algolia: :valid_func) == [ + attributes_for_faceting: ["age2"], + searchable_attributes: ["full_name2"] + ] + end + + test "with a function with invalid return" do + assert_raise(Algoliax.InvalidAlgoliaSettingsFunctionError, fn -> + Algoliax.Utils.algolia_settings(AlgoliaSettingsFunction, algolia: :invalid_return_func) + end) + end + + test "with an unknown function" do + assert_raise(Algoliax.InvalidAlgoliaSettingsFunctionError, fn -> + Algoliax.Utils.algolia_settings(AlgoliaSettingsFunction, algolia: :unknown_func) + end) + end + + test "with an non-0-arity function" do + assert_raise(Algoliax.InvalidAlgoliaSettingsFunctionError, fn -> + Algoliax.Utils.algolia_settings(AlgoliaSettingsFunction, algolia: :invalid_arity_func) + end) + end + + test "with a map" do + assert_raise(Algoliax.InvalidAlgoliaSettingsConfigurationError, fn -> + Algoliax.Utils.algolia_settings(AlgoliaSettingsFunction, algolia: 42) + end) + end + end + + describe "default_filters/2" do + test "not provided" do + assert Algoliax.Utils.default_filters(NoDefaultFilters, []) == %{} + end + + test "provided in settings" do + assert Algoliax.Utils.default_filters(DefaultFiltersInSettings, + default_filters: %{where: [age: 42]} + ) == %{where: [age: 42]} + end + + test "provided as a function" do + assert Algoliax.Utils.default_filters(DefaultFiltersWithFunction, + default_filters: :default_filters + ) == %{where: [age: 43]} + end + end + + describe "synonyms_settings/3" do + test "should work with hardcoded list" do + assert Algoliax.Utils.synonyms_settings(NoRepo, [synonyms: []], "index_name") == [] + end + + test "should work with hardcoded nil" do + assert Algoliax.Utils.synonyms_settings(NoRepo, [synonyms: nil], "index_name") == nil + end + + test "should work with no settings" do + assert Algoliax.Utils.synonyms_settings(NoRepo, [], "index_name") == nil + end + + test "should work with arity-1 function that returns nil" do + assert Algoliax.Utils.synonyms_settings( + SynonymsWithFunc, + [synonyms: :get_synonyms], + "nil" + ) == nil + end + + test "should work with arity-1 function that returns list" do + assert Algoliax.Utils.synonyms_settings( + SynonymsWithFunc, + [synonyms: :get_synonyms], + "list" + ) == [] + end + + test "should fail with arity-1 function that returns map" do + assert_raise(Algoliax.InvalidSynonymsSettingsFunctionError, fn -> + Algoliax.Utils.synonyms_settings( + SynonymsWithFunc, + [synonyms: :get_synonyms], + "invalid" + ) + end) + end + + test "should fail with 0-arity function" do + end + + test "should if settings is not an atom, a list, nor nil" do + assert_raise(Algoliax.InvalidSynonymsSettingsConfigurationError, fn -> + Algoliax.Utils.synonyms_settings( + SynonymsWithFunc, + [synonyms: "invalid settings"], + "index_name" + ) + end) end end end diff --git a/test/support/api_mock_server.ex b/test/support/api_mock_server.ex index 04d6c64..777eaec 100644 --- a/test/support/api_mock_server.ex +++ b/test/support/api_mock_server.ex @@ -51,6 +51,16 @@ defmodule Algoliax.ApiMockServer do send_resp(conn, 200, Jason.encode!(response)) end + # saves synonyms: https://www.algolia.com/doc/rest-api/search/#tag/Synonyms/operation/saveSynonyms + post "/:application_id/:mode/:index_name/synonyms/batch" do + response = %{ + updatedAt: DateTime.utc_now(), + taskID: :rand.uniform(10_000) + } + + send_resp(conn, 200, Jason.encode!(response)) + end + @max_retries_before_published 3 # get tasks info diff --git a/test/support/repo.ex b/test/support/repo.ex index 2fd3dc7..1d35104 100644 --- a/test/support/repo.ex +++ b/test/support/repo.ex @@ -2,27 +2,4 @@ defmodule Algoliax.Repo do use Ecto.Repo, otp_app: :algoliax, adapter: Ecto.Adapters.Postgres - - def init(_type, config) do - config = - config - |> put_env(:hostname) - |> put_env(:port) - |> put_env(:username) - |> put_env(:password) - - {:ok, config} - end - - defp put_env(config, key) do - case load_env(key) do - nil -> config - val -> Keyword.put(config, key, val) - end - end - - defp load_env(:username), do: System.get_env("DB_USERNAME") - defp load_env(:password), do: System.get_env("DB_PASSWORD") - defp load_env(:hostname), do: System.get_env("POSTGRES_HOST") - defp load_env(:port), do: System.get_env("POSTGRES_PORT") end diff --git a/test/support/schemas/beer_with_filters.ex b/test/support/schemas/beer_with_filters.ex new file mode 100644 index 0000000..e5f8506 --- /dev/null +++ b/test/support/schemas/beer_with_filters.ex @@ -0,0 +1,26 @@ +defmodule Algoliax.Schemas.BeerWithFilters do + @moduledoc false + + alias Algoliax.Schemas.Beer + + use Algoliax.Indexer, + index_name: :algoliax_beer_with_filters, + repo: Algoliax.Repo, + schemas: [Beer], + algolia: [ + attributes_for_faceting: ["kind", "name"], + searchable_attributes: ["kind", "name"], + custom_ranking: ["desc(updated_at)"] + ], + default_filters: %{where: [kind: "blonde"]} + + def build_object(beer) do + %{ + kind: beer.kind, + name: beer.name, + updated_at: ~U[2019-01-01 00:00:00Z] |> DateTime.to_unix() + } + end + + def to_be_indexed?(_beer), do: true +end diff --git a/test/support/schemas/beer_with_schema_filters.ex b/test/support/schemas/beer_with_schema_filters.ex new file mode 100644 index 0000000..5ed302c --- /dev/null +++ b/test/support/schemas/beer_with_schema_filters.ex @@ -0,0 +1,30 @@ +defmodule Algoliax.Schemas.BeerWithSchemaFilters do + @moduledoc false + + alias Algoliax.Schemas.Beer + + use Algoliax.Indexer, + index_name: :algoliax_beer_with_schema_filters, + repo: Algoliax.Repo, + schemas: [Beer], + algolia: [ + attributes_for_faceting: ["kind", "name"], + searchable_attributes: ["kind", "name"], + custom_ranking: ["desc(updated_at)"] + ], + default_filters: :get_filters + + def get_filters do + %{Beer => %{where: [kind: "brune"]}} + end + + def build_object(beer) do + %{ + kind: beer.kind, + name: beer.name, + updated_at: ~U[2019-01-01 00:00:00Z] |> DateTime.to_unix() + } + end + + def to_be_indexed?(_beer), do: true +end diff --git a/test/support/schemas/flower.ex b/test/support/schemas/flower.ex new file mode 100644 index 0000000..1cb0191 --- /dev/null +++ b/test/support/schemas/flower.ex @@ -0,0 +1,15 @@ +defmodule Algoliax.Schemas.Flower do + @moduledoc false + use Ecto.Schema + + schema "flowers" do + field(:kind) + + belongs_to( + :people_with_association_multiple_indexes, + Algoliax.Schemas.PeopleWithAssociationMultipleIndexes + ) + + timestamps() + end +end diff --git a/test/support/schemas/people_ecto_fail_multiple_indexes.ex b/test/support/schemas/people_ecto_fail_multiple_indexes.ex new file mode 100644 index 0000000..a046910 --- /dev/null +++ b/test/support/schemas/people_ecto_fail_multiple_indexes.ex @@ -0,0 +1,41 @@ +defmodule Algoliax.Schemas.PeopleEctoFailMultipleIndexes do + @moduledoc false + + use Ecto.Schema + + use Algoliax.Indexer, + index_name: [:algoliax_people_fail_en, :algoliax_people_fail_fr], + repo: Algoliax.Repo, + object_id: :reference, + cursor_field: :id, + algolia: [ + attributes_for_faceting: ["age", "gender"], + searchable_attributes: ["full_name", "gender"], + custom_ranking: ["desc(updated_at)"] + ] + + @primary_key {:reference, Ecto.UUID, autogenerate: true} + schema "peoples_fail" do + field(:last_name) + field(:first_name) + field(:age, :integer) + field(:gender, :string) + + timestamps() + end + + def build_object(people) do + %{ + first_name: people.first_name, + last_name: people.last_name, + age: people.age, + updated_at: ~U[2019-01-01 00:00:00Z] |> DateTime.to_unix(), + full_name: Map.get(people, :first_name, "") <> " " <> Map.get(people, :last_name, ""), + nickname: Map.get(people, :first_name, "") |> String.downcase() + } + end + + def to_be_indexed?(people) do + people.age > 10 + end +end diff --git a/test/support/schemas/people_ecto_multiple_indexes.ex b/test/support/schemas/people_ecto_multiple_indexes.ex new file mode 100644 index 0000000..cca5090 --- /dev/null +++ b/test/support/schemas/people_ecto_multiple_indexes.ex @@ -0,0 +1,40 @@ +defmodule Algoliax.Schemas.PeopleEctoMultipleIndexes do + @moduledoc false + + use Ecto.Schema + + use Algoliax.Indexer, + index_name: [:algoliax_people_en, :algoliax_people_fr], + repo: Algoliax.Repo, + object_id: :reference, + algolia: [ + attributes_for_faceting: ["age", "gender"], + searchable_attributes: ["full_name", "gender"], + custom_ranking: ["desc(updated_at)"] + ] + + schema "peoples" do + field(:reference, Ecto.UUID) + field(:last_name) + field(:first_name) + field(:age, :integer) + field(:gender, :string) + + timestamps() + end + + def build_object(people) do + %{ + first_name: people.first_name, + last_name: people.last_name, + age: people.age, + updated_at: ~U[2019-01-01 00:00:00Z] |> DateTime.to_unix(), + full_name: Map.get(people, :first_name, "") <> " " <> Map.get(people, :last_name, ""), + nickname: Map.get(people, :first_name, "") |> String.downcase() + } + end + + def to_be_indexed?(people) do + people.age > 10 + end +end diff --git a/test/support/schemas/people_struct_multiple_indexes.ex b/test/support/schemas/people_struct_multiple_indexes.ex new file mode 100644 index 0000000..1d9764f --- /dev/null +++ b/test/support/schemas/people_struct_multiple_indexes.ex @@ -0,0 +1,29 @@ +defmodule Algoliax.Schemas.PeopleStructMultipleIndexes do + @moduledoc false + + use Algoliax.Indexer, + index_name: [:algoliax_people_struct_en, :algoliax_people_struct_fr], + object_id: :reference, + algolia: [ + attributes_for_faceting: ["age"], + searchable_attributes: ["full_name"], + custom_ranking: ["desc(update_at)"] + ] + + defstruct reference: nil, last_name: nil, first_name: nil, age: nil + + def build_object(people) do + %{ + first_name: people.first_name, + last_name: people.last_name, + age: people.age, + updated_at: ~U[2019-01-01 00:00:00Z] |> DateTime.to_unix(), + full_name: Map.get(people, :first_name, "") <> " " <> Map.get(people, :last_name, ""), + nickname: Map.get(people, :first_name, "") |> String.downcase() + } + end + + def to_be_indexed?(people) do + people.age > 50 + end +end diff --git a/test/support/schemas/people_struct_runtime_multiple_indexes.ex b/test/support/schemas/people_struct_runtime_multiple_indexes.ex new file mode 100644 index 0000000..c5ebe97 --- /dev/null +++ b/test/support/schemas/people_struct_runtime_multiple_indexes.ex @@ -0,0 +1,18 @@ +defmodule Algoliax.Schemas.PeopleStructRuntimeMultipleIndexes do + @moduledoc false + + use Algoliax.Indexer, + index_name: :method_to_fetch_index_name, + object_id: :reference, + algolia: [ + attributes_for_faceting: ["age"], + searchable_attributes: ["full_name"], + custom_ranking: ["desc(update_at)"] + ] + + defstruct reference: nil, last_name: nil, first_name: nil, age: nil + + def method_to_fetch_index_name do + [:people_runtime_index_name_en, :people_runtime_index_name_fr] + end +end diff --git a/test/support/schemas/people_with_association_multiple_indexes.ex b/test/support/schemas/people_with_association_multiple_indexes.ex new file mode 100644 index 0000000..569feb2 --- /dev/null +++ b/test/support/schemas/people_with_association_multiple_indexes.ex @@ -0,0 +1,43 @@ +defmodule Algoliax.Schemas.PeopleWithAssociationMultipleIndexes do + @moduledoc false + + use Ecto.Schema + + use Algoliax.Indexer, + index_name: [ + :algoliax_people_ecto_with_association_en, + :algoliax_people_ecto_with_association_fr + ], + repo: Algoliax.Repo, + object_id: :reference, + schemas: [ + {__MODULE__, [:flowers]} + ], + algolia: [ + attributes_for_faceting: ["age", "gender"], + searchable_attributes: ["full_name", "gender"], + custom_ranking: ["desc(updated_at)"] + ] + + schema "people_with_associations_multiple_indexes" do + field(:reference, Ecto.UUID) + field(:last_name) + field(:first_name) + field(:age, :integer) + field(:gender, :string) + has_many(:flowers, Algoliax.Schemas.Flower) + + timestamps() + end + + def build_object(people) do + %{ + flowers: + Enum.map(people.flowers, fn flower -> + %{ + kind: flower.kind + } + end) + } + end +end diff --git a/test/support/schemas/people_with_custom_object_id_multiple_indexes.ex b/test/support/schemas/people_with_custom_object_id_multiple_indexes.ex new file mode 100644 index 0000000..f6057d8 --- /dev/null +++ b/test/support/schemas/people_with_custom_object_id_multiple_indexes.ex @@ -0,0 +1,37 @@ +defmodule Algoliax.Schemas.PeopleWithCustomObjectIdMultipleIndexes do + @moduledoc false + + use Ecto.Schema + + use Algoliax.Indexer, + index_name: [ + :algoliax_people_with_custom_object_id_en, + :algoliax_people_with_custom_object_id_fr + ], + repo: Algoliax.Repo, + algolia: [ + attributes_for_faceting: ["age", "gender"], + searchable_attributes: ["full_name", "gender"], + custom_ranking: ["desc(updated_at)"] + ] + + schema "peoples" do + field(:reference, Ecto.UUID) + field(:last_name) + field(:first_name) + field(:age, :integer) + field(:gender, :string) + + timestamps() + end + + def build_object(people) do + %{ + last_name: people.last_name + } + end + + def get_object_id(people) do + "people-" <> people.reference + end +end diff --git a/test/support/schemas/people_with_invalid_replicas.ex b/test/support/schemas/people_with_invalid_replicas.ex new file mode 100644 index 0000000..b73b12d --- /dev/null +++ b/test/support/schemas/people_with_invalid_replicas.ex @@ -0,0 +1,36 @@ +defmodule Algoliax.Schemas.PeopleWithInvalidReplicas do + @moduledoc false + + use Algoliax.Indexer, + index_name: :algoliax_people_replicas, + object_id: :reference, + algolia: [ + attributes_for_faceting: ["age"], + searchable_attributes: ["full_name"], + custom_ranking: ["desc(update_at)"] + ], + replicas: [ + [ + index_name: :algoliax_people_replicas_asc, + inherit: true, + algolia: [ + searchable_attributes: ["age"], + ranking: ["asc(age)"] + ], + if: :ok + ] + ] + + defstruct reference: nil, last_name: nil, first_name: nil, age: nil + + def build_object(people) do + %{ + first_name: people.first_name, + last_name: people.last_name, + age: people.age, + updated_at: ~U[2019-01-01 00:00:00Z] |> DateTime.to_unix(), + full_name: Map.get(people, :first_name, "") <> " " <> Map.get(people, :last_name, ""), + nickname: Map.get(people, :first_name, "") |> String.downcase() + } + end +end diff --git a/test/support/schemas/people_with_replicas.ex b/test/support/schemas/people_with_replicas.ex index 02b9c33..9682250 100644 --- a/test/support/schemas/people_with_replicas.ex +++ b/test/support/schemas/people_with_replicas.ex @@ -4,25 +4,28 @@ defmodule Algoliax.Schemas.PeopleWithReplicas do use Algoliax.Indexer, index_name: :algoliax_people_replicas, object_id: :reference, - algolia: [ - attributes_for_faceting: ["age"], - searchable_attributes: ["full_name"], - custom_ranking: ["desc(update_at)"] - ], + algolia: :runtime_algolia_settings, replicas: [ [ index_name: :algoliax_people_replicas_asc, inherit: true, - algolia: [ - searchable_attributes: ["age"], - ranking: ["asc(age)"] - ] + algolia: :runtime_replica_algolia_settings ], [ index_name: :algoliax_people_replicas_desc, inherit: false, algolia: [ranking: ["desc(age)"]] ] + ], + synonyms: [ + synonyms: [ + %{ + objectID: "synonym1", + type: "synonym", + synonyms: ["synonym1", "synonym2"] + } + ], + replace_existing_synonyms: true ] defstruct reference: nil, last_name: nil, first_name: nil, age: nil @@ -37,4 +40,19 @@ defmodule Algoliax.Schemas.PeopleWithReplicas do nickname: Map.get(people, :first_name, "") |> String.downcase() } end + + def runtime_algolia_settings do + [ + attributes_for_faceting: ["age"], + searchable_attributes: ["full_name"], + custom_ranking: ["desc(update_at)"] + ] + end + + def runtime_replica_algolia_settings do + [ + searchable_attributes: ["age"], + ranking: ["asc(age)"] + ] + end end diff --git a/test/support/schemas/people_with_replicas_and_custom_synonyms.ex b/test/support/schemas/people_with_replicas_and_custom_synonyms.ex new file mode 100644 index 0000000..15ff0f1 --- /dev/null +++ b/test/support/schemas/people_with_replicas_and_custom_synonyms.ex @@ -0,0 +1,62 @@ +defmodule Algoliax.Schemas.PeopleWithReplicasAndCustomSynonyms do + @moduledoc false + + use Algoliax.Indexer, + index_name: :algoliax_people_replicas_synonyms, + object_id: :reference, + algolia: :runtime_algolia_settings, + replicas: [ + [ + index_name: :algoliax_people_replicas_synonyms_asc, + inherit: true, + algolia: :runtime_replica_algolia_settings, + synonyms: [ + synonyms: [ + %{ + objectID: "synonym2", + type: "synonym", + synonyms: ["synonym3", "synonym4"] + } + ] + ] + ] + ], + synonyms: [ + synonyms: [ + %{ + objectID: "synonym1", + type: "synonym", + synonyms: ["synonym1", "synonym2"] + } + ], + forward_to_replicas: false + ] + + defstruct reference: nil, last_name: nil, first_name: nil, age: nil + + def build_object(people) do + %{ + first_name: people.first_name, + last_name: people.last_name, + age: people.age, + updated_at: ~U[2019-01-01 00:00:00Z] |> DateTime.to_unix(), + full_name: Map.get(people, :first_name, "") <> " " <> Map.get(people, :last_name, ""), + nickname: Map.get(people, :first_name, "") |> String.downcase() + } + end + + def runtime_algolia_settings do + [ + attributes_for_faceting: ["age"], + searchable_attributes: ["full_name"], + custom_ranking: ["desc(update_at)"] + ] + end + + def runtime_replica_algolia_settings do + [ + searchable_attributes: ["age"], + ranking: ["asc(age)"] + ] + end +end diff --git a/test/support/schemas/people_with_replicas_multiple_indexes.ex b/test/support/schemas/people_with_replicas_multiple_indexes.ex new file mode 100644 index 0000000..408cac5 --- /dev/null +++ b/test/support/schemas/people_with_replicas_multiple_indexes.ex @@ -0,0 +1,77 @@ +defmodule Algoliax.Schemas.PeopleWithReplicasMultipleIndexes do + @moduledoc false + + use Algoliax.Indexer, + index_name: [:algoliax_people_replicas_en, :algoliax_people_replicas_fr], + object_id: :reference, + algolia: [ + attributes_for_faceting: ["age"], + searchable_attributes: ["full_name"], + custom_ranking: ["desc(update_at)"] + ], + replicas: [ + [ + index_name: [:algoliax_people_replicas_asc_en, :algoliax_people_replicas_asc_fr], + inherit: true, + algolia: [ + searchable_attributes: ["age"], + ranking: ["asc(age)"] + ] + ], + [ + index_name: [:algoliax_people_replicas_desc_en, :algoliax_people_replicas_desc_fr], + inherit: false, + algolia: [ranking: ["desc(age)"]], + if: true + ], + [ + index_name: [:algoliax_people_replicas_skipped_en, :algoliax_people_replicas_skipped_fr], + inherit: true, + algolia: [ + searchable_attributes: ["age"], + ranking: ["asc(age)"] + ], + if: :do_not_deploy + ], + [ + index_name: [ + :algoliax_people_replicas_skipped_too_en, + :algoliax_people_replicas_skipped_too_fr + ], + inherit: true, + algolia: [ + searchable_attributes: ["age"], + ranking: ["asc(age)"] + ], + if: false + ], + [ + index_name: [ + :algoliax_people_replicas_not_skipped_en, + :algoliax_people_replicas_not_skipped_fr + ], + inherit: true, + algolia: [ + searchable_attributes: ["age"], + ranking: ["asc(age)"] + ], + if: :do_deploy + ] + ] + + defstruct reference: nil, last_name: nil, first_name: nil, age: nil + + def build_object(people) do + %{ + first_name: people.first_name, + last_name: people.last_name, + age: people.age, + updated_at: ~U[2019-01-01 00:00:00Z] |> DateTime.to_unix(), + full_name: Map.get(people, :first_name, "") <> " " <> Map.get(people, :last_name, ""), + nickname: Map.get(people, :first_name, "") |> String.downcase() + } + end + + def do_not_deploy, do: false + def do_deploy, do: true +end diff --git a/test/support/schemas/people_with_schemas_multiple_indexes.ex b/test/support/schemas/people_with_schemas_multiple_indexes.ex new file mode 100644 index 0000000..72e53aa --- /dev/null +++ b/test/support/schemas/people_with_schemas_multiple_indexes.ex @@ -0,0 +1,35 @@ +defmodule Algoliax.Schemas.PeopleWithSchemasMultipleIndexes do + @moduledoc false + + use Ecto.Schema + + alias Algoliax.Schemas.Beer + + use Algoliax.Indexer, + index_name: [:algoliax_with_schemas_en, :algoliax_with_schemas_fr], + repo: Algoliax.Repo, + schemas: [ + Beer + ], + algolia: [ + attributes_for_faceting: ["age", "gender"], + searchable_attributes: ["full_name", "gender"], + custom_ranking: ["desc(updated_at)"] + ] + + schema "peoples" do + field(:reference, Ecto.UUID) + field(:last_name) + field(:first_name) + field(:age, :integer) + field(:gender, :string) + + timestamps() + end + + def build_object(%Beer{} = beer) do + %{ + name: beer.name + } + end +end diff --git a/test/support/schemas/people_without_id_ecto_multiple_indexes.ex b/test/support/schemas/people_without_id_ecto_multiple_indexes.ex new file mode 100644 index 0000000..664af16 --- /dev/null +++ b/test/support/schemas/people_without_id_ecto_multiple_indexes.ex @@ -0,0 +1,26 @@ +defmodule Algoliax.Schemas.PeopleWithoutIdEctoMultipleIndexes do + @moduledoc false + + use Ecto.Schema + + use Algoliax.Indexer, + index_name: [:algoliax_people_without_id_en, :algoliax_people_without_id_fr], + repo: Algoliax.Repo, + object_id: :reference, + cursor_field: :inserted_at, + algolia: [ + attributes_for_faceting: ["age", "gender"], + searchable_attributes: ["firstname", "lastname"], + custom_ranking: ["desc(updated_at)"] + ] + + @primary_key {:reference, Ecto.UUID, autogenerate: true} + schema "peoples_without_id" do + field(:last_name) + field(:first_name) + field(:age, :integer) + field(:gender, :string) + + timestamps() + end +end