Searching

Copy Markdown

Searches are performed on the source table, optionally narrowed by a base query and by selected facet values. A search returns two things together: the matching rows, and the facet data for those matches - which facet values are available and how many results each would yield.

A base query can restrict the search further - for example, to a text match or a subset of rows - and the results can be sorted and paginated.

Facet counts are always computed over the full set of matching rows, independent of sorting and pagination, so paging through results does not change the facets.

Creating a search query

Use Refine.search/2 to perform a search.

Parameters

  • config is the configuration map used to create the facets table. Here it is used to read references to the source and facet tables.
  • options may include a base query, selected facet values, and fields to return in the results.

Options

See the following sections.

Faceted search together with Flop

The Flop adapter translates Flop parameters into a Refine faceted search: Flop ➚ provides filtering, ordering, and pagination, while Refine provides the facets. This is useful when you already use Flop for searching and want to add faceting to existing queries.

See Flop adapter.

Examples

Refine.search(config,
  facets: %{"draft" => ["true"]},
  order_by: [{"updated_at", :desc}],
  repo: Repo
)

The response includes the matching data rows, the available facets and facet options (labels, values and counts) for display, plus facet type information.

Refine.search(config, options)
{:ok,
  %{
    rows: [...],
    facets: %{...},
    types: %{...},
    total_count: 999
  }
}

See Return value ↓ for an example of the shape of this data.

Parameters

  • config is the configuration map used to create the facets table. Here it is used to read references to the source and facet tables.
  • options may include a base query, selected facet values, and fields to return in the results.

query

Provide a base query, for example to list products from a specific category or apply a text search.

Examples

With where filter and select fields:

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

Refine.search(config,
  query: query
)

With select fields from joined tables:

query =
  from article in Article,
    left_join: author_article in AuthorArticle,
    on: article.id == author_article.article_id,
    left_join: author in Author,
    on: author.id == author_article.author_id,
    left_join: role in Role,
    on: role.id == author.role_id,
    select: %{
      title: article.title,
      summary: article.summary,
      author: author.full_name,
      role: role.name
    }

Refine.search(config,
  query: query
)

You may provide pagination attributes in the query attributes:

query = from a in Article,
  limit: 10,
  offset: 10

Refine.search(config,
  query: query
)

For more query options, see: Ecto.Query ➚.

facets

The facets option is a map of the currently selected values for each facet. The map keys are facet names, as defined in the configuration; the values are the selected option values as stored in the facets table.

Facet option values are always strings.

These selections narrow the search: only rows matching the selected values are returned. Facet data itself comes from the facets table, which holds precomputed counts for every facet value.

For each facet, the search reports which values are still available given the current selections - so a user can see what further filtering is possible and how many results each choice would yield. As selections narrow the results, the available facet values narrow with them.

Examples

Refine.search(config,
  facets: %{
    "draft" => ["true"],
    "color" => ["blue", "red"]
  }
)

If a facet has multiple selected values (here: blue and red), the filter for that facet is applied using an OR operator. In this case, the logic becomes: draft=true AND (color=blue OR color=red).

order_by

Sorts the search results.

order_by takes a list of tuples, each in one of two forms:

  • {column, direction}
  • {column, direction, null_sort}

Where:

  • Value column is the column name (a string).
  • Value direction is :asc or :desc.
  • Value null_sort defines how null values are placed: :first, :last or :default. With :default (or the two-element form), the database default applies - nulls last for :asc, nulls first for :desc.

You can sort by:

  • Any column of the source table, whether or not it is included in the query's select
  • Any field included in the query's select, including joined fields (referenced by their select key)

Sorting by a column that is neither a source-table column nor a selected field raises an error.

Results are always ordered by the identity column as a final tiebreaker, so the ordering is stable even when the sorted columns contain duplicate values.

Examples

Sort by a single source column, descending:

Refine.search(config,
  order_by: [{"updated_at", :desc}],
  repo: Repo
)

Sort by multiple source columns:

order_by: [{"updated_at", :desc}, {"title", :asc}]

Sort by a source column that isn't selected:

query = from a in Article, select: %{id: a.id, title: a.title}

Refine.search(config,
  query: query,
  order_by: [{"updated_at", :desc}]
  repo: Repo,
)

Sort by a joined and selected field, referenced by its select key:

query =
  from a in Article,
    join: c in assoc(a, :category),
    select: %{id: a.id, category_name: c.name}

Refine.search(config,
  query: query,
  order_by: [{"category_name", :asc}],
  repo: Repo
)

Sort with null values first:

order_by: [{"title", :asc, :first}]
order_by: [{"title", :desc, :first}]

Sort with null values last:

order_by: [{"title", :asc, :last}]
order_by: [{"title", :desc, :last}]

Default null placement depends on direction:

order_by: [{"title", :asc}]   # nulls last (Postgres default for :asc)
order_by: [{"title", :desc}]  # nulls first (Postgres default for :desc)

limit and offset

Paginate the search results using limit and offset.

If pagination is used, both limit and offset must be specified.

If neither option is specified, the following defaults are used:

  • limit: 10
  • offset: 0

Examples

Refine.search(config,
  query: query,
  limit: 5,
  offset: 2
)

result_fields

The result_fields option specifies which fields to include in the result set. It provides a convenient alternative to defining a custom query. Use it to limit the returned columns or to include fields that are not defined in the source Ecto schema.

result_fields takes precedence over any select expression in query.

Joined tables

result_fields only works with fields from the source table. When selecting fields from joined tables, use Ecto.Query.select/3 on the query instead.

Examples

Refine.search(config,
  result_fields: ["identity", "title"]
)

repo and timeout

  • repo - The Ecto Repo module that contains a Postgres adapter. Omit when the repo is configured globally.
  • timeout - Postgrex timeout option.

Return value

The returned map is quite similar to an Elasticsearch facets query result ➚:

{:ok,
  %{
    total_count: 999,
    rows: [
      %{
        "draft" => "true",
        "identity" => 1,
        "title" => "Géographie des marges",
        ...
      },
      ...
    ],
    facets: %{
      "draft" => %{
        "buckets" => [
          %{
            "count" => 6,
            "label" => "false",
            "selected" => false,
            "value" => "false"
          },
          %{
            "count" => 4,
            "label" => "true",
            "selected" => false,
            "value" => "true"
          }
        ],
        "label" => "draft"
      },
      ...
    },
    types: %{
      "draft" => "boolean",
      "identity" => "integer",
      "title" => "text",
      ...
    }
  }
}