Search query caching
Cette révision appartient à :
Parent
8b56f2e76e
révision
6bf7ee3438
4 fichiers modifiés avec 140 ajouts et 135 suppressions
24
cache/cache.go
externe
24
cache/cache.go
externe
|
@ -5,12 +5,12 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ewhal/nyaa/common"
|
||||||
"github.com/ewhal/nyaa/model"
|
"github.com/ewhal/nyaa/model"
|
||||||
"github.com/ewhal/nyaa/util/search"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cache = make(map[search.SearchParam]*list.Element, 10)
|
cache = make(map[common.SearchParam]*list.Element, 10)
|
||||||
ll = list.New()
|
ll = list.New()
|
||||||
totalUsed int
|
totalUsed int
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
@ -31,18 +31,17 @@ type Key struct {
|
||||||
|
|
||||||
// Single cache entry
|
// Single cache entry
|
||||||
type store struct {
|
type store struct {
|
||||||
// Controls general access to the contents of the struct, except for size
|
sync.Mutex // Controls general access to the contents of the struct
|
||||||
sync.Mutex
|
|
||||||
lastFetched time.Time
|
lastFetched time.Time
|
||||||
key search.SearchParam
|
key common.SearchParam
|
||||||
data []model.Torrents
|
data []model.Torrent
|
||||||
size int
|
size int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the cache for and existing record. If miss, run fn to retrieve fresh
|
// Check the cache for and existing record. If miss, run fn to retrieve fresh
|
||||||
// values.
|
// values.
|
||||||
func CheckCache(key search.SearchParam, fn func() ([]model.Torrents, error)) (
|
func Get(key common.SearchParam, fn func() ([]model.Torrent, error)) (
|
||||||
[]model.Torrents, error,
|
[]model.Torrent, error,
|
||||||
) {
|
) {
|
||||||
s := getStore(key)
|
s := getStore(key)
|
||||||
|
|
||||||
|
@ -59,12 +58,12 @@ func CheckCache(key search.SearchParam, fn func() ([]model.Torrents, error)) (
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
s.lastFetched = time.Now()
|
s.update(data)
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve a store from the cache or create a new one
|
// Retrieve a store from the cache or create a new one
|
||||||
func getStore(k search.SearchParam) (s *store) {
|
func getStore(k common.SearchParam) (s *store) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
@ -85,7 +84,7 @@ func Clear() {
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|
||||||
ll = list.New()
|
ll = list.New()
|
||||||
cache = make(map[search.SearchParam]*list.Element, 10)
|
cache = make(map[common.SearchParam]*list.Element, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the total used memory counter and evict, if over limit
|
// Update the total used memory counter and evict, if over limit
|
||||||
|
@ -109,7 +108,7 @@ func (s *store) isFresh() bool {
|
||||||
|
|
||||||
// Stores the new values of s. Calculates and stores the new size. Passes the
|
// Stores the new values of s. Calculates and stores the new size. Passes the
|
||||||
// delta to the central cache to fire eviction checks.
|
// delta to the central cache to fire eviction checks.
|
||||||
func (s *store) update(data []model.Torrents) {
|
func (s *store) update(data []model.Torrent) {
|
||||||
newSize := 0
|
newSize := 0
|
||||||
for _, d := range data {
|
for _, d := range data {
|
||||||
newSize += d.Size()
|
newSize += d.Size()
|
||||||
|
@ -117,6 +116,7 @@ func (s *store) update(data []model.Torrents) {
|
||||||
s.data = data
|
s.data = data
|
||||||
delta := newSize - s.size
|
delta := newSize - s.size
|
||||||
s.size = newSize
|
s.size = newSize
|
||||||
|
s.lastFetched = time.Now()
|
||||||
|
|
||||||
// Technically it is possible to update the size even when the store is
|
// Technically it is possible to update the size even when the store is
|
||||||
// already evicted, but that should never happen, unless you have a very
|
// already evicted, but that should never happen, unless you have a very
|
||||||
|
|
46
common/search.go
Fichier normal
46
common/search.go
Fichier normal
|
@ -0,0 +1,46 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
type Status uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
ShowAll Status = iota
|
||||||
|
FilterRemakes
|
||||||
|
Trusted
|
||||||
|
APlus
|
||||||
|
)
|
||||||
|
|
||||||
|
type SortMode uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
ID SortMode = iota
|
||||||
|
Name
|
||||||
|
Date
|
||||||
|
Downloads
|
||||||
|
Size
|
||||||
|
)
|
||||||
|
|
||||||
|
type Category struct {
|
||||||
|
Main, Sub uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Category) String() (s string) {
|
||||||
|
if c.Main != 0 {
|
||||||
|
s += strconv.Itoa(int(c.Main))
|
||||||
|
}
|
||||||
|
s += "_"
|
||||||
|
if c.Sub != 0 {
|
||||||
|
s += strconv.Itoa(int(c.Sub))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchParam struct {
|
||||||
|
Order bool // True means acsending
|
||||||
|
Status Status
|
||||||
|
Sort SortMode
|
||||||
|
Category Category
|
||||||
|
Max uint
|
||||||
|
Query string
|
||||||
|
}
|
|
@ -4,11 +4,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/ewhal/nyaa/common"
|
||||||
"github.com/ewhal/nyaa/model"
|
"github.com/ewhal/nyaa/model"
|
||||||
"github.com/ewhal/nyaa/service/captcha"
|
"github.com/ewhal/nyaa/service/captcha"
|
||||||
"github.com/ewhal/nyaa/service/user"
|
"github.com/ewhal/nyaa/service/user"
|
||||||
userForms "github.com/ewhal/nyaa/service/user/form"
|
userForms "github.com/ewhal/nyaa/service/user/form"
|
||||||
"github.com/ewhal/nyaa/util/search"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ type UserLoginFormVariables struct {
|
||||||
|
|
||||||
type UserProfileVariables struct {
|
type UserProfileVariables struct {
|
||||||
UserProfile *model.User
|
UserProfile *model.User
|
||||||
FormInfos map[string][]string
|
FormInfos map[string][]string
|
||||||
Search SearchForm
|
Search SearchForm
|
||||||
Navigation Navigation
|
Navigation Navigation
|
||||||
User *model.User
|
User *model.User
|
||||||
|
@ -114,22 +114,22 @@ type UploadTemplateVariables struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type PanelIndexVbs struct {
|
type PanelIndexVbs struct {
|
||||||
Torrents []model.Torrent
|
Torrents []model.Torrent
|
||||||
Users []model.User
|
Users []model.User
|
||||||
Comments []model.Comment
|
Comments []model.Comment
|
||||||
}
|
}
|
||||||
|
|
||||||
type PanelTorrentListVbs struct {
|
type PanelTorrentListVbs struct {
|
||||||
Torrents []model.Torrent
|
Torrents []model.Torrent
|
||||||
}
|
}
|
||||||
type PanelUserListVbs struct {
|
type PanelUserListVbs struct {
|
||||||
Users []model.User
|
Users []model.User
|
||||||
}
|
}
|
||||||
type PanelCommentListVbs struct {
|
type PanelCommentListVbs struct {
|
||||||
Comments []model.Comment
|
Comments []model.Comment
|
||||||
}
|
}
|
||||||
type PanelTorrentEdVbs struct {
|
type PanelTorrentEdVbs struct {
|
||||||
Torrent model.Torrent
|
Torrent model.Torrent
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -143,7 +143,7 @@ type Navigation struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchForm struct {
|
type SearchForm struct {
|
||||||
search.SearchParam
|
common.SearchParam
|
||||||
Category string
|
Category string
|
||||||
HideAdvancedSearch bool
|
HideAdvancedSearch bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,71 +1,33 @@
|
||||||
package search
|
package search
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/ewhal/nyaa/db"
|
|
||||||
"github.com/ewhal/nyaa/model"
|
|
||||||
"github.com/ewhal/nyaa/service/torrent"
|
|
||||||
"github.com/ewhal/nyaa/util/log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/ewhal/nyaa/cache"
|
||||||
|
"github.com/ewhal/nyaa/common"
|
||||||
|
"github.com/ewhal/nyaa/db"
|
||||||
|
"github.com/ewhal/nyaa/model"
|
||||||
|
"github.com/ewhal/nyaa/service/torrent"
|
||||||
|
"github.com/ewhal/nyaa/util/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Status uint8
|
func SearchByQuery(r *http.Request, pagenum int) (search common.SearchParam, tor []model.Torrent, count int, err error) {
|
||||||
|
|
||||||
const (
|
|
||||||
ShowAll Status = iota
|
|
||||||
FilterRemakes
|
|
||||||
Trusted
|
|
||||||
APlus
|
|
||||||
)
|
|
||||||
|
|
||||||
type SortMode uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
ID SortMode = iota
|
|
||||||
Name
|
|
||||||
Date
|
|
||||||
Downloads
|
|
||||||
Size
|
|
||||||
)
|
|
||||||
|
|
||||||
type Category struct {
|
|
||||||
Main, Sub uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Category) String() (s string) {
|
|
||||||
if c.Main != 0 {
|
|
||||||
s += strconv.Itoa(int(c.Main))
|
|
||||||
}
|
|
||||||
s += "_"
|
|
||||||
if c.Sub != 0 {
|
|
||||||
s += strconv.Itoa(int(c.Sub))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type SearchParam struct {
|
|
||||||
Order bool // True means acsending
|
|
||||||
Status Status
|
|
||||||
Sort SortMode
|
|
||||||
Category Category
|
|
||||||
Max uint
|
|
||||||
Query string
|
|
||||||
}
|
|
||||||
|
|
||||||
func SearchByQuery(r *http.Request, pagenum int) (search SearchParam, tor []model.Torrent, count int, err error) {
|
|
||||||
search, tor, count, err = searchByQuery(r, pagenum, true)
|
search, tor, count, err = searchByQuery(r, pagenum, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func SearchByQueryNoCount(r *http.Request, pagenum int) (search SearchParam, tor []model.Torrent, err error) {
|
func SearchByQueryNoCount(r *http.Request, pagenum int) (search common.SearchParam, tor []model.Torrent, err error) {
|
||||||
search, tor, _, err = searchByQuery(r, pagenum, false)
|
search, tor, _, err = searchByQuery(r, pagenum, false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func searchByQuery(r *http.Request, pagenum int, countAll bool) (search SearchParam, tor []model.Torrent, count int, err error) {
|
func searchByQuery(r *http.Request, pagenum int, countAll bool) (
|
||||||
|
search common.SearchParam, tor []model.Torrent, count int, err error,
|
||||||
|
) {
|
||||||
max, err := strconv.ParseUint(r.URL.Query().Get("max"), 10, 32)
|
max, err := strconv.ParseUint(r.URL.Query().Get("max"), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
max = 50 // default Value maxPerPage
|
max = 50 // default Value maxPerPage
|
||||||
|
@ -78,11 +40,11 @@ func searchByQuery(r *http.Request, pagenum int, countAll bool) (search SearchPa
|
||||||
|
|
||||||
switch s := r.URL.Query().Get("s"); s {
|
switch s := r.URL.Query().Get("s"); s {
|
||||||
case "1":
|
case "1":
|
||||||
search.Status = FilterRemakes
|
search.Status = common.FilterRemakes
|
||||||
case "2":
|
case "2":
|
||||||
search.Status = Trusted
|
search.Status = common.Trusted
|
||||||
case "3":
|
case "3":
|
||||||
search.Status = APlus
|
search.Status = common.APlus
|
||||||
}
|
}
|
||||||
|
|
||||||
catString := r.URL.Query().Get("c")
|
catString := r.URL.Query().Get("c")
|
||||||
|
@ -107,16 +69,16 @@ func searchByQuery(r *http.Request, pagenum int, countAll bool) (search SearchPa
|
||||||
|
|
||||||
switch s := r.URL.Query().Get("sort"); s {
|
switch s := r.URL.Query().Get("sort"); s {
|
||||||
case "1":
|
case "1":
|
||||||
search.Sort = Name
|
search.Sort = common.Name
|
||||||
orderBy += "torrent_name"
|
orderBy += "torrent_name"
|
||||||
case "2":
|
case "2":
|
||||||
search.Sort = Date
|
search.Sort = common.Date
|
||||||
orderBy += "date"
|
orderBy += "date"
|
||||||
case "3":
|
case "3":
|
||||||
search.Sort = Downloads
|
search.Sort = common.Downloads
|
||||||
orderBy += "downloads"
|
orderBy += "downloads"
|
||||||
case "4":
|
case "4":
|
||||||
search.Sort = Size
|
search.Sort = common.Size
|
||||||
orderBy += "filesize"
|
orderBy += "filesize"
|
||||||
default:
|
default:
|
||||||
orderBy += "torrent_id"
|
orderBy += "torrent_id"
|
||||||
|
@ -132,66 +94,63 @@ func searchByQuery(r *http.Request, pagenum int, countAll bool) (search SearchPa
|
||||||
orderBy += "desc"
|
orderBy += "desc"
|
||||||
}
|
}
|
||||||
|
|
||||||
userID := r.URL.Query().Get("userID")
|
tor, err = cache.Get(search, func() (tor []model.Torrent, err error) {
|
||||||
|
parameters := torrentService.WhereParams{
|
||||||
|
Params: make([]interface{}, 0, 64),
|
||||||
|
}
|
||||||
|
conditions := make([]string, 0, 64)
|
||||||
|
if search.Category.Main != 0 {
|
||||||
|
conditions = append(conditions, "category = ?")
|
||||||
|
parameters.Params = append(parameters.Params, string(catString[0]))
|
||||||
|
}
|
||||||
|
if search.Category.Sub != 0 {
|
||||||
|
conditions = append(conditions, "sub_category = ?")
|
||||||
|
parameters.Params = append(parameters.Params, string(catString[2]))
|
||||||
|
}
|
||||||
|
if search.Status != 0 {
|
||||||
|
if search.Status == 3 {
|
||||||
|
conditions = append(conditions, "status != ?")
|
||||||
|
} else {
|
||||||
|
conditions = append(conditions, "status = ?")
|
||||||
|
}
|
||||||
|
parameters.Params = append(parameters.Params, strconv.Itoa(int(search.Status)+1))
|
||||||
|
}
|
||||||
|
|
||||||
parameters := torrentService.WhereParams{
|
searchQuerySplit := strings.Fields(search.Query)
|
||||||
Params: make([]interface{}, 0, 64),
|
for i, word := range searchQuerySplit {
|
||||||
}
|
firstRune, _ := utf8.DecodeRuneInString(word)
|
||||||
conditions := make([]string, 0, 64)
|
if len(word) == 1 && unicode.IsPunct(firstRune) {
|
||||||
if search.Category.Main != 0 {
|
// some queries have a single punctuation character
|
||||||
conditions = append(conditions, "category = ?")
|
// which causes a full scan instead of using the index
|
||||||
parameters.Params = append(parameters.Params, string(catString[0]))
|
// and yields no meaningful results.
|
||||||
}
|
// due to len() == 1 we're just looking at 1-byte/ascii
|
||||||
if search.Category.Sub != 0 {
|
// punctuation characters.
|
||||||
conditions = append(conditions, "sub_category = ?")
|
continue
|
||||||
parameters.Params = append(parameters.Params, string(catString[2]))
|
}
|
||||||
}
|
|
||||||
if userID != "" {
|
// SQLite has case-insensitive LIKE, but no ILIKE
|
||||||
conditions = append(conditions, "uploader = ?")
|
var operator string
|
||||||
parameters.Params = append(parameters.Params, userID)
|
if db.ORM.Dialect().GetName() == "sqlite3" {
|
||||||
}
|
operator = "LIKE ?"
|
||||||
if search.Status != 0 {
|
} else {
|
||||||
if search.Status == 3 {
|
operator = "ILIKE ?"
|
||||||
conditions = append(conditions, "status != ?")
|
}
|
||||||
|
|
||||||
|
// TODO: make this faster ?
|
||||||
|
conditions = append(conditions, "torrent_name "+operator)
|
||||||
|
parameters.Params = append(parameters.Params, "%"+searchQuerySplit[i]+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters.Conditions = strings.Join(conditions[:], " AND ")
|
||||||
|
log.Infof("SQL query is :: %s\n", parameters.Conditions)
|
||||||
|
if countAll {
|
||||||
|
tor, count, err = torrentService.GetTorrentsOrderBy(¶meters, orderBy, int(search.Max), int(search.Max)*(pagenum-1))
|
||||||
} else {
|
} else {
|
||||||
conditions = append(conditions, "status = ?")
|
tor, err = torrentService.GetTorrentsOrderByNoCount(¶meters, orderBy, int(search.Max), int(search.Max)*(pagenum-1))
|
||||||
}
|
|
||||||
parameters.Params = append(parameters.Params, strconv.Itoa(int(search.Status)+1))
|
|
||||||
}
|
|
||||||
|
|
||||||
searchQuerySplit := strings.Fields(search.Query)
|
|
||||||
for i, word := range searchQuerySplit {
|
|
||||||
firstRune, _ := utf8.DecodeRuneInString(word)
|
|
||||||
if len(word) == 1 && unicode.IsPunct(firstRune) {
|
|
||||||
// some queries have a single punctuation character
|
|
||||||
// which causes a full scan instead of using the index
|
|
||||||
// and yields no meaningful results.
|
|
||||||
// due to len() == 1 we're just looking at 1-byte/ascii
|
|
||||||
// punctuation characters.
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TEMP: Workaround to at least make SQLite search testable for
|
return
|
||||||
// development.
|
})
|
||||||
// TODO: Actual case-insensitive search for SQLite
|
|
||||||
var operator string
|
|
||||||
if db.ORM.Dialect().GetName() == "sqlite3" {
|
|
||||||
operator = "LIKE ?"
|
|
||||||
} else {
|
|
||||||
operator = "ILIKE ?"
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: make this faster ?
|
|
||||||
conditions = append(conditions, "torrent_name "+operator)
|
|
||||||
parameters.Params = append(parameters.Params, "%"+searchQuerySplit[i]+"%")
|
|
||||||
}
|
|
||||||
|
|
||||||
parameters.Conditions = strings.Join(conditions[:], " AND ")
|
|
||||||
log.Infof("SQL query is :: %s\n", parameters.Conditions)
|
|
||||||
if countAll {
|
|
||||||
tor, count, err = torrentService.GetTorrentsOrderBy(¶meters, orderBy, int(search.Max), int(search.Max)*(pagenum-1))
|
|
||||||
} else {
|
|
||||||
tor, err = torrentService.GetTorrentsOrderByNoCount(¶meters, orderBy, int(search.Max), int(search.Max)*(pagenum-1))
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
Référencer dans un nouveau ticket