vadimshvetsov
How to properly sanitize multiple ts_vector fields with reusable macro?
I’ve crawled couple related topics but still can’t implement reusable module for full-text search. I’m stuck with this module and can’t understand why it can’t find any row even when it has to. Looks like there is interpolation problem with creating multiple ts_vector but I’m not sure.
My goals:
- to abstract later search for other entities and have this api API
- to speed up this search with creating indexes while it has some joins (is it possible?) And is it normal to search within related tables?
- Consider to add tsv field with triggering updates and creating indexes and having to_tsvector value in db.
defmodule MyApp.Performer.Search do
import Ecto.Query
@fields ~w(bio first_name last_name phone email city)
@nullable_fields ~w(name)
defmacro tsquery(fields, terms, language \\ "english") do
quote do
fragment(
"to_tsvector(?, ?) @@ to_tsquery(?, ?)",
unquote(language),
unquote(fields),
unquote(language),
unquote(terms)
)
end
end
defmacro tsrankcd(fields, terms, language \\ "english") do
quote do
fragment(
"ts_rank_cd(to_tsvector(?, ?), to_tsquery(?, ?))",
unquote(language),
unquote(fields),
unquote(language),
unquote(terms)
)
end
end
def run(query, search_term) do
search_term = normalize(search_term)
fields = fields_to_tsvector_fields(@fields, @nullable_fields)
queryable =
from q in query,
join: p in assoc(q, :profile),
left_join: t in assoc(q, :topics),
where: tsquery(^fields, ^search_term),
order_by: [
desc: tsrankcd(^fields, ^search_term)
],
distinct: [q.id]
end
defp normalize(terms, operator \\ "&") do
terms
|> String.downcase()
|> String.trim()
|> String.replace(~r/\s+/u, " #{operator} ")
|> (&(&1 <> ":*")).()
end
def fields_to_tsvector_fields(non_nullable_fields, nullable_fields) do
nullable_fields
|> Enum.map(fn field -> "coalesce(#{field}, ' ')" end)
|> (&(non_nullable_fields ++ &1)).()
|> Enum.join(" || ' ' || ")
end
end
Would highly appreciate any advice or suggestion, thanks.
Marked As Solved
thiagomajesk
Hi @vadimshvetsov! At work, we have implemented a small lib that helps us abstract what we need for full-text search. Take a look at our repo and see if it helps: GitHub - thiagomajesk/searchy: Full-text search capabilities for Ecto · GitHub.
Also Liked
vadimshvetsov
Thanks a lot for helping me.
I’ve starred GitHub - thiagomajesk/searchy: Full-text search capabilities for Ecto · GitHub and will keep an eye on it. The reason why I’ve done this on my own is because I need to rank results and search in related tables.
So I’ve ended up with this one:
At first I’ve added pg_term extension for incomplete word search:
defmodule MyApp.Repo.Migrations.AddPgTrgmExtension do
@moduledoc """
Create postgres pg_trgm extension and indices
"""
use Ecto.Migration
def up do
execute("CREATE EXTENSION pg_trgm")
end
def down do
execute("DROP EXTENSION pg_trgm")
end
end
Then migrated searchable tables and add tsvector field, trigger and index:
defmodule MyApp.Repo.Migrations.AddPerformersSearch do
use Ecto.Migration
def up do
alter table("performers") do
add :tsvector, :tsvector
end
create index(:performers, [:tsvector], using: "GIN")
execute("""
CREATE OR REPLACE FUNCTION performers_tsvector_trigger()
RETURNS trigger AS $$
begin
new.tsvector := setweight(to_tsvector('russian', coalesce(new.bio, '')), 'A');
return new;
end
$$ LANGUAGE plpgsql;
""")
execute("""
CREATE TRIGGER performers_tsvector_update
BEFORE INSERT OR UPDATE ON performers
FOR EACH ROW EXECUTE PROCEDURE performers_tsvector_trigger();
""")
alter table("users") do
add :tsvector, :tsvector
end
create index(:users, [:tsvector], using: "GIN")
execute("""
CREATE OR REPLACE FUNCTION users_tsvector_trigger()
RETURNS trigger AS $$
begin
new.tsvector := setweight(to_tsvector('russian', coalesce(new.last_name, '')), 'A') ||
setweight(to_tsvector('russian', coalesce(new.first_name, '')), 'B') ||
setweight(to_tsvector('russian', coalesce(new.email, '')), 'C') ||
setweight(to_tsvector('russian', coalesce(new.phone, '')), 'C') ||
setweight(to_tsvector('russian', coalesce(new.city, '')), 'D');
return new;
end
$$ LANGUAGE plpgsql;
""")
execute("""
CREATE TRIGGER users_tsvector_update
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW EXECUTE PROCEDURE users_tsvector_trigger();
""")
alter table("topics") do
add :tsvector, :tsvector
end
create index(:topics, [:tsvector], using: "GIN")
execute("""
CREATE OR REPLACE FUNCTION topics_tsvector_trigger()
RETURNS trigger AS $$
begin
new.tsvector := setweight(to_tsvector('russian', coalesce(new.name, '')), 'A');
return new;
end
$$ LANGUAGE plpgsql;
""")
execute("""
CREATE TRIGGER topics_tsvector_update
BEFORE INSERT OR UPDATE ON topics
FOR EACH ROW EXECUTE PROCEDURE topics_tsvector_trigger();
""")
end
def down do
execute("DROP TRIGGER performers_tsvector_update on performers;")
execute("DROP FUNCTION performers_tsvector_trigger();")
alter table("performers") do
remove :tsvector
end
drop index("performers", [:tsvector])
execute("DROP TRIGGER users_tsvector_update on performers;")
execute("DROP FUNCTION users_tsvector_trigger();")
alter table("users") do
remove :tsvector
end
drop index("users", [:tsvector])
execute("DROP TRIGGER topics_tsvector_update on performers;")
execute("DROP FUNCTION topics_tsvector_trigger();")
alter table("topics") do
remove :tsvector
end
drop index("topics", [:tsvector])
end
end
Added tsvector type taken from GitHub - thiagomajesk/searchy: Full-text search capabilities for Ecto · GitHub source:
defmodule MyApp.Ecto.Types.TSVector do
use Ecto.Type
def type, do: :tsvector
def cast(tsvector), do: {:ok, tsvector}
def load(tsvector), do: {:ok, tsvector}
def dump(tsvector), do: {:ok, tsvector}
def embed_as(_), do: :self
def equal?(term1, term2), do: term1 == term2
end
Added tsvector field to all searchable ecto schemas:
field :tsvector, Proling.Ecto.Types.TSVector
Added Search.Helpers module for convience:
defmodule MyApp.Search.Helpers do
defmacro tsquery(tsvector, terms, language \\ "russian") do
quote do
fragment(
"? @@ to_tsquery(?, ?)",
unquote(tsvector),
unquote(language),
unquote(terms)
)
end
end
defmacro tsrankcd(tsvector, terms, language \\ "russian") do
quote do
fragment(
"ts_rank_cd(?, to_tsquery(?, ?))",
unquote(tsvector),
unquote(language),
unquote(terms)
)
end
end
end
And finally added API for using in context:
defmodule MyApp.Production.Performer.Search do
import Ecto.Query
import MyApp.Search.Helpers
@spec search(Ecto.Query.t(), any()) :: Ecto.Query.t()
def search(query, search_term) do
search_term = normalize(search_term)
from q in query,
join: p in assoc(q, :profile),
left_join: t in assoc(q, :topics),
where:
tsquery(p.tsvector, ^search_term) or tsquery(q.tsvector, ^search_term) or
tsquery(t.tsvector, ^search_term),
order_by: [
desc: tsrankcd(p.tsvector, ^search_term),
desc: tsrankcd(q.tsvector, ^search_term),
desc: tsrankcd(t.tsvector, ^search_term)
],
distinct: [q.id]
end
defp normalize(terms, operator \\ "&") do
terms
|> String.downcase()
|> String.trim()
|> String.replace(~r/\(|\)\[|\]\{|\}/u, "")
|> String.replace(~r/\s+/u, " #{operator} ")
|> (&(&1 <> ":*")).()
end
end
I’m gonna star @thiagomajesk answer with searchy because it`s source code greatly led me to the final destination. Also I would like to mention this awesome answer - Search Bar Feature (Phoenix Searching) - #4 by idi527
Hope this step-by-step example could help someone to get things done.
benwilson512
Ecto does not interpolate any values at all, ever. Rather it uses SQL parameters to avoid SQL injection. If you turn on debug logging you should be able to see the query that Ecto runs. Try running that in PSQL and tweaking the values until you get what you want, then adjust your ecto query accordingly.
thiagomajesk
I don’t know if I understood exactly what you need but as long as you have a ts_vector field generated for the table you want to search, I guess you are good to go. Our use-case is very simplistic and the lib itself is just a thin wrapper that translates the queries according to the postgres docs. If this is not exactly what you need or it’s incomplete, feel free to open a discussion on the repo so we can talk about it
.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








