From b642562792107f18f2e4d06f493e29f3b1d26a7c Mon Sep 17 00:00:00 2001 From: Noah Date: Mon, 17 Feb 2020 20:26:30 -0800 Subject: [PATCH] Blog multi-tag query: whitelist and blacklist * Can query blog posts by multiple tags now. * e.g. /tagged/blog,updates,-photos would query all posts that have tags "blog" OR "updates" but NOT show any post with tag "photos" --- pkg/models/posts.go | 79 +++++++++++++++++++++++++++++++++++++++++---- pkg/models/tags.go | 2 +- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/pkg/models/posts.go b/pkg/models/posts.go index ded3d56..5d42e5b 100644 --- a/pkg/models/posts.go +++ b/pkg/models/posts.go @@ -141,14 +141,16 @@ func (m postMan) GetPostsByTag(tag, privacy string, page, perPage int) (PagedPos pp.PerPage = 20 } - // Get the distinct post IDs for this tag. - var tags []TaggedPost - var postIDs []uint - r := DB.Where("tag = ?", tag).Find(&tags) - for _, taggedPost := range tags { - postIDs = append(postIDs, taggedPost.PostID) + // Multi-tag query. + whitelist, blacklist, err := ParseMultitag(tag) + if err != nil { + return pp, err } + // Query the whitelist of post IDs which match the whitelist tags. + postIDs := getPostIDsByTag("tag IN (?)", whitelist) + notPostIDs := getPostIDsByTag("tag IN (?)", blacklist) + postIDs = narrowWhitelistByBlacklist(postIDs, notPostIDs) if len(postIDs) == 0 { return pp, errors.New("no posts found") } @@ -162,7 +164,7 @@ func (m postMan) GetPostsByTag(tag, privacy string, page, perPage int) (PagedPos query.Model(&Post{}).Count(&pp.Total) // Query the paginated slice of results. - r = query. + r := query. Offset((page - 1) * perPage). Limit(perPage). Find(&pp.Posts) @@ -182,6 +184,43 @@ func (m postMan) GetPostsByTag(tag, privacy string, page, perPage int) (PagedPos return pp, r.Error } +// getPostIDsByTag helper function returns the post IDs that either whitelist, +// or blacklist, a set of tags. +func getPostIDsByTag(query string, value []string) []int { + var tags []TaggedPost + var result []int + + if len(value) == 0 { + return result + } + + DB.Where(query, value).Find(&tags) + for _, tag := range tags { + result = append(result, tag.PostID) + } + + return result +} + +// narrowWhitelistByBlacklist removes IDs in whitelist that appear in blacklist. +func narrowWhitelistByBlacklist(wl []int, bl []int) []int { + // Map the blacklist into a hash map. + var blacklist = map[int]interface{}{} + for _, id := range bl { + blacklist[id] = nil + } + + // Limit the whitelist by the blacklist. + var result []int + for _, id := range wl { + if _, ok := blacklist[id]; !ok { + result = append(result, id) + } + } + + return result +} + // GetArchive queries the archive view of the blog. // Set private=true to return private posts, false returns public only. func (m postMan) GetArchive(private bool) ([]*PostArchive, error) { @@ -271,6 +310,32 @@ func (m postMan) CountComments(posts ...Post) (map[int]int, error) { return result, nil } +// ParseMultitag parses a tag string to return arrays for IN and NOT IN queries from DB. +// +// Example input: "blog,updates,-photo,-ask" +// Returns: ["blog", "updates"], ["photo", "ask"] +func ParseMultitag(tagline string) (whitelist, blacklist []string, err error) { + words := strings.Split(tagline, ",") + for _, word := range words { + word = strings.TrimSpace(word) + if len(word) == 0 { + continue + } + + // Negation + if strings.HasPrefix(word, "-") { + blacklist = append(blacklist, strings.TrimPrefix(word, "-")) + } else { + whitelist = append(whitelist, word) + } + } + + if len(whitelist) == 0 && len(blacklist) == 0 { + err = errors.New("parsing error") + } + return +} + // CountComments on the posts in a PagedPosts list. func (pp *PagedPosts) CountComments() error { counts, err := Posts.CountComments(pp.Posts...) diff --git a/pkg/models/tags.go b/pkg/models/tags.go index d9ba9f6..1a60840 100644 --- a/pkg/models/tags.go +++ b/pkg/models/tags.go @@ -8,7 +8,7 @@ import ( type TaggedPost struct { ID int `gorm:"primary_key"` Tag string - PostID uint // foreign key to Post + PostID int // foreign key to Post } // SummarizeTags returns the list of all tags ordered by frequency used.