Better search terms include+exclude

This commit is contained in:
Noah 2025-04-04 23:36:36 -07:00
parent ede472a128
commit ee0e6cf696
4 changed files with 108 additions and 5 deletions

View File

@ -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)
}

View File

@ -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.

72
pkg/search/search.go Normal file
View File

@ -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
}

View File

@ -8,7 +8,13 @@
{{ $Pager := .V.posts }}
<p>
<em>{{ $Pager.Total}} results found for:</em> {{.V.search}}
<em>{{ $Pager.Total}} results found for:</em>
{{ range .V.terms.Includes }}
<span class="tag is-success">{{.}}</span>
{{end}}
{{ range .V.terms.Excludes }}
<span class="tag is-danger">-{{.}}</span>
{{end}}
</p>
<div class="row">