forked from vikunja/vikunja
feat(tasks): make sorting and filtering work with Typesense
This commit is contained in:
parent
09cfe41e4f
commit
2ca193e63b
|
@ -18,6 +18,7 @@ package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.vikunja.io/api/pkg/db"
|
"code.vikunja.io/api/pkg/db"
|
||||||
|
"code.vikunja.io/api/pkg/log"
|
||||||
"code.vikunja.io/web"
|
"code.vikunja.io/web"
|
||||||
"github.com/typesense/typesense-go/typesense/api"
|
"github.com/typesense/typesense-go/typesense/api"
|
||||||
"github.com/typesense/typesense-go/typesense/api/pointer"
|
"github.com/typesense/typesense-go/typesense/api/pointer"
|
||||||
|
@ -39,15 +40,14 @@ type dbTaskSearcher struct {
|
||||||
hasFavoritesProject bool
|
hasFavoritesProject bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) {
|
func getOrderByDBStatement(opts *taskSearchOptions) (orderby string, err error) {
|
||||||
// Since xorm does not use placeholders for order by, it is possible to expose this with sql injection if we're directly
|
// Since xorm does not use placeholders for order by, it is possible to expose this with sql injection if we're directly
|
||||||
// passing user input to the db.
|
// passing user input to the db.
|
||||||
// As a workaround to prevent this, we check for valid column names here prior to passing it to the db.
|
// As a workaround to prevent this, we check for valid column names here prior to passing it to the db.
|
||||||
var orderby string
|
|
||||||
for i, param := range opts.sortby {
|
for i, param := range opts.sortby {
|
||||||
// Validate the params
|
// Validate the params
|
||||||
if err := param.validate(); err != nil {
|
if err := param.validate(); err != nil {
|
||||||
return nil, totalCount, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mysql sorts columns with null values before ones without null value.
|
// Mysql sorts columns with null values before ones without null value.
|
||||||
|
@ -70,6 +70,16 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) {
|
||||||
|
|
||||||
|
orderby, err := getOrderByDBStatement(opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
// Some filters need a special treatment since they are in a separate table
|
// Some filters need a special treatment since they are in a separate table
|
||||||
reminderFilters := []builder.Cond{}
|
reminderFilters := []builder.Cond{}
|
||||||
assigneeFilters := []builder.Cond{}
|
assigneeFilters := []builder.Cond{}
|
||||||
|
@ -242,10 +252,91 @@ type typesenseTaskSearcher struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) {
|
func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) {
|
||||||
|
|
||||||
|
var sortbyFields []string
|
||||||
|
for i, param := range opts.sortby {
|
||||||
|
// Validate the params
|
||||||
|
if err := param.validate(); err != nil {
|
||||||
|
return nil, totalCount, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Typesense does not allow sorting by ID, so we sort by created timestamp instead
|
||||||
|
if param.sortBy == "id" {
|
||||||
|
param.sortBy = "created"
|
||||||
|
}
|
||||||
|
|
||||||
|
sortbyFields = append(sortbyFields, param.sortBy+"(missing_values:last):"+param.orderBy.String())
|
||||||
|
|
||||||
|
if i == 2 {
|
||||||
|
// Typesense supports up to 3 sorting parameters
|
||||||
|
// https://typesense.org/docs/0.25.0/api/search.html#ranking-and-sorting-parameters
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortby := strings.Join(sortbyFields, ",")
|
||||||
|
|
||||||
projectIDStrings := []string{}
|
projectIDStrings := []string{}
|
||||||
for _, id := range opts.projectIDs {
|
for _, id := range opts.projectIDs {
|
||||||
projectIDStrings = append(projectIDStrings, strconv.FormatInt(id, 10))
|
projectIDStrings = append(projectIDStrings, strconv.FormatInt(id, 10))
|
||||||
}
|
}
|
||||||
|
filterBy := []string{
|
||||||
|
"project_id: [" + strings.Join(projectIDStrings, ", ") + "]",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range opts.filters {
|
||||||
|
|
||||||
|
filter := f.field
|
||||||
|
|
||||||
|
switch f.comparator {
|
||||||
|
case taskFilterComparatorEquals:
|
||||||
|
filter += ":="
|
||||||
|
case taskFilterComparatorNotEquals:
|
||||||
|
filter += ":!="
|
||||||
|
case taskFilterComparatorGreater:
|
||||||
|
filter += ":>"
|
||||||
|
case taskFilterComparatorGreateEquals:
|
||||||
|
filter += ":>="
|
||||||
|
case taskFilterComparatorLess:
|
||||||
|
filter += ":<"
|
||||||
|
case taskFilterComparatorLessEquals:
|
||||||
|
filter += ":<="
|
||||||
|
case taskFilterComparatorLike:
|
||||||
|
filter += ":"
|
||||||
|
//case taskFilterComparatorIn:
|
||||||
|
//filter += "["
|
||||||
|
case taskFilterComparatorInvalid:
|
||||||
|
// Nothing to do
|
||||||
|
default:
|
||||||
|
filter += ":="
|
||||||
|
}
|
||||||
|
|
||||||
|
switch f.value.(type) {
|
||||||
|
case string:
|
||||||
|
filter += f.value.(string)
|
||||||
|
case int:
|
||||||
|
filter += strconv.Itoa(f.value.(int))
|
||||||
|
case int64:
|
||||||
|
filter += strconv.FormatInt(f.value.(int64), 10)
|
||||||
|
case bool:
|
||||||
|
if f.value.(bool) {
|
||||||
|
filter += "true"
|
||||||
|
} else {
|
||||||
|
filter += "false"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Errorf("Unknown search type %s=%v", f.field, f.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
filterBy = append(filterBy, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////
|
||||||
|
// Actual search
|
||||||
|
|
||||||
|
if opts.search == "" {
|
||||||
|
opts.search = "*"
|
||||||
|
}
|
||||||
|
|
||||||
params := &api.SearchCollectionParams{
|
params := &api.SearchCollectionParams{
|
||||||
Q: opts.search,
|
Q: opts.search,
|
||||||
|
@ -253,7 +344,11 @@ func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task,
|
||||||
Page: pointer.Int(opts.page),
|
Page: pointer.Int(opts.page),
|
||||||
PerPage: pointer.Int(opts.perPage),
|
PerPage: pointer.Int(opts.perPage),
|
||||||
ExhaustiveSearch: pointer.True(),
|
ExhaustiveSearch: pointer.True(),
|
||||||
FilterBy: pointer.String("project_id: [" + strings.Join(projectIDStrings, ", ") + "]"),
|
FilterBy: pointer.String(strings.Join(filterBy, " && ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
if sortby != "" {
|
||||||
|
params.SortBy = pointer.String(sortby)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := typesenseClient.Collection("tasks").
|
result, err := typesenseClient.Collection("tasks").
|
||||||
|
@ -275,6 +370,14 @@ func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task,
|
||||||
|
|
||||||
tasks = []*Task{}
|
tasks = []*Task{}
|
||||||
|
|
||||||
err = t.s.In("id", taskIDs).Find(&tasks)
|
orderby, err := getOrderByDBStatement(opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = t.s.
|
||||||
|
In("id", taskIDs).
|
||||||
|
OrderBy(orderby).
|
||||||
|
Find(&tasks)
|
||||||
return tasks, int64(*result.Found), err
|
return tasks, int64(*result.Found), err
|
||||||
}
|
}
|
||||||
|
|
|
@ -309,7 +309,7 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks, totalItems, err = searcher.Search(opts)
|
tasks, totalItems, err = searcher.Search(opts)
|
||||||
return tasks, len(tasks), totalItems, nil
|
return tasks, len(tasks), totalItems, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts *taskSearchOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
|
func getTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts *taskSearchOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
|
||||||
|
|
|
@ -58,14 +58,17 @@ func CreateTypesenseCollections() error {
|
||||||
{
|
{
|
||||||
Name: "id",
|
Name: "id",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
|
Sort: pointer.True(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "title",
|
Name: "title",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
|
Sort: pointer.True(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "description",
|
Name: "description",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
|
Sort: pointer.True(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "done",
|
Name: "done",
|
||||||
|
@ -110,6 +113,7 @@ func CreateTypesenseCollections() error {
|
||||||
{
|
{
|
||||||
Name: "hex_color",
|
Name: "hex_color",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
|
Sort: pointer.True(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "percent_done",
|
Name: "percent_done",
|
||||||
|
@ -118,6 +122,7 @@ func CreateTypesenseCollections() error {
|
||||||
{
|
{
|
||||||
Name: "identifier",
|
Name: "identifier",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
|
Sort: pointer.True(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "index",
|
Name: "index",
|
||||||
|
@ -126,6 +131,7 @@ func CreateTypesenseCollections() error {
|
||||||
{
|
{
|
||||||
Name: "uid",
|
Name: "uid",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
|
Sort: pointer.True(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "cover_image_attachment_id",
|
Name: "cover_image_attachment_id",
|
||||||
|
@ -244,7 +250,6 @@ func reindexTasks(s *xorm.Session, tasks map[int64]*Task) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
typesenseTasks = append(typesenseTasks, searchTask)
|
typesenseTasks = append(typesenseTasks, searchTask)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = typesenseClient.Collection("tasks").
|
_, err = typesenseClient.Collection("tasks").
|
||||||
|
@ -265,14 +270,14 @@ type typesenseTask struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Done bool `json:"done"`
|
Done bool `json:"done"`
|
||||||
DoneAt int64 `json:"done_at"`
|
DoneAt *int64 `json:"done_at"`
|
||||||
DueDate int64 `json:"due_date"`
|
DueDate *int64 `json:"due_date"`
|
||||||
ProjectID int64 `json:"project_id"`
|
ProjectID int64 `json:"project_id"`
|
||||||
RepeatAfter int64 `json:"repeat_after"`
|
RepeatAfter int64 `json:"repeat_after"`
|
||||||
RepeatMode int `json:"repeat_mode"`
|
RepeatMode int `json:"repeat_mode"`
|
||||||
Priority int64 `json:"priority"`
|
Priority int64 `json:"priority"`
|
||||||
StartDate int64 `json:"start_date"`
|
StartDate *int64 `json:"start_date"`
|
||||||
EndDate int64 `json:"end_date"`
|
EndDate *int64 `json:"end_date"`
|
||||||
HexColor string `json:"hex_color"`
|
HexColor string `json:"hex_color"`
|
||||||
PercentDone float64 `json:"percent_done"`
|
PercentDone float64 `json:"percent_done"`
|
||||||
Identifier string `json:"identifier"`
|
Identifier string `json:"identifier"`
|
||||||
|
@ -299,14 +304,14 @@ func convertTaskToTypesenseTask(task *Task) *typesenseTask {
|
||||||
Title: task.Title,
|
Title: task.Title,
|
||||||
Description: task.Description,
|
Description: task.Description,
|
||||||
Done: task.Done,
|
Done: task.Done,
|
||||||
DoneAt: task.DoneAt.UTC().Unix(),
|
DoneAt: pointer.Int64(task.DoneAt.UTC().Unix()),
|
||||||
DueDate: task.DueDate.UTC().Unix(),
|
DueDate: pointer.Int64(task.DueDate.UTC().Unix()),
|
||||||
ProjectID: task.ProjectID,
|
ProjectID: task.ProjectID,
|
||||||
RepeatAfter: task.RepeatAfter,
|
RepeatAfter: task.RepeatAfter,
|
||||||
RepeatMode: int(task.RepeatMode),
|
RepeatMode: int(task.RepeatMode),
|
||||||
Priority: task.Priority,
|
Priority: task.Priority,
|
||||||
StartDate: task.StartDate.UTC().Unix(),
|
StartDate: pointer.Int64(task.StartDate.UTC().Unix()),
|
||||||
EndDate: task.EndDate.UTC().Unix(),
|
EndDate: pointer.Int64(task.EndDate.UTC().Unix()),
|
||||||
HexColor: task.HexColor,
|
HexColor: task.HexColor,
|
||||||
PercentDone: task.PercentDone,
|
PercentDone: task.PercentDone,
|
||||||
Identifier: task.Identifier,
|
Identifier: task.Identifier,
|
||||||
|
@ -327,16 +332,16 @@ func convertTaskToTypesenseTask(task *Task) *typesenseTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
if task.DoneAt.IsZero() {
|
if task.DoneAt.IsZero() {
|
||||||
tt.DoneAt = 0
|
tt.DoneAt = nil
|
||||||
}
|
}
|
||||||
if task.DueDate.IsZero() {
|
if task.DueDate.IsZero() {
|
||||||
tt.DueDate = 0
|
tt.DueDate = nil
|
||||||
}
|
}
|
||||||
if task.StartDate.IsZero() {
|
if task.StartDate.IsZero() {
|
||||||
tt.StartDate = 0
|
tt.StartDate = nil
|
||||||
}
|
}
|
||||||
if task.EndDate.IsZero() {
|
if task.EndDate.IsZero() {
|
||||||
tt.EndDate = 0
|
tt.EndDate = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return tt
|
return tt
|
||||||
|
|
Loading…
Reference in New Issue