diff --git a/src/base/preferences.cpp b/src/base/preferences.cpp
index ed9ecd316dc1..6a6b143edf70 100644
--- a/src/base/preferences.cpp
+++ b/src/base/preferences.cpp
@@ -655,6 +655,32 @@ void Preferences::setSearchEnabled(const bool enabled)
     setValue(u"Preferences/Search/SearchEnabled"_s, enabled);
 }
 
+bool Preferences::storeOpenedSearchTabs() const
+{
+    return value(u"Search/StoreOpenedSearchTabs"_s, false);
+}
+
+void Preferences::setStoreOpenedSearchTabs(const bool enabled)
+{
+    if (enabled == storeOpenedSearchTabs())
+        return;
+
+    setValue(u"Search/StoreOpenedSearchTabs"_s, enabled);
+}
+
+bool Preferences::storeOpenedSearchTabResults() const
+{
+    return value(u"Search/StoreOpenedSearchTabResults"_s, false);
+}
+
+void Preferences::setStoreOpenedSearchTabResults(const bool enabled)
+{
+    if (enabled == storeOpenedSearchTabResults())
+        return;
+
+    setValue(u"Search/StoreOpenedSearchTabResults"_s, enabled);
+}
+
 bool Preferences::isWebUIEnabled() const
 {
 #ifdef DISABLE_GUI
diff --git a/src/base/preferences.h b/src/base/preferences.h
index b1a912477bda..7e07446d6541 100644
--- a/src/base/preferences.h
+++ b/src/base/preferences.h
@@ -172,6 +172,12 @@ class Preferences final : public QObject
     bool isSearchEnabled() const;
     void setSearchEnabled(bool enabled);
 
+    // Search UI
+    bool storeOpenedSearchTabs() const;
+    void setStoreOpenedSearchTabs(bool enabled);
+    bool storeOpenedSearchTabResults() const;
+    void setStoreOpenedSearchTabResults(bool enabled);
+
     // HTTP Server
     bool isWebUIEnabled() const;
     void setWebUIEnabled(bool enabled);
diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp
index 676612146589..ba4ee232c8a3 100644
--- a/src/gui/optionsdialog.cpp
+++ b/src/gui/optionsdialog.cpp
@@ -164,6 +164,7 @@ OptionsDialog::OptionsDialog(IGUIApplication *app, QWidget *parent)
     m_ui->tabSelection->item(TAB_DOWNLOADS)->setIcon(UIThemeManager::instance()->getIcon(u"download"_s, u"folder-download"_s));
     m_ui->tabSelection->item(TAB_SPEED)->setIcon(UIThemeManager::instance()->getIcon(u"speedometer"_s, u"chronometer"_s));
     m_ui->tabSelection->item(TAB_RSS)->setIcon(UIThemeManager::instance()->getIcon(u"application-rss"_s, u"application-rss+xml"_s));
+    m_ui->tabSelection->item(TAB_SEARCH)->setIcon(UIThemeManager::instance()->getIcon(u"edit-find"_s));
 #ifdef DISABLE_WEBUI
     m_ui->tabSelection->item(TAB_WEBUI)->setHidden(true);
 #else
@@ -190,6 +191,7 @@ OptionsDialog::OptionsDialog(IGUIApplication *app, QWidget *parent)
     loadSpeedTabOptions();
     loadBittorrentTabOptions();
     loadRSSTabOptions();
+    loadSearchTabOptions();
 #ifndef DISABLE_WEBUI
     loadWebUITabOptions();
 #endif
@@ -1273,6 +1275,25 @@ void OptionsDialog::saveRSSTabOptions() const
     autoDownloader->setDownloadRepacks(m_ui->checkSmartFilterDownloadRepacks->isChecked());
 }
 
