diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OFulltext.scala b/modules/backend/src/main/scala/docspell/backend/ops/OFulltext.scala index beb1ca82..18501aad 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OFulltext.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OFulltext.scala @@ -60,7 +60,8 @@ object OFulltext { } yield () def findItems(q: Query, ftsQ: String, batch: Batch): F[Vector[ListItem]] = - findItemsFts(q, ftsQ, batch, itemSearch.findItems) + findItemsFts(q, ftsQ, batch.first, itemSearch.findItems) + .drop(batch.offset.toLong) .take(batch.limit.toLong) .compile .toVector @@ -70,28 +71,33 @@ object OFulltext { ftsQ: String, batch: Batch ): F[Vector[ListItemWithTags]] = - findItemsFts(q, ftsQ, batch, itemSearch.findItemsWithTags) + findItemsFts(q, ftsQ, batch.first, itemSearch.findItemsWithTags) + .drop(batch.offset.toLong) .take(batch.limit.toLong) .compile .toVector - private def findItemsFts[A]( + private def findItemsFts[A: ItemId]( q: Query, ftsQ: String, batch: Batch, search: (Query, Batch) => F[Vector[A]] ): Stream[F, A] = { - val fq = FtsQuery(ftsQ, q.collective, Nil, batch.limit, batch.offset) + + val sqlResult = search(q, batch) + val fq = FtsQuery(ftsQ, q.collective, Set.empty, batch.limit, batch.offset) val qres = for { - items <- + items <- sqlResult + ids = items.map(a => ItemId[A].itemId(a)) + ftsQ = fq.copy(items = ids.toSet) + ftsR <- fts - .search(fq) + .search(ftsQ) .map(_.results.map(_.itemId)) .map(_.toSet) - sq = q.copy(itemIds = Some(items)) - res <- search(sq, batch) + res = items.filter(a => ftsR.contains(ItemId[A].itemId(a))) } yield res Stream.eval(qres).flatMap { v => @@ -100,6 +106,23 @@ object OFulltext { else results ++ findItemsFts(q, ftsQ, batch.next, search) } } - }) + + trait ItemId[A] { + def itemId(a: A): Ident + } + object ItemId { + def apply[A](implicit ev: ItemId[A]): ItemId[A] = ev + + def from[A](f: A => Ident): ItemId[A] = + new ItemId[A] { + def itemId(a: A) = f(a) + } + + implicit val listItemId: ItemId[ListItem] = + ItemId.from(_.id) + + implicit val listItemWithTagsId: ItemId[ListItemWithTags] = + ItemId.from(_.item.id) + } } diff --git a/modules/fts-client/src/main/scala/docspell/ftsclient/FtsClient.scala b/modules/fts-client/src/main/scala/docspell/ftsclient/FtsClient.scala index a1bec931..b678b27f 100644 --- a/modules/fts-client/src/main/scala/docspell/ftsclient/FtsClient.scala +++ b/modules/fts-client/src/main/scala/docspell/ftsclient/FtsClient.scala @@ -1,6 +1,9 @@ package docspell.ftsclient import fs2.Stream +import cats.implicits._ +import cats.effect._ +import org.log4s.getLogger import docspell.common._ /** The fts client is the interface for docspell to a fulltext search @@ -90,3 +93,29 @@ trait FtsClient[F[_]] { def clear(logger: Logger[F], collective: Ident): F[Unit] } + +object FtsClient { + + def none[F[_]: Sync] = + new FtsClient[F] { + private[this] val logger = Logger.log4s[F](getLogger) + + def initialize: F[Unit] = + logger.info("Full-text search is disabled!") + + def search(q: FtsQuery): F[FtsResult] = + logger.warn("Full-text search is disabled!") *> FtsResult.empty.pure[F] + + def updateIndex(logger: Logger[F], data: Stream[F, TextData]): F[Unit] = + logger.warn("Full-text search is disabled!") + + def indexData(logger: Logger[F], data: Stream[F, TextData]): F[Unit] = + logger.warn("Full-text search is disabled!") + + def clearAll(logger: Logger[F]): F[Unit] = + logger.warn("Full-text search is disabled!") + + def clear(logger: Logger[F], collective: Ident): F[Unit] = + logger.warn("Full-text search is disabled!") + } +} diff --git a/modules/fts-client/src/main/scala/docspell/ftsclient/FtsQuery.scala b/modules/fts-client/src/main/scala/docspell/ftsclient/FtsQuery.scala index ca0d68a7..276d7589 100644 --- a/modules/fts-client/src/main/scala/docspell/ftsclient/FtsQuery.scala +++ b/modules/fts-client/src/main/scala/docspell/ftsclient/FtsQuery.scala @@ -13,7 +13,7 @@ import docspell.common._ final case class FtsQuery( q: String, collective: Ident, - items: List[Ident], + items: Set[Ident], limit: Int, offset: Int ) { diff --git a/modules/fts-client/src/main/scala/docspell/ftsclient/FtsResult.scala b/modules/fts-client/src/main/scala/docspell/ftsclient/FtsResult.scala index 8aa85dd3..cec71a51 100644 --- a/modules/fts-client/src/main/scala/docspell/ftsclient/FtsResult.scala +++ b/modules/fts-client/src/main/scala/docspell/ftsclient/FtsResult.scala @@ -14,6 +14,9 @@ final case class FtsResult( object FtsResult { + val empty = + FtsResult(Duration.millis(0), 0, 0.0, Map.empty, Nil) + sealed trait MatchData case class AttachmentData(attachId: Ident) extends MatchData case object ItemData extends MatchData diff --git a/modules/fts-solr/src/main/scala/docspell/ftssolr/QueryData.scala b/modules/fts-solr/src/main/scala/docspell/ftssolr/QueryData.scala index 8c877c3b..b2638ac3 100644 --- a/modules/fts-solr/src/main/scala/docspell/ftssolr/QueryData.scala +++ b/modules/fts-solr/src/main/scala/docspell/ftssolr/QueryData.scala @@ -39,7 +39,7 @@ object QueryData { val items = fq.items.map(_.id).mkString(" ") val collQ = s"""${Field.collectiveId.name}:"${fq.collective.id}"""" val filterQ = fq.items match { - case Nil => + case s if s.isEmpty => collQ case _ => (collQ :: List(s"""${Field.itemId.name}:($items)""")).mkString(" AND ") diff --git a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala index bb6728da..fe3fc586 100644 --- a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala +++ b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala @@ -3,6 +3,10 @@ package docspell.joex import cats.implicits._ import cats.effect._ import emil.javamail._ +import fs2.concurrent.SignallingRef +import scala.concurrent.ExecutionContext +import org.http4s.client.Client +import org.http4s.client.blaze.BlazeClientBuilder import docspell.common._ import docspell.backend.ops._ import docspell.joex.hk._ @@ -15,10 +19,8 @@ import docspell.joexapi.client.JoexClient import docspell.store.Store import docspell.store.queue._ import docspell.store.records.RJobLog +import docspell.ftsclient.FtsClient import docspell.ftssolr.SolrFtsClient -import fs2.concurrent.SignallingRef -import scala.concurrent.ExecutionContext -import org.http4s.client.blaze.BlazeClientBuilder final class JoexAppImpl[F[_]: ConcurrentEffect: ContextShift: Timer]( cfg: Config, @@ -78,7 +80,7 @@ object JoexAppImpl { nodeOps <- ONode(store) joex <- OJoex(client, store) upload <- OUpload(store, queue, cfg.files, joex) - fts <- SolrFtsClient(cfg.fullTextSearch.solr, httpClient) + fts <- createFtsClient(cfg)(httpClient) javaEmil = JavaMailEmil(blocker, Settings.defaultSettings.copy(debug = cfg.mailDebug)) sch <- SchedulerBuilder(cfg.scheduler, blocker, store) @@ -137,4 +139,10 @@ object JoexAppImpl { app = new JoexAppImpl(cfg, nodeOps, store, queue, pstore, termSignal, sch, psch) appR <- Resource.make(app.init.map(_ => app))(_.shutdown) } yield appR + + private def createFtsClient[F[_]: ConcurrentEffect: ContextShift]( + cfg: Config + )(client: Client[F]): Resource[F, FtsClient[F]] = + if (cfg.fullTextSearch.enabled) SolrFtsClient(cfg.fullTextSearch.solr, client) + else Resource.pure[F, FtsClient[F]](FtsClient.none[F]) } diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala b/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala index 25820e1f..220bf066 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala @@ -39,5 +39,6 @@ object RestAppImpl { private def createFtsClient[F[_]: ConcurrentEffect: ContextShift]( cfg: Config )(client: Client[F]): Resource[F, FtsClient[F]] = - SolrFtsClient(cfg.fullTextSearch.solr, client) + if (cfg.fullTextSearch.enabled) SolrFtsClient(cfg.fullTextSearch.solr, client) + else Resource.pure[F, FtsClient[F]](FtsClient.none[F]) } diff --git a/modules/restserver/src/main/scala/docspell/restserver/webapp/Flags.scala b/modules/restserver/src/main/scala/docspell/restserver/webapp/Flags.scala index 0dea6b6f..0ceb466c 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/webapp/Flags.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/webapp/Flags.scala @@ -13,7 +13,8 @@ case class Flags( baseUrl: LenientUri, signupMode: SignupConfig.Mode, docspellAssetPath: String, - integrationEnabled: Boolean + integrationEnabled: Boolean, + fullTextSearchEnabled: Boolean ) object Flags { @@ -23,7 +24,8 @@ object Flags { cfg.baseUrl, cfg.backend.signup.mode, s"/app/assets/docspell-webapp/${BuildInfo.version}", - cfg.integrationEndpoint.enabled + cfg.integrationEndpoint.enabled, + cfg.fullTextSearch.enabled ) implicit val jsonEncoder: Encoder[Flags] = diff --git a/modules/store/src/main/scala/docspell/store/queries/QItem.scala b/modules/store/src/main/scala/docspell/store/queries/QItem.scala index eaedb6c2..abb6202d 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala @@ -199,6 +199,9 @@ object QItem { def next: Batch = Batch(offset + limit, limit) + + def first: Batch = + Batch(0, limit) } object Batch { diff --git a/modules/webapp/src/main/elm/Data/Flags.elm b/modules/webapp/src/main/elm/Data/Flags.elm index 2be9c862..16fd0122 100644 --- a/modules/webapp/src/main/elm/Data/Flags.elm +++ b/modules/webapp/src/main/elm/Data/Flags.elm @@ -15,6 +15,7 @@ type alias Config = , signupMode : String , docspellAssetPath : String , integrationEnabled : Bool + , fullTextSearchEnabled : Bool }