Algoliax.Indexer behaviour (Algoliax v0.10.0)

View Source

Usage

  • :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__]
  • :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.

Example

defmodule People do
  use Algoliax.Indexer,
    index_name: :people,
    object_id: :reference,
    algolia: [
      attribute_for_faceting: ["age"],
      custom_ranking: ["desc(updated_at)"]
    ]

  defstruct reference: nil, last_name: nil, first_name: nil, age: nil
end

Customize object

By default the object contains only algolia objectID. To add more attributes to objects, override build_object/1 functions to return a Map (objectID is automatically set by Algoliax)

defmodule People do
  use Algoliax.Indexer,
    index_name: :people,
    object_id: :reference,
    algolia: [
      attribute_for_faceting: ["age"],
      custom_ranking: ["desc(updated_at)"]
    ]

  defstruct reference: nil, last_name: nil, first_name: nil, age: nil

  @impl Algoliax.Indexer
  def build_object(person) do
    %{
      age: person.age,
      last_name: person.last_name,
      first_name: person.first_name
    }
  end
end

Schemas

:schemas options allows to define a list of module you want to index into the current index. By default only the module defining the indexer.

defmodule Global do
  use Algoliax.Indexer,
    index_name: :people,
    object_id: :reference,
    schemas: [People, Animal],
    algolia: [
      attribute_for_faceting: ["age"],
      custom_ranking: ["desc(updated_at)"]
    ]

end

The example above will introduce a compile-time dependency on People and Animal, so it is preferable to provide the name of a function that will return the list of modules.

defmodule Global do
  use Algoliax.Indexer,
    index_name: :people,
    object_id: :reference,
    schemas: :index_schemas,
    algolia: [
      attribute_for_faceting: ["age"],
      custom_ranking: ["desc(updated_at)"]
    ]

  def index_schemas, do: [People, Animal]
end

This option allows to define also the preloads use during reindex/reindex_atomic (preload on save_object and save_objects have to be done manually)

  defmodule People do
    use Algoliax.Indexer,
      index_name: :people,
      object_id: :reference,
      schemas: [
        {__MODULE__, [:animals]}
      ]
      algolia: [
        attribute_for_faceting: ["age"],
        custom_ranking: ["desc(updated_at)"]
      ]

  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.

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.

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

Summary

Callbacks

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

Configure index

Delete objects from algolia. By passing a matching filter query, the records are retrieved and deleted.Filters

Delete index

Delete object from algolia. By passing the model, the object is retrieved using the object_id configured

Fetch object from algolia. By passing the model, the object is retrieved using the object_id configured

Override this function to provide custom objectID for the model

Get index settings from Algolia

Reindex all objects (Ecto specific)

Reindex a subset of records by providing an Ecto query or query filters as a Map(Ecto specific)

Reindex atomically (Ecto specific)

Add/update object. The object is added/updated to algolia with the object_id configured.

Save multiple object at once

Search for index values

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

Callbacks

build_object(model)

@callback build_object(model :: map()) :: map()

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
def build_object(person) do
  %{
    age: person.age,
    last_name: person.last_name,
    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

build_object(model, index)

@callback build_object(model :: map(), index :: String.t()) :: map()

configure_index()

@callback configure_index() :: {:ok, map()} | {:ok, [map()]} | {:error, map()}

Configure index

delete_by(matching_filter)

@callback delete_by(matching_filter :: String.t()) ::
  {:ok, Algoliax.Response.t()}
  | {:ok, [Algoliax.Responses.t()]}
  | {:error, map()}

Delete objects from algolia. By passing a matching filter query, the records are retrieved and deleted.Filters

Example:

People.delete_by("age > 18")

delete_index()

@callback delete_index() :: {:ok, map()} | {:ok, [map()]} | {:error, map()}

Delete index

delete_object(model)

@callback delete_object(model :: map() | struct()) ::
  {:ok, Algoliax.Response.t()}
  | {:ok, [Algoliax.Responses.t()]}
  | {:error, map()}

Delete object from algolia. By passing the model, the object is retrieved using the object_id configured

Example

people = %People{reference: 10, last_name: "Doe", first_name: "John", age: 20}

People.delete_object(people)

get_object(model)

@callback get_object(model :: map() | struct()) ::
  {:ok, Algoliax.Response.t()}
  | {:ok, [Algoliax.Responses.t()]}
  | {:error, map()}

Fetch object from algolia. By passing the model, the object is retrieved using the object_id configured

Example

people = %People{reference: 10, last_name: "Doe", first_name: "John", age: 20}

People.get_object(people)

get_object_id(model)

@callback get_object_id(model :: map()) :: binary() | :default

Override this function to provide custom objectID for the model

Example

@impl Algoliax.Indexer
def get_object_id(%Cat{id: id}), do: "Cat:" <> to_string(id)
def get_object_id(%Dog{id: id}), do: "Dog:" <> to_string(id)

get_settings()

@callback get_settings() :: {:ok, map()} | {:ok, [map()]} | {:error, map()}

Get index settings from Algolia

reindex(opts)

@callback reindex(opts :: Keyword.t()) ::
  {:ok, [Algoliax.Response.t()]} | {:ok, [Algoliax.Responses.t()]}

Reindex all objects (Ecto specific)

Example

People.reindex(query)

Available options:

  • :force_delete: delete objects where to_be_indexed? is false

reindex(query, opts)

@callback reindex(query :: Ecto.Query.t(), opts :: Keyword.t()) ::
  {:ok, [Algoliax.Response.t()]} | {:ok, [Algoliax.Responses.t()]}

Reindex a subset of records by providing an Ecto query or query filters as a Map(Ecto specific)

Example

import Ecto.Query

query = from(
  p in People,
  where: p.age > 45
)

People.reindex(query)

# OR
filters = %{where: [name: "john"]}
People.reindex(filters)

Available options:

  • :force_delete: delete objects that are in query and where to_be_indexed? is false

NOTE: filters as Map supports only :where and equality

reindex_atomic()

@callback reindex_atomic() :: {:ok, :completed} | [{:ok, :completed}]

Reindex atomically (Ecto specific)

save_object(object)

@callback save_object(object :: map() | struct()) ::
  {:ok, Algoliax.Response.t()}
  | {:ok, [Algoliax.Responses.t()]}
  | {:not_indexable, model :: map()}

Add/update object. The object is added/updated to algolia with the object_id configured.

Example

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, ...}, ...]}, ...]}