+void OptionsDialog::loadSearchTabOptions()
+{
+    const auto *pref = Preferences::instance();
+
+    m_ui->groupStoreOpenedTabs->setChecked(pref->storeOpenedSearchTabs());
+    m_ui->checkStoreTabsSearchResults->setChecked(pref->storeOpenedSearchTabResults());
+
+    connect(m_ui->groupStoreOpenedTabs, &QGroupBox::toggled, this, &OptionsDialog::enableApplyButton);
+    connect(m_ui->checkStoreTabsSearchResults, &QCheckBox::toggled, this, &OptionsDialog::enableApplyButton);
+}
+
+void OptionsDialog::saveSearchTabOptions() const
+{
+    auto *pref = Preferences::instance();
+
+    pref->setStoreOpenedSearchTabs(m_ui->groupStoreOpenedTabs->isChecked());
+    pref->setStoreOpenedSearchTabResults(m_ui->checkStoreTabsSearchResults->isChecked());
+}
+
 #ifndef DISABLE_WEBUI
 void OptionsDialog::loadWebUITabOptions()
 {
@@ -1465,6 +1486,7 @@ void OptionsDialog::saveOptions() const
     saveSpeedTabOptions();
     saveBittorrentTabOptions();
     saveRSSTabOptions();
+    saveSearchTabOptions();
 #ifndef DISABLE_WEBUI
     saveWebUITabOptions();
 #endif
diff --git a/src/gui/optionsdialog.h b/src/gui/optionsdialog.h
index 534ca4078ef8..c1e29c1d6c3c 100644
--- a/src/gui/optionsdialog.h
+++ b/src/gui/optionsdialog.h
@@ -73,6 +73,7 @@ class OptionsDialog final : public GUIApplicationComponent<QDialog>
         TAB_CONNECTION,
         TAB_SPEED,
         TAB_BITTORRENT,
+        TAB_SEARCH,
         TAB_RSS,
         TAB_WEBUI,
         TAB_ADVANCED
@@ -136,6 +137,9 @@ private slots:
     void loadRSSTabOptions();
     void saveRSSTabOptions() const;
 
+    void loadSearchTabOptions();
+    void saveSearchTabOptions() const;
+
 #ifndef DISABLE_WEBUI
     void loadWebUITabOptions();
     void saveWebUITabOptions() const;
diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui
index 16484c71705e..94309cd9a802 100644
--- a/src/gui/optionsdialog.ui
+++ b/src/gui/optionsdialog.ui
@@ -72,6 +72,11 @@
         <string>BitTorrent</string>
        </property>
       </item>
+      <item>
+       <property name="text">
+        <string>Search</string>
+       </property>
+      </item>
       <item>
        <property name="text">
         <string>RSS</string>
@@ -3210,6 +3215,85 @@ Disable encryption: Only connect to peers without protocol encryption</string>
         </item>
        </layout>
       </widget>
+      <widget class="QWidget" name="tabSearchPage">
+       <layout class="QVBoxLayout" name="searchPageLayout">
+        <property name="leftMargin">
+         <number>0</number>
+        </property>
+        <property name="topMargin">
+         <number>0</number>
+        </property>
+        <property name="rightMargin">
+         <number>0</number>
+        </property>
+        <property name="bottomMargin">
+         <number>0</number>
+        </property>
+        <item>
+         <widget class="QScrollArea" name="searchPageScrollArea">
+          <property name="widgetResizable">
+           <bool>true</bool>
+          </property>
+          <widget class="QWidget" name="searchPageScrollAreaWidgetContents">
+           <property name="geometry">
+            <rect>
+             <x>0</x>
+             <y>0</y>
+             <width>521</width>
+             <height>541</height>
+            </rect>
+           </property>
+           <layout class="QVBoxLayout" name="searchPageScrollAreaWidgetContentsLayout">
+            <item>
+             <widget class="QGroupBox" name="groupSearchUI">
+              <property name="title">
+               <string>Search UI</string>
+              </property>
+              <layout class="QVBoxLayout" name="groupSearchUILayout">
+               <item>
+                <widget class="QGroupBox" name="groupStoreOpenedTabs">
+                 <property name="title">
+                  <string>Store opened tabs</string>
+                 </property>
+                 <property name="checkable">
+                  <bool>true</bool>
+                 </property>
+                 <property name="checked">
+                  <bool>false</bool>
+                 </property>
+                 <layout class="QVBoxLayout" name="groupStoreOpenedTabsLayout">
+                  <item>
+                   <widget class="QCheckBox" name="checkStoreTabsSearchResults">
+                    <property name="text">
+                     <string>Also store search results</string>
+                    </property>
+                   </widget>
+                  </item>
+                 </layout>
+                </widget>
+               </item>
+              </layout>
+             </widget>
+            </item>
+            <item>
+             <spacer name="searchPageScrollAreaSpacer">
+              <property name="orientation">
+               <enum>Qt::Orientation::Vertical</enum>
+              </property>
+              <property name="sizeHint" stdset="0">
+               <size>
+                <width>20</width>
+                <height>422</height>
+               </size>
+              </property>
+             </spacer>
+            </item>
+           </layout>
+          </widget>
+         </widget>
+        </item>
+       </layout>
+      </widget>
       <widget class="QWidget" name="tabRSSPage">
        <layout class="QVBoxLayout" name="verticalLayout_25">
         <property name="leftMargin">
diff --git a/src/gui/search/searchjobwidget.cpp b/src/gui/search/searchjobwidget.cpp
index 75a9bdbc2822..aae330ce42fa 100644
--- a/src/gui/search/searchjobwidget.cpp
+++ b/src/gui/search/searchjobwidget.cpp
@@ -82,10 +82,11 @@ namespace
     }
 }
 
-SearchJobWidget::SearchJobWidget(SearchHandler *searchHandler, IGUIApplication *app, QWidget *parent)
+SearchJobWidget::SearchJobWidget(const QString &id, IGUIApplication *app, QWidget *parent)
     : GUIApplicationComponent(app, parent)
-    , m_ui {new Ui::SearchJobWidget}
     , m_nameFilteringMode {u"Search/FilteringMode"_s}
