Sifter (Sifter v0.2.0)

View Source

Sifter is a query filtering library for Elixir that converts search syntax from frontend applications into Ecto queries with full-text search support.

Overview

Sifter enables frontend JavaScript clients to build flexible queries that can be sent as simple strings to your backend API. It automatically handles field validation, type casting, association joins, and PostgreSQL full-text search while providing security through field allow-lists.

Basic Usage

# Simple field filtering
{query, meta} = Sifter.filter!(Post, "status:published priority>3",
  schema: Post,
  allowed_fields: ["status", "priority"]
)

# With full-text search
{query, meta} = Sifter.filter!(Post, "machine learning status:published",
  schema: Post,
  allowed_fields: ["status"],
  search_fields: ["title", "content"],        # Full-text search fields
  search_strategy: {:tsquery, "english"}      # Full-text search strategy
)

posts = Repo.all(query)

Query Syntax

Sifter supports a rich query syntax:

  • Field predicates: status:published, priority>5, createdAt<='2024-01-01'
  • Boolean logic: status:draft OR status:review, published AND priority>3
  • Lists: status IN (draft, published), tag NOT IN (spam, test), labels ALL (urgent, backend)
  • Wildcards: title:data*, email:*@example.com
  • Full-text search: Any unqualified terms search configured text fields

Configuration

The main configuration options:

  • :schema - The Ecto schema module (required if not inferrable from query)
  • :allowed_fields - Field allow-list with optional aliases
  • :search_fields - Fields to full-text search for unqualified terms
  • :search_strategy - How to perform full-text search (:ilike, {:tsquery, "config"}, etc.)
  • :unknown_field - How to handle unknown fields (:ignore, :warn, :error)

Result Metadata

Every query returns metadata about the filtering operation:

meta = %{
  uses_full_text?: true,           # Whether full-text search was used
  added_select_fields: [:search_rank],  # Fields added to SELECT
  recommended_order: [search_rank: :desc],  # Suggested ordering
  warnings: []                      # Any warnings generated
}

Summary

Types

meta()

@type meta() :: %{
  uses_full_text?: boolean(),
  added_select_fields: [atom()],
  recommended_order: [{atom(), :asc | :desc}] | nil
}

opts()

@type opts() :: [
  schema: module(),
  allowed_fields: [String.t() | %{as: String.t(), field: String.t()}],
  search_fields: :column | [String.t()],
  search_strategy: :ilike | {:tsquery, String.t()}
]

Functions

filter(queryable, query, opts \\ [])

@spec filter(Ecto.Queryable.t(), String.t(), opts()) ::
  {:ok, Ecto.Query.t(), meta()} | {:error, Sifter.Error.t()}

filter!(queryable, query, opts \\ [])

@spec filter!(Ecto.Queryable.t(), String.t(), opts()) :: {Ecto.Query.t(), meta()}

to_sql(queryable, query, adapter, opts \\ [])

@spec to_sql(Ecto.Queryable.t(), String.t(), module(), opts()) ::
  {:ok, String.t(), [term()], meta()} | {:error, Sifter.Error.t()}

to_sql!(queryable, query, adapter, opts \\ [])

@spec to_sql!(Ecto.Queryable.t(), String.t(), module(), opts()) ::
  {String.t(), [term()], meta()}