diff --git a/pkg/controllers/search.go b/pkg/controllers/search.go index 6f1c19d..58f46f3 100644 --- a/pkg/controllers/search.go +++ b/pkg/controllers/search.go @@ -6,6 +6,7 @@ import ( "git.kirsle.net/apps/gophertype/pkg/models" "git.kirsle.net/apps/gophertype/pkg/responses" + "git.kirsle.net/apps/gophertype/pkg/search" "git.kirsle.net/apps/gophertype/pkg/session" ) @@ -21,7 +22,10 @@ func BlogSearch(w http.ResponseWriter, r *http.Request) { page = a } - pp, err := models.Posts.SearchPosts(query, page, 24) + // Parse their search string (includes and excludes) + search := search.ParseSearchString(query) + + pp, err := models.Posts.SearchPosts(search, page, 24) if err != nil { session.FlashError(w, r, "Error searching posts: %s", err) responses.Redirect(w, r, "/") @@ -31,6 +35,7 @@ func BlogSearch(w http.ResponseWriter, r *http.Request) { v := responses.NewTemplateVars(w, r) v.V["posts"] = pp v.V["search"] = query + v.V["terms"] = search responses.RenderTemplate(w, r, "_builtin/blog/search.gohtml", v) } diff --git a/pkg/models/posts.go b/pkg/models/posts.go index f0c39b2..32711ff 100644 --- a/pkg/models/posts.go +++ b/pkg/models/posts.go @@ -14,6 +14,7 @@ import ( "git.kirsle.net/apps/gophertype/pkg/markdown" "git.kirsle.net/apps/gophertype/pkg/mogrify" "git.kirsle.net/apps/gophertype/pkg/rng" + "git.kirsle.net/apps/gophertype/pkg/search" "github.com/albrow/forms" ) @@ -145,7 +146,7 @@ func (m postMan) GetIndexPosts(privacy string, page, perPage int) (PagedPosts, e } // SearchPosts does a full text search over posts (public only). -func (m postMan) SearchPosts(search string, page, perPage int) (PagedPosts, error) { +func (m postMan) SearchPosts(search *search.Search, page, perPage int) (PagedPosts, error) { var pp = PagedPosts{ Page: page, PerPage: perPage, @@ -158,10 +159,29 @@ func (m postMan) SearchPosts(search string, page, perPage int) (PagedPosts, erro pp.PerPage = 20 } - var like = fmt.Sprintf("%%%s%%", search) + var ( + wheres = []string{} + placeholders = []interface{}{} + ) + + // Global filters. + wheres = append(wheres, "privacy = ?") + placeholders = append(placeholders, Public) + + // Search terms. + for _, term := range search.Includes { + var ilike = "%" + strings.ToLower(term) + "%" + wheres = append(wheres, "(title ILIKE ? OR body ILIKE ?)") + placeholders = append(placeholders, ilike, ilike) + } + for _, term := range search.Excludes { + var ilike = "%" + strings.ToLower(term) + "%" + wheres = append(wheres, "(title NOT ILIKE ? AND body NOT ILIKE ?)") + placeholders = append(placeholders, ilike, ilike) + } query := DB.Preload("Author").Preload("Tags"). - Where("privacy = 'public' AND body ILIKE ?", like). + Where(strings.Join(wheres, " AND "), placeholders...). Order("sticky desc, created_at desc") // Count the total number of rows for paging purposes. diff --git a/pkg/search/search.go b/pkg/search/search.go new file mode 100644 index 0000000..93a4e4c --- /dev/null +++ b/pkg/search/search.go @@ -0,0 +1,72 @@ +package search + +import "strings" + +// Search represents a parsed search query with inclusions and exclusions. +type Search struct { + Includes []string + Excludes []string +} + +// ParseSearchString parses a user search query and supports "quoted phrases" and -negations. +func ParseSearchString(input string) *Search { + var result = new(Search) + + var ( + negate bool + phrase bool + buf = []rune{} + commit = func() { + var text = strings.TrimSpace(string(buf)) + if len(text) == 0 { + return + } + if negate { + result.Excludes = append(result.Excludes, text) + negate = false + } else { + result.Includes = append(result.Includes, text) + } + buf = []rune{} + } + ) + + for _, char := range input { + // Inside a quoted phrase? + if phrase { + if char == '"' { + // End of quoted phrase. + commit() + phrase = false + continue + } + buf = append(buf, char) + continue + } + + // Start a quoted phrase? + if char == '"' { + phrase = true + continue + } + + // Negation indicator? + if len(buf) == 0 && char == '-' { + negate = true + continue + } + + // End of a word? + if char == ' ' { + commit() + continue + } + + buf = append(buf, char) + } + + // Last word? + commit() + + return result +} diff --git a/pvt-www/_builtin/blog/search.gohtml b/pvt-www/_builtin/blog/search.gohtml index c1ac597..9520345 100644 --- a/pvt-www/_builtin/blog/search.gohtml +++ b/pvt-www/_builtin/blog/search.gohtml @@ -8,7 +8,13 @@ {{ $Pager := .V.posts }}
- {{ $Pager.Total}} results found for: {{.V.search}} + {{ $Pager.Total}} results found for: + {{ range .V.terms.Includes }} + {{.}} + {{end}} + {{ range .V.terms.Excludes }} + -{{.}} + {{end}}