+    , m_id {id}
+    , m_ui {new Ui::SearchJobWidget}
 {
     m_ui->setupUi(this);
 
@@ -151,9 +152,6 @@ SearchJobWidget::SearchJobWidget(SearchHandler *searchHandler, IGUIApplication *
     connect(header(), &QHeaderView::sortIndicatorChanged, this, &SearchJobWidget::saveSettings);
 
     fillFilterComboBoxes();
-    setStatusTip(statusText(m_status));
-
-    assignSearchHandler(searchHandler);
 
     m_lineEditSearchResultsFilter = new LineEdit(this);
     m_lineEditSearchResultsFilter->setPlaceholderText(tr("Filter search results..."));
@@ -186,19 +184,42 @@ SearchJobWidget::SearchJobWidget(SearchHandler *searchHandler, IGUIApplication *
     connect(UIThemeManager::instance(), &UIThemeManager::themeChanged, this, &SearchJobWidget::onUIThemeChanged);
 }
 
+SearchJobWidget::SearchJobWidget(const QString &id, const QString &searchPattern
+        , const QList<SearchResult> &searchResults, IGUIApplication *app, QWidget *parent)
+    : SearchJobWidget(id, app, parent)
+{
+    m_searchPattern = searchPattern;
+    m_proxyModel->setNameFilter(m_searchPattern);
+    updateFilter();
+
+    appendSearchResults(searchResults);
+}
+
+SearchJobWidget::SearchJobWidget(const QString &id, SearchHandler *searchHandler, IGUIApplication *app, QWidget *parent)
+    : SearchJobWidget(id, app, parent)
+{
+    assignSearchHandler(searchHandler);
+}
+
 SearchJobWidget::~SearchJobWidget()
 {
     saveSettings();
     delete m_ui;
 }
 
+QString SearchJobWidget::id() const
+{
+    return m_id;
+}
+
 QString SearchJobWidget::searchPattern() const
 {
-    Q_ASSERT(m_searchHandler);
-    if (!m_searchHandler) [[unlikely]]
-        return {};
+    return m_searchPattern;
+}
 
-    return m_searchHandler->pattern();
+QList<SearchResult> SearchJobWidget::searchResults() const
+{
+    return m_searchResults;
 }
 
 void SearchJobWidget::onItemDoubleClicked(const QModelIndex &index)
@@ -264,6 +285,7 @@ void SearchJobWidget::assignSearchHandler(SearchHandler *searchHandler)
     if (!searchHandler) [[unlikely]]
         return;
 
+    m_searchResults.clear();
     m_searchListModel->removeRows(0, m_searchListModel->rowCount());
     delete m_searchHandler;
 
@@ -273,7 +295,9 @@ void SearchJobWidget::assignSearchHandler(SearchHandler *searchHandler)
     connect(m_searchHandler, &SearchHandler::searchFinished, this, &SearchJobWidget::searchFinished);
     connect(m_searchHandler, &SearchHandler::searchFailed, this, &SearchJobWidget::searchFailed);
 
-    m_proxyModel->setNameFilter(m_searchHandler->pattern());
+    m_searchPattern = m_searchHandler->pattern();
+
+    m_proxyModel->setNameFilter(m_searchPattern);
     updateFilter();
 
     setStatus(Status::Ongoing);
@@ -281,8 +305,7 @@ void SearchJobWidget::assignSearchHandler(SearchHandler *searchHandler)
 
 void SearchJobWidget::cancelSearch()
 {
-    Q_ASSERT(m_searchHandler);
-    if (!m_searchHandler) [[unlikely]]
+    if (!m_searchHandler)
         return;
 
     m_searchHandler->cancelSearch();
@@ -363,7 +386,7 @@ void SearchJobWidget::downloadTorrent(const QModelIndex &rowIndex, const AddTorr
     }
     else
     {
-        SearchDownloadHandler *downloadHandler = m_searchHandler->manager()->downloadTorrent(engineName, torrentUrl);
+        SearchDownloadHandler *downloadHandler = SearchPluginManager::instance()->downloadTorrent(engineName, torrentUrl);
         connect(downloadHandler, &SearchDownloadHandler::downloadFinished
             , this, [this, option](const QString &source) { addTorrentToSession(source, option); });
         connect(downloadHandler, &SearchDownloadHandler::downloadFinished, downloadHandler, &SearchDownloadHandler::deleteLater);
@@ -605,6 +628,7 @@ void SearchJobWidget::appendSearchResults(const QList<SearchResult> &results)
         setModelData(SearchSortModel::PUB_DATE, QLocale().toString(result.pubDate.toLocalTime(), QLocale::ShortFormat), result.pubDate);
     }
 
+    m_searchResults.append(results);
     updateResultsCount();
 }
 
diff --git a/src/gui/search/searchjobwidget.h b/src/gui/search/searchjobwidget.h
index ca3ab6ce219c..7ef36cf3884d 100644
--- a/src/gui/search/searchjobwidget.h
+++ b/src/gui/search/searchjobwidget.h
@@ -69,6 +69,7 @@ class SearchJobWidget final : public GUIApplicationComponent<QWidget>
 
     enum class Status
     {
+        Ready,
         Ongoing,
         Finished,
         Error,
@@ -76,10 +77,13 @@ class SearchJobWidget final : public GUIApplicationComponent<QWidget>
         NoResults
     };
 
-    SearchJobWidget(SearchHandler *searchHandler, IGUIApplication *app, QWidget *parent = nullptr);
+    SearchJobWidget(const QString &id, const QString &searchPattern, const QList<SearchResult> &searchResults, IGUIApplication *app, QWidget *parent = nullptr);
+    SearchJobWidget(const QString &id, SearchHandler *searchHandler, IGUIApplication *app, QWidget *parent = nullptr);
     ~SearchJobWidget() override;
 
+    QString id() const;
     QString searchPattern() const;
+    QList<SearchResult> searchResults() const;
     Status status() const;
     int visibleResultsCount() const;
     LineEdit *lineEditSearchResultsFilter() const;
@@ -98,6 +102,8 @@ private slots:
     void displayColumnHeaderMenu();
 
 private:
+    SearchJobWidget(const QString &id, IGUIApplication *app, QWidget *parent);
+
     void loadSettings();
     void saveSettings() const;
     void updateFilter();
@@ -127,15 +133,18 @@ private slots:
     void copyTorrentNames() const;
     void copyField(int column) const;
 
+    SettingValue<NameFilteringMode> m_nameFilteringMode;
+
+    QString m_id;
+    QString m_searchPattern;
+    QList<SearchResult> m_searchResults;
     Ui::SearchJobWidget *m_ui = nullptr;
     SearchHandler *m_searchHandler = nullptr;
     QStandardItemModel *m_searchListModel = nullptr;
     SearchSortModel *m_proxyModel = nullptr;
     LineEdit *m_lineEditSearchResultsFilter = nullptr;
-    Status m_status = Status::Ongoing;
+    Status m_status = Status::Ready;
     bool m_noSearchResults = true;
-
-    SettingValue<NameFilteringMode> m_nameFilteringMode;
 };
 
 Q_DECLARE_METATYPE(SearchJobWidget::NameFilteringMode)
diff --git a/src/gui/search/searchwidget.cpp b/src/gui/search/searchwidget.cpp
index 7aa678b2d7e4..ceb13c2146e6 100644
--- a/src/gui/search/searchwidget.cpp
+++ b/src/gui/search/searchwidget.cpp
@@ -1,6 +1,6 @@
 /*
  * Bittorrent Client using Qt and libtorrent.
- * Copyright (C) 2015-2024  Vladimir Golovnev <glassez@yandex.ru>
+ * Copyright (C) 2015-2025  Vladimir Golovnev <glassez@yandex.ru>
  * Copyright (C) 2020, Will Da Silva <will@willdasilva.xyz>
  * Copyright (C) 2006  Christophe Dumez <chris@qbittorrent.org>
  *
@@ -34,12 +34,13 @@
 
 #include <utility>
 
-#ifdef Q_OS_WIN
-#include <cstdlib>
-#endif
-
 #include <QDebug>
 #include <QEvent>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonParseError>
+#include <QJsonValue>
 #include <QList>
 #include <QMenu>
 #include <QMessageBox>
@@ -47,11 +48,18 @@
 #include <QObject>
 #include <QRegularExpression>
 #include <QShortcut>
+#include <QThread>
 
 #include "base/global.h"
+#include "base/logger.h"
+#include "base/preferences.h"
+#include "base/profile.h"
 #include "base/search/searchhandler.h"
 #include "base/search/searchpluginmanager.h"
+#include "base/utils/datetime.h"
+#include "base/utils/fs.h"
 #include "base/utils/foreignapps.h"
+#include "base/utils/io.h"
 #include "gui/desktopintegration.h"
 #include "gui/interfaces/iguiapplication.h"
 #include "gui/uithememanager.h"
@@ -59,11 +67,37 @@
 #include "searchjobwidget.h"
 #include "ui_searchwidget.h"
 
-#define SEARCHHISTORY_MAXSIZE 50
-#define URL_COLUMN 5
+const QString DATA_FOLDER_NAME = u"SearchUI"_s;
+const QString SESSION_FILE_NAME = u"Session.json"_s;
+
+const QString KEY_SESSION_TABS = u"Tabs"_s;
+const QString KEY_SESSION_CURRENTTAB = u"CurrentTab"_s;
+const QString KEY_TAB_ID = u"ID"_s;
+const QString KEY_TAB_SEARCHPATTERN = u"SearchPattern"_s;
+const QString KEY_RESULT_FILENAME = u"FileName"_s;
+const QString KEY_RESULT_FILEURL = u"FileURL"_s;
+const QString KEY_RESULT_FILESIZE = u"FileSize"_s;
+const QString KEY_RESULT_SEEDERSCOUNT = u"SeedersCount"_s;
+const QString KEY_RESULT_LEECHERSCOUNT = u"LeechersCount"_s;
+const QString KEY_RESULT_ENGINENAME = u"EngineName"_s;
+const QString KEY_RESULT_SITEURL = u"SiteURL"_s;
+const QString KEY_RESULT_DESCRLINK = u"DescrLink"_s;
+const QString KEY_RESULT_PUBDATE = u"PubDate"_s;
 
 namespace
 {
+    struct TabData
+    {
+        QString tabID;
+        QString searchPattern;
+    };
+
+    struct SessionData
+    {
+        QList<TabData> tabs;
+        QString currentTabID;
+    };
+
     QString statusIconName(const SearchJobWidget::Status st)
     {
         switch (st)
@@ -81,11 +115,187 @@ namespace
             return {};
         }
     }
+
+    Path makeDataFilePath(const QString &fileName)
+    {
+        return specialFolderLocation(SpecialFolder::Data) / Path(DATA_FOLDER_NAME) / Path(fileName);
+    }
+
+    QString makeTabName(SearchJobWidget *searchJobWdget)
+    {
+        Q_ASSERT(searchJobWdget);
+        if (!searchJobWdget) [[unlikely]]
+            return {};
+
+        QString tabName = searchJobWdget->searchPattern();
+        tabName.replace(QRegularExpression(u"&{1}"_s), u"&&"_s);
+        return tabName;
+    }
+
+    nonstd::expected<SessionData, QString> loadSession(const Path &filePath)
+    {
+        const int fileMaxSize = 10 * 1024 * 1024;
+        const auto readResult = Utils::IO::readFile(filePath, fileMaxSize);
+        if (!readResult)
+        {
+            if (readResult.error().status == Utils::IO::ReadError::NotExist)
+                return {};
+
+            return nonstd::make_unexpected(readResult.error().message);
+        }
+
+        const QString formatErrorMsg = SearchWidget::tr("Invalid data format.");
+        QJsonParseError jsonError;
+        const QJsonDocument sessionDoc = QJsonDocument::fromJson(readResult.value(), &jsonError);
+        if (jsonError.error != QJsonParseError::NoError)
+            return nonstd::make_unexpected(jsonError.errorString());
+
+        if (!sessionDoc.isObject())
+            return nonstd::make_unexpected(formatErrorMsg);
+
+        const QJsonObject sessionObj = sessionDoc.object();
+        const QJsonValue tabsVal = sessionObj[KEY_SESSION_TABS];
+        if (!tabsVal.isArray())
+            return nonstd::make_unexpected(formatErrorMsg);
+
+        QList<TabData> tabs;
+        QSet<QString> tabIDs;
+        for (const QJsonValue &tabVal : asConst(tabsVal.toArray()))
+        {
+            if (!tabVal.isObject())
+                return nonstd::make_unexpected(formatErrorMsg);
+
+            const QJsonObject tabObj = tabVal.toObject();
+
+            const QJsonValue tabIDVal = tabObj[KEY_TAB_ID];
+            if (!tabIDVal.isString())
+                return nonstd::make_unexpected(formatErrorMsg);
+
+            const QJsonValue patternVal = tabObj[KEY_TAB_SEARCHPATTERN];
+            if (!patternVal.isString())
+                return nonstd::make_unexpected(formatErrorMsg);
+
+            const QString tabID = tabIDVal.toString();
+            tabIDs.insert(tabID);
+            tabs.emplaceBack(TabData {tabID, patternVal.toString()});
+            if (tabs.size() != tabIDs.size()) // duplicate ID
+                return nonstd::make_unexpected(formatErrorMsg);
+        }
+
+        const QJsonValue currentTabVal = sessionObj[KEY_SESSION_CURRENTTAB];
+        if (!currentTabVal.isString())
+            return nonstd::make_unexpected(formatErrorMsg);
+
+        return SessionData {.tabs = tabs, .currentTabID = currentTabVal.toString()};
+    }
+
+    nonstd::expected<QList<SearchResult>, QString> loadSearchResults(const Path &filePath)
+    {
+        const int fileMaxSize = 10 * 1024 * 1024;
+        const auto readResult = Utils::IO::readFile(filePath, fileMaxSize);
+        if (!readResult)
+        {
+            if (readResult.error().status != Utils::IO::ReadError::NotExist)
+            {
+                return nonstd::make_unexpected(readResult.error().message);
+            }
+
+            return {};
+        }
+
+        const QString formatErrorMsg = SearchWidget::tr("Invalid data format.");
+        QJsonParseError jsonError;
+        const QJsonDocument searchResultsDoc = QJsonDocument::fromJson(readResult.value(), &jsonError);
+        if (jsonError.error != QJsonParseError::NoError)
+            return nonstd::make_unexpected(jsonError.errorString());
+
+        if (!searchResultsDoc.isArray())
+            return nonstd::make_unexpected(formatErrorMsg);
+
+        const QJsonArray resultsList = searchResultsDoc.array();
+        QList<SearchResult> searchResults;
+        for (const QJsonValue &resultVal : resultsList)
+        {
+            if (!resultVal.isObject())
+                return nonstd::make_unexpected(formatErrorMsg);
+
+            const QJsonObject resultObj = resultVal.toObject();
+            SearchResult &searchResult = searchResults.emplaceBack();
+
+            if (const QJsonValue fileNameVal = resultObj[KEY_RESULT_FILENAME]; fileNameVal.isString())
+                searchResult.fileName = fileNameVal.toString();
+            else
+                return nonstd::make_unexpected(formatErrorMsg);
+
+            if (const QJsonValue fileURLVal = resultObj[KEY_RESULT_FILEURL]; fileURLVal.isString())
+                searchResult.fileUrl= fileURLVal.toString();
+            else
+                return nonstd::make_unexpected(formatErrorMsg);
+
+            if (const QJsonValue fileSizeVal = resultObj[KEY_RESULT_FILESIZE]; fileSizeVal.isDouble())
+                searchResult.fileSize= fileSizeVal.toInteger();
+            else
+                return nonstd::make_unexpected(formatErrorMsg);
+
+            if (const QJsonValue seedersCountVal = resultObj[KEY_RESULT_SEEDERSCOUNT]; seedersCountVal.isDouble())
+                searchResult.nbSeeders = seedersCountVal.toInteger();
+            else
+                return nonstd::make_unexpected(formatErrorMsg);
+
+            if (const QJsonValue leechersCountVal = resultObj[KEY_RESULT_LEECHERSCOUNT]; leechersCountVal.isDouble())
+                searchResult.nbLeechers = leechersCountVal.toInteger();
+            else
+                return nonstd::make_unexpected(formatErrorMsg);
+
+            if (const QJsonValue engineNameVal = resultObj[KEY_RESULT_ENGINENAME]; engineNameVal.isString())
+                searchResult.engineName= engineNameVal.toString();
+            else
+                return nonstd::make_unexpected(formatErrorMsg);
+
+            if (const QJsonValue siteURLVal = resultObj[KEY_RESULT_SITEURL]; siteURLVal.isString())
+                searchResult.siteUrl= siteURLVal.toString();
+            else
+                return nonstd::make_unexpected(formatErrorMsg);
+
+            if (const QJsonValue descrLinkVal = resultObj[KEY_RESULT_DESCRLINK]; descrLinkVal.isString())
+                searchResult.descrLink= descrLinkVal.toString();
+            else
+                return nonstd::make_unexpected(formatErrorMsg);
+
+            if (const QJsonValue pubDateVal = resultObj[KEY_RESULT_PUBDATE]; pubDateVal.isDouble())
+                searchResult.pubDate = QDateTime::fromSecsSinceEpoch(pubDateVal.toInteger());
+            else
+                return nonstd::make_unexpected(formatErrorMsg);
+        }
+
+        return searchResults;
+    }
 }
 
+class SearchWidget::DataStorage final : public QObject
+{
+    Q_OBJECT
+    Q_DISABLE_COPY_MOVE(DataStorage)
+
+public:
+    using QObject::QObject;
+
+    void loadSession(bool withSearchResults);
+    void storeSession(const SessionData &sessionData);
+    void removeSession();
+    void storeTab(const QString &tabID, const QList<SearchResult> &searchResults);
+    void removeTab(const QString &tabID);
+
+signals:
+    void sessionLoaded(const SessionData &sessionData);
+    void tabLoaded(const QString &tabID, const QString &searchPattern, const QList<SearchResult> &searchResults);
+};
+
 SearchWidget::SearchWidget(IGUIApplication *app, QWidget *parent)
     : GUIApplicationComponent(app, parent)
     , m_ui {new Ui::SearchWidget()}
+    , m_ioThread {new QThread}
+    , m_dataStorage {new DataStorage(this)}
 {
     m_ui->setupUi(this);
 
@@ -120,6 +330,8 @@ SearchWidget::SearchWidget(IGUIApplication *app, QWidget *parent)
 #endif
     connect(m_ui->tabWidget, &QTabWidget::tabCloseRequested, this, &SearchWidget::closeTab);
     connect(m_ui->tabWidget, &QTabWidget::currentChanged, this, &SearchWidget::currentTabChanged);
+    connect(m_ui->tabWidget, &QTabWidget::currentChanged, this, &SearchWidget::saveSession);
+    connect(m_ui->tabWidget->tabBar(), &QTabBar::tabMoved, this, &SearchWidget::saveSession);
 
     connect(m_ui->tabWidget, &QTabWidget::tabBarDoubleClicked, this, [this](const int tabIndex)
     {
@@ -166,6 +378,17 @@ SearchWidget::SearchWidget(IGUIApplication *app, QWidget *parent)
     connect(focusSearchHotkey, &QShortcut::activated, this, &SearchWidget::toggleFocusBetweenLineEdits);
     const auto *focusSearchHotkeyAlternative = new QShortcut((Qt::CTRL | Qt::Key_E), this);
     connect(focusSearchHotkeyAlternative, &QShortcut::activated, this, &SearchWidget::toggleFocusBetweenLineEdits);
+
+    m_storeOpenedTabs = Preferences::instance()->storeOpenedSearchTabs();
+    m_storeOpenedTabsResults = Preferences::instance()->storeOpenedSearchTabResults();
+    connect(Preferences::instance(), &Preferences::changed, this, &SearchWidget::onPreferencesChanged);
+
+    m_dataStorage->moveToThread(m_ioThread.get());
+    connect(m_ioThread.get(), &QThread::finished, m_dataStorage, &QObject::deleteLater);
+    m_ioThread->setObjectName("SearchWidget m_ioThread");
+    m_ioThread->start();
+
+    restoreSession();
 }
 
 bool SearchWidget::eventFilter(QObject *object, QEvent *event)
@@ -199,6 +422,55 @@ bool SearchWidget::eventFilter(QObject *object, QEvent *event)
     return QWidget::eventFilter(object, event);
 }
 
+void SearchWidget::onPreferencesChanged()
+{
+    const auto *pref = Preferences::instance();
+
+    const bool storeOpenedTabs = pref->storeOpenedSearchTabs();
+    const bool isStoreOpenedTabsChanged = storeOpenedTabs != m_storeOpenedTabs;
+    if (isStoreOpenedTabsChanged)
+    {
+        m_storeOpenedTabs = storeOpenedTabs;
+        if (m_storeOpenedTabs)
+        {
+            saveSession();
+        }
+        else
+        {
+            QMetaObject::invokeMethod(m_dataStorage, [this] { m_dataStorage->removeSession(); });
+        }
+    }
+
+
+    const bool storeOpenedTabsResults = pref->storeOpenedSearchTabResults();
+    const bool isStoreOpenedTabsResultsChanged = storeOpenedTabsResults != m_storeOpenedTabsResults;
+    if (isStoreOpenedTabsResultsChanged)
+        m_storeOpenedTabsResults = storeOpenedTabsResults;
+
+    if (isStoreOpenedTabsResultsChanged || isStoreOpenedTabsChanged)
+    {
+        if (m_storeOpenedTabsResults)
+        {
+            for (int tabIndex = (m_ui->tabWidget->count() - 1); tabIndex >= 0; --tabIndex)
+            {
+                const auto *tab = static_cast<SearchJobWidget *>(m_ui->tabWidget->widget(tabIndex));
+                QMetaObject::invokeMethod(m_dataStorage, [this, tabID = tab->id(), searchResults = tab->searchResults()]
+                {
+                    m_dataStorage->storeTab(tabID, searchResults);
+                });
+            }
+        }
+        else
+        {
+            for (int tabIndex = (m_ui->tabWidget->count() - 1); tabIndex >= 0; --tabIndex)
+            {
+                const auto *tab = static_cast<SearchJobWidget *>(m_ui->tabWidget->widget(tabIndex));
+                QMetaObject::invokeMethod(m_dataStorage, [this, tabID = tab->id()] { m_dataStorage->removeTab(tabID); });
+            }
+        }
+    }
+}
+
 void SearchWidget::fillCatCombobox()
 {
     m_ui->comboCategory->clear();
@@ -259,6 +531,65 @@ QStringList SearchWidget::selectedPlugins() const
     return {itemText};
 }
 
+QString SearchWidget::generateTabID() const
+{
+    for (;;)
+    {
+        const QString tabID = QString::number(qHash(QDateTime::currentDateTimeUtc()));
+        if (!m_tabWidgets.contains(tabID))
+            return tabID;
+    }
+
+    return {};
+}
+
+int SearchWidget::addTab(const QString &tabID, SearchJobWidget *searchJobWdget)
+{
+    Q_ASSERT(!m_tabWidgets.contains(tabID));
+
+    connect(searchJobWdget, &SearchJobWidget::statusChanged, this, [this, searchJobWdget]() { tabStatusChanged(searchJobWdget); });
+    m_tabWidgets.insert(tabID, searchJobWdget);
+    return m_ui->tabWidget->addTab(searchJobWdget, makeTabName(searchJobWdget));
+}
+
+void SearchWidget::saveSession() const
+{
+    if (!m_storeOpenedTabs)
+        return;
+
+    const int currentIndex = m_ui->tabWidget->currentIndex();
+    SessionData sessionData;
+    for (int tabIndex = 0; tabIndex < m_ui->tabWidget->count(); ++tabIndex)
+    {
+        auto *searchJobWidget = static_cast<SearchJobWidget *>(m_ui->tabWidget->widget(tabIndex));
+        sessionData.tabs.emplaceBack(TabData {searchJobWidget->id(), searchJobWidget->searchPattern()});
+        if (currentIndex == tabIndex)
+            sessionData.currentTabID = searchJobWidget->id();
+    }
+
+    QMetaObject::invokeMethod(m_dataStorage, [this, sessionData] { m_dataStorage->storeSession(sessionData); });
+}
+
+void SearchWidget::restoreSession()
+{
+    if (!m_storeOpenedTabs)
+        return;
+
+    connect(m_dataStorage, &DataStorage::tabLoaded, this
+            , [this](const QString &tabID, const QString &searchPattern, const QList<SearchResult> &searchResults)
+    {
+        auto *restoredTab = new SearchJobWidget(tabID, searchPattern, searchResults, app(), this);
+        addTab(tabID, restoredTab);
+    });
+
+    connect(m_dataStorage, &DataStorage::sessionLoaded, this, [this](const SessionData &sessionData)
+    {
+        m_ui->tabWidget->setCurrentWidget(m_tabWidgets.value(sessionData.currentTabID));
+    });
+
+    QMetaObject::invokeMethod(m_dataStorage, [this] { m_dataStorage->loadSession(m_storeOpenedTabsResults); });
+}
+
 void SearchWidget::selectActivePage()
 {
     if (SearchPluginManager::instance()->allPlugins().isEmpty())
@@ -412,16 +743,14 @@ void SearchWidget::searchButtonClicked()
     auto *searchHandler = SearchPluginManager::instance()->startSearch(pattern, selectedCategory(), selectedPlugins());
 
     // Tab Addition
-    auto *newTab = new SearchJobWidget(searchHandler, app(), this);
-
-    QString tabName = pattern;
-    tabName.replace(QRegularExpression(u"&{1}"_s), u"&&"_s);
-    m_ui->tabWidget->addTab(newTab, tabName);
+    const QString newTabID = generateTabID();
+    auto *newTab = new SearchJobWidget(newTabID, searchHandler, app(), this);
+    const int tabIndex = addTab(newTabID, newTab);
+    m_ui->tabWidget->setTabToolTip(tabIndex, newTab->statusTip());
+    m_ui->tabWidget->setTabIcon(tabIndex, UIThemeManager::instance()->getIcon(statusIconName(newTab->status())));
     m_ui->tabWidget->setCurrentWidget(newTab);
-
-    connect(newTab, &SearchJobWidget::statusChanged, this, [this, newTab]() { tabStatusChanged(newTab); });
-
-    tabStatusChanged(newTab);
+    adjustSearchButton();
+    saveSession();
 }
 
 void SearchWidget::stopButtonClicked()
@@ -442,19 +771,38 @@ void SearchWidget::tabStatusChanged(SearchJobWidget *tab)
             adjustSearchButton();
 
         emit searchFinished(tab->status() == SearchJobWidget::Status::Error);
+
+        if (m_storeOpenedTabsResults)
+        {
+            QMetaObject::invokeMethod(m_dataStorage, [this, tabID = tab->id(), searchResults = tab->searchResults()]
+            {
+                m_dataStorage->storeTab(tabID, searchResults);
+            });
+        }
     }
 }
 
 void SearchWidget::closeTab(const int index)
 {
-    const QWidget *tab = m_ui->tabWidget->widget(index);
-    delete tab;
+    const auto *tab = static_cast<SearchJobWidget *>(m_ui->tabWidget->widget(index));
+    const QString tabID = tab->id();
+    delete m_tabWidgets.take(tabID);
+
+    QMetaObject::invokeMethod(m_dataStorage, [this, tabID] { m_dataStorage->removeTab(tabID); });
+    saveSession();
 }
 
 void SearchWidget::closeAllTabs()
 {
-    for (int i = (m_ui->tabWidget->count() - 1); i >= 0; --i)
-        closeTab(i);
+    for (int tabIndex = (m_ui->tabWidget->count() - 1); tabIndex >= 0; --tabIndex)
+    {
+        const auto *tab = static_cast<SearchJobWidget *>(m_ui->tabWidget->widget(tabIndex));
+        const QString tabID = tab->id();
+        delete m_tabWidgets.take(tabID);
+        QMetaObject::invokeMethod(m_dataStorage, [this, tabID] { m_dataStorage->removeTab(tabID); });
+    }
+
+    saveSession();
 }
 
 void SearchWidget::refreshTab(SearchJobWidget *searchJobWidget)
@@ -468,5 +816,106 @@ void SearchWidget::refreshTab(SearchJobWidget *searchJobWidget)
     // Re-launch search
     auto *searchHandler = SearchPluginManager::instance()->startSearch(searchJobWidget->searchPattern(), selectedCategory(), selectedPlugins());
     searchJobWidget->assignSearchHandler(searchHandler);
-    tabStatusChanged(searchJobWidget);
 }
+
+void SearchWidget::DataStorage::loadSession(const bool withSearchResults)
+{
+    const Path sessionFilePath = makeDataFilePath(SESSION_FILE_NAME);
+    const auto loadResult = ::loadSession(sessionFilePath);
+    if (!loadResult)
+    {
+        LogMsg(tr("Failed to load Search UI saved state data. File: \"%1\". Error: \"%2\"")
+                .arg(sessionFilePath.toString(), loadResult.error()), Log::WARNING);
+        return;
+    }
+
+    const SessionData &sessionData = loadResult.value();
+
+    for (const auto &[tabID, searchPattern] : sessionData.tabs)
+    {
+        QList<SearchResult> searchResults;
+
+        if (withSearchResults)
+        {
+            const Path tabStateFilePath = makeDataFilePath(tabID + u".json");
+            if (const auto loadTabStateResult = loadSearchResults(tabStateFilePath))
+            {
+                searchResults = loadTabStateResult.value();
+            }
+            else
+            {
+                LogMsg(tr("Failed to load saved search results. Tab: \"%1\". File: \"%2\". Error: \"%3\"")
+                        .arg(searchPattern, tabStateFilePath.toString(), loadTabStateResult.error()), Log::WARNING);
+            }
+        }
+
+        emit tabLoaded(tabID, searchPattern, searchResults);
+    }
+
+    emit sessionLoaded(sessionData);
+}
+
+void SearchWidget::DataStorage::storeSession(const SessionData &sessionData)
+{
+    QJsonArray tabsList;
+    for (const auto &[tabID, searchPattern] : sessionData.tabs)
+    {
+        const QJsonObject tabObj {
+            {u"ID"_s, tabID},
+            {u"SearchPattern"_s, searchPattern}
+        };
+        tabsList.append(tabObj);
+    }
+
+    const QJsonObject sessionObj {
+        {u"Tabs"_s, tabsList},
+        {u"CurrentTab"_s, sessionData.currentTabID}
+    };
+
+    const Path sessionFilePath = makeDataFilePath(SESSION_FILE_NAME);
+    const auto saveResult = Utils::IO::saveToFile(sessionFilePath, QJsonDocument(sessionObj).toJson());
+    if (!saveResult)
+    {
+        LogMsg(tr("Failed to save Search UI state. File: \"%1\". Error: \"%2\"")
+                .arg(sessionFilePath.toString(), saveResult.error()), Log::WARNING);
+    }
+}
+
+void SearchWidget::DataStorage::removeSession()
+{
+    Utils::Fs::removeFile(makeDataFilePath(SESSION_FILE_NAME));
+}
+
+void SearchWidget::DataStorage::storeTab(const QString &tabID, const QList<SearchResult> &searchResults)
+{
+    QJsonArray searchResultsArray;
+    for (const SearchResult &searchResult : searchResults)
+    {
+        searchResultsArray.append(QJsonObject {
+            {KEY_RESULT_FILENAME, searchResult.fileName},
+            {KEY_RESULT_FILEURL, searchResult.fileUrl},
+            {KEY_RESULT_FILESIZE, searchResult.fileSize},
+            {KEY_RESULT_SEEDERSCOUNT, searchResult.nbSeeders},
+            {KEY_RESULT_LEECHERSCOUNT, searchResult.nbLeechers},
+            {KEY_RESULT_ENGINENAME, searchResult.engineName},
+            {KEY_RESULT_SITEURL, searchResult.siteUrl},
+            {KEY_RESULT_DESCRLINK, searchResult.descrLink},
+            {KEY_RESULT_PUBDATE, Utils::DateTime::toSecsSinceEpoch(searchResult.pubDate)}
+        });
+    }
+
+    const Path filePath = makeDataFilePath(tabID + u".json");
+    const auto saveResult = Utils::IO::saveToFile(filePath, QJsonDocument(searchResultsArray).toJson());
+    if (!saveResult)
+    {
+        LogMsg(tr("Failed to save search results. Tab: \"%1\". File: \"%2\". Error: \"%3\"")
+                .arg(tabID, filePath.toString(), saveResult.error()), Log::WARNING);
+    }
+}
+
+void SearchWidget::DataStorage::removeTab(const QString &tabID)
+{
+    Utils::Fs::removeFile(makeDataFilePath(tabID + u".json"));
+}
+
+#include "searchwidget.moc"
diff --git a/src/gui/search/searchwidget.h b/src/gui/search/searchwidget.h
index 9eca603ebe0f..330450c374c3 100644
--- a/src/gui/search/searchwidget.h
+++ b/src/gui/search/searchwidget.h
@@ -31,8 +31,10 @@
 #pragma once
 
 #include <QPointer>
+#include <QSet>
 #include <QWidget>
 
+#include "base/utils/thread.h"
 #include "gui/guiapplicationcomponent.h"
 
 class QEvent;
@@ -62,6 +64,8 @@ class SearchWidget : public GUIApplicationComponent<QWidget>
 private:
     bool eventFilter(QObject *object, QEvent *event) override;
 
+    void onPreferencesChanged();
+
     void pluginsButtonClicked();
     void searchButtonClicked();
     void stopButtonClicked();
@@ -86,7 +90,22 @@ class SearchWidget : public GUIApplicationComponent<QWidget>
     QString selectedCategory() const;
     QStringList selectedPlugins() const;
 
+    QString generateTabID() const;
+    int addTab(const QString &tabID, SearchJobWidget *searchJobWdget);
+
+    void saveSession() const;
+    void restoreSession();
+
     Ui::SearchWidget *m_ui = nullptr;
     QPointer<SearchJobWidget> m_currentSearchTab; // Selected tab
     bool m_isNewQueryString = false;
+    QHash<QString, SearchJobWidget *> m_tabWidgets;
+
+    bool m_storeOpenedTabs = false;
+    bool m_storeOpenedTabsResults = false;
+
+    Utils::Thread::UniquePtr m_ioThread;
+
+    class DataStorage;
+    DataStorage *m_dataStorage = nullptr;
 };