mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-05 22:55:58 +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)))
|
.flatMap(r => Stream.emits(r.results.map(_.itemId)))
|
||||||
.compile
|
.compile
|
||||||
.to(Set)
|
.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))
|
res <- store.transact(QItem.searchStats(q))
|
||||||
} yield res
|
} yield res
|
||||||
}
|
}
|
||||||
@ -220,7 +220,7 @@ object OFulltext {
|
|||||||
.flatMap(r => Stream.emits(r.results.map(_.itemId)))
|
.flatMap(r => Stream.emits(r.results.map(_.itemId)))
|
||||||
.compile
|
.compile
|
||||||
.to(Set)
|
.to(Set)
|
||||||
qnext = q.withCond(_.copy(itemIds = items.some))
|
qnext = q.withFix(_.copy(itemIds = items.some))
|
||||||
res <- store.transact(QItem.searchStats(qnext))
|
res <- store.transact(QItem.searchStats(qnext))
|
||||||
} yield res
|
} 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
|
Query
|
||||||
.empty(ctx.args.account)
|
.empty(ctx.args.account)
|
||||||
.withOrder(orderAsc = _.dueDate)
|
.withOrder(orderAsc = _.dueDate)
|
||||||
.withCond(
|
.withCond(_ =>
|
||||||
_.copy(
|
Query.QueryForm.empty.copy(
|
||||||
states = ItemState.validStates.toList,
|
states = ItemState.validStates.toList,
|
||||||
tagsInclude = ctx.args.tagsInclude,
|
tagsInclude = ctx.args.tagsInclude,
|
||||||
tagsExclude = ctx.args.tagsExclude,
|
tagsExclude = ctx.args.tagsExclude,
|
||||||
|
@ -12,7 +12,7 @@ object ItemQueryParser {
|
|||||||
ExprParser.exprParser
|
ExprParser.exprParser
|
||||||
.parseAll(input.trim)
|
.parseAll(input.trim)
|
||||||
.left
|
.left
|
||||||
.map(_.toString)
|
.map(pe => s"Error parsing: '${input.trim}': $pe")
|
||||||
.map(expr => ItemQuery(expr, Some(input.trim)))
|
.map(expr => ItemQuery(expr, Some(input.trim)))
|
||||||
|
|
||||||
def parseUnsafe(input: String): ItemQuery =
|
def parseUnsafe(input: String): ItemQuery =
|
||||||
|
@ -145,8 +145,8 @@ trait Conversions {
|
|||||||
|
|
||||||
def mkQuery(m: ItemSearch, account: AccountId): OItemSearch.Query =
|
def mkQuery(m: ItemSearch, account: AccountId): OItemSearch.Query =
|
||||||
OItemSearch.Query(
|
OItemSearch.Query(
|
||||||
OItemSearch.Query.Fix(account, None),
|
OItemSearch.Query.Fix(account, None, None),
|
||||||
OItemSearch.Query.QueryCond(
|
OItemSearch.Query.QueryForm(
|
||||||
m.name,
|
m.name,
|
||||||
if (m.inbox) Seq(ItemState.Created)
|
if (m.inbox) Seq(ItemState.Created)
|
||||||
else ItemState.validStates.toList,
|
else ItemState.validStates.toList,
|
||||||
|
@ -25,5 +25,9 @@ object QueryParam {
|
|||||||
|
|
||||||
object QueryOpt extends OptionalQueryParamDecoderMatcher[QueryString]("q")
|
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")
|
object WithFallback extends OptionalQueryParamDecoderMatcher[Boolean]("withFallback")
|
||||||
}
|
}
|
||||||
|
@ -4,21 +4,20 @@ import cats.Monoid
|
|||||||
import cats.data.NonEmptyList
|
import cats.data.NonEmptyList
|
||||||
import cats.effect._
|
import cats.effect._
|
||||||
import cats.implicits._
|
import cats.implicits._
|
||||||
|
|
||||||
import docspell.backend.BackendApp
|
import docspell.backend.BackendApp
|
||||||
import docspell.backend.auth.AuthToken
|
import docspell.backend.auth.AuthToken
|
||||||
import docspell.backend.ops.OCustomFields.{RemoveValue, SetValue}
|
import docspell.backend.ops.OCustomFields.{RemoveValue, SetValue}
|
||||||
import docspell.backend.ops.OFulltext
|
import docspell.backend.ops.OFulltext
|
||||||
import docspell.backend.ops.OItemSearch.Batch
|
import docspell.backend.ops.OItemSearch.{Batch, Query}
|
||||||
import docspell.common._
|
import docspell.common._
|
||||||
import docspell.common.syntax.all._
|
import docspell.common.syntax.all._
|
||||||
|
import docspell.query.ItemQueryParser
|
||||||
import docspell.restapi.model._
|
import docspell.restapi.model._
|
||||||
import docspell.restserver.Config
|
import docspell.restserver.Config
|
||||||
import docspell.restserver.conv.Conversions
|
import docspell.restserver.conv.Conversions
|
||||||
import docspell.restserver.http4s.BinaryUtil
|
import docspell.restserver.http4s.BinaryUtil
|
||||||
import docspell.restserver.http4s.Responses
|
import docspell.restserver.http4s.Responses
|
||||||
import docspell.restserver.http4s.{QueryParam => QP}
|
import docspell.restserver.http4s.{QueryParam => QP}
|
||||||
|
|
||||||
import org.http4s.HttpRoutes
|
import org.http4s.HttpRoutes
|
||||||
import org.http4s.circe.CirceEntityDecoder._
|
import org.http4s.circe.CirceEntityDecoder._
|
||||||
import org.http4s.circe.CirceEntityEncoder._
|
import org.http4s.circe.CirceEntityEncoder._
|
||||||
@ -46,6 +45,33 @@ object ItemRoutes {
|
|||||||
resp <- Ok(Conversions.basicResult(res, "Task submitted"))
|
resp <- Ok(Conversions.basicResult(res, "Task submitted"))
|
||||||
} yield resp
|
} 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" =>
|
case req @ POST -> Root / "search" =>
|
||||||
for {
|
for {
|
||||||
mask <- req.as[ItemSearch]
|
mask <- req.as[ItemSearch]
|
||||||
|
@ -5,14 +5,14 @@ import cats.effect.Sync
|
|||||||
import cats.effect.concurrent.Ref
|
import cats.effect.concurrent.Ref
|
||||||
import cats.implicits._
|
import cats.implicits._
|
||||||
import fs2.Stream
|
import fs2.Stream
|
||||||
|
|
||||||
import docspell.common.syntax.all._
|
import docspell.common.syntax.all._
|
||||||
import docspell.common.{IdRef, _}
|
import docspell.common.{IdRef, _}
|
||||||
|
import docspell.query.ItemQuery
|
||||||
import docspell.store.Store
|
import docspell.store.Store
|
||||||
import docspell.store.qb.DSL._
|
import docspell.store.qb.DSL._
|
||||||
import docspell.store.qb._
|
import docspell.store.qb._
|
||||||
|
import docspell.store.qb.generator.{ItemQueryGenerator, Tables}
|
||||||
import docspell.store.records._
|
import docspell.store.records._
|
||||||
|
|
||||||
import doobie.implicits._
|
import doobie.implicits._
|
||||||
import doobie.{Query => _, _}
|
import doobie.{Query => _, _}
|
||||||
import org.log4s.getLogger
|
import org.log4s.getLogger
|
||||||
@ -172,10 +172,13 @@ object QItem {
|
|||||||
.leftJoin(pers1, pers1.pid === i.concPerson && pers1.cid === coll)
|
.leftJoin(pers1, pers1.pid === i.concPerson && pers1.cid === coll)
|
||||||
.leftJoin(equip, equip.eid === i.concEquipment && equip.cid === coll),
|
.leftJoin(equip, equip.eid === i.concEquipment && equip.cid === coll),
|
||||||
where(
|
where(
|
||||||
i.cid === coll && or(
|
i.cid === coll &&? q.itemIds.map(s =>
|
||||||
i.folder.isNull,
|
Nel.fromList(s.toList).map(nel => i.id.in(nel)).getOrElse(i.id.isNull)
|
||||||
i.folder.in(QFolder.findMemberFolderIds(q.account))
|
|
||||||
)
|
)
|
||||||
|
&& or(
|
||||||
|
i.folder.isNull,
|
||||||
|
i.folder.in(QFolder.findMemberFolderIds(q.account))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
).distinct.orderBy(
|
).distinct.orderBy(
|
||||||
q.orderAsc
|
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 &&?
|
Condition.unit &&?
|
||||||
q.direction.map(d => i.incoming === d) &&?
|
q.direction.map(d => i.incoming === d) &&?
|
||||||
q.name.map(n => i.name.like(QueryWildcard.lower(n))) &&?
|
q.name.map(n => i.name.like(QueryWildcard.lower(n))) &&?
|
||||||
@ -221,6 +224,19 @@ object QItem {
|
|||||||
findCustomFieldValuesForColl(coll, q.customValues)
|
findCustomFieldValuesForColl(coll, q.customValues)
|
||||||
.map(itemIds => i.id.in(itemIds))
|
.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(
|
def findItems(
|
||||||
q: Query,
|
q: Query,
|
||||||
maxNoteLen: Int,
|
maxNoteLen: Int,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package docspell.store.queries
|
package docspell.store.queries
|
||||||
|
|
||||||
import docspell.common._
|
import docspell.common._
|
||||||
|
import docspell.query.ItemQuery
|
||||||
import docspell.store.qb.Column
|
import docspell.store.qb.Column
|
||||||
import docspell.store.records.RItem
|
import docspell.store.records.RItem
|
||||||
|
|
||||||
@ -9,14 +10,23 @@ case class Query(fix: Query.Fix, cond: Query.QueryCond) {
|
|||||||
copy(cond = f(cond))
|
copy(cond = f(cond))
|
||||||
|
|
||||||
def withOrder(orderAsc: RItem.Table => Column[_]): Query =
|
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 {
|
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],
|
name: Option[String],
|
||||||
states: Seq[ItemState],
|
states: Seq[ItemState],
|
||||||
direction: Option[Direction],
|
direction: Option[Direction],
|
||||||
@ -37,10 +47,10 @@ object Query {
|
|||||||
itemIds: Option[Set[Ident]],
|
itemIds: Option[Set[Ident]],
|
||||||
customValues: Seq[CustomValue],
|
customValues: Seq[CustomValue],
|
||||||
source: Option[String]
|
source: Option[String]
|
||||||
)
|
) extends QueryCond
|
||||||
object QueryCond {
|
object QueryForm {
|
||||||
val empty =
|
val empty =
|
||||||
QueryCond(
|
QueryForm(
|
||||||
None,
|
None,
|
||||||
Seq.empty,
|
Seq.empty,
|
||||||
None,
|
None,
|
||||||
@ -64,7 +74,9 @@ object Query {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case class QueryExpr(q: ItemQuery) extends QueryCond
|
||||||
|
|
||||||
def empty(account: AccountId): Query =
|
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