Uni Ecto Plugin !!top!! May 2026
:ok end
Using PostgreSQL's Full-Text Search # lib/my_app/repo.ex defmodule MyApp.Repo do use Ecto.Repo, otp_app: :my_app Custom full-text search function def full_text_search(queryable, search_term, fields) do search_term = search_term |> String.trim() |> String.replace(~r/\s+/, " & ") # Convert spaces to & for AND operator uni ecto plugin
import Ecto.Query def search(queryable, search_term, fields \\ @search_fields) do search_term = format_search_term(search_term) from q in queryable, where: full_text_match(q, ^search_term, ^fields) end defp full_text_match(query, search_term, fields) do dynamic = Enum.map(fields, fn field -> dynamic([q], fragment("to_tsvector(?, ?)", unquote(@search_language), field(q, ^field))) end) combined_vector = Enum.reduce(dynamic, fn d, acc -> dynamic([q], fragment("? || ?", ^acc, ^d)) end) dynamic([q], fragment("? @@ to_tsquery(?, ?)", ^combined_vector, unquote(@search_language), ^search_term)) end defp format_search_term(term) do term |> String.trim() |> String.replace(~r/\s+/, " & ") |> sanitize_search_term() end defp sanitize_search_term(term) do term |> String.replace(~r/[^\w\s&|!()]/, " ") |> String.replace(~r/\s+/, " ") |> String.trim() end end end end # lib/my_app/blog/post.ex defmodule MyApp.Blog.Post do use Ecto.Schema use MyApp.Ecto.FullTextSearch, language: "english", fields: [:title, :content, :tags] schema "posts" do field :title, :string field :content, :string field :tags, :array, :string field :search_vector, :tsvector # Optional: precomputed vector Testing the Search # test/my_app/search_test
:noreply, assign(socket, search_term: query, results: results, searching: query != "" ) end fields) do search_term = search_term |>
defp rank_by_relevance(query, nil), do: query defp rank_by_relevance(query, term) when term == "", do: query defp rank_by_relevance(query, term) do from q in query, select_merge: % coalesce(?, '')", q.title, q.content), ^term ) , order_by: [desc: fragment("relevance")] end end # lib/my_app/blog/blog.ex defmodule MyApp.Blog do import Ecto.Query alias MyApp.Repo alias MyApp.Blog.Post def search_posts(search_term, filters \ []) do Post |> search(search_term) # From plugin |> apply_filters(filters) |> order_by_relevance(search_term) |> Repo.all() end
defp order_by_relevance(query, nil), do: query defp order_by_relevance(query, term) do from q in query, order_by: [desc: fragment( "ts_rank(to_tsvector('english', ?), plainto_tsquery('english', ?))", fragment("coalesce(?, '') || ' ' || coalesce(?, '')", q.title, q.content), ^term )] end end # lib/my_app_web/controllers/search_controller.ex defmodule MyAppWeb.SearchController do use MyAppWeb, :controller alias MyApp.Blog
<%= if @searching do %> <div class="mt-4"> <h3>Found @results results</h3> <div class="space-y-2"> <%= for result <- @results do %> <div class="p-4 border rounded"> <h4 class="font-bold"><%= result.title %></h4> <p><%= result.content %></p> </div> <% end %> </div> </div> <% end %> </div> """ end end # config/config.exs config :my_app, :search, language: "english", min_word_length: 2, stop_words: ["the", "a", "an", "and", "or"], highlight: true, highlight_tag: "<mark>" 10. Testing the Search # test/my_app/search_test.exs defmodule MyApp.SearchTest do use MyApp.DataCase alias MyApp.Blog alias MyApp.Blog.Post