diff --git a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala
index 59d27658..b94fa742 100644
--- a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala
+++ b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala
@@ -11,6 +11,7 @@ import scala.concurrent.ExecutionContext
 import cats.effect._
 
 import docspell.backend.auth.Login
+import docspell.backend.fulltext.CreateIndex
 import docspell.backend.ops._
 import docspell.backend.signup.OSignup
 import docspell.ftsclient.FtsClient
@@ -69,7 +70,8 @@ object BackendApp {
       uploadImpl     <- OUpload(store, queue, cfg.files, joexImpl)
       nodeImpl       <- ONode(store)
       jobImpl        <- OJob(store, joexImpl)
-      itemImpl       <- OItem(store, ftsClient, queue, joexImpl)
+      createIndex    <- CreateIndex.resource(ftsClient, store)
+      itemImpl       <- OItem(store, ftsClient, createIndex, queue, joexImpl)
       itemSearchImpl <- OItemSearch(store)
       fulltextImpl   <- OFulltext(itemSearchImpl, ftsClient, store, queue, joexImpl)
       javaEmil =
diff --git a/modules/backend/src/main/scala/docspell/backend/fulltext/CreateIndex.scala b/modules/backend/src/main/scala/docspell/backend/fulltext/CreateIndex.scala
new file mode 100644
index 00000000..e0865cf1
--- /dev/null
+++ b/modules/backend/src/main/scala/docspell/backend/fulltext/CreateIndex.scala
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2020 Docspell Contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package docspell.backend.fulltext
+
+import cats.data.NonEmptyList
+import cats.effect._
+
+import docspell.common._
+import docspell.ftsclient.FtsClient
+import docspell.ftsclient.TextData
+import docspell.store.Store
+import docspell.store.queries.QAttachment
+import docspell.store.queries.QItem
+
+trait CreateIndex[F[_]] {
+
+  /** Low-level function to re-index data. It is not submitted as a job,
+    * but invoked on the current machine.
+    */
+  def reIndexData(
+      logger: Logger[F],
+      collective: Option[Ident],
+      itemIds: Option[NonEmptyList[Ident]],
+      chunkSize: Int
+  ): F[Unit]
+
+}
+
+object CreateIndex {
+
+  def resource[F[_]](fts: FtsClient[F], store: Store[F]): Resource[F, CreateIndex[F]] =
+    Resource.pure(apply(fts, store))
+
+  def apply[F[_]](fts: FtsClient[F], store: Store[F]): CreateIndex[F] =
+    new CreateIndex[F] {
+      def reIndexData(
+          logger: Logger[F],
+          collective: Option[Ident],
+          itemIds: Option[NonEmptyList[Ident]],
+          chunkSize: Int
+      ): F[Unit] = {
+        val attachs = store
+          .transact(QAttachment.allAttachmentMetaAndName(collective, itemIds, chunkSize))
+          .map(caa =>
+            TextData
+              .attachment(
+                caa.item,
+                caa.id,
+                caa.collective,
+                caa.folder,
+                caa.lang,
+                caa.name,
+                caa.content
+              )
+          )
+
+        val items = store
+          .transact(QItem.allNameAndNotes(collective, itemIds, chunkSize))
+          .map(nn =>
+            TextData.item(nn.id, nn.collective, nn.folder, Option(nn.name), nn.notes)
+          )
+
+        fts.indexData(logger, attachs ++ items)
+      }
+    }
+
+}
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala
index cc2ebfed..eb82a641 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala
@@ -11,6 +11,7 @@ import cats.effect.{Async, Resource}
 import cats.implicits._
 
 import docspell.backend.JobFactory
+import docspell.backend.fulltext.CreateIndex
 import docspell.common._
 import docspell.ftsclient.FtsClient
 import docspell.store.queries.{QAttachment, QItem, QMoveAttachment}
@@ -212,6 +213,7 @@ object OItem {
   def apply[F[_]: Async](
       store: Store[F],
       fts: FtsClient[F],
+      createIndex: CreateIndex[F],
       queue: JobQueue[F],
       joex: OJoex[F]
   ): Resource[F, OItem[F]] =
@@ -588,12 +590,13 @@ object OItem {
             items: NonEmptyList[Ident],
             collective: Ident
         ): F[UpdateResult] =
-          UpdateResult.fromUpdate(
-            store
+          UpdateResult.fromUpdate(for {
+            n <- store
               .transact(
                 RItem.restoreStateForCollective(items, ItemState.Created, collective)
               )
-          )
+            _ <- createIndex.reIndexData(logger, collective.some, items.some, 10)
+          } yield n)
 
         def setItemDate(
             items: NonEmptyList[Ident],
@@ -628,7 +631,10 @@ object OItem {
           } yield n
 
         def setDeletedState(items: NonEmptyList[Ident], collective: Ident): F[Int] =
-          store.transact(RItem.setState(items, collective, ItemState.Deleted))
+          for {
+            n <- store.transact(RItem.setState(items, collective, ItemState.Deleted))
+            _ <- items.traverse(id => fts.removeItem(logger, id))
+          } yield n
 
         def getProposals(item: Ident, collective: Ident): F[MetaProposalList] =
           store.transact(QAttachment.getMetaProposals(item, collective))
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala b/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala
index 2ae4cd14..a5fab21e 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala
@@ -231,13 +231,16 @@ object OSimpleSearch {
         case Some(ftq) if settings.useFTS =>
           if (q.isEmpty) {
             logger.debug(s"Using index only search: $fulltextQuery")
-            fts
-              .findIndexOnly(settings.maxNoteLen)(
-                OFulltext.FtsInput(ftq),
-                q.fix.account,
-                settings.batch
-              )
-              .map(Items.ftsItemsFull(true))
+            if (settings.searchMode == SearchMode.Trashed)
+              Items.ftsItemsFull(true)(Vector.empty).pure[F]
+            else
+              fts
+                .findIndexOnly(settings.maxNoteLen)(
+                  OFulltext.FtsInput(ftq),
+                  q.fix.account,
+                  settings.batch
+                )
+                .map(Items.ftsItemsFull(true))
           } else if (settings.resolveDetails) {
             logger.debug(
               s"Using index+sql search with tags: $validItemQuery / $fulltextQuery"
diff --git a/modules/fts-client/src/main/scala/docspell/ftsclient/TextData.scala b/modules/fts-client/src/main/scala/docspell/ftsclient/TextData.scala
index cb92e095..16de2515 100644
--- a/modules/fts-client/src/main/scala/docspell/ftsclient/TextData.scala
+++ b/modules/fts-client/src/main/scala/docspell/ftsclient/TextData.scala
@@ -72,4 +72,5 @@ object TextData {
       notes: Option[String]
   ): TextData =
     Item(item, collective, folder, name, notes)
+
 }
diff --git a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala
index 37113d5b..e03a05aa 100644
--- a/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala
+++ b/modules/joex/src/main/scala/docspell/joex/JoexAppImpl.scala
@@ -13,6 +13,7 @@ import cats.implicits._
 import fs2.concurrent.SignallingRef
 
 import docspell.analysis.TextAnalyser
+import docspell.backend.fulltext.CreateIndex
 import docspell.backend.ops._
 import docspell.common._
 import docspell.ftsclient.FtsClient
@@ -116,7 +117,8 @@ object JoexAppImpl {
       joex          <- OJoex(client, store)
       upload        <- OUpload(store, queue, cfg.files, joex)
       fts           <- createFtsClient(cfg)(httpClient)
-      itemOps       <- OItem(store, fts, queue, joex)
+      createIndex   <- CreateIndex.resource(fts, store)
+      itemOps       <- OItem(store, fts, createIndex, queue, joex)
       itemSearchOps <- OItemSearch(store)
       analyser      <- TextAnalyser.create[F](cfg.textAnalysis.textAnalysisConfig)
       regexNer      <- RegexNerFile(cfg.textAnalysis.regexNerFileConfig, store)
@@ -155,14 +157,14 @@ object JoexAppImpl {
         .withTask(
           JobTask.json(
             MigrationTask.taskName,
-            MigrationTask[F](cfg.fullTextSearch, fts),
+            MigrationTask[F](cfg.fullTextSearch, fts, createIndex),
             MigrationTask.onCancel[F]
           )
         )
         .withTask(
           JobTask.json(
             ReIndexTask.taskName,
-            ReIndexTask[F](cfg.fullTextSearch, fts),
+            ReIndexTask[F](cfg.fullTextSearch, fts, createIndex),
             ReIndexTask.onCancel[F]
           )
         )
diff --git a/modules/joex/src/main/scala/docspell/joex/fts/FtsContext.scala b/modules/joex/src/main/scala/docspell/joex/fts/FtsContext.scala
index 97012dfc..201dbaf7 100644
--- a/modules/joex/src/main/scala/docspell/joex/fts/FtsContext.scala
+++ b/modules/joex/src/main/scala/docspell/joex/fts/FtsContext.scala
@@ -6,6 +6,7 @@
 
 package docspell.joex.fts
 
+import docspell.backend.fulltext.CreateIndex
 import docspell.common.Logger
 import docspell.ftsclient.FtsClient
 import docspell.joex.Config
@@ -15,6 +16,7 @@ import docspell.store.Store
 case class FtsContext[F[_]](
     cfg: Config.FullTextSearch,
     store: Store[F],
+    fulltext: CreateIndex[F],
     fts: FtsClient[F],
     logger: Logger[F]
 )
@@ -24,7 +26,8 @@ object FtsContext {
   def apply[F[_]](
       cfg: Config.FullTextSearch,
       fts: FtsClient[F],
+      fulltext: CreateIndex[F],
       ctx: Context[F, _]
   ): FtsContext[F] =
-    FtsContext(cfg, ctx.store, fts, ctx.logger)
+    FtsContext(cfg, ctx.store, fulltext, fts, ctx.logger)
 }
diff --git a/modules/joex/src/main/scala/docspell/joex/fts/FtsWork.scala b/modules/joex/src/main/scala/docspell/joex/fts/FtsWork.scala
index da7fad95..9ce88c28 100644
--- a/modules/joex/src/main/scala/docspell/joex/fts/FtsWork.scala
+++ b/modules/joex/src/main/scala/docspell/joex/fts/FtsWork.scala
@@ -10,11 +10,11 @@ import cats._
 import cats.data.{Kleisli, NonEmptyList}
 import cats.implicits._
 
+import docspell.backend.fulltext.CreateIndex
 import docspell.common._
 import docspell.ftsclient._
 import docspell.joex.Config
 import docspell.joex.scheduler.Context
-import docspell.store.queries.{QAttachment, QItem}
 
 object FtsWork {
   import syntax._
@@ -88,36 +88,8 @@ object FtsWork {
     log[F](_.info("Inserting all data to index")) ++ FtsWork
       .all(
         FtsWork(ctx =>
-          ctx.fts.indexData(
-            ctx.logger,
-            ctx.store
-              .transact(
-                QAttachment
-                  .allAttachmentMetaAndName(coll, ctx.cfg.migration.indexAllChunk)
-              )
-              .map(caa =>
-                TextData
-                  .attachment(
-                    caa.item,
-                    caa.id,
-                    caa.collective,
-                    caa.folder,
-                    caa.lang,
-                    caa.name,
-                    caa.content
-                  )
-              )
-          )
-        ),
-        FtsWork(ctx =>
-          ctx.fts.indexData(
-            ctx.logger,
-            ctx.store
-              .transact(QItem.allNameAndNotes(coll, ctx.cfg.migration.indexAllChunk * 5))
-              .map(nn =>
-                TextData.item(nn.id, nn.collective, nn.folder, Option(nn.name), nn.notes)
-              )
-          )
+          ctx.fulltext
+            .reIndexData(ctx.logger, coll, None, ctx.cfg.migration.indexAllChunk)
         )
       )
 
@@ -133,9 +105,10 @@ object FtsWork {
 
       def forContext(
           cfg: Config.FullTextSearch,
-          fts: FtsClient[F]
+          fts: FtsClient[F],
+          fulltext: CreateIndex[F]
       ): Kleisli[F, Context[F, _], Unit] =
-        mt.local(ctx => FtsContext(cfg, fts, ctx))
+        mt.local(ctx => FtsContext(cfg, fts, fulltext, ctx))
     }
   }
 }
diff --git a/modules/joex/src/main/scala/docspell/joex/fts/Migration.scala b/modules/joex/src/main/scala/docspell/joex/fts/Migration.scala
index a3c64382..d23ab072 100644
--- a/modules/joex/src/main/scala/docspell/joex/fts/Migration.scala
+++ b/modules/joex/src/main/scala/docspell/joex/fts/Migration.scala
@@ -11,6 +11,7 @@ import cats.effect._
 import cats.implicits._
 import cats.{Applicative, FlatMap, Traverse}
 
+import docspell.backend.fulltext.CreateIndex
 import docspell.common._
 import docspell.ftsclient._
 import docspell.joex.Config
@@ -38,9 +39,10 @@ object Migration {
       cfg: Config.FullTextSearch,
       fts: FtsClient[F],
       store: Store[F],
+      createIndex: CreateIndex[F],
       logger: Logger[F]
   ): Kleisli[F, List[Migration[F]], Unit] = {
-    val ctx = FtsContext(cfg, store, fts, logger)
+    val ctx = FtsContext(cfg, store, createIndex, fts, logger)
     Kleisli { migs =>
       if (migs.isEmpty) logger.info("No fulltext search migrations to run.")
       else Traverse[List].sequence(migs.map(applySingle[F](ctx))).map(_ => ())
diff --git a/modules/joex/src/main/scala/docspell/joex/fts/MigrationTask.scala b/modules/joex/src/main/scala/docspell/joex/fts/MigrationTask.scala
index c303118f..c56e57e2 100644
--- a/modules/joex/src/main/scala/docspell/joex/fts/MigrationTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/fts/MigrationTask.scala
@@ -9,6 +9,7 @@ package docspell.joex.fts
 import cats.effect._
 import cats.implicits._
 
+import docspell.backend.fulltext.CreateIndex
 import docspell.common._
 import docspell.ftsclient._
 import docspell.joex.Config
@@ -20,7 +21,8 @@ object MigrationTask {
 
   def apply[F[_]: Async](
       cfg: Config.FullTextSearch,
-      fts: FtsClient[F]
+      fts: FtsClient[F],
+      createIndex: CreateIndex[F]
   ): Task[F, Unit, Unit] =
     Task
       .log[F, Unit](_.info(s"Running full-text-index migrations now"))
@@ -28,7 +30,7 @@ object MigrationTask {
         Task(ctx =>
           for {
             migs <- migrationTasks[F](fts)
-            res  <- Migration[F](cfg, fts, ctx.store, ctx.logger).run(migs)
+            res  <- Migration[F](cfg, fts, ctx.store, createIndex, ctx.logger).run(migs)
           } yield res
         )
       )
diff --git a/modules/joex/src/main/scala/docspell/joex/fts/ReIndexTask.scala b/modules/joex/src/main/scala/docspell/joex/fts/ReIndexTask.scala
index e5e7cf48..42a9e9ad 100644
--- a/modules/joex/src/main/scala/docspell/joex/fts/ReIndexTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/fts/ReIndexTask.scala
@@ -8,6 +8,7 @@ package docspell.joex.fts
 
 import cats.effect._
 
+import docspell.backend.fulltext.CreateIndex
 import docspell.common._
 import docspell.ftsclient._
 import docspell.joex.Config
@@ -22,12 +23,15 @@ object ReIndexTask {
 
   def apply[F[_]: Async](
       cfg: Config.FullTextSearch,
-      fts: FtsClient[F]
+      fts: FtsClient[F],
+      fulltext: CreateIndex[F]
   ): Task[F, Args, Unit] =
     Task
       .log[F, Args](_.info(s"Running full-text re-index now"))
       .flatMap(_ =>
-        Task(ctx => clearData[F](ctx.args.collective).forContext(cfg, fts).run(ctx))
+        Task(ctx =>
+          clearData[F](ctx.args.collective).forContext(cfg, fts, fulltext).run(ctx)
+        )
       )
 
   def onCancel[F[_]]: Task[F, Args, Unit] =
diff --git a/modules/store/src/main/scala/docspell/store/queries/QAttachment.scala b/modules/store/src/main/scala/docspell/store/queries/QAttachment.scala
index 2085b62e..81674e8c 100644
--- a/modules/store/src/main/scala/docspell/store/queries/QAttachment.scala
+++ b/modules/store/src/main/scala/docspell/store/queries/QAttachment.scala
@@ -7,6 +7,7 @@
 package docspell.store.queries
 
 import cats.data.OptionT
+import cats.data.{NonEmptyList => Nel}
 import cats.effect.Sync
 import cats.implicits._
 import fs2.Stream
@@ -174,6 +175,7 @@ object QAttachment {
   )
   def allAttachmentMetaAndName(
       coll: Option[Ident],
+      itemIds: Option[Nel[Ident]],
       chunkSize: Int
   ): Stream[ConnectionIO, ContentAndName] =
     Select(
@@ -190,8 +192,11 @@ object QAttachment {
         .innerJoin(am, am.id === a.id)
         .innerJoin(item, item.id === a.itemId)
         .innerJoin(c, c.id === item.cid)
-    ).where(coll.map(cid => item.cid === cid))
-      .build
+    ).where(
+      item.state.in(ItemState.validStates) &&?
+        itemIds.map(ids => item.id.in(ids)) &&?
+        coll.map(cid => item.cid === cid)
+    ).build
       .query[ContentAndName]
       .streamWithChunkSize(chunkSize)
 
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 0aefa0db..402c9612 100644
--- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala
+++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala
@@ -502,6 +502,7 @@ object QItem {
         .leftJoin(m3, m3.id === r.fileId),
       where(
         i.cid === collective &&
+          i.state.in(ItemState.validStates) &&
           Condition.Or(fms.map(m => m.checksum === checksum)) &&?
           Nel
             .fromList(excludeFileMeta.toList)
@@ -519,6 +520,7 @@ object QItem {
   )
   def allNameAndNotes(
       coll: Option[Ident],
+      itemIds: Option[Nel[Ident]],
       chunkSize: Int
   ): Stream[ConnectionIO, NameAndNotes] = {
     val i = RItem.as("i")
@@ -526,8 +528,11 @@ object QItem {
     Select(
       select(i.id, i.cid, i.folder, i.name, i.notes),
       from(i)
-    ).where(coll.map(cid => i.cid === cid))
-      .build
+    ).where(
+      i.state.in(ItemState.validStates) &&?
+        itemIds.map(ids => i.id.in(ids)) &&?
+        coll.map(cid => i.cid === cid)
+    ).build
       .query[NameAndNotes]
       .streamWithChunkSize(chunkSize)
   }