defp update_search_vector(changeset) do case changeset do %Ecto.Changesetvalid?: true -> Ecto.Changeset.put_change(changeset, :search_vector, fragment( "to_tsvector('english', coalesce(?, '') || ' ' || coalesce(?, ''))", changeset.data.title, changeset.data.content )) _ -> changeset end end end # priv/repo/migrations/20240101000000_add_full_text_search.exs defmodule MyApp.Repo.Migrations.AddFullTextSearch do use Ecto.Migration def change do # Add GIN index for full-text search execute(""" CREATE INDEX posts_search_idx ON posts USING GIN(to_tsvector('english', title || ' ' || content)) """)
def autocomplete(conn, %"term" => term) do suggestions = Blog.search_posts(term, []) |> Enum.take(5) |> Enum.map(&%title: &1.title, id: &1.id)
from q in queryable, where: fragment( """ to_tsvector('english', ?) @@ to_tsquery('english', ?) """, fragment("concat_ws(' ', ?)", ^fields), ^search_term ) end end # lib/my_app/ecto/full_text_search.ex defmodule MyApp.Ecto.FullTextSearch do @moduledoc """ Ecto plugin for full-text search functionality """ defmacro using (opts) do quote bind_quoted: [opts: opts] do @search_language opts[:language] || "english" @search_fields opts[:fields] || []
defp apply_filters(query, []), do: query defp apply_filters(query, filters) do Enum.reduce(filters, query, fn :category, category, q -> from p in q, where: p.category == ^category :published_after, date, q -> from p in q, where: p.inserted_at >= ^date end) end
base_query |> apply_filters(params) |> apply_full_text_search(params) |> rank_by_relevance(params[:search_term]) end