mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-04 14:15:59 +00:00
Basic poc to search via custom query
This commit is contained in:
parent
186014a1c6
commit
e9ed998e3a
@ -164,7 +164,7 @@ object OFulltext {
|
||||
.flatMap(r => Stream.emits(r.results.map(_.itemId)))
|
||||
.compile
|
||||
.to(Set)
|
||||
q = Query.empty(account).withCond(_.copy(itemIds = itemIds.some))
|
||||
q = Query.empty(account).withFix(_.copy(itemIds = itemIds.some))
|
||||
res <- store.transact(QItem.searchStats(q))
|
||||
} yield res
|
||||
}
|
||||
@ -220,7 +220,7 @@ object OFulltext {
|
||||
.flatMap(r => Stream.emits(r.results.map(_.itemId)))
|
||||
.compile
|
||||
.to(Set)
|
||||
qnext = q.withCond(_.copy(itemIds = items.some))
|
||||
qnext = q.withFix(_.copy(itemIds = items.some))
|
||||
res <- store.transact(QItem.searchStats(qnext))
|
||||
} yield res
|
||||
|
||||
|
@ -0,0 +1,13 @@
|
||||
package docspell.backend.ops
|
||||
|
||||
import docspell.backend.ops.OItemSearch.ListItemWithTags
|
||||
import docspell.common.ItemQueryString
|
||||
import docspell.store.qb.Batch
|
||||
|
||||
trait OSimpleSearch[F[_]] {
|
||||
|
||||
def searchByString(q: ItemQueryString, batch: Batch): F[Vector[ListItemWithTags]]
|
||||
|
||||
}
|
||||
|
||||
object OSimpleSearch {}
|
@ -0,0 +1,3 @@
|
||||
package docspell.common
|
||||
|
||||
case class ItemQueryString(query: String)
|
@ -73,8 +73,8 @@ object NotifyDueItemsTask {
|
||||
Query
|
||||
.empty(ctx.args.account)
|
||||
.withOrder(orderAsc = _.dueDate)
|
||||
.withCond(
|
||||
_.copy(
|
||||
.withCond(_ =>
|
||||
Query.QueryForm.empty.copy(
|
||||
states = ItemState.validStates.toList,
|
||||
tagsInclude = ctx.args.tagsInclude,
|
||||
tagsExclude = ctx.args.tagsExclude,
|
||||
|
@ -12,7 +12,7 @@ object ItemQueryParser {
|
||||
ExprParser.exprParser
|
||||
.parseAll(input.trim)
|
||||
.left
|
||||
.map(_.toString)
|
||||
.map(pe => s"Error parsing: '${input.trim}': $pe")
|
||||
.map(expr => ItemQuery(expr, Some(input.trim)))
|
||||
|
||||
def parseUnsafe(input: String): ItemQuery =
|
||||
|
@ -145,8 +145,8 @@ trait Conversions {
|
||||
|
||||
def mkQuery(m: ItemSearch, account: AccountId): OItemSearch.Query =
|
||||
OItemSearch.Query(
|
||||
OItemSearch.Query.Fix(account, None),
|
||||
OItemSearch.Query.QueryCond(
|
||||
OItemSearch.Query.Fix(account, None, None),
|
||||
OItemSearch.Query.QueryForm(
|
||||
m.name,
|
||||
if (m.inbox) Seq(ItemState.Created)
|
||||
else ItemState.validStates.toList,
|
||||
|
@ -25,5 +25,9 @@ object QueryParam {
|
||||
|
||||
object QueryOpt extends OptionalQueryParamDecoderMatcher[QueryString]("q")
|
||||
|
||||
object Query extends OptionalQueryParamDecoderMatcher[String]("q")
|
||||
object Limit extends OptionalQueryParamDecoderMatcher[Int]("limit")
|
||||
object Offset extends OptionalQueryParamDecoderMatcher[Int]("offset")
|
||||
|
||||
object WithFallback extends OptionalQueryParamDecoderMatcher[Boolean]("withFallback")
|
||||
}
|
||||
|
@ -4,21 +4,20 @@ import cats.Monoid
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.backend.ops.OCustomFields.{RemoveValue, SetValue}
|
||||
import docspell.backend.ops.OFulltext
|
||||
import docspell.backend.ops.OItemSearch.Batch
|
||||
import docspell.backend.ops.OItemSearch.{Batch, Query}
|
||||
import docspell.common._
|
||||
import docspell.common.syntax.all._
|
||||
import docspell.query.ItemQueryParser
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions
|
||||
import docspell.restserver.http4s.BinaryUtil
|
||||
import docspell.restserver.http4s.Responses
|
||||
import docspell.restserver.http4s.{QueryParam => QP}
|
||||
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
@ -46,6 +45,33 @@ object ItemRoutes {
|
||||
resp <- Ok(Conversions.basicResult(res, "Task submitted"))
|
||||
} yield resp
|
||||
|
||||
case GET -> Root / "search" :? QP.Query(q) :? QP.Limit(limit) :? QP.Offset(
|
||||
offset
|
||||
) =>
|
||||
val query =
|
||||
q.map(ItemQueryParser.parse) match {
|
||||
case Some(Right(q)) =>
|
||||
Right(Query(Query.Fix(user.account, None, None), Query.QueryExpr(q)))
|
||||
case Some(Left(err)) =>
|
||||
Left(err)
|
||||
case None =>
|
||||
Right(Query(Query.Fix(user.account, None, None), Query.QueryForm.empty))
|
||||
}
|
||||
val li = limit.getOrElse(cfg.maxItemPageSize)
|
||||
val of = offset.getOrElse(0)
|
||||
query match {
|
||||
case Left(err) =>
|
||||
BadRequest(BasicResult(false, err))
|
||||
case Right(sq) =>
|
||||
for {
|
||||
items <- backend.itemSearch.findItems(cfg.maxNoteLength)(
|
||||
sq,
|
||||
Batch(of, li).restrictLimitTo(cfg.maxItemPageSize)
|
||||
)
|
||||
ok <- Ok(Conversions.mkItemList(items))
|
||||
} yield ok
|
||||
}
|
||||
|
||||
case req @ POST -> Root / "search" =>
|
||||
for {
|
||||
mask <- req.as[ItemSearch]
|
||||
|
@ -5,14 +5,14 @@ import cats.effect.Sync
|
||||
import cats.effect.concurrent.Ref
|
||||
import cats.implicits._
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.common.syntax.all._
|
||||
import docspell.common.{IdRef, _}
|
||||
import docspell.query.ItemQuery
|
||||
import docspell.store.Store
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
import docspell.store.qb.generator.{ItemQueryGenerator, Tables}
|
||||
import docspell.store.records._
|
||||
|
||||
import doobie.implicits._
|
||||
import doobie.{Query => _, _}
|
||||
import org.log4s.getLogger
|
||||
@ -172,10 +172,13 @@ object QItem {
|
||||
.leftJoin(pers1, pers1.pid === i.concPerson && pers1.cid === coll)
|
||||
.leftJoin(equip, equip.eid === i.concEquipment && equip.cid === coll),
|
||||
where(
|
||||
i.cid === coll && or(
|
||||
i.folder.isNull,
|
||||
i.folder.in(QFolder.findMemberFolderIds(q.account))
|
||||
i.cid === coll &&? q.itemIds.map(s =>
|
||||
Nel.fromList(s.toList).map(nel => i.id.in(nel)).getOrElse(i.id.isNull)
|
||||
)
|
||||
&& or(
|
||||
i.folder.isNull,
|
||||
i.folder.in(QFolder.findMemberFolderIds(q.account))
|
||||
)
|
||||
)
|
||||
).distinct.orderBy(
|
||||
q.orderAsc
|
||||
@ -184,7 +187,7 @@ object QItem {
|
||||
)
|
||||
}
|
||||
|
||||
def queryCondition(coll: Ident, q: Query.QueryCond): Condition =
|
||||
def queryCondFromForm(coll: Ident, q: Query.QueryForm): Condition =
|
||||
Condition.unit &&?
|
||||
q.direction.map(d => i.incoming === d) &&?
|
||||
q.name.map(n => i.name.like(QueryWildcard.lower(n))) &&?
|
||||
@ -221,6 +224,19 @@ object QItem {
|
||||
findCustomFieldValuesForColl(coll, q.customValues)
|
||||
.map(itemIds => i.id.in(itemIds))
|
||||
|
||||
def queryCondFromExpr(coll: Ident, q: ItemQuery): Condition = {
|
||||
val tables = Tables(i, org, pers0, pers1, equip, f, a, m)
|
||||
ItemQueryGenerator.fromExpr(tables, coll)(q.expr)
|
||||
}
|
||||
|
||||
def queryCondition(coll: Ident, cond: Query.QueryCond): Condition =
|
||||
cond match {
|
||||
case fm: Query.QueryForm =>
|
||||
queryCondFromForm(coll, fm)
|
||||
case expr: Query.QueryExpr =>
|
||||
queryCondFromExpr(coll, expr.q)
|
||||
}
|
||||
|
||||
def findItems(
|
||||
q: Query,
|
||||
maxNoteLen: Int,
|
||||
|
@ -1,6 +1,7 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import docspell.common._
|
||||
import docspell.query.ItemQuery
|
||||
import docspell.store.qb.Column
|
||||
import docspell.store.records.RItem
|
||||
|
||||
@ -9,14 +10,23 @@ case class Query(fix: Query.Fix, cond: Query.QueryCond) {
|
||||
copy(cond = f(cond))
|
||||
|
||||
def withOrder(orderAsc: RItem.Table => Column[_]): Query =
|
||||
copy(fix = fix.copy(orderAsc = Some(orderAsc)))
|
||||
withFix(_.copy(orderAsc = Some(orderAsc)))
|
||||
|
||||
def withFix(f: Query.Fix => Query.Fix): Query =
|
||||
copy(fix = f(fix))
|
||||
}
|
||||
|
||||
object Query {
|
||||
|
||||
case class Fix(account: AccountId, orderAsc: Option[RItem.Table => Column[_]])
|
||||
case class Fix(
|
||||
account: AccountId,
|
||||
itemIds: Option[Set[Ident]],
|
||||
orderAsc: Option[RItem.Table => Column[_]]
|
||||
)
|
||||
|
||||
case class QueryCond(
|
||||
sealed trait QueryCond
|
||||
|
||||
case class QueryForm(
|
||||
name: Option[String],
|
||||
states: Seq[ItemState],
|
||||
direction: Option[Direction],
|
||||
@ -37,10 +47,10 @@ object Query {
|
||||
itemIds: Option[Set[Ident]],
|
||||
customValues: Seq[CustomValue],
|
||||
source: Option[String]
|
||||
)
|
||||
object QueryCond {
|
||||
) extends QueryCond
|
||||
object QueryForm {
|
||||
val empty =
|
||||
QueryCond(
|
||||
QueryForm(
|
||||
None,
|
||||
Seq.empty,
|
||||
None,
|
||||
@ -64,7 +74,9 @@ object Query {
|
||||
)
|
||||
}
|
||||
|
||||
case class QueryExpr(q: ItemQuery) extends QueryCond
|
||||
|
||||
def empty(account: AccountId): Query =
|
||||
Query(Fix(account, None), QueryCond.empty)
|
||||
Query(Fix(account, None, None), QueryForm.empty)
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user