Skip to content

Commit

Permalink
Use full-text search implementation for web search (#606)
Browse files Browse the repository at this point in the history
* Use full-text search implementation for web search

Closes #356

* Fix full-text search without preloads

* Add a search results header
  • Loading branch information
doughsay authored Jul 15, 2023
1 parent f493833 commit 1fa7404
Show file tree
Hide file tree
Showing 15 changed files with 133 additions and 344 deletions.
16 changes: 0 additions & 16 deletions lib/ambry/authors.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,11 @@ defmodule Ambry.Authors do
Functions for dealing with Authors.
"""

import Ambry.SearchUtils
import Ecto.Query

alias Ambry.Authors.Author
alias Ambry.Repo

@doc """
Finds authors that match a query string.
Returns a list of tuples of the form `{jaro_distance, author}`.
"""
def search(query_string, limit \\ 15) do
name_query = "%#{query_string}%"
query = from a in Author, where: ilike(a.name, ^name_query), limit: ^limit

query
|> preload(:person)
|> Repo.all()
|> sort_by_jaro(query_string, :name)
end

@doc """
Returns all authors for use in `Select` components.
"""
Expand Down
21 changes: 4 additions & 17 deletions lib/ambry/books.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ defmodule Ambry.Books do
Functions for dealing with Books.
"""

import Ambry.{FileUtils, SearchUtils, Utils}
import Ambry.{FileUtils, Utils}
import Ecto.Query

alias Ambry.Books.{Book, BookFlat}
alias Ambry.Media.Media
alias Ambry.{PubSub, Repo}

@book_direct_assoc_preloads [book_authors: [:author], series_books: [:series]]
@book_direct_assoc_preloads [:authors, book_authors: [:author], series_books: [:series]]

def standard_preloads, do: @book_direct_assoc_preloads

@doc """
Returns a limited list of books and whether or not there are more.
Expand Down Expand Up @@ -185,21 +187,6 @@ defmodule Ambry.Books do
{books_to_return, books != books_to_return}
end

@doc """
Finds books that match a query string.
Returns a list of tuples of the form `{jaro_distance, book}`.
"""
def search(query_string, limit \\ 15) do
title_query = "%#{query_string}%"
query = from b in Book, where: ilike(b.title, ^title_query), limit: ^limit

query
|> preload([:authors, series_books: :series])
|> Repo.all()
|> sort_by_jaro(query_string, :title)
end

@doc """
Returns all books for use in `Select` components.
"""
Expand Down
16 changes: 0 additions & 16 deletions lib/ambry/narrators.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,11 @@ defmodule Ambry.Narrators do
Functions for dealing with Narrators.
"""

import Ambry.SearchUtils
import Ecto.Query

alias Ambry.Narrators.Narrator
alias Ambry.Repo

@doc """
Finds narrators that match a query string.
Returns a list of tuples of the form `{jaro_distance, narrator}`.
"""
def search(query_string, limit \\ 15) do
name_query = "%#{query_string}%"
query = from n in Narrator, where: ilike(n.name, ^name_query), limit: ^limit

query
|> preload(:person)
|> Repo.all()
|> sort_by_jaro(query_string, :name)
end

@doc """
Returns all narrators for use in `Select` components.
"""
Expand Down
2 changes: 2 additions & 0 deletions lib/ambry/people.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ defmodule Ambry.People do

@person_direct_assoc_preloads [:authors, :narrators]

def standard_preloads, do: @person_direct_assoc_preloads

@doc """
Returns a limited list of people and whether or not there are more.
Expand Down
69 changes: 25 additions & 44 deletions lib/ambry/search.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,24 @@ defmodule Ambry.Search do

import Ecto.Query

alias Ambry.{Authors, Books, Narrators, Series}
alias Ambry.{Books, People, Series}

alias Ambry.Books.Book
alias Ambry.People.Person
alias Ambry.Repo
alias Ambry.Search.Record
alias Ambry.Series.Series, as: SeriesSchema

# Old search implementation (used in web app)

def search(query) do
authors = Authors.search(query, 10)
books = Books.search(query, 10)
narrators = Narrators.search(query, 10)
series = Series.search(query, 10)

[
{:authors, authors},
{:books, books},
{:narrators, narrators},
{:series, series}
]
|> Enum.reject(&(elem(&1, 1) == []))
|> Enum.sort_by(
fn {_label, items} ->
items
|> Enum.map(&elem(&1, 0))
|> average()
end,
:desc
def search(query_string) do
query_string
|> query()
|> all(
books_preload: Books.standard_preloads(),
series_preload: Series.standard_preloads(),
people_preload: People.standard_preloads()
)
end

defp average(floats) do
Enum.sum(floats) / length(floats)
end

# New search implementation (used by graphql)

def query(query_string) do
like = "%#{query_string}%"

Expand Down Expand Up @@ -89,17 +68,17 @@ defmodule Ambry.Search do
]
end

def all(query) do
def all(query, opts \\ []) do
references =
query
|> Repo.all()
|> Enum.map(& &1.reference)

{book_ids, person_ids, series_ids} = partition_references(references)

books = fetch_books(book_ids)
people = fetch_people(person_ids)
series = fetch_series(series_ids)
books = fetch_books(book_ids, opts[:books_preload])
people = fetch_people(person_ids, opts[:people_preload])
series = fetch_series(series_ids, opts[:series_preload])

recombine(references, books, people, series)
end
Expand All @@ -117,21 +96,23 @@ defmodule Ambry.Search do
defp do_partition(%{type: :series, id: id}, {books, people, series}),
do: {books, people, [id | series]}

defp fetch_books(ids) do
query = from(b in Book, where: b.id in ^ids)
query |> Repo.all() |> Map.new(&{&1.id, &1})
end
defp fetch_books(ids, preload), do: fetch(from(b in Book, where: b.id in ^ids), preload)

defp fetch_people(ids) do
query = from(p in Person, where: p.id in ^ids)
query |> Repo.all() |> Map.new(&{&1.id, &1})
end
defp fetch_people(ids, preload), do: fetch(from(p in Person, where: p.id in ^ids), preload)

defp fetch_series(ids, preload),
do: fetch(from(s in SeriesSchema, where: s.id in ^ids), preload)

defp fetch_series(ids) do
query = from(s in SeriesSchema, where: s.id in ^ids)
query |> Repo.all() |> Map.new(&{&1.id, &1})
defp fetch(query, preload) do
query
|> maybe_add_preload(preload)
|> Repo.all()
|> Map.new(&{&1.id, &1})
end

defp maybe_add_preload(query, nil), do: query
defp maybe_add_preload(query, preload), do: from(q in query, preload: ^preload)

defp recombine(references, books, people, series) do
Enum.map(references, fn reference ->
case reference do
Expand Down
32 changes: 8 additions & 24 deletions lib/ambry/series.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ defmodule Ambry.Series do
Functions for dealing with Series.
"""

import Ambry.{SearchUtils, Utils}
import Ambry.Utils
import Ecto.Query

alias Ambry.{PubSub, Repo}
alias Ambry.Series.{Series, SeriesBook, SeriesFlat}
alias Ambry.Series.{Series, SeriesFlat}

@series_direct_assoc_preloads [series_books: [book: [:authors]]]

def standard_preloads, do: @series_direct_assoc_preloads

@doc """
Returns a limited list of series and whether or not there are more.
Expand Down Expand Up @@ -62,10 +66,8 @@ defmodule Ambry.Series do
** (Ecto.NoResultsError)
"""
def get_series!(id) do
series_book_query = from sb in SeriesBook, order_by: [asc: sb.book_number]

Series
|> preload(series_books: ^{series_book_query, [:book]})
|> preload(^@series_direct_assoc_preloads)
|> Repo.get!(id)
end

Expand Down Expand Up @@ -140,29 +142,11 @@ defmodule Ambry.Series do
Books are listed in ascending order based on series book number.
"""
def get_series_with_books!(series_id) do
series_book_query = from sb in SeriesBook, order_by: [asc: sb.book_number]

Series
|> preload(series_books: ^{series_book_query, [book: [:authors, series_books: :series]]})
|> preload(series_books: [book: [:authors, series_books: :series]])
|> Repo.get!(series_id)
end

@doc """
Finds series that match a query string.
Returns a list of tuples of the form `{jaro_distance, series}`.
"""
def search(query_string, limit \\ 15) do
name_query = "%#{query_string}%"
query = from s in Series, where: ilike(s.name, ^name_query), limit: ^limit
series_book_query = from sb in SeriesBook, order_by: [asc: sb.book_number]

query
|> preload(series_books: ^{series_book_query, [book: [:authors, series_books: :series]]})
|> Repo.all()
|> sort_by_jaro(query_string, :name)
end

@doc """
Returns all series for use in `Select` components.
"""
Expand Down
2 changes: 1 addition & 1 deletion lib/ambry/series/series.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defmodule Ambry.Series.Series do

schema "series" do
many_to_many :books, Book, join_through: "books_series"
has_many :series_books, SeriesBook
has_many :series_books, SeriesBook, preload_order: [asc: :book_number]
has_many :authors, through: [:books, :authors]

field :name, :string
Expand Down
63 changes: 36 additions & 27 deletions lib/ambry_web/components/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -796,33 +796,7 @@ defmodule AmbryWeb.CoreComponents do
~H"""
<.grid>
<%= for {book, number} <- books_with_numbers(@books) do %>
<div class="text-center">
<%= if number do %>
<p class="font-bold text-zinc-900 dark:text-zinc-100 sm:text-lg">Book <%= number %></p>
<% end %>
<div class="group">
<.link navigate={~p"/books/#{book}"}>
<span class="block aspect-1">
<img
src={book.image_path}
class="h-full w-full rounded-lg border border-zinc-200 object-cover object-center shadow-md dark:border-zinc-900"
/>
</span>
</.link>
<p class="font-bold text-zinc-900 group-hover:underline dark:text-zinc-100 sm:text-lg">
<.link navigate={~p"/books/#{book}"}>
<%= book.title %>
</.link>
</p>
</div>
<p class="text-sm text-zinc-800 dark:text-zinc-200 sm:text-base">
by <.people_links people={book.authors} />
</p>
<div class="text-xs text-zinc-600 dark:text-zinc-400 sm:text-sm">
<.series_book_links series_books={book.series_books} />
</div>
</div>
<.book_tile book={book} number={number} />
<% end %>
<%= if @show_load_more do %>
Expand Down Expand Up @@ -853,6 +827,41 @@ defmodule AmbryWeb.CoreComponents do
"""
end

attr :book, Book, required: true
attr :number, Decimal, default: nil

def book_tile(assigns) do
~H"""
<div class="text-center">
<%= if @number do %>
<p class="font-bold text-zinc-900 dark:text-zinc-100 sm:text-lg">Book <%= @number %></p>
<% end %>
<div class="group">
<.link navigate={~p"/books/#{@book}"}>
<span class="block aspect-1">
<img
src={@book.image_path}
class="h-full w-full rounded-lg border border-zinc-200 object-cover object-center shadow-md dark:border-zinc-900"
/>
</span>
</.link>
<p class="font-bold text-zinc-900 group-hover:underline dark:text-zinc-100 sm:text-lg">
<.link navigate={~p"/books/#{@book}"}>
<%= @book.title %>
</.link>
</p>
</div>
<p class="text-sm text-zinc-800 dark:text-zinc-200 sm:text-base">
by <.people_links people={@book.authors} />
</p>
<div class="text-xs text-zinc-600 dark:text-zinc-400 sm:text-sm">
<.series_book_links series_books={@book.series_books} />
</div>
</div>
"""
end

defp books_with_numbers(books_assign) do
case books_assign do
[] -> []
Expand Down
10 changes: 9 additions & 1 deletion lib/ambry_web/live/search_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@ defmodule AmbryWeb.SearchLive do
def render(assigns) do
~H"""
<div class="mx-auto max-w-md space-y-16 p-4 sm:max-w-none sm:space-y-24 sm:p-10 md:max-w-screen-2xl md:p-12 lg:space-y-32 lg:p-16">
<.results :for={{type, items} <- @results} type={type} items={Enum.map(items, &elem(&1, 1))} />
<section>
<.section_header>
Results for "<%= @query %>"
</.section_header>
<.grid>
<.result_tile :for={result <- @results} result={result} />
</.grid>
</section>
</div>
"""
end
Expand Down
Loading

0 comments on commit 1fa7404

Please sign in to comment.