diff --git a/CHANGES.md b/CHANGES.md
index f5a0d5dbb1..9a3d06617b 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -14,9 +14,11 @@ Improvements 🙌:
  - Room profile: BigImageViewerActivity now only display the image. Use the room setting to change or delete the room Avatar
  - Better visibility of text reactions in dark theme (#1118)
  - Room member profile: Add action to create (or open) a DM (#2310)
+ - Highlight text in the body of the displayed result (#2200)
 
 Bugfix 🐛:
  - Messages encrypted with no way to decrypt after SDK update from 0.18 to 1.0.0 (#2252)
+ - Search Result | scroll jumps after pagination (#2238)
 
 Translations 🗣:
  -
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt
index 10dc9254d8..201e9a4f82 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt
@@ -52,8 +52,6 @@ class SearchFragment @Inject constructor(
     private val fragmentArgs: SearchArgs by args()
     private val searchViewModel: SearchViewModel by fragmentViewModel()
 
-    private var pendingScrollToPosition: Int? = null
-
     override fun getLayoutResId() = R.layout.fragment_search
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -70,12 +68,6 @@ class SearchFragment @Inject constructor(
         searchResultRecycler.configureWith(controller, showDivider = false)
         (searchResultRecycler.layoutManager as? LinearLayoutManager)?.stackFromEnd = true
         controller.listener = this
-
-        controller.addModelBuildListener {
-            pendingScrollToPosition?.let {
-                searchResultRecycler.smoothScrollToPosition(it)
-            }
-        }
     }
 
     override fun onDestroy() {
@@ -100,10 +92,8 @@ class SearchFragment @Inject constructor(
                 }
             }
         } else {
-            pendingScrollToPosition = (state.lastBatchSize - 1).coerceAtLeast(0)
-
-            stateView.state = StateView.State.Content
             controller.setData(state)
+            stateView.state = StateView.State.Content
         }
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt
index c917c4557d..b927fb5ff3 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt
@@ -16,16 +16,24 @@
 
 package im.vector.app.features.home.room.detail.search
 
+import android.graphics.Typeface
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.style.StyleSpan
+import com.airbnb.epoxy.EpoxyModel
 import com.airbnb.epoxy.TypedEpoxyController
 import com.airbnb.epoxy.VisibilityState
+import im.vector.app.R
 import im.vector.app.core.date.DateFormatKind
 import im.vector.app.core.date.VectorDateFormatter
 import im.vector.app.core.epoxy.loadingItem
-import im.vector.app.core.ui.list.genericItemHeader
+import im.vector.app.core.epoxy.noResultItem
+import im.vector.app.core.resources.StringProvider
+import im.vector.app.core.ui.list.GenericItemHeader_
 import im.vector.app.features.home.AvatarRenderer
 import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.Event
-import org.matrix.android.sdk.api.session.search.EventAndSender
 import org.matrix.android.sdk.api.util.toMatrixItem
 import java.util.Calendar
 import javax.inject.Inject
@@ -33,6 +41,7 @@ import javax.inject.Inject
 class SearchResultController @Inject constructor(
         private val session: Session,
         private val avatarRenderer: AvatarRenderer,
+        private val stringProvider: StringProvider,
         private val dateFormatter: VectorDateFormatter
 ) : TypedEpoxyController<SearchViewState>() {
 
@@ -52,6 +61,8 @@ class SearchResultController @Inject constructor(
     override fun buildModels(data: SearchViewState?) {
         data ?: return
 
+        val searchItems = buildSearchResultItems(data)
+
         if (data.hasMoreResult) {
             loadingItem {
                 // Always use a different id, because we can be notified several times of visibility state changed
@@ -62,35 +73,85 @@ class SearchResultController @Inject constructor(
                     }
                 }
             }
+        } else {
+            if (searchItems.isEmpty()) {
+                // All returned results by the server has been filtered out and there is no more result
+                noResultItem {
+                    id("noResult")
+                    text(stringProvider.getString(R.string.no_result_placeholder))
+                }
+            } else {
+                noResultItem {
+                    id("noMoreResult")
+                    text(stringProvider.getString(R.string.no_more_results))
+                }
+            }
         }
 
-        buildSearchResultItems(data.searchResult)
+        searchItems.forEach { add(it) }
     }
 
-    private fun buildSearchResultItems(events: List<EventAndSender>) {
+    /**
+     * @return the list of EpoxyModel (date items and search result items), or an empty list if all items have been filtered out
+     */
+    private fun buildSearchResultItems(data: SearchViewState): List<EpoxyModel<*>> {
         var lastDate: Calendar? = null
+        val result = mutableListOf<EpoxyModel<*>>()
+
+        data.searchResult.forEach { eventAndSender ->
+            val event = eventAndSender.event
+
+            @Suppress("UNCHECKED_CAST")
+            // Take new content first
+            val text = ((event.content?.get("m.new_content") as? Content) ?: event.content)?.get("body") as? String ?: return@forEach
+            val spannable = setHighLightedText(text, data.highlights) ?: return@forEach
 
-        events.forEach { eventAndSender ->
             val eventDate = Calendar.getInstance().apply {
                 timeInMillis = eventAndSender.event.originServerTs ?: System.currentTimeMillis()
             }
             if (lastDate?.get(Calendar.DAY_OF_YEAR) != eventDate.get(Calendar.DAY_OF_YEAR)) {
-                genericItemHeader {
-                    id(eventDate.hashCode())
-                    text(dateFormatter.format(eventDate.timeInMillis, DateFormatKind.EDIT_HISTORY_HEADER))
-                }
+                GenericItemHeader_()
+                        .id(eventDate.hashCode())
+                        .text(dateFormatter.format(eventDate.timeInMillis, DateFormatKind.EDIT_HISTORY_HEADER))
+                        .let { result.add(it) }
             }
             lastDate = eventDate
 
-            searchResultItem {
-                id(eventAndSender.event.eventId)
-                avatarRenderer(avatarRenderer)
-                dateFormatter(dateFormatter)
-                event(eventAndSender.event)
-                sender(eventAndSender.sender
-                        ?: eventAndSender.event.senderId?.let { session.getUser(it) }?.toMatrixItem())
-                listener { listener?.onItemClicked(eventAndSender.event) }
+            SearchResultItem_()
+                    .id(eventAndSender.event.eventId)
+                    .avatarRenderer(avatarRenderer)
+                    .formattedDate(dateFormatter.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE))
+                    .spannable(spannable)
+                    .sender(eventAndSender.sender
+                            ?: eventAndSender.event.senderId?.let { session.getUser(it) }?.toMatrixItem())
+                    .listener { listener?.onItemClicked(eventAndSender.event) }
+                    .let { result.add(it) }
+        }
+
+        return result
+    }
+
+    /**
+     * Highlight the text. If the text is not found, return null to ignore this result
+     * See https://github.com/matrix-org/synapse/issues/8686
+     */
+    private fun setHighLightedText(text: String, highlights: List<String>): Spannable? {
+        val wordToSpan: Spannable = SpannableString(text)
+        var found = false
+        highlights.forEach { highlight ->
+            var searchFromIndex = 0
+            while (searchFromIndex < text.length) {
+                val indexOfHighlight = text.indexOf(highlight, searchFromIndex, ignoreCase = true)
+                searchFromIndex = if (indexOfHighlight == -1) {
+                    Integer.MAX_VALUE
+                } else {
+                    // bold
+                    found = true
+                    wordToSpan.setSpan(StyleSpan(Typeface.BOLD), indexOfHighlight, indexOfHighlight + highlight.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+                    indexOfHighlight + 1
+                }
             }
         }
+        return wordToSpan.takeIf { found }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt
index 10407c64e0..a3e5983c3a 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt
@@ -21,23 +21,20 @@ import android.widget.TextView
 import com.airbnb.epoxy.EpoxyAttribute
 import com.airbnb.epoxy.EpoxyModelClass
 import im.vector.app.R
-import im.vector.app.core.date.DateFormatKind
-import im.vector.app.core.date.VectorDateFormatter
 import im.vector.app.core.epoxy.ClickListener
 import im.vector.app.core.epoxy.VectorEpoxyHolder
 import im.vector.app.core.epoxy.VectorEpoxyModel
 import im.vector.app.core.epoxy.onClick
 import im.vector.app.core.extensions.setTextOrHide
 import im.vector.app.features.home.AvatarRenderer
-import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.util.MatrixItem
 
 @EpoxyModelClass(layout = R.layout.item_search_result)
 abstract class SearchResultItem : VectorEpoxyModel<SearchResultItem.Holder>() {
 
     @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
-    @EpoxyAttribute var dateFormatter: VectorDateFormatter? = null
-    @EpoxyAttribute lateinit var event: Event
+    @EpoxyAttribute var formattedDate: String? = null
+    @EpoxyAttribute lateinit var spannable: CharSequence
     @EpoxyAttribute var sender: MatrixItem? = null
     @EpoxyAttribute var listener: ClickListener? = null
 
@@ -47,9 +44,8 @@ abstract class SearchResultItem : VectorEpoxyModel<SearchResultItem.Holder>() {
         holder.view.onClick(listener)
         sender?.let { avatarRenderer.render(it, holder.avatarImageView) }
         holder.memberNameView.setTextOrHide(sender?.getBestName())
-        holder.timeView.text = dateFormatter?.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
-        // TODO Improve that (use formattedBody, etc.)
-        holder.contentView.text = event.content?.get("body") as? String
+        holder.timeView.text = formattedDate
+        holder.contentView.text = spannable
     }
 
     class Holder : VectorEpoxyHolder() {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt
index f61bcbd029..ab440f6b5f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt
@@ -145,6 +145,7 @@ class SearchViewModel @AssistedInject constructor(
         setState {
             copy(
                     searchResult = accumulatedResult,
+                    highlights = searchResult.highlights.orEmpty(),
                     hasMoreResult = !nextBatch.isNullOrEmpty(),
                     lastBatchSize = searchResult.results.orEmpty().size,
                     asyncSearchRequest = Success(Unit)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt
index 9f700b6e31..41fecbb5e2 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt
@@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.search.EventAndSender
 data class SearchViewState(
         // Accumulated search result
         val searchResult: List<EventAndSender> = emptyList(),
+        val highlights: List<String> = emptyList(),
         val hasMoreResult: Boolean = false,
         // Last batch size, will help RecyclerView to position itself
         val lastBatchSize: Int = 0,
diff --git a/vector/src/main/res/layout/fragment_search.xml b/vector/src/main/res/layout/fragment_search.xml
index 330e70d86b..84547a4355 100644
--- a/vector/src/main/res/layout/fragment_search.xml
+++ b/vector/src/main/res/layout/fragment_search.xml
@@ -8,8 +8,10 @@
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/searchResultRecycler"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom"
         android:overScrollMode="always"
+        tools:itemCount="2"
         tools:listitem="@layout/item_search_result" />
 
 </im.vector.app.core.platform.StateView>
\ No newline at end of file
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index c5a11d5ab4..45d9d40ba6 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -173,6 +173,7 @@
     <string name="no_conversation_placeholder">No conversations</string>
     <string name="no_contact_access_placeholder">You didn’t allow Element to access your local contacts</string>
     <string name="no_result_placeholder">No results</string>
+    <string name="no_more_results">No more results</string>
     <string name="people_no_identity_server">No identity server configured.</string>
 
     <!-- Rooms fragment -->