Skip to content

Commit

Permalink
Merge pull request #89 from bmf-san/feature/support-fulltext-search
Browse files Browse the repository at this point in the history
Implement search API
  • Loading branch information
bmf-san authored Apr 23, 2023
2 parents a32b038 + 24ddf40 commit 8fee00a
Show file tree
Hide file tree
Showing 9 changed files with 530 additions and 84 deletions.
1 change: 1 addition & 0 deletions app/infrastructure/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func Route(connm *sql.DB, connr *redis.Client, l domain.Logger) *goblin.Router {

r.UseGlobal(mw.CORS)
r.Methods(http.MethodGet).Handler(`/posts`, postController.Index())
r.Methods(http.MethodGet).Handler(`/posts/search`, postController.IndexByKeyword())
r.Methods(http.MethodGet).Handler(`/posts/categories/:name`, postController.IndexByCategory())
r.Methods(http.MethodGet).Handler(`/posts/tags/:name`, postController.IndexByTag())
r.Methods(http.MethodGet).Handler(`/posts/:title`, postController.Show())
Expand Down
26 changes: 26 additions & 0 deletions app/interfaces/controller/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,32 @@ func (pc *PostController) Index() http.Handler {
})
}

// IndexByKeyword displays a listing of the resource.
func (pc *PostController) IndexByKeyword() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req request.IndexPostByKeyword
req.Keyword = r.URL.Query().Get("keyword")
req.Page, _ = strconv.Atoi(r.URL.Query().Get("page"))
req.Limit, _ = strconv.Atoi(r.URL.Query().Get("limit"))
ps, pn, herr := pc.PostInteractor.IndexByKeyword(req)
if herr != nil {
pc.Logger.Error(herr.Error())
JSONResponse(w, herr.Code, []byte(herr.Message))
}
ips := response.MakeResponseIndexPost(ps)
res, err := json.Marshal(ips)
if err != nil {
pc.Logger.Error(err.Error())
JSONResponse(w, http.StatusInternalServerError, []byte(err.Error()))
}
w.Header().Set("Pagination-Count", pn.Count)
w.Header().Set("Pagination-Pagecount", pn.PageCount)
w.Header().Set("Pagination-Page", pn.Page)
w.Header().Set("Pagination-Limit", pn.Limit)
JSONResponse(w, http.StatusOK, res)
})
}

// IndexByCategory displays a listing of the resource.
func (pc *PostController) IndexByCategory() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
260 changes: 260 additions & 0 deletions app/interfaces/repository/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,25 @@ func (pr *Post) CountAll() (int, error) {
return count, nil
}

// CountAllPublishByKeyword count all publish entities by keyword.
func (pr *Post) CountAllPublishByKeyword(keyword string) (int, error) {
row := pr.ConnMySQL.QueryRow(`
SELECT
count(*)
FROM
view_posts
WHERE MATCH (title, md_body)
AGAINST (? IN BOOLEAN MODE)
AND
status = "publish"
`, keyword)
var count int
if err := row.Scan(&count); err != nil {
return 0, err
}
return count, nil
}

