Basic poc to search via custom query

This commit is contained in:
Eike Kettner 2021-02-25 22:40:17 +01:00
parent 186014a1c6
commit e9ed998e3a
10 changed files with 97 additions and 23 deletions

View File

@ -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

View File

@ -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 {}

View File

@ -0,0 +1,3 @@
package docspell.common
case class ItemQueryString(query: String)

View File

@ -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,

View File

@ -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 =

View File

@ -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,

View File

@ -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")
} }

View File

@ -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]

View File

@ -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,

View File

@ -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)
} }