Skip to content

Commit

Permalink
Further improve matching by sorting using actual title in book detail…
Browse files Browse the repository at this point in the history
…s instead of title in book overview
  • Loading branch information
ahobsonsayers committed Dec 26, 2024
1 parent 85b65fb commit 6a0340d
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 55 deletions.
24 changes: 0 additions & 24 deletions goodreads/goodreads.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,27 +173,3 @@ func (c *Client) GetBookByTitle(ctx context.Context, bookTitle string, bookAutho

return result.Work, nil
}

// SearchBooks search for a book by its title and optionally an author (which can give better results)
// https://www.goodreads.com/api/index#search.books
func (c *Client) SearchBooks(ctx context.Context, bookTitle string, bookAuthor *string) ([]Book, error) {
var bookOverviews []BookOverview
var err error
if bookAuthor == nil || *bookAuthor == "" {
// If author is not set, search for books by title
bookOverviews, err = c.searchBooksByTitle(ctx, bookTitle)
} else {
bookOverviews, err = c.searchBooksByTitleAndAuthor(ctx, bookTitle, *bookAuthor)
}
if err != nil {
return nil, err
}

// Get book details using their ids
bookDetails, err := c.GetBooksByIds(ctx, BookIds(bookOverviews))
if err != nil {
return nil, err
}

return bookDetails, nil
}
30 changes: 15 additions & 15 deletions goodreads/goodreads_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,46 @@ import (
)

const (
TheHobbitBookId = "5907"
TheHobbitBookTitle = "The Hobbit"
TheHobbitBookAuthor = "J.R.R. Tolkien"
GameOfThronesBookId = "13496"
GameOfThronesBookTitle = "A Game of Thrones"
GameOfThronesBookAuthor = "George R.R. Martin"
)

func TestGetBookById(t *testing.T) {
book, err := goodreads.DefaultClient.GetBookById(context.Background(), TheHobbitBookId)
book, err := goodreads.DefaultClient.GetBookById(context.Background(), GameOfThronesBookId)
require.NoError(t, err)
checkTheHobbitBookDetails(t, book)
}

func TestGetBookByTitle(t *testing.T) {
book, err := goodreads.DefaultClient.GetBookByTitle(context.Background(), TheHobbitBookTitle, nil)
book, err := goodreads.DefaultClient.GetBookByTitle(context.Background(), GameOfThronesBookTitle, nil)
require.NoError(t, err)
checkTheHobbitBookDetails(t, book)
}

func TestSearchTitle(t *testing.T) {
books, err := goodreads.DefaultClient.SearchBooks(context.Background(), TheHobbitBookTitle, nil)
books, err := goodreads.DefaultClient.SearchBooks(context.Background(), GameOfThronesBookTitle, nil)
require.NoError(t, err)
checkTheHobbitBookDetails(t, books[0])
}

func TestSearchTitleAndAuthor(t *testing.T) {
books, err := goodreads.DefaultClient.SearchBooks(
context.Background(),
TheHobbitBookTitle,
lo.ToPtr(TheHobbitBookAuthor),
GameOfThronesBookTitle,
lo.ToPtr(GameOfThronesBookAuthor),
)
require.NoError(t, err)
checkTheHobbitBookDetails(t, books[0])
}

func checkTheHobbitBookDetails(t *testing.T, book goodreads.Book) {
require.Equal(t, TheHobbitBookTitle, book.Work.Title)
require.Equal(t, TheHobbitBookId, book.BestEdition.Id)
require.Regexp(t, "1546071216l/5907.jpg$", book.BestEdition.ImageURL)
require.Equal(t, GameOfThronesBookTitle, book.Work.Title())
require.Equal(t, GameOfThronesBookId, book.BestEdition.Id)
require.Regexp(t, "1562726234l/13496.jpg$", book.BestEdition.ImageURL)
require.Equal(t, "English", book.BestEdition.Language)
require.Equal(t, TheHobbitBookAuthor, book.Authors[0].Name)
require.Equal(t, "Middle Earth", book.Series[0].Series.Title)
require.Equal(t, "0", *book.Series[0].BookPosition)
require.EqualValues(t, []string{"Fantasy", "Fiction", "Classic"}, book.Genres)
require.Equal(t, GameOfThronesBookAuthor, book.Authors[0].Name)
require.Equal(t, "A Song of Ice and Fire", book.Series[0].Series.Title)
require.Equal(t, "1", *book.Series[0].BookPosition)
require.EqualValues(t, []string{"Fantasy", "Fiction", "High Fantasy"}, book.Genres)
}
40 changes: 30 additions & 10 deletions goodreads/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,42 @@ import (
"github.com/samber/lo"
)

