Refine (Refine v0.1.3)

Copy Markdown

Refine is an Elixir library for implementing fast faceted search.

Summary

Facets table functions

Identical to create_facets_table_if_not_exists/2 but returns an error if the table exists.

Creates a facets table based on a source table and populates it with bitmap data, facet labels, and facet values.

Drops the facets table. Pass the name of the facets table with an optional prefix.

Aggregates and applies membership changes from the deltas table to update the facets table.

Search functions

Performs a search on the source table.

Helper functions

Tests whether a table exists. Pass a qualified table name (including schema prefix) if the table is not in the "public" schema.

Facets table functions

create_facets_table(config, opts \\ [])

@spec create_facets_table(facets_table_config(), [database_option()]) ::
  create_facets_table_return()

Identical to create_facets_table_if_not_exists/2 but returns an error if the table exists.

Examples

Creating a new facets table.

Refine.create_facets_table(config,
  repo: MyApp.Repo
)
{:ok, "articles_facets"}

Recreating the facets table.

Refine.create_facets_table(config,
  repo: MyApp.Repo
)
{:error, :facets_table_exists, "Table 'articles_facets' exists. ..."}

create_facets_table_if_not_exists(config, opts \\ [])

@spec create_facets_table_if_not_exists(
  facets_table_config(),
  [database_option()]
) :: create_facets_table_return()

Creates a facets table based on a source table and populates it with bitmap data, facet labels, and facet values.

  • Creates the facets table only if the table does not yet exist. If the table does exist: skips table creation silently without returning an error. If you need to recreate an existing facets table, first call drop_facets_table/2. See also: create_facets_table/2.
  • Creates the roaringbitmap extension (if it isn't created already).

Configuration

See: Facets table configuration

Options

  • repo - The Ecto Repo module that contains a Postgres adapter.
  • timeout - Postgrex timeout option.

Examples

The following example:

  • Creates table articles_facets (only if it doesn't exist yet).
  • Adds an identity column named "identity" to the source table "articles" (only if no valid integer ID column is found in the table).
  • Adds a single facet "draft" that references the column of the same name in the source table.
config = %{
  facets_table: "articles_facets",
  source_table: "articles",
  add_identity_column_if_not_exists: true,
  identity_column: "identity",
  facets: [
    %{facet_name: "draft"}
  ]
}

Refine.create_facets_table_if_not_exists(config,
  repo: MyApp.Repo
)
{:ok, "articles_facets"}

Create a the facets table in database schema classifications.

config = %{
  facets_table: "classifications.categories_facets",
  source_table: "classifications.categories",
  ...
}

drop_facets_table(config, opts \\ [])

@spec drop_facets_table(facets_table_config(), [database_option()]) ::
  {:ok, String.t()} | {:error, :table_not_found} | {:error, Exception.t()}

Drops the facets table. Pass the name of the facets table with an optional prefix.

Options:

  • repo - The Ecto Repo module that contains a Postgres adapter
  • timeout - Postgrex timeout option.

Examples

Refine.drop_facets_table("articles_facets", repo: MyApp.Repo)
{:ok, "articles_facets"}

merge_deltas(config, options \\ [])

@spec merge_deltas(facets_table_config(), [database_option()]) ::
  :ok | {:error, [Exception.t()]}

Aggregates and applies membership changes from the deltas table to update the facets table.

Search functions

search(config, options \\ [])

@spec search(facets_table_config(), [search_option()]) :: search_return()

Performs a search on the source table.

Parameter config is the configuration map used to create the facets table. Here it is used to read references to the source and facet tables.

Parameter options may include a base query, selected facet values, and fields to return in the results.

Examples

Return unfiltered rows in the source database:

Refine.search(config)

Apply pagination with limit and offset:

Refine.search(config, limit: 10, offset: 10)

Filter by facet values:

Refine.search(config,
  facets: %{draft: ["true"]},
  result_fields: [:id, :title]
)

Filter a query by facet values:

query = from a in Article,
  where: ilike(a.title, ^"%memory%"),
  select: %{id: a.id, title: a.title}
facets = %{draft: ["true"]}

Refine.search(config,
  query: query,
  facets: facets
)

Options

query

An Ecto query applied to the source table to filter or shape the data.

With where filter and select fields:

query = from a in Article,
  where: ilike(a.title, ^"%memory%"),
  select: %{id: a.id, title: a.title, draft: a.draft}

For further options, see: Ecto.Query ➚.

facets

A map containing the selected values for each facet.

The map keys are facet names (as defined in the configuration), while the values are the selected option values stored in the facets table. Facet values are always strings.

%{
  draft: ["true"],
  color: ["blue", "red"]
}

If the list of facet options contains more than 1 value (blue and red), the filter on that facet is performed with an OR operator. In the example above, the logic becomes: draft=true AND (color=blue OR color=red).

limit

Limits the number of rows returned. Default: 10.

offset

The number of skipped rows. Default: 0.

result_fields

List of source fields to include in the result list. Use this to limit returned data when not using query, or to include columns not defined in the source Ecto schema. The values override any select expression in query.

result_fields = [:identity, :title]

repo

The Ecto Repo module that contains a Postgres adapter. This can be omitted when the repo is configured globally - see: Installation.

timeout

Postgrex timeout option.

Helper functions

table_exists?(qualified_table_name, repo)

@spec table_exists?(String.t(), repo()) :: boolean()

Tests whether a table exists. Pass a qualified table name (including schema prefix) if the table is not in the "public" schema.

Performs a database query using pg_class.

Examples

Refine.table_exists?("articles_facets", MyApp.Repo)

Refine.table_exists?("classifications.tags", MyApp.Repo)

Types

create_facets_table_return()

@type create_facets_table_return() ::
  {:ok, String.t()}
  | {:error, :column_not_found, String.t()}
  | {:error, :column_names_not_unique}
  | {:error, :facets_table_exists, String.t()}
  | {:error, :identity_column_already_exists, String.t()}
  | {:error, :identity_column_invalid_type, String.t()}
  | {:error, :identity_column_not_found, String.t()}
  | {:error, :invalid_table_name, String.t()}
  | {:error, :table_names_not_unique}
  | {:error, Exception.t()}

database_option()

@type database_option() :: {:repo, repo()} | postgrex_option()

facet_config()

@type facet_config() :: %{
  facet_name: String.t(),
  facet_label: String.t() | nil,
  value_column: String.t() | nil,
  label_column: String.t() | nil,
  value_path: String.t() | nil,
  label_path: String.t() | nil,
  width_bucket: [integer() | float()] | nil
}

facets_config()

@type facets_config() :: [facet_config()]

facets_table_config()

@type facets_table_config() :: %{
  facets_table: String.t(),
  source_table: String.t(),
  add_identity_column_if_not_exists: boolean() | nil,
  identity_column: String.t(),
  facets: facets_config(),
  roaringbitmap_type: String.t() | nil
}

postgrex_option()

@type postgrex_option() :: {:timeout, integer() | :infinity} | {:log, boolean()}

repo()

@type repo() :: module()

search_option()

@type search_option() ::
  {:query, Ecto.Query.t()}
  | {:facets, map()}
  | {:result_fields, [atom()]}
  | {:limit, integer()}
  | {:offset, integer()}
  | {:repo, repo()}
  | postgrex_option()

search_return()

@type search_return() :: {:ok, search_return_data()} | {:error, Exception.t()}

search_return_data()

@type search_return_data() :: %{
  types: map(),
  total_count: integer(),
  rows: [map()],
  facets: map()
}