Rank ParadeDB search results by BM25 relevance only for pure-text searches
over a plain project scope. Numeric searches (the `OR index = N` branch) and
the Favorites view (the `id IN (<subquery>)` scope) keep the default ordering
(unranked, as on main): pdb.score rejects both as unsupported query shapes, and
the contortions previously needed to score them (two-arm numeric merge with
in-memory pagination, a favorites LEFT JOIN) added far more complexity than the
ranking was worth. Neither path was ranked before this PR, so leaving them at
the default order is no regression.
When ParadeDB is in use and a search is run, results now keep the current
fuzzy/OR matching but are ordered by BM25 relevance so tasks matching all
query words rank above tasks matching only some.
Details:
- ParadeDB exposes the BM25 score via pdb.score(<key_field>); Vikunja's
key_field is id, so we order by pdb.score(tasks.id) DESC, then the
existing order-by (ending in a stable tasks.id tiebreak).
- Gating: relevance ordering only applies when ParadeDB is available, a
search term is present, AND the user did not pass an explicit sort_by.
An explicit user sort still wins; relevance only replaces the default
(id / position) sort.
- DISTINCT requires every ORDER BY expression to appear in the SELECT
list, so pdb.score(tasks.id) is added to the selected columns too (for
both the plain and task_positions-join query shapes). Because xorm's
Distinct() quotes each column and corrupts the function call, the
ranking path uses Select(rawColumns).Distinct() instead.
- ParadeDB-only by nature: pdb.score is invalid SQL on sqlite, mysql and
plain postgres, so those paths are completely unchanged.
A test (TestTaskSearchRelevanceRanking) creates a task matching all query
words plus tasks matching only one, then searches a multi-word query. On
ParadeDB it asserts the all-words task ranks first; on other databases it
only asserts the matching tasks are returned, so it stays green across the
whole CI database matrix. The CI ParadeDB matrix entry exercises the
ranking assertion.
Follow-up (not in this change): boosting results where the words appear in
order / in close proximity above plain all-words matches.
Fixes#2690
v1's TaskCollection.ReadAll is polymorphic: a kanban view returns
[]*Bucket, everything else []*Task. v2 splits the task list into a
flat-tasks endpoint and a separate buckets-with-tasks endpoint, so the
flat endpoint needs ReadAll to return tasks even for a kanban view.
SetForceFlatTasks toggles that; v1 leaves it unset and keeps its shape.
Add ProjectView CRUD on /api/v2 under the nested path
/projects/{project}/views[/{view}], establishing the two-path-param
binding pattern for sub-resources. Mirrors the labels.go handler shape
and reuses handler.Do* so permission checks stay at the model layer.
Both {project} and {view} are bound on every operation; {project} is
threaded onto ProjectView.ProjectID (ReadOne resolves via
GetProjectViewByIDAndProject, which needs the parent id). List wraps the
[]*models.ProjectView slice in the shared Paginated envelope, read sends
an ETag for If-None-Match/304, and AutoPatch synthesises PATCH.
Also:
- Tag exposed ProjectView / ProjectViewBucketConfiguration / nested
TaskCollection fields with doc: descriptions; mark server-controlled
fields (id, project_id, created, updated) readOnly. Safe for v1.
- Give ProjectViewKind and BucketConfigurationModeKind a huma.SchemaProvider
so the string-serialised enums reflect as string schemas instead of
Huma's default integer schema (which rejected the string form with 422).
Routes registered in registerAPIRoutesV2 before EnableAutoPatch.
The `expand` query parameter only supported the `expand[]=foo` array
format, but the swagger docs described it as a plain string parameter.
This adds support for both formats (`expand=foo` and `expand[]=foo`),
matching the existing pattern used by `sort_by` and `order_by`
parameters.
Closes#2408
---------
Co-authored-by: kolaente <k@knt.li>
The filter_include_nulls property from the filter in a view would override the property set through the query string. Because we don't have a way in the UI to set this for filters in views, this makes the setting pretty opaque and unpredictable. Since we want to remove the nulls option anyways, we can just ignore it here.
Resolves https://github.com/go-vikunja/vikunja/issues/1781
This fixes a bug where the "include nulls" query parameter would get overridden when the current view had a filter set, even if that filter didn't specify the parameter.
This fixes a bug which caused fetching saved filter and favorite projects to crash, because the respective project ID is not a valid project id without special handling.
This fixes two closely-related bugs:
1. When loading tasks from a bucket of a saved filter, the saved filter query would override the user-supplied filter, which would cause to only tasks matching the saved filter query to be returned.
2. When a filter query for a bucket was specified, the function would only check if one of the top level filters was a filter for tasks in a specific bucket. That means a filter like "bucket_id = 42 && labels = foo" would return the expected result, while a filter like "labels = foo && (bucket_id = 42 && priority = 1)" would fail with an error 500 because the task_buckets table was not joined to the sql query. The fix from the first bug caused such filter queries.
This change fixes a bug where Typesense would try to sort by the project view of a saved filter. The view position is not indexed in Typesense, hence filtering fails. Because sorting by position is not a feature in saved filters, I've removed the logic for sorting saved filters with Typesense.
BREAKING CHANGE: the position of tasks now can't be updated anymore via the task update endpoint. Instead, there is a new endpoint which takes the project view into account as well.