// CountAllPublishByCategory count all publish entities by category.
func (pr *Post) CountAllPublishByCategory(name string) (int, error) {
row := pr.ConnMySQL.QueryRow(`
Expand All @@ -58,6 +77,8 @@ func (pr *Post) CountAllPublishByCategory(name string) (int, error) {
view_posts
WHERE
category_name = ?
AND
status = "publish"
`, name)
var count int
if err := row.Scan(&count); err != nil {
Expand Down Expand Up @@ -88,6 +109,8 @@ func (pr *Post) CountAllPublishByTag(name string) (int, error) {
WHERE
tags.name = ?
)
AND
status = "publish"
`, name)
var count int
if err := row.Scan(&count); err != nil {
Expand Down Expand Up @@ -332,6 +355,243 @@ func (pr *Post) FindAllPublish(page int, limit int) (domain.Posts, error) {
return posts, nil
}

// FindAllPublishByKeyword returns all entities by keyword.
func (pr *Post) FindAllPublishByKeyword(page int, limit int, keyword string) (domain.Posts, error) {
var posts domain.Posts
rows, err := pr.ConnMySQL.Query(`
SELECT
*
FROM
view_posts
WHERE MATCH (title, md_body)
AGAINST (? IN BOOLEAN MODE)
AND
status = "publish"
ORDER BY id
DESC
LIMIT ?, ?
`, keyword, page*limit-limit, limit)

defer func() {
if rerr := rows.Close(); rerr != nil {
err = rerr
}
}()

if err != nil {
return nil, err
}

for rows.Next() {
var (
postID int
adminID int
categoryID int
postTitle string
postMDBody string
postHTMLBody string
postStatus string
postCreatedAt time.Time
postUpdatedAt time.Time
adminName string
adminEmail string
adminPassword string
adminCreatedAt time.Time
adminUpdatedAt time.Time
categoryName string
categoryCreatedAt time.Time
categoryUpdatedAt time.Time
)
if err = rows.Scan(
&postID,
&adminID,
&categoryID,
&postTitle,
&postMDBody,
&postHTMLBody,
&postStatus,
&postCreatedAt,
&postUpdatedAt,
&adminName,
&adminEmail,
&adminPassword,
&adminCreatedAt,
&adminUpdatedAt,
&categoryName,
&categoryCreatedAt,
&categoryUpdatedAt,
); err != nil {
return nil, err
}
post := domain.Post{
ID: postID,
Admin: domain.Admin{
ID: adminID,
Name: adminName,
Email: adminEmail,
Password: adminPassword,
CreatedAt: adminCreatedAt,
UpdatedAt: adminUpdatedAt,
},
Category: domain.Category{
ID: categoryID,
Name: categoryName,
CreatedAt: categoryCreatedAt,
UpdatedAt: categoryUpdatedAt,
},
Title: postTitle,
MDBody: postMDBody,
HTMLBody: postHTMLBody,
Status: postStatus,
CreatedAt: postCreatedAt,
UpdatedAt: postUpdatedAt,
}
posts = append(posts, post)
}

if err = rows.Err(); err != nil {
return nil, err
}

postIDs := []int{}
for _, p := range posts {
postIDs = append(postIDs, p.ID)
}

queryTag := `
SELECT
tag_post.post_id AS tag_post_post_id,
tags.id AS tag_id,
tags.name AS tag_name,
tags.created_at AS tag_created_at,
tags.updated_at AS tag_updated_at
FROM
tags
LEFT JOIN
tag_post
ON
tags.id = tag_post.tag_id
WHERE
tag_post.post_id
IN
(%s)
`

var stmt string
if len(postIDs) == 0 {
stmt = fmt.Sprintf(queryTag, `""`)
} else {
stmt = fmt.Sprintf(queryTag, strings.Trim(strings.Replace(fmt.Sprint(postIDs), " ", ",", -1), "[]"))
}

rows, err = pr.ConnMySQL.Query(stmt)

defer func() {
if rerr := rows.Close(); rerr != nil {
err = rerr
}
}()

if err != nil {
return nil, err
}

for rows.Next() {
var (
tagPostPostID int
tagID int
tagName string
tagCreatedAt time.Time
tagUpdatedAt time.Time
)
if err = rows.Scan(&tagPostPostID, &tagID, &tagName, &tagCreatedAt, &tagUpdatedAt); err != nil {
return nil, err
}

for p := range posts {
if posts[p].ID == tagPostPostID {
posts[p].Tags = append(posts[p].Tags, domain.Tag{
ID: tagID,
Name: tagName,
CreatedAt: tagCreatedAt,
UpdatedAt: tagUpdatedAt,
})
}

}
}

if err = rows.Err(); err != nil {
return nil, err
}

rows, err = pr.ConnMySQL.Query(`
SELECT
comments.id,
comments.post_id,
comments.body,
comments.status,
comments.created_at,
comments.updated_at
FROM
comments
JOIN
posts
ON posts.id = comments.post_id
WHERE
posts.status = "publish"
AND comments.status = "publish"
ORDER BY
posts.id
DESC
LIMIT ?, ?
`, page*limit-limit, limit)

defer func() {
if rerr := rows.Close(); rerr != nil {
err = rerr
}
}()

if err != nil {
return nil, err
}

for rows.Next() {
var (
commentID int
commentPostID int
commentBody string
commentStatus string
commentCreatedAt time.Time
commentUpdatedAt time.Time
)
if err = rows.Scan(&commentID, &commentPostID, &commentBody, &commentStatus, &commentCreatedAt, &commentUpdatedAt); err != nil {
return nil, err
}

for p := range posts {
if posts[p].ID == commentPostID {
posts[p].Comments = append(posts[p].Comments, domain.Comment{
ID: commentID,
PostID: commentPostID,
Body: commentBody,
Status: commentStatus,
CreatedAt: commentCreatedAt,
UpdatedAt: commentUpdatedAt,
})
}

}
}

if err = rows.Err(); err != nil {
return nil, err
}

return posts, nil
}

// FindAllPublishByCategory returns all entities by category.
func (pr *Post) FindAllPublishByCategory(page int, limit int, name string) (domain.Posts, error) {
var posts domain.Posts
Expand Down
7 changes: 7 additions & 0 deletions app/usecase/dto/request/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ type IndexPost struct {
Limit int `json:"limit"`
}

// A IndexPostByKeyword represents the singular of IndexPostByKeyword.
type IndexPostByKeyword struct {
Keyword string `json:"keyword"`
Page int `json:"page"`
Limit int `json:"limit"`
}

// A IndexPostByName represents the singular of IndexPostByName.
type IndexPostByName struct {
Name string `json:"name"`
Expand Down
16 changes: 16 additions & 0 deletions app/usecase/interactor/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@ func (pi *PostInteractor) Index(req request.IndexPost) (domain.Posts, Pagination
return posts, pagination, nil
}

// IndexByKeyword returns a listing of the resource.
func (pi *PostInteractor) IndexByKeyword(req request.IndexPostByKeyword) (domain.Posts, Pagination, *HTTPError) {
var ps domain.Posts
var pn Pagination
count, err := pi.Post.CountAllPublishByKeyword(req.Keyword)
if err != nil {
return ps, pn, NewHTTPError(http.StatusInternalServerError, err.Error())
}
posts, err := pi.Post.FindAllPublishByKeyword(req.Page, req.Limit, req.Keyword)
if err != nil {
return ps, pn, NewHTTPError(http.StatusInternalServerError, err.Error())
}
pagination := pn.NewPagination(count, req.Page, req.Limit)
return posts, pagination, nil
}

// IndexByCategory returns a listing of the resource.
func (pi *PostInteractor) IndexByCategory(req request.IndexPostByName) (domain.Posts, Pagination, *HTTPError) {
var ps domain.Posts
Expand Down
1 change: 1 addition & 0 deletions app/usecase/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
// A Post represents a Post.
type Post interface {
Index(request.IndexPost) (domain.Posts, interactor.Pagination, *interactor.HTTPError)
IndexByKeyword(request.IndexPostByKeyword) (domain.Posts, interactor.Pagination, *interactor.HTTPError)
IndexByCategory(request.IndexPostByName) (domain.Posts, interactor.Pagination, *interactor.HTTPError)
IndexByTag(request.IndexPostByName) (domain.Posts, interactor.Pagination, *interactor.HTTPError)
IndexPrivate(request.IndexPost) (domain.Posts, interactor.Pagination, *interactor.HTTPError)
Expand Down
2 changes: 2 additions & 0 deletions app/usecase/repository/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (
type Post interface {
CountAllPublish() (int, error)
CountAll() (int, error)
CountAllPublishByKeyword(keyword string) (int, error)
CountAllPublishByCategory(name string) (int, error)
CountAllPublishByTag(name string) (int, error)
FindAllPublish(page int, limit int) (domain.Posts, error)
FindAllPublishByKeyword(page int, limit int, keyword string) (domain.Posts, error)
FindAllPublishByCategory(page int, limit int, name string) (domain.Posts, error)
FindAllPublishByTag(page int, limit int, name string) (domain.Posts, error)
FindAll(page int, limit int) (domain.Posts, error)
Expand Down
Loading

0 comments on commit 8fee00a

Please sign in to comment.