func (c *Client) searchBooksByTitle(ctx context.Context, bookTitle string) ([]BookOverview, error) {
return c.searchBooksManyPages(ctx, searchBooksManyPagesInput{
Query: bookTitle,
// SearchBooks search for a book by its title and optionally an author (which can give better results)
// https://www.goodreads.com/api/index#search.books
func (c *Client) SearchBooks(ctx context.Context, title string, author *string) ([]Book, error) {
if author == nil || *author == "" {
// If author is not set, search for books by title
return c.searchBooksByTitle(ctx, title)
}

return c.searchBooksByTitleAndAuthor(ctx, title, *author)
}

func (c *Client) searchBooksByTitle(ctx context.Context, title string) ([]Book, error) {
bookOverviews, err := c.searchBooksManyPages(ctx, searchBooksManyPagesInput{
Query: title,
SearchType: BookSearchTypeTitle,
Page: 1,
NumPages: 5,
})
if err != nil {
return nil, err
}

return c.GetBooksByIds(ctx, BookIds(bookOverviews))
}

func (c *Client) searchBooksByTitleAndAuthor(
ctx context.Context,
bookTitle string,
bookAuthor string,
) ([]BookOverview, error) {
title string,
author string,
) ([]Book, error) {
// If searching by title and author, search by author ONLY first to get their books.
// We will then search the books for title.
// We do NOT search goodreads by title AND author together using the 'all' search type
// as goodreads returns awful results, including sometimes none at all.
bookOverviews, err := c.searchBooksManyPages(ctx, searchBooksManyPagesInput{
Query: bookAuthor,
Query: author,
SearchType: BookSearchTypeAuthor,
Page: 1,
NumPages: 15,
Expand All @@ -38,10 +54,14 @@ func (c *Client) searchBooksByTitleAndAuthor(
return nil, err
}

// In author books, sort books by similarity to title
sortBookOverviewsByTitleSimilarity(bookOverviews, bookTitle)
books, err := c.GetBooksByIds(ctx, BookIds(bookOverviews))
if err != nil {
return nil, err
}

sortBookByTitleSimilarity(books, title)

return bookOverviews, nil
return books, nil
}

type searchBooksManyPagesInput struct {
Expand Down
12 changes: 6 additions & 6 deletions goodreads/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ import (
"golang.org/x/exp/slices"
)

func sortBookOverviewsByTitleSimilarity(bookOverviews []BookOverview, title string) {
func sortBookByTitleSimilarity(books []Book, title string) {
normalisedDesiredTitle := normaliseBookTitle(title)

similarities := make(map[string]float64)
for _, book := range bookOverviews {
similarities[book.Title] = strutil.Similarity(
normaliseBookTitle(book.Title),
for _, book := range books {
similarities[book.Work.FullTitle] = strutil.Similarity(
normaliseBookTitle(book.Work.FullTitle),
normalisedDesiredTitle,
metrics.NewJaroWinkler(),
)
}

slices.SortStableFunc(bookOverviews, func(i, j BookOverview) bool {
return similarities[i.Title] > similarities[j.Title]
slices.SortStableFunc(books, func(i, j Book) bool {
return similarities[i.Work.FullTitle] > similarities[j.Work.FullTitle]
})
}

Expand Down

0 comments on commit 6a0340d

Please sign in to comment.