Allow to search in soft-deleted items

A new query/request parameter can be used to apply a search to only
soft-deleted items.

The query expression `Trashed` has been introduced which selects only
items with state `Deleted`. This is another option an analog to
`ValidItemStates` (both cannot be used together as they would select
no items). This new query node is not added to the parser, because
users may not use it in their own queries - it must be part of the
"fixed" query so the application can control in which subset to search
(it would otherwise be possible to select any items).
This commit is contained in:
eikek 2021-08-14 14:53:05 +02:00
parent cb777e30c0
commit a7b74bd5ae
8 changed files with 62 additions and 21 deletions

View File

@ -39,7 +39,7 @@ trait OSimpleSearch[F[_]] {
* and not the results.
*/
def searchSummary(
useFTS: Boolean
settings: StatsSettings
)(q: Query, fulltextQuery: Option[String]): F[SearchSummary]
/** Calls `search` by parsing the given query string into a query that
@ -56,12 +56,12 @@ trait OSimpleSearch[F[_]] {
* results.
*/
final def searchSummaryByString(
useFTS: Boolean
settings: StatsSettings
)(fix: Query.Fix, q: ItemQueryString)(implicit
F: Applicative[F]
): F[StringSearchResult[SearchSummary]] =
OSimpleSearch.applySearch[F, SearchSummary](fix, q)((iq, fts) =>
searchSummary(useFTS)(iq, fts)
searchSummary(settings)(iq, fts)
)
}
@ -86,7 +86,12 @@ object OSimpleSearch {
batch: Batch,
useFTS: Boolean,
resolveDetails: Boolean,
maxNoteLen: Int
maxNoteLen: Int,
deleted: Boolean
)
final case class StatsSettings(
useFTS: Boolean,
deleted: Boolean
)
sealed trait Items {
@ -217,7 +222,9 @@ object OSimpleSearch {
// 1. fulltext only if fulltextQuery.isDefined && q.isEmpty && useFTS
// 2. sql+fulltext if fulltextQuery.isDefined && q.nonEmpty && useFTS
// 3. sql-only else (if fulltextQuery.isEmpty || !useFTS)
val validItemQuery = q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates))
val validItemQuery =
if (settings.deleted) q.withFix(_.andQuery(ItemQuery.Expr.Trashed))
else q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates))
fulltextQuery match {
case Some(ftq) if settings.useFTS =>
if (q.isEmpty) {
@ -270,11 +277,13 @@ object OSimpleSearch {
}
def searchSummary(
useFTS: Boolean
settings: StatsSettings
)(q: Query, fulltextQuery: Option[String]): F[SearchSummary] = {
val validItemQuery = q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates))
val validItemQuery =
if (settings.deleted) q.withFix(_.andQuery(ItemQuery.Expr.Trashed))
else q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates))
fulltextQuery match {
case Some(ftq) if useFTS =>
case Some(ftq) if settings.useFTS =>
if (q.isEmpty)
fts.findIndexOnlySummary(q.fix.account, OFulltext.FtsInput(ftq))
else

View File

@ -125,7 +125,8 @@ object ItemQuery {
final case class ChecksumMatch(checksum: String) extends Expr
final case class AttachId(id: String) extends Expr
case object ValidItemStates extends Expr
final case object ValidItemStates extends Expr
final case object Trashed extends Expr
// things that can be expressed with terms above
sealed trait MacroExpr extends Expr {

View File

@ -75,9 +75,10 @@ object ExprUtil {
expr
case AttachId(_) =>
expr
case ValidItemStates =>
expr
case Trashed =>
expr
}
private def spliceAnd(nodes: Nel[Expr]): Nel[Expr] =

View File

@ -1478,6 +1478,7 @@ paths:
- $ref: "#/components/parameters/limit"
- $ref: "#/components/parameters/offset"
- $ref: "#/components/parameters/withDetails"
- $ref: "#/components/parameters/deleted"
responses:
200:
description: Ok
@ -1576,6 +1577,7 @@ paths:
- authTokenHeader: []
parameters:
- $ref: "#/components/parameters/q"
- $ref: "#/components/parameters/deleted"
responses:
200:
description: Ok
@ -4112,6 +4114,12 @@ components:
withDetails:
type: boolean
default: false
deleted:
type: boolean
default: false
description: |
If this is true, the search performed only for
soft-deleted items.
query:
type: string
description: |
@ -5838,6 +5846,12 @@ components:
description: Whether to return details to each item.
schema:
type: boolean
deleted:
name: deleted
in: query
description: Whether to search in soft-deleted items only.
schema:
type: boolean
name:
name: name
in: path

View File

@ -35,6 +35,7 @@ object QueryParam {
object Limit extends OptionalQueryParamDecoderMatcher[Int]("limit")
object Offset extends OptionalQueryParamDecoderMatcher[Int]("offset")
object WithDetails extends OptionalQueryParamDecoderMatcher[Boolean]("withDetails")
object Deleted extends OptionalQueryParamDecoderMatcher[Boolean]("deleted")
object WithFallback extends OptionalQueryParamDecoderMatcher[Boolean]("withFallback")
}

View File

@ -49,7 +49,7 @@ object ItemRoutes {
HttpRoutes.of {
case GET -> Root / "search" :? QP.Query(q) :? QP.Limit(limit) :? QP.Offset(
offset
) :? QP.WithDetails(detailFlag) =>
) :? QP.WithDetails(detailFlag) :? QP.Deleted(deletedFlag) =>
val batch = Batch(offset.getOrElse(0), limit.getOrElse(cfg.maxItemPageSize))
.restrictLimitTo(cfg.maxItemPageSize)
val itemQuery = ItemQueryString(q)
@ -57,15 +57,20 @@ object ItemRoutes {
batch,
cfg.fullTextSearch.enabled,
detailFlag.getOrElse(false),
cfg.maxNoteLength
cfg.maxNoteLength,
deletedFlag.getOrElse(false)
)
val fixQuery = Query.Fix(user.account, None, None)
searchItems(backend, dsl)(settings, fixQuery, itemQuery)
case GET -> Root / "searchStats" :? QP.Query(q) =>
case GET -> Root / "searchStats" :? QP.Query(q) :? QP.Deleted(deletedFlag) =>
val itemQuery = ItemQueryString(q)
val fixQuery = Query.Fix(user.account, None, None)
searchItemStats(backend, dsl)(cfg.fullTextSearch.enabled, fixQuery, itemQuery)
val settings = OSimpleSearch.StatsSettings(
useFTS = cfg.fullTextSearch.enabled,
deleted = deletedFlag.getOrElse(false)
)
searchItemStats(backend, dsl)(settings, fixQuery, itemQuery)
case req @ POST -> Root / "search" =>
for {
@ -81,7 +86,8 @@ object ItemRoutes {
batch,
cfg.fullTextSearch.enabled,
userQuery.withDetails.getOrElse(false),
cfg.maxNoteLength
cfg.maxNoteLength,
deleted = userQuery.deleted.getOrElse(false)
)
fixQuery = Query.Fix(user.account, None, None)
resp <- searchItems(backend, dsl)(settings, fixQuery, itemQuery)
@ -92,11 +98,11 @@ object ItemRoutes {
userQuery <- req.as[ItemQuery]
itemQuery = ItemQueryString(userQuery.query)
fixQuery = Query.Fix(user.account, None, None)
resp <- searchItemStats(backend, dsl)(
cfg.fullTextSearch.enabled,
fixQuery,
itemQuery
settings = OSimpleSearch.StatsSettings(
useFTS = cfg.fullTextSearch.enabled,
deleted = userQuery.deleted.getOrElse(false)
)
resp <- searchItemStats(backend, dsl)(settings, fixQuery, itemQuery)
} yield resp
case req @ POST -> Root / "searchIndex" =>
@ -443,10 +449,15 @@ object ItemRoutes {
private def searchItemStats[F[_]: Sync](
backend: BackendApp[F],
dsl: Http4sDsl[F]
)(ftsEnabled: Boolean, fixQuery: Query.Fix, itemQuery: ItemQueryString) = {
)(
settings: OSimpleSearch.StatsSettings,
fixQuery: Query.Fix,
itemQuery: ItemQueryString
) = {
import dsl._
backend.simpleSearch
.searchSummaryByString(ftsEnabled)(fixQuery, itemQuery)
.searchSummaryByString(settings)(fixQuery, itemQuery)
.flatMap {
case StringSearchResult.Success(summary) =>
Ok(Conversions.mkSearchStats(summary))

View File

@ -126,6 +126,9 @@ object ItemQueryGenerator {
case Expr.ValidItemStates =>
tables.item.state.in(ItemState.validStates)
case Expr.Trashed =>
tables.item.state === ItemState.Deleted
case Expr.TagIdsMatch(op, tags) =>
val ids = tags.toList.flatMap(s => Ident.fromString(s).toOption)
Nel

View File

@ -79,6 +79,7 @@ request mq =
, limit = Nothing
, withDetails = Just True
, query = renderMaybe mq
, deleted = Just False
}