mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-06 15:15:58 +00:00
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:
parent
cb777e30c0
commit
a7b74bd5ae
@ -39,7 +39,7 @@ trait OSimpleSearch[F[_]] {
|
|||||||
* and not the results.
|
* and not the results.
|
||||||
*/
|
*/
|
||||||
def searchSummary(
|
def searchSummary(
|
||||||
useFTS: Boolean
|
settings: StatsSettings
|
||||||
)(q: Query, fulltextQuery: Option[String]): F[SearchSummary]
|
)(q: Query, fulltextQuery: Option[String]): F[SearchSummary]
|
||||||
|
|
||||||
/** Calls `search` by parsing the given query string into a query that
|
/** Calls `search` by parsing the given query string into a query that
|
||||||
@ -56,12 +56,12 @@ trait OSimpleSearch[F[_]] {
|
|||||||
* results.
|
* results.
|
||||||
*/
|
*/
|
||||||
final def searchSummaryByString(
|
final def searchSummaryByString(
|
||||||
useFTS: Boolean
|
settings: StatsSettings
|
||||||
)(fix: Query.Fix, q: ItemQueryString)(implicit
|
)(fix: Query.Fix, q: ItemQueryString)(implicit
|
||||||
F: Applicative[F]
|
F: Applicative[F]
|
||||||
): F[StringSearchResult[SearchSummary]] =
|
): F[StringSearchResult[SearchSummary]] =
|
||||||
OSimpleSearch.applySearch[F, SearchSummary](fix, q)((iq, fts) =>
|
OSimpleSearch.applySearch[F, SearchSummary](fix, q)((iq, fts) =>
|
||||||
searchSummary(useFTS)(iq, fts)
|
searchSummary(settings)(iq, fts)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +86,12 @@ object OSimpleSearch {
|
|||||||
batch: Batch,
|
batch: Batch,
|
||||||
useFTS: Boolean,
|
useFTS: Boolean,
|
||||||
resolveDetails: Boolean,
|
resolveDetails: Boolean,
|
||||||
maxNoteLen: Int
|
maxNoteLen: Int,
|
||||||
|
deleted: Boolean
|
||||||
|
)
|
||||||
|
final case class StatsSettings(
|
||||||
|
useFTS: Boolean,
|
||||||
|
deleted: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed trait Items {
|
sealed trait Items {
|
||||||
@ -217,7 +222,9 @@ object OSimpleSearch {
|
|||||||
// 1. fulltext only if fulltextQuery.isDefined && q.isEmpty && useFTS
|
// 1. fulltext only if fulltextQuery.isDefined && q.isEmpty && useFTS
|
||||||
// 2. sql+fulltext if fulltextQuery.isDefined && q.nonEmpty && useFTS
|
// 2. sql+fulltext if fulltextQuery.isDefined && q.nonEmpty && useFTS
|
||||||
// 3. sql-only else (if fulltextQuery.isEmpty || !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 {
|
fulltextQuery match {
|
||||||
case Some(ftq) if settings.useFTS =>
|
case Some(ftq) if settings.useFTS =>
|
||||||
if (q.isEmpty) {
|
if (q.isEmpty) {
|
||||||
@ -270,11 +277,13 @@ object OSimpleSearch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def searchSummary(
|
def searchSummary(
|
||||||
useFTS: Boolean
|
settings: StatsSettings
|
||||||
)(q: Query, fulltextQuery: Option[String]): F[SearchSummary] = {
|
)(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 {
|
fulltextQuery match {
|
||||||
case Some(ftq) if useFTS =>
|
case Some(ftq) if settings.useFTS =>
|
||||||
if (q.isEmpty)
|
if (q.isEmpty)
|
||||||
fts.findIndexOnlySummary(q.fix.account, OFulltext.FtsInput(ftq))
|
fts.findIndexOnlySummary(q.fix.account, OFulltext.FtsInput(ftq))
|
||||||
else
|
else
|
||||||
|
@ -125,7 +125,8 @@ object ItemQuery {
|
|||||||
final case class ChecksumMatch(checksum: String) extends Expr
|
final case class ChecksumMatch(checksum: String) extends Expr
|
||||||
final case class AttachId(id: 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
|
// things that can be expressed with terms above
|
||||||
sealed trait MacroExpr extends Expr {
|
sealed trait MacroExpr extends Expr {
|
||||||
|
@ -75,9 +75,10 @@ object ExprUtil {
|
|||||||
expr
|
expr
|
||||||
case AttachId(_) =>
|
case AttachId(_) =>
|
||||||
expr
|
expr
|
||||||
|
|
||||||
case ValidItemStates =>
|
case ValidItemStates =>
|
||||||
expr
|
expr
|
||||||
|
case Trashed =>
|
||||||
|
expr
|
||||||
}
|
}
|
||||||
|
|
||||||
private def spliceAnd(nodes: Nel[Expr]): Nel[Expr] =
|
private def spliceAnd(nodes: Nel[Expr]): Nel[Expr] =
|
||||||
|
@ -1478,6 +1478,7 @@ paths:
|
|||||||
- $ref: "#/components/parameters/limit"
|
- $ref: "#/components/parameters/limit"
|
||||||
- $ref: "#/components/parameters/offset"
|
- $ref: "#/components/parameters/offset"
|
||||||
- $ref: "#/components/parameters/withDetails"
|
- $ref: "#/components/parameters/withDetails"
|
||||||
|
- $ref: "#/components/parameters/deleted"
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Ok
|
description: Ok
|
||||||
@ -1576,6 +1577,7 @@ paths:
|
|||||||
- authTokenHeader: []
|
- authTokenHeader: []
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/q"
|
- $ref: "#/components/parameters/q"
|
||||||
|
- $ref: "#/components/parameters/deleted"
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Ok
|
description: Ok
|
||||||
@ -4112,6 +4114,12 @@ components:
|
|||||||
withDetails:
|
withDetails:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
deleted:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
description: |
|
||||||
|
If this is true, the search performed only for
|
||||||
|
soft-deleted items.
|
||||||
query:
|
query:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
@ -5838,6 +5846,12 @@ components:
|
|||||||
description: Whether to return details to each item.
|
description: Whether to return details to each item.
|
||||||
schema:
|
schema:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
deleted:
|
||||||
|
name: deleted
|
||||||
|
in: query
|
||||||
|
description: Whether to search in soft-deleted items only.
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
name:
|
name:
|
||||||
name: name
|
name: name
|
||||||
in: path
|
in: path
|
||||||
|
@ -35,6 +35,7 @@ object QueryParam {
|
|||||||
object Limit extends OptionalQueryParamDecoderMatcher[Int]("limit")
|
object Limit extends OptionalQueryParamDecoderMatcher[Int]("limit")
|
||||||
object Offset extends OptionalQueryParamDecoderMatcher[Int]("offset")
|
object Offset extends OptionalQueryParamDecoderMatcher[Int]("offset")
|
||||||
object WithDetails extends OptionalQueryParamDecoderMatcher[Boolean]("withDetails")
|
object WithDetails extends OptionalQueryParamDecoderMatcher[Boolean]("withDetails")
|
||||||
|
object Deleted extends OptionalQueryParamDecoderMatcher[Boolean]("deleted")
|
||||||
|
|
||||||
object WithFallback extends OptionalQueryParamDecoderMatcher[Boolean]("withFallback")
|
object WithFallback extends OptionalQueryParamDecoderMatcher[Boolean]("withFallback")
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ object ItemRoutes {
|
|||||||
HttpRoutes.of {
|
HttpRoutes.of {
|
||||||
case GET -> Root / "search" :? QP.Query(q) :? QP.Limit(limit) :? QP.Offset(
|
case GET -> Root / "search" :? QP.Query(q) :? QP.Limit(limit) :? QP.Offset(
|
||||||
offset
|
offset
|
||||||
) :? QP.WithDetails(detailFlag) =>
|
) :? QP.WithDetails(detailFlag) :? QP.Deleted(deletedFlag) =>
|
||||||
val batch = Batch(offset.getOrElse(0), limit.getOrElse(cfg.maxItemPageSize))
|
val batch = Batch(offset.getOrElse(0), limit.getOrElse(cfg.maxItemPageSize))
|
||||||
.restrictLimitTo(cfg.maxItemPageSize)
|
.restrictLimitTo(cfg.maxItemPageSize)
|
||||||
val itemQuery = ItemQueryString(q)
|
val itemQuery = ItemQueryString(q)
|
||||||
@ -57,15 +57,20 @@ object ItemRoutes {
|
|||||||
batch,
|
batch,
|
||||||
cfg.fullTextSearch.enabled,
|
cfg.fullTextSearch.enabled,
|
||||||
detailFlag.getOrElse(false),
|
detailFlag.getOrElse(false),
|
||||||
cfg.maxNoteLength
|
cfg.maxNoteLength,
|
||||||
|
deletedFlag.getOrElse(false)
|
||||||
)
|
)
|
||||||
val fixQuery = Query.Fix(user.account, None, None)
|
val fixQuery = Query.Fix(user.account, None, None)
|
||||||
searchItems(backend, dsl)(settings, fixQuery, itemQuery)
|
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 itemQuery = ItemQueryString(q)
|
||||||
val fixQuery = Query.Fix(user.account, None, None)
|
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" =>
|
case req @ POST -> Root / "search" =>
|
||||||
for {
|
for {
|
||||||
@ -81,7 +86,8 @@ object ItemRoutes {
|
|||||||
batch,
|
batch,
|
||||||
cfg.fullTextSearch.enabled,
|
cfg.fullTextSearch.enabled,
|
||||||
userQuery.withDetails.getOrElse(false),
|
userQuery.withDetails.getOrElse(false),
|
||||||
cfg.maxNoteLength
|
cfg.maxNoteLength,
|
||||||
|
deleted = userQuery.deleted.getOrElse(false)
|
||||||
)
|
)
|
||||||
fixQuery = Query.Fix(user.account, None, None)
|
fixQuery = Query.Fix(user.account, None, None)
|
||||||
resp <- searchItems(backend, dsl)(settings, fixQuery, itemQuery)
|
resp <- searchItems(backend, dsl)(settings, fixQuery, itemQuery)
|
||||||
@ -92,11 +98,11 @@ object ItemRoutes {
|
|||||||
userQuery <- req.as[ItemQuery]
|
userQuery <- req.as[ItemQuery]
|
||||||
itemQuery = ItemQueryString(userQuery.query)
|
itemQuery = ItemQueryString(userQuery.query)
|
||||||
fixQuery = Query.Fix(user.account, None, None)
|
fixQuery = Query.Fix(user.account, None, None)
|
||||||
resp <- searchItemStats(backend, dsl)(
|
settings = OSimpleSearch.StatsSettings(
|
||||||
cfg.fullTextSearch.enabled,
|
useFTS = cfg.fullTextSearch.enabled,
|
||||||
fixQuery,
|
deleted = userQuery.deleted.getOrElse(false)
|
||||||
itemQuery
|
|
||||||
)
|
)
|
||||||
|
resp <- searchItemStats(backend, dsl)(settings, fixQuery, itemQuery)
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req @ POST -> Root / "searchIndex" =>
|
case req @ POST -> Root / "searchIndex" =>
|
||||||
@ -443,10 +449,15 @@ object ItemRoutes {
|
|||||||
private def searchItemStats[F[_]: Sync](
|
private def searchItemStats[F[_]: Sync](
|
||||||
backend: BackendApp[F],
|
backend: BackendApp[F],
|
||||||
dsl: Http4sDsl[F]
|
dsl: Http4sDsl[F]
|
||||||
)(ftsEnabled: Boolean, fixQuery: Query.Fix, itemQuery: ItemQueryString) = {
|
)(
|
||||||
|
settings: OSimpleSearch.StatsSettings,
|
||||||
|
fixQuery: Query.Fix,
|
||||||
|
itemQuery: ItemQueryString
|
||||||
|
) = {
|
||||||
import dsl._
|
import dsl._
|
||||||
|
|
||||||
backend.simpleSearch
|
backend.simpleSearch
|
||||||
.searchSummaryByString(ftsEnabled)(fixQuery, itemQuery)
|
.searchSummaryByString(settings)(fixQuery, itemQuery)
|
||||||
.flatMap {
|
.flatMap {
|
||||||
case StringSearchResult.Success(summary) =>
|
case StringSearchResult.Success(summary) =>
|
||||||
Ok(Conversions.mkSearchStats(summary))
|
Ok(Conversions.mkSearchStats(summary))
|
||||||
|
@ -126,6 +126,9 @@ object ItemQueryGenerator {
|
|||||||
case Expr.ValidItemStates =>
|
case Expr.ValidItemStates =>
|
||||||
tables.item.state.in(ItemState.validStates)
|
tables.item.state.in(ItemState.validStates)
|
||||||
|
|
||||||
|
case Expr.Trashed =>
|
||||||
|
tables.item.state === ItemState.Deleted
|
||||||
|
|
||||||
case Expr.TagIdsMatch(op, tags) =>
|
case Expr.TagIdsMatch(op, tags) =>
|
||||||
val ids = tags.toList.flatMap(s => Ident.fromString(s).toOption)
|
val ids = tags.toList.flatMap(s => Ident.fromString(s).toOption)
|
||||||
Nel
|
Nel
|
||||||
|
@ -79,6 +79,7 @@ request mq =
|
|||||||
, limit = Nothing
|
, limit = Nothing
|
||||||
, withDetails = Just True
|
, withDetails = Just True
|
||||||
, query = renderMaybe mq
|
, query = renderMaybe mq
|
||||||
|
, deleted = Just False
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user