diff --git a/keyserver/src/database/search-utils.js b/keyserver/src/database/search-utils.js
--- a/keyserver/src/database/search-utils.js
+++ b/keyserver/src/database/search-utils.js
@@ -134,9 +134,25 @@
   }
 }
 
+const fulltextOperands = ['+', '<', '>', '~'];
+
+function processQueryForSearch(query: string): string {
+  if (query === '') {
+    return '';
+  }
+  const stemmedQuery = segmentAndStem(query);
+
+  return stemmedQuery
+    .split(' ')
+    .filter(word => !fulltextOperands.includes(word))
+    .map(segment => `+${segment}`)
+    .join(' ');
+}
+
 export {
   processMessagesForSearch,
   processMessagesInDBForSearch,
   segmentAndStem,
   stopwords,
+  processQueryForSearch,
 };
diff --git a/keyserver/src/database/search-utils.test.js b/keyserver/src/database/search-utils.test.js
--- a/keyserver/src/database/search-utils.test.js
+++ b/keyserver/src/database/search-utils.test.js
@@ -1,5 +1,10 @@
 // @flow
-import { segmentAndStem, stopwords } from './search-utils.js';
+
+import {
+  segmentAndStem,
+  stopwords,
+  processQueryForSearch,
+} from './search-utils.js';
 
 const alphaNumericRegex = /^[A-Za-z0-9 ]*$/;
 const lowerCaseRegex = /^[a-z ]*$/;
@@ -50,3 +55,14 @@
     );
   });
 });
+
+describe('processQueryForSearch(query: string)', () => {
+  it('should add + before every word', () => {
+    expect(processQueryForSearch('test')).toBe('+test');
+    expect(processQueryForSearch('test hello')).toBe('+test +hello');
+    expect(processQueryForSearch('test  \nhello')).toBe('+test +hello');
+  });
+  it('should remove + < > ~ from the query', () => {
+    expect(processQueryForSearch('+ < > ~')).toBe('');
+  });
+});
diff --git a/keyserver/src/fetchers/message-fetchers.js b/keyserver/src/fetchers/message-fetchers.js
--- a/keyserver/src/fetchers/message-fetchers.js
+++ b/keyserver/src/fetchers/message-fetchers.js
@@ -26,6 +26,7 @@
   type FetchPinnedMessagesResult,
   isMessageSidebarSourceReactionOrEdit,
 } from 'lib/types/message-types.js';
+import { defaultNumberPerThread } from 'lib/types/message-types.js';
 import { threadPermissions } from 'lib/types/thread-types.js';
 import { ServerError } from 'lib/utils/errors.js';
 
@@ -39,6 +40,7 @@
   mergeOrConditions,
   mergeAndConditions,
 } from '../database/database.js';
+import { processQueryForSearch } from '../database/search-utils.js';
 import type { SQLStatementType } from '../database/types.js';
 import type { PushInfo } from '../push/send.js';
 import type { Viewer } from '../session/viewer.js';
@@ -906,6 +908,60 @@
   return [...rawMessageInfos, ...rawRelatedMessageInfos];
 }
 
+const searchMessagesPageSize: number = defaultNumberPerThread + 1;
+
+async function searchMessagesInSingleChat(
+  inputQuery: string,
+  threadID: string,
+  viewer?: Viewer,
+  cursor?: string,
+): Promise<$ReadOnlyArray<RawMessageInfo>> {
+  if (inputQuery === '') {
+    console.warn('received empty search query');
+    return [];
+  }
+  const pattern = processQueryForSearch(inputQuery);
+
+  const query = SQL`
+    SELECT m.id, m.thread AS threadID, m.content, m.time, m.type, m.creation,
+    m.user AS creatorID, m.target_message as targetMessageID,
+    stm.permissions AS subthread_permissions, up.id AS uploadID,
+    up.type AS uploadType, up.secret AS uploadSecret, up.extra AS uploadExtra
+    FROM message_search s
+    LEFT JOIN messages m ON m.id = s.original_message_id
+    LEFT JOIN memberships stm ON m.type = ${messageTypes.CREATE_SUB_THREAD}
+      AND stm.thread = m.content AND stm.user = m.user
+    LEFT JOIN uploads up ON up.container = m.id
+    LEFT JOIN messages m2 ON m2.target_message = m.id 
+      AND m2.type = ${messageTypes.SIDEBAR_SOURCE} AND m2.thread = ${threadID}
+    WHERE MATCH(s.processed_content) AGAINST(${pattern} IN BOOLEAN MODE)
+      AND (m.thread = ${threadID} OR m2.id IS NOT NULL)
+  `;
+
+  if (cursor) {
+    query.append(SQL`AND m.id < ${cursor} `);
+  }
+  query.append(SQL`   
+    ORDER BY m.time DESC, m.id DESC
+    LIMIT ${searchMessagesPageSize}
+  `);
+
+  const [results] = await dbQuery(query);
+  if (results.length === 0) {
+    return [];
+  }
+
+  const rawMessageInfos = await rawMessageInfoForRowsAndRelatedMessages(
+    results,
+    viewer,
+  );
+
+  return shimUnsupportedRawMessageInfos(
+    rawMessageInfos,
+    viewer?.platformDetails,
+  );
+}
+
 export {
   fetchCollapsableNotifs,
   fetchMessageInfos,
@@ -919,4 +975,6 @@
   fetchPinnedMessageInfos,
   fetchRelatedMessages,
   rawMessageInfoForRowsAndRelatedMessages,
+  searchMessagesInSingleChat,
+  searchMessagesPageSize,
 };