save_objects(models, opts)

@callback save_objects(models :: [map()] | [struct()], opts :: Keyword.t()) ::
  {:ok, Algoliax.Response.t()}
  | {:ok, [Algoliax.Responses.t()]}
  | {:error, map()}

Save multiple object at once

Options

  • :force_delete - if true will trigger a "deleteObject" on object that must not be indexed. Default false

Example

peoples = [
  %People{reference: 10, last_name: "Doe", first_name: "John", age: 20},
  %People{reference: 89, last_name: "Einstein", first_name: "Albert", age: 65}
]

People.save_objects(peoples)
People.save_objects(peoples, force_delete: true)

search(query, params)

@callback search(query :: binary(), params :: map()) ::
  {:ok, Algoliax.Response.t()}
  | {:ok, [Algoliax.Responses.t()]}
  | {:not_indexable, model :: map()}

Search for index values

Example

iex> People.search("John")

{:ok,
  %{
    "exhaustiveNbHits" => true,
    "hits" => [
      %{
        "_highlightResult" => %{
          "full_name" => %{
            "fullyHighlighted" => false,
            "matchLevel" => "full",
            "matchedWords" => ["john"],
            "value" => "Pierre <em>Jon</em>es"
          }
        },
        "age" => 69,
        "first_name" => "Pierre",
        "full_name" => "Pierre Jones",
        "indexed_at" => 1570908223,
        "last_name" => "Jones",
        "objectID" => "b563deb6-2a06-4428-8e5a-ca1ecc08f4e2"
      },
      %{
        "_highlightResult" => %{
          "full_name" => %{
            "fullyHighlighted" => false,
            "matchLevel" => "full",
            "matchedWords" => ["john"],
            "value" => "Glennie <em>Jon</em>es"
          }
        },
        "age" => 27,
        "first_name" => "Glennie",
        "full_name" => "Glennie Jones",
        "indexed_at" => 1570908223,
        "last_name" => "Jones",
        "objectID" => "58e8ff8d-2794-41e1-a4ef-6f8db8d432b6"
      },
      ...
    ],
"hitsPerPage" => 20,
"nbHits" => 16,
"nbPages" => 1,
"page" => 0,
"params" => "query=john",
"processingTimeMS" => 1,
"query" => "john"
}}

iex> PeopleWithMultipleIndexes.search("John")
{:ok, [%{index_name: "people", responses: [%{"exhaustiveNbHits" => true, ...}, ...]}, ...]}

search_facet(facet_name, facet_query, params)

@callback search_facet(facet_name :: binary(), facet_query :: binary(), params :: map()) ::
  {:ok, Algoliax.Response.t()}
  | {:ok, [Algoliax.Responses.t()]}
  | {:not_indexable, model :: map()}

Search for facet values

Example

iex> People.search_facet("age")
{:ok,
  %{
    "exhaustiveFacetsCount" => true,
    "facetHits" => [
      %{"count" => 22, "highlighted" => "46", "value" => "46"},
      %{"count" => 21, "highlighted" => "38", "value" => "38"},
      %{"count" => 19, "highlighted" => "54", "value" => "54"},
      %{"count" => 19, "highlighted" => "99", "value" => "99"},
      %{"count" => 18, "highlighted" => "36", "value" => "36"},
      %{"count" => 18, "highlighted" => "45", "value" => "45"},
      %{"count" => 18, "highlighted" => "52", "value" => "52"},
      %{"count" => 18, "highlighted" => "56", "value" => "56"},
      %{"count" => 18, "highlighted" => "59", "value" => "59"},
      %{"count" => 18, "highlighted" => "86", "value" => "86"}
    ],
    "processingTimeMS" => 1
  }}

iex> PeopleWithMultipleIndexes.search_facet("age")
{:ok, [%{index_name: "people", responses: [%{"exhaustiveNbHits" => true, ...}, ...]}, ...]}

to_be_indexed?(model)

@callback to_be_indexed?(model :: map()) :: true | false

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

Example

defmodule People do
  use Algoliax.Indexer,
    index_name: :people,
    object_id: :reference,
    algolia: [
      attribute_for_faceting: ["age"],
      custom_ranking: ["desc(update_at)"]
    ]

  #....

  @impl Algoliax.Indexer
  def to_be_indexed?(model) do
    model.age > 50
  end
end