_binary From 86dd130373233a765b6b355b0735efb11033a266 Mon Sep 17 00:00:00 2001
From: Rushikesh Padia <padiarushi3012@gmail.com>
Date: Mon, 22 May 2023 16:52:36 -0700
Subject: [PATCH] Improved Cache System

---
 src/configs/Config.php                        |   2 +-
 src/configs/ConfigureTool.php                 |   2 +-
 src/configs/PublicHelpPages.php               |   2 +-
 src/controllers/SearchController.php          |  10 +-
 .../components/SystemComponent.php            |  16 +-
 src/executables/CacheTopicTrainer.php         | 119 ++++++++
 src/library/CountVectorizer.php               | 164 ++++++++++
 src/library/FileCache.php                     | 252 ++++------------
 src/library/KMeansClustering.php              | 236 +++++++++++++++
 src/library/cache/Cache.php                   | 240 +++++++++++++++
 src/library/cache/CacheMetricsWrapper.php     | 123 ++++++++
 src/library/cache/LRUCache.php                | 285 ++++++++++++++++++
 src/library/cache/MarkersCache.php            | 161 ++++++++++
 src/library/cache/StaticCache.php             |  92 ++++++
 src/library/cache/StaticDynamicCache.php      | 100 ++++++
 src/library/cache/StaticTopicDynamicCache.php | 107 +++++++
 src/library/cache/TopicalCache.php            | 136 +++++++++
 .../media_jobs/StaticCacheRefreshJob.php      | 190 ++++++++++++
 src/locale/en_US/configure.ini                |   7 +-
 src/models/ProfileModel.php                   |   2 +-
 src/views/elements/ServersettingsElement.php  |  14 +-
 tests/FileCacheTest.php                       |  95 ++++++
 22 files changed, 2142 insertions(+), 213 deletions(-)
 create mode 100644 src/executables/CacheTopicTrainer.php
 create mode 100644 src/library/CountVectorizer.php
 create mode 100644 src/library/KMeansClustering.php
 create mode 100644 src/library/cache/Cache.php
 create mode 100644 src/library/cache/CacheMetricsWrapper.php
 create mode 100644 src/library/cache/LRUCache.php
 create mode 100644 src/library/cache/MarkersCache.php
 create mode 100644 src/library/cache/StaticCache.php
 create mode 100644 src/library/cache/StaticDynamicCache.php
 create mode 100644 src/library/cache/StaticTopicDynamicCache.php
 create mode 100644 src/library/cache/TopicalCache.php
 create mode 100644 src/library/media_jobs/StaticCacheRefreshJob.php
 create mode 100644 tests/FileCacheTest.php

diff --git a/src/configs/Config.php b/src/configs/Config.php
index 6fcace61e..4a892f9c5 100755
--- a/src/configs/Config.php
+++ b/src/configs/Config.php
@@ -459,7 +459,7 @@ if (file_exists(WORK_DIRECTORY . PROFILE_FILE_NAME)) {
     nsconddefine('PROFILE', false);
     nsdefine('RECOVERY_MODE', EMAIL_RECOVERY);
     nsconddefine('DEBUG_LEVEL', NO_DEBUG_INFO);
-    nsdefine('USE_FILECACHE', false);
+    nsdefine('FILECACHE', 'None');
     nsdefine('WEB_ACCESS', true);
     nsdefine('RSS_ACCESS', true);
     nsdefine('API_ACCESS', true);
diff --git a/src/configs/ConfigureTool.php b/src/configs/ConfigureTool.php
index 853d30d6a..b8fc1d772 100644
--- a/src/configs/ConfigureTool.php
+++ b/src/configs/ConfigureTool.php
@@ -345,7 +345,7 @@ class ConfigureTool
         e("NAME SERVER SETTINGS\n====================\n");
         e("Server Key: [".$data["AUTH_KEY"]."]\n");
         e("Name Server URL: [".$data["NAME_SERVER"]."]\n");
-        $settings = ["USE_FILECACHE" => "Use File Cache"];
+        $settings = ["FILECACHE" => "Cache Type"];
         $items = ["serverKey" => "Edit Server Key",
             "nameServer" => "Edit Name Server Url"];
         foreach ($settings as $setting => $setting_string) {
diff --git a/src/configs/PublicHelpPages.php b/src/configs/PublicHelpPages.php
index 5618767a1..f3748bb51 100644
--- a/src/configs/PublicHelpPages.php
+++ b/src/configs/PublicHelpPages.php
@@ -43246,7 +43246,7 @@ END_HEAD_VARSYioop can be run in a single machine or multi-machine setting. In a
 
 &lt;br /&gt;
 
-When someone enters a query into a Yioop set-up, they typically enter the query on the name server. The &#039;&#039;&#039;Use Filecache&#039;&#039;&#039; checkbox controls whether the query results are cached in a file so that they don&#039;t have to be recalculated when someone enters the same query again. The file cache is purged periodically so that it doesn&#039;t get too large. Sometimes it is useful, however, to know the file cache has just been cleared. The &#039;&#039;&#039;Clear Cache&#039;&#039;&#039; link clears both the file cache as well as the local domain name to IP address cache.
+When someone enters a query into a Yioop set-up, they typically enter the query on the name server. The &#039;&#039;&#039;Cache Type&#039;&#039;&#039;  dropdown controls which algorithm should be used to cache query results in a file. If query results are cached they don&#39;t have to be recalculated when someone enters the same query again. The file cache is purged periodically so that it doesn&#39;t get too large. Sometimes it is useful, however, to know the file cache has just been cleared. The &#039;&#039;&#039;Clear Cache&#039;&#039;&#039; link clears both the file cache as well as the local domain name to IP address cache.
 EOD;
 $help_pages["en-US"]["New_Crawl"] = <<< 'EOD'
 page_type=standard
diff --git a/src/controllers/SearchController.php b/src/controllers/SearchController.php
index c00a38ac9..a6b0938ed 100755
--- a/src/controllers/SearchController.php
+++ b/src/controllers/SearchController.php
@@ -100,11 +100,11 @@ class SearchController extends Controller implements CrawlConstants
     {
         parent::__construct($web_site);
         if (empty($_SERVER["USE_CACHE"])) {
-            if (C\USE_FILECACHE) {
+            if (C\FILECACHE !== "None") {
                 $phrase_model = $this->model("phrase");
                 $phrase_model::$cache =
                     new FileCache(C\WORK_DIRECTORY . "/cache/queries",
-                    $this->web_site);
+                    $this->web_site, C\FILECACHE);
                 $_SERVER["USE_CACHE"] = true;
             } else {
                 $_SERVER["USE_CACHE"] = false;
@@ -1882,10 +1882,13 @@ EOD;
     {
         if (!C\API_ACCESS) return false;
         ob_start();
-        $this->cacheRequestAndOutput($url, $ui_flags, $terms,
+        $crawl_item = $this->cacheRequestAndOutput($url, $ui_flags, $terms,
             $crawl_time);
         $cached_page = ob_get_contents();
         ob_end_clean();
+        if(in_array("no_render", $ui_flags)){
+            return $crawl_item;
+        }
         return $cached_page;
     }
     //*********END SEARCH API *********
@@ -2119,6 +2122,7 @@ EOD;
             $cache->set($hash_key, $new_doc);
         }
         echo $new_doc;
+        return $crawl_item;
     }
     /**
      * A fieldRequest is a special kind of cache request in which only
diff --git a/src/controllers/components/SystemComponent.php b/src/controllers/components/SystemComponent.php
index 889a7dd88..7999613ef 100755
--- a/src/controllers/components/SystemComponent.php
+++ b/src/controllers/components/SystemComponent.php
@@ -656,7 +656,7 @@ class SystemComponent extends Component
             case "update":
                 $parent->updateProfileFields($data, $profile,
                     ['AD_LOCATION', 'SEND_MAIL_MEDIA_UPDATER',
-                    'USE_FILECACHE', 'USE_MAIL_PHP', 'USE_PROXY']);
+                    'FILECACHE', 'USE_MAIL_PHP', 'USE_PROXY']);
                 $old_profile =
                     $profile_model->getProfile(C\WORK_DIRECTORY);
                 if (strcmp($old_profile["MONETIZATION_TYPE"],
@@ -745,7 +745,7 @@ class SystemComponent extends Component
             $_REQUEST['DBMS'] = ucfirst($data['DBMS']);
             $parent->updateProfileFields($data, $profile,
                 ['AD_LOCATION', 'SEND_MAIL_MEDIA_UPDATER',
-                'USE_FILECACHE', 'USE_MAIL_PHP', 'USE_PROXY']);
+                'FILECACHE', 'USE_MAIL_PHP', 'USE_PROXY']);
             $old_profile = $profile_model->getProfile(C\WORK_DIRECTORY);
             $profile_model->updateProfile(C\WORK_DIRECTORY, $profile,
                 $old_profile);
@@ -762,6 +762,18 @@ class SystemComponent extends Component
                 $data['SCRIPT'] .= "logindbms['$dbms'] = false;\n";
             }
         }
+        $data['FILECACHES'] = [
+            'no_cache' =>
+                tl('system_component_filecache_none'),
+            'markers_cache' =>
+                tl('system_component_filecache_markers'),
+            'lru_cache' =>
+                tl('system_component_filecache_lru'),
+            'static_dynamic_cache' =>
+                tl('system_component_filecache_static_dynamic'),
+            'static_topic_dynamic_cache' =>
+                tl('system_component_filecache_static_topic_dynamic'),
+        ];
         $data['REGISTRATION_TYPES'] = [
                 'disable_registration' =>
                     tl('system_component_configure_disable_registration'),
diff --git a/src/executables/CacheTopicTrainer.php b/src/executables/CacheTopicTrainer.php
new file mode 100644
index 000000000..81d41342e
--- /dev/null
+++ b/src/executables/CacheTopicTrainer.php
@@ -0,0 +1,119 @@
+<?php
+/**
+ * SeekQuarry/Yioop --
+ * Open Source Pure PHP Search Engine, Crawler, and Indexer
+ *
+ * Copyright (C) 2009 - 2023  Chris Pollett chris@pollett.org
+ *
+ * LICENSE:
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * END LICENSE
+ *
+ * @author Rushikesh Padia padiarushi3012@gmail.com
+ * @license https://www.gnu.org/licenses/ GPL3
+ * @link https://www.seekquarry.com/
+ * @copyright 2009 - 2023
+ * @filesource
+ */
+namespace seekquarry\yioop\executables;
+
+use seekquarry\yioop\configs as C;
+use seekquarry\yioop\controllers\SearchController;
+use seekquarry\yioop\library\ClickedUrlDocumentIterator;
+use seekquarry\yioop\library\CountVectorizer;
+use seekquarry\yioop\library\CrawlConstants;
+use seekquarry\yioop\library\KMeansClustering;
+use function seekquarry\yioop\examples\outputQueryData;
+
+if (php_sapi_name() != 'cli' ||
+    defined("seekquarry\\yioop\\configs\\IS_OWN_WEB_SERVER")) {
+    echo "BAD REQUEST"; exit();
+}
+/** This tool does not need logging*/
+$_SERVER["LOG_TO_FILES"] = false;
+/** USE_CACHE false rules out file cache as well*/
+$_SERVER["USE_CACHE"] = false;
+/**  For crawlHash, crawlHashWord function */
+require_once __DIR__."/../library/Utility.php";
+if (!C\PROFILE) {
+    echo "Please configure the search engine instance by visiting" .
+        "its web interface on localhost.\n";
+    exit();
+}
+ini_set("memory_limit", C\ARC_TOOL_MEMORY_LIMIT);   /*reading in a whole
+    shard might take a fair bit of memory
+*/
+/*
+ * We'll set up multi-byte string handling to use UTF-8
+ */
+mb_internal_encoding("UTF-8");
+mb_regex_encoding("UTF-8");
+
+/**
+ * Class to train k-means model for Topical cache
+ *
+ * @author Rushikesh Padia
+ */
+class CacheTopicTrainer
+{
+    /**
+     * Creates document-term matrix using count vectorizer
+     * and use the matrix to train k-means algorithm
+     */
+    static function run(): void
+    {
+        $cv = new CountVectorizer();
+        $documents = self::getDocuments();
+        $cv->createVocabulary($documents);
+        $matrix = $cv->vectorizeAll($documents);
+        $km = new KMeansClustering();
+        $k = 10;
+        $km->train($matrix, $k, $cv->getVectorSize());
+    }
+    /**
+     * Finds document from cache oll unique query, url pairs
+     * @return array list of clicked url documents from cache
+     */
+    static function getDocuments(): array
+    {
+        $controller = new SearchController();
+        $documents = [];
+        $seen_query_url_pairs = [];
+        $file = fopen(C\BASE_DIR . "/data/dataset.csv", 'r');
+        if ($file) {
+            fgetcsv($file);
+            $counter = 0;
+            while (($row = fgetcsv($file)) !== false && $counter < 10000) {
+                if(!empty($row[0]) && !empty($row[1])){
+                    $query_url_pair = $row[0] . $row[1];
+                    if(!in_array($query_url_pair, $seen_query_url_pairs)){
+                        $output = $row[0] . " ";
+                        $cache_result = $controller->cacheRequest(
+                            $row[1], ["summaries", "no_render"]);
+                        if ($cache_result !== null) {
+                            $output += $cache_result[CrawlConstants::DESCRIPTION];
+                        }
+                        $documents[] = $output;
+                        $counter++;
+                    }
+                }
+            }
+            fclose($file);
+        }
+        return $documents;
+    }
+}
+CacheTopicTrainer::run();
\ No newline at end of file
diff --git a/src/library/CountVectorizer.php b/src/library/CountVectorizer.php
new file mode 100644
index 000000000..ab4884938
--- /dev/null
+++ b/src/library/CountVectorizer.php
@@ -0,0 +1,164 @@
+<?php
+/**
+ * SeekQuarry/Yioop --
+ * Open Source Pure PHP Search Engine, Crawler, and Indexer
+ *
+ * Copyright (C) 2009 - 2023  Chris Pollett chris@pollett.org
+ *
+ * LICENSE:
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * END LICENSE
+ *
+ *
+ * @author Rushikesh Padia padiarushi3012@gmail.com
+ * @license https://www.gnu.org/licenses/ GPL3
+ * @link https://www.seekquarry.com/
+ * @copyright 2009 - 2023
+ * @filesource
+ */
+namespace seekquarry\yioop\library;
+
+use seekquarry\yioop\configs as C;
+/**
+ * The class converts a document into vector.
+ * The class has to generate vocabulary first from a corpus
+ * of documents. Each word is assigned a unique index in the vocabulary.
+ * Word embedding is created by incrementing count of index of every
+ * word present in the document.
+ *
+ * @author Rushikesh Padia
+ */
+class CountVectorizer
+{
+    /**
+     * Relative path to the vectorizer
+     */
+    const VECTORIZER_PATH = C\WORK_DIRECTORY . "/topicmodel/kmeans";
+    /**
+     * Vectorizer filename
+     */
+    const VECTORIZER_FILENAME = "/vocab.txt";
+    /**
+     * @var array Contains vocabulary generated from corpus
+     */
+    private array $vocabulary;
+
+    /**
+     * Initializes vectorizer by loading vocabulary
+     */
+    public function __construct()
+    {
+        $this->loadState();
+    }
+    /**
+     * Extracts all phrases (sequences of adjacent words) from $string. Does
+     * extract terms within those phrase
+     * @param mixed $document raw text
+     * @return array list of terms extracted
+     */
+    private static function getWords(mixed $document): array
+    {
+        $extracted_data = PhraseParser::extractPhrasesInLists($document,
+            "en-US");
+        $word_lists = $extracted_data['WORD_LIST'];
+        return array_keys($word_lists);
+    }
+    /**
+     * Generates vocabulary from a corpus
+     * @param mixed $documents_iterator iterates over the corpus
+     */
+    function createVocabulary(mixed $documents_iterator): void
+    {
+        $this->vocabulary = [];
+        foreach ($documents_iterator as $document) {
+            if($document != "") {
+                $words = self::getWords($document);
+                foreach ($words as $word) {
+                    if (!isset($this->vocabulary[$word])) {
+                        $this->vocabulary[$word] = count($this->vocabulary);
+                    }
+                }
+            }
+        }
+        $this->saveState();
+    }
+    /**
+     * Converts corpus of documents into sparse matrix of word embeddings
+     * @param mixed $documents_iterator iterates over the corpus
+     * @return array document-term sparse matrix
+     */
+    function vectorizeAll(mixed $documents_iterator): array
+    {
+        $matrix = [];
+        foreach ($documents_iterator as $document) {
+            if($document != "") {
+                $matrix[] = $this->vectorizeOne($document);
+            }
+        }
+        return $matrix;
+    }
+    /**
+     * Creates sparse vector from document
+     * @param string $document text to be converted into vector
+     * @return array sparse vector
+     */
+    function vectorizeOne(string $document): array
+    {
+        $words = self::getWords($document);
+        $row = [];
+        foreach ($words as $word) {
+            if (isset($this->vocabulary[$word])) {
+                if(array_key_exists($this->vocabulary[$word], $row)){
+                    $row[$this->vocabulary[$word]] += 1;
+                }else{
+                    $row[$this->vocabulary[$word]] = 1;
+                }
+            }
+        }
+        return $row;
+    }
+    /**
+     * Returns size of vectors
+     * @return int size of vocabulary
+     */
+    function getVectorSize(): int
+    {
+        return count($this->vocabulary);
+    }
+    /**
+     * Loads the saved state of vectorizer
+     */
+    private function loadState(): void
+    {
+        $vocab_file = self::VECTORIZER_PATH .
+            self::VECTORIZER_FILENAME;
+        if(file_exists($vocab_file)){
+            $this->vocabulary = unserialize(file_get_contents($vocab_file));
+        }
+    }
+    /**
+     * Saves the state of vectorizer so that it does not
+     * have to create vocabulary after every restart
+     */
+    private function saveState(): void
+    {
+        if(!is_dir(self::VECTORIZER_PATH)){
+            mkdir(self::VECTORIZER_PATH, "0777", true);
+        }
+        $vocab = self::VECTORIZER_PATH . self::VECTORIZER_FILENAME;
+        file_put_contents($vocab, serialize($this->vocabulary));
+    }
+}
\ No newline at end of file
diff --git a/src/library/FileCache.php b/src/library/FileCache.php
index 42447b951..19ebfd2bc 100644
--- a/src/library/FileCache.php
+++ b/src/library/FileCache.php
@@ -22,7 +22,9 @@
  *
  * END LICENSE
  *
+ *
  * @author Chris Pollett chris@pollett.org
+ * @author Rushikesh Padia padiarushi3012@gmail.com
  * @license https://www.gnu.org/licenses/ GPL3
  * @link https://www.seekquarry.com/
  * @copyright 2009 - 2023
@@ -30,116 +32,70 @@
  */
 namespace seekquarry\yioop\library;
 
-use seekquarry\yioop\configs as C;
-use seekquarry\yioop\models\datasources as D;
+use seekquarry\yioop\library\cache\Cache;
+use seekquarry\yioop\library\cache\MarkersCache;
+use seekquarry\yioop\library\cache\CacheMetricsWrapper;
+use seekquarry\yioop\library\cache\StaticDynamicCache;
+use seekquarry\yioop\library\cache\LRUCache;
+use seekquarry\yioop\library\cache\StaticTopicDynamicCache;
 
 /** For Yioop global defines */
 require_once __DIR__ . "/../configs/Config.php";
+
 /**
  * Library of functions used to implement a simple file cache
+ * The class acts as a proxy to internal cache implementation
  *
  * @author Chris Pollett
+ * @author Rushikesh Padia
  */
-class FileCache
+class FileCache extends Cache
 {
     /**
-     * File used to serve last cache request
-     * @var string
-     */
-    public $cache_file;
-    /**
-     * Folder name to use for this FileCache
-     * @var string
-     */
-    public $dir_name;
-    /**
-     * Reference to WebSite object that the current code is running under
-     * @var object
-     */
-    public $web_site;
-    /**
-     * Total number of bins to cycle between
-     */
-    const NUMBER_OF_BINS = 50;
-    /**
-     * Maximum number of files in a bin
-     */
-    const MAX_FILES_IN_A_BIN = 5000;
-    /**
-     * Creates the directory for the file cache, sets how frequently
-     * all items in the cache expire
-     *
-     * @param string $dir_name folder name of where to put the file cache
-     * @param WebSite an optional object that might be used to serve webpages
-     *      when Yioop run in CLI mode. This object has fileGetContents and
-     *      filePutContents methods which allow RAM caching of files.
+     * @var Cache Implementation to be used for caching
      */
-    public function __construct($dir_name, $web_site = null)
-    {
-        $this->dir_name = $dir_name;
-        if (!is_dir($this->dir_name)) {
-            mkdir($this->dir_name);
-            chmod($this->dir_name, 0777);
-            $db_class = C\NS_DATASOURCES . ucfirst(C\DBMS) . "Manager";
-            $db = new $db_class();
-            $db->setWorldPermissionsRecursive($this->dir_name, true);
-        }
-        $this->web_site = $web_site;
-    }
+    private Cache $cache;
     /**
-     * Either a wrapper for file_get_contents, or if a WebSite object is being
-     * used to serve pages, it reads it in using blocking I/O
-     * file_get_contents() and caches it before return its string contents.
-     * Note this function assumes that only the web server is performing I/O
-     * with this file. filemtime() can be used to see if a file on disk has been
-     * changed and then you can use $force_read = true below to force re-
-     * reading the file into the cache
-     *
-     * @param string $filename name of file to get contents of
-     * @param bool $force_read whether to force the file to be read from
-     *      persistent storage rather than the cache
-     * @return string contents of the file given by $filename
+     * @param string $dir_name name of directory
+     * @param mixed|null $web_site Website
      */
-    public function fileGetContents($filename, $force_read = false)
+    public function __construct(string $dir_name, mixed $web_site = null,
+                                string $cacheAlgorithm = "markers_cache")
     {
-        if (!empty($this->web_site)) {
-            return $this->web_site->fileGetContents($filename, $force_read);
-        }
-        return file_get_contents($filename);
-    }
-    /**
-     * Either a wrapper for file_put_contents, or if a WebSite object is being
-     * used to serve pages, writes $data to the persistent file with name
-     * $filename. Saves a copy in the RAM cache if there is a copy already
-     * there.
-     *
-     * @param string $filename name of file to write to persistent storages
-     * @param string $data string of data to store in file
-     */
-    public function filePutContents($filename, $data)
-    {
-        if (!empty($this->web_site)) {
-            return $this->web_site->filePutContents($filename, $data);
-        }
-        return file_put_contents($filename, $data);
+        parent::__construct();
+        switch ($cacheAlgorithm){
+            case "lru_cache":
+                file_put_contents("php://stdout", "--------------Using LRU" . "\n", FILE_APPEND);
+                $cache = new LRUCache($dir_name, $web_site);
+                break;
+            case "static_dynamic_cache":
+                file_put_contents("php://stdout", "--------------Using SD" . "\n", FILE_APPEND);
+                $cache = new StaticDynamicCache($dir_name, $web_site);
+                break;
+            case "static_topic_dynamic_cache":
+                file_put_contents("php://stdout", "--------------Using STD" . "\n", FILE_APPEND);
+                $cache = new StaticTopicDynamicCache($dir_name, $web_site);
+                file_put_contents("php://stdout", "--------------Done STD" . "\n", FILE_APPEND);
+                break;
+            case "markers_cache":
+            default:
+            file_put_contents("php://stdout", "--------------Using Marker" . "\n", FILE_APPEND);
+                $cache = new MarkersCache($dir_name, $web_site);
+                break;
+        }
+        file_put_contents("php://stdout", "--------------Using metric wrapper" . "\n", FILE_APPEND);
+        $this->cache = new CacheMetricsWrapper($cache);
     }
     /**
      * Retrieve data associated with a key that has been put in the cache
      *
-     * @param string $key the key to look up
-     * @return mixed the data associated with the key if it exists, false
+     * @param mixed $key the key to look up
+     * @return mixed|false the data associated with the key if it exists, false
      *     otherwise
      */
-    public function get($key)
+    public function get(mixed $key): mixed
     {
-        $checksum_block = $this->checksum($key);
-        $checksum_dir = $this->dir_name . "/$checksum_block";
-        $this->cache_file = $checksum_dir . "/c" . webencode($key);
-        if (file_exists($this->cache_file)) {
-            $this->updateCache($key);
-            return unserialize($this->fileGetContents($this->cache_file));
-        }
-        return false;
+        return $this->cache->get($key);
     }
     /**
      * Stores in the file cache a key-value pair
@@ -147,123 +103,27 @@ class FileCache
      * @param string $key to associate with value
      * @param mixed $value to store
      */
-    public function set($key, $value)
+    public function set(string $key, mixed $value): bool
     {
-        $checksum_block = $this->checksum($key);
-        $checksum_dir = $this->dir_name . "/$checksum_block";
-        if (!file_exists($checksum_dir)) {
-            mkdir($checksum_dir);
-            chmod($checksum_dir, 0777);
-        }
-        $cache_file = "$checksum_dir/c".webencode($key);
-        $this->updateCache($key);
-        $this->filePutContents($cache_file, serialize($value));
+        return $this->cache->set($key, $value);
     }
     /**
-     * Makes a 0 - self::NUMBER_OF_BINS value out of the provided key
-     *
-     * @param string $key to convert to a random value between
-     *     0 - self::NUMBER_OF_BINS
-     * @return int value between 0 and self::NUMBER_OF_BINS
-     */
-    public function checksum($key)
-    {
-        $len = strlen($key);
-        $value = 0;
-        for ($i = 0; $i < $len; $i++) {
-            $value += ord($key[$i]);
-        }
-        return ($value % self::NUMBER_OF_BINS);
-    }
-    /**
-     * Deletes cache key value files and ram copies of key values stored in the
+     * Deletes cache key value files and ram copies of key values stored in
      * this file cache
      */
-    public function clear()
+    public function clear(): void
     {
-        if (!empty($this->web_site)) {
-            $this->web_site->clearFileCache();
-        }
-        if (is_dir($this->dir_name)) {
-            $db_class = C\NS_DATASOURCES . ucfirst(C\DBMS)."Manager";
-            $db = new $db_class();
-            $db->unlinkRecursive($this->dir_name, false);
-        }
+        $this->cache->clear();
     }
     /**
-     * Used to mark a cache item, and keep track of rounds according to the
-     * marker algorithm. This function determine if the cache is too full
-     * and if so eject an item.
+     * Returns the calculated performance metrics of the cache.
      *
-     * @param string $key that was just read from or written to. Might need
-     *  to be marked according to Marker algorithm
+     * @return array an associative array containing 'total', 'hits',
+     * and 'misses' representing total number of queries, total number
+     * of hits and misses respectively.
      */
-    protected function updateCache($key)
+    public function getPerformanceMetrics(): array
     {
-        $checksum_block = $this->checksum($key);
-        $checksum_dir = $this->dir_name . "/$checksum_block";
-        $marker_file = "$checksum_dir/cache_markers.txt";
-        if (!file_exists($checksum_dir)) {
-            mkdir($checksum_dir);
-            chmod($checksum_dir, 0777);
-        }
-        if (file_exists($marker_file)) {
-            $data = unserialize($this->fileGetContents($marker_file));
-        } else {
-            $data = [];
-        }
-        if (empty($data['MARKED'])) {
-            $data['MARKED'] = [];
-        }
-        if (empty($data['UNMARKED'])) {
-            $data['UNMARKED'] = [];
-        }
-        $now = time();
-        if (empty($data['TIME'])) {
-            $data['TIME'] = $now;
-        }
-        if (empty($data['UNMARKED'][$key]) && empty($data['MARKED'][$key])) {
-            $data['UNMARKED'][$key] = true;
-        }
-        if (!empty($data['UNMARKED'][$key])) {
-            $data['MARKED'][$key] = true;
-            unset($data['UNMARKED'][$key]);
-        }
-        $num_marked = count($data['MARKED']);
-        if ($num_marked > self::MAX_FILES_IN_A_BIN) {
-            $data['UNMARKED'] = array_merge($data['UNMARKED'], $data['MARKED']);
-            $data['MARKED'] = [];
-        }
-        $num_unmarked = count($data['UNMARKED']);
-        $total_in_cache = $num_marked + $num_unmarked;
-        if ($total_in_cache > self::MAX_FILES_IN_A_BIN) {
-            $num_delete = $total_in_cache - self::MAX_FILES_IN_A_BIN;
-            $num_unmarked_delete = min($num_unmarked, $num_delete);
-            for ($i = 0; $i < $num_unmarked_delete; $i++) {
-                $keys = array_keys($data['UNMARKED']);
-                $eject_key = mt_rand(0, $num_unmarked - 1);
-                unset($data['UNMARKED'][$eject_key]);
-                $num_unmarked--;
-                $delete_file = $checksum_dir . "/" . webencode($eject_key);
-                if (file_exists($delete_file)) {
-                    unlink($delete_file);
-                }
-            }
-            if ($now - $data["TIME"] > C\MIN_QUERY_CACHE_TIME) {
-                $in_cache_files = array_flip(glob($checksum_dir . "/c*"));
-                $keys = array_keys($data['UNMARKED']);
-                foreach ($keys as $check_key) {
-                    $check_file = $checksum_dir . "/" . webencode($check_key);
-                    if (isset($in_cache_files[$check_file])) {
-                        unset($in_cache_files[$check_file]);
-                    }
-                }
-                foreach ($in_cache_files as $to_delete => $num) {
-                    unlink($to_delete);
-                }
-                $data["TIME"] = $now;
-            }
-        }
-        $this->filePutContents($marker_file, serialize($data));
+        return $this->cache->getPerformanceMetrics();
     }
 }
diff --git a/src/library/KMeansClustering.php b/src/library/KMeansClustering.php
new file mode 100644
index 000000000..455c470bd
--- /dev/null
+++ b/src/library/KMeansClustering.php
@@ -0,0 +1,236 @@
+<?php
+/**
+ * SeekQuarry/Yioop --
+ * Open Source Pure PHP Search Engine, Crawler, and Indexer
+ *
+ * Copyright (C) 2009 - 2023  Chris Pollett chris@pollett.org
+ *
+ * LICENSE:
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * END LICENSE
+ *
+ *
+ * @author Rushikesh Padia padiarushi3012@gmail.com
+ * @license https://www.gnu.org/licenses/ GPL3
+ * @link https://www.seekquarry.com/
+ * @copyright 2009 - 2023
+ * @filesource
+ */
+namespace seekquarry\yioop\library;
+
+use seekquarry\yioop\configs as C;
+/**
+ * A class implementing K-Means clustering algorithm
+ * The class implements training and prediction of data point
+ * Data points are sparse vector of word embeddings
+ *
+ * @author Rushikesh Padia
+ */
+class KMeansClustering
+{
+    /**
+     * Base directory to store model state
+     */
+    public const DIR_NAME = C\WORK_DIRECTORY . "/topicmodel/kmeans";
+    /**
+     * file to store centroids of k-means
+     */
+    public const MODEL_PATH = self::DIR_NAME . "/centroids.txt";
+    /**
+     * file to store the size of vocabulary
+     */
+    public const VECTOR_SIZE_PATH = self::DIR_NAME . "/vocabSize.txt";
+    /**
+     * @var array centroids of the clusters
+     */
+    private array $centroids = [];
+    /**
+     * @var int size of vector of centroids
+     */
+    private int $vector_size;
+    /**
+     * @var float farthest allowed distance in prediction
+     */
+    private float $distance_threshold;
+
+    /**
+     * Initializes the k-means model, loads state of model from files
+     */
+    public function __construct()
+    {
+        $this->loadState();
+    }
+    /**
+     * Trains the K-Means algorithm by calculating the centroids
+     * @param array $points of sparse matrix of data points for training
+     * @param int $k number of clusters
+     * @param int $vector_size size of vectors used by vectorizer
+     * @param int $max_iterations maximum iterations
+     */
+    public function train(array $points, int $k, int $vector_size,
+                          int $max_iterations = 100):
+    void
+    {
+        $this->vector_size = $vector_size;
+        $indices = array_rand($points, $k);
+        foreach ($indices as $index) {
+            $centroid = array_fill(0, $this->vector_size, 0);
+            foreach (array_keys($points[$index]) as $term){
+                $centroid[$term] += 1;
+            }
+            $this->centroids[] = $centroid;
+        }
+        for ($iteration = 1; $iteration <= $max_iterations; $iteration++) {
+            $clusters = array_fill(0, $k, []);
+            foreach ($points as $point) {
+                $index = $this->findClosestCentroid($point);
+                $clusters[$index][] = $point;
+            }
+            $new_centroids = [];
+            foreach ($clusters as $cluster) {
+                $cluster_size = count($cluster);
+                $centroid = array_fill(0, $this->vector_size, 0);
+                if($cluster_size>0) {
+                    foreach ($cluster as $point) {
+                        foreach (array_keys($point) as $term) {
+                            $centroid[$term] += $point[$term];
+                        }
+                    }
+                    for ($i = 0; $i < $this->vector_size; $i++) {
+                        $centroid[$i] /= $cluster_size;
+                    }
+                }
+                $new_centroids[] = $centroid;
+            }
+            if ($this->centroids == $new_centroids) {
+                break;
+            }
+            $this->centroids = $new_centroids;
+        }
+        $this->updateDistanceThreshold();
+        $this->saveState();
+    }
+    /**
+     * Loads the saved state of k-means algorithm
+     */
+    private function loadState(): void
+    {;
+        if(file_exists(self::VECTOR_SIZE_PATH)){
+            $this->vector_size = unserialize(
+                file_get_contents(self::VECTOR_SIZE_PATH));
+        }
+        if(file_exists(self::MODEL_PATH)){
+            $this->centroids = unserialize(
+                file_get_contents(self::MODEL_PATH));
+        }
+        $this->updateDistanceThreshold();
+    }
+    /**
+     * Saves the state of k-means so that it does not
+     * have to be trained after every restart
+     */
+    private function saveState(): void
+    {
+        if(!is_dir(self::DIR_NAME)){
+            mkdir(self::DIR_NAME, "0777", true);
+        }
+        file_put_contents(self::MODEL_PATH, serialize($this->centroids));
+        file_put_contents(self::VECTOR_SIZE_PATH,
+            serialize($this->vector_size));
+    }
+    /**
+     * Predicts the cluster of data point
+     * @param $point array sparse vector of data point to be predicted
+     * @return int index of cluster to which data point belongs
+     */
+    public function predict(array $point):int
+    {
+        $min_distance = INF;
+        $closest_centroid_index = 0;
+        foreach ($this->centroids as $i => $centroid) {
+            $distance = $this->euclideanDistance($point, $centroid);
+            if ($distance < $min_distance) {
+                $min_distance = $distance;
+                $closest_centroid_index = $i;
+            }
+        }
+        if($min_distance < $this->distance_threshold){
+            return $closest_centroid_index;
+        }else{
+            return -1;
+        }
+    }
+    /**
+     * Calculates Euclidean distance between two points
+     * @param array $point data point
+     * @param array $centroid centroid vector
+     * @return float euclidean distance
+     */
+    private function euclideanDistance(array $point, array $centroid): float
+    {
+        $sum = 0;
+        foreach (array_keys($point) as $term) {
+            $difference = $point[$term] - $centroid[$term];
+            $sum += pow($difference, 2);
+        }
+        return $sum;
+    }
+    /**
+     * Return index of the centroid closest to the data point.
+     * @param $point array data point
+     * @return int index of the closest centroid
+     */
+    private function findClosestCentroid(array $point):int
+    {
+        $min_distance = INF;
+        $closest_centroid_index = 0;
+        foreach ($this->centroids as $i => $centroid) {
+            $distance = $this->euclideanDistance($point, $centroid);
+            if ($distance < $min_distance) {
+                $min_distance = $distance;
+                $closest_centroid_index = $i;
+            }
+        }
+        return $closest_centroid_index;
+    }
+    /**
+     * Returns centroids of k-means
+     * @return array centroids of k-means
+     */
+    public function getCentroids():array{
+        return $this->centroids;
+    }
+    /**
+     * Method updates the distanceThreshold.
+     * distanceThreshold is used as a threshold value to predict whether
+     * a point is too far from all cluster to be assigned a cluster.
+     * This is important in  topical cache when no cluster has to
+     * be assigned a point.
+     */
+    private function updateDistanceThreshold():void
+    {
+        $max_distance = 0;
+        $k = count($this->centroids);
+        for($i = 0; $i < $k; $i++){
+              for($j = $i+1; $j < $k; $j++){
+                  $distance = self::euclideanDistance($this->centroids[$i],
+                      $this->centroids[$j]);
+                  $max_distance = max($max_distance, $distance);
+              }
+        }
+        $this->distance_threshold = $max_distance;
+    }
+}
\ No newline at end of file
diff --git a/src/library/cache/Cache.php b/src/library/cache/Cache.php
new file mode 100644
index 000000000..bb270d23d
--- /dev/null
+++ b/src/library/cache/Cache.php
@@ -0,0 +1,240 @@
+<?php
+/**
+ * SeekQuarry/Yioop --
+ * Open Source Pure PHP Search Engine, Crawler, and Indexer
+ *
+ * Copyright (C) 2009 - 2023  Chris Pollett chris@pollett.org
+ *
+ * LICENSE:
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * END LICENSE
+ *
+ *
+ * @author Rushikesh Padia padiarushi3012@gmail.com
+ * @license https://www.gnu.org/licenses/ GPL3
+ * @link https://www.seekquarry.com/
+ * @copyright 2009 - 2023
+ * @filesource
+ */
+namespace seekquarry\yioop\library\cache;
+
+use seekquarry\yioop\library\WebSite;
+use seekquarry\yioop\configs as C;
+use function seekquarry\yioop\library\webencode;
+
+/** For Yioop global defines */
+require_once __DIR__ . "/../../configs/Config.php";
+/**
+ * Contains common methods for cache that requires persistence
+ * to store the cache values in files
+ *
+ * @author Chris Pollett
+ * @author Rushikesh Padia
+ */
+abstract class Cache {
+    /**
+     * Maximum number of files in a bin
+     */
+    protected const MAX_FILES_IN_A_BIN = 5000;
+    /**
+     * Total number of bins to cycle between
+     */
+    protected const NUMBER_OF_BINS = 50;
+    /**
+     * File used to serve last cache request
+     * @var string
+     */
+    protected string $cache_file;
+    /**
+     * Folder name to use for this FileCache
+     * @var string
+     */
+    protected string $dir_name;
+    /**
+     * PPH web server for RAM Caching
+     * @var WebSite|null
+     */
+    protected ?WebSite $web_site;
+
+    /**
+     * Creates the directory for the file cache, sets how frequently
+     * all items in the cache expire
+     *
+     * @param string|null $dir_name folder name of where to put the file cache
+     * @param WebSite|null $web_site an optional object that might be used
+     * to serve webpages when Yioop run in CLI mode. This object has
+     * fileGetContents and filePutContents methods which allow RAM caching
+     * of files.
+     */
+    public function __construct(string $dir_name = null,
+                                WebSite $web_site = null)
+    {
+        if(!empty($dir_name)) {
+            $this->dir_name = $dir_name;
+            if (!is_dir($this->dir_name)) {
+                mkdir($this->dir_name, 0777, true);
+                $db_class = C\NS_DATASOURCES . ucfirst(C\DBMS) . "Manager";
+                $db = new $db_class();
+                $db->setWorldPermissionsRecursive($this->dir_name, true);
+            }
+            $this->web_site = $web_site;
+        }
+    }
+    /**
+     * Method to get the value of given key from the cache.
+     * Based on the implementation, state of the cache is updated if hit occurs
+     * else false is returned.
+     *
+     * @param string $key for which value is required
+     * @return mixed|false returns value if key is present in the cache,
+     * else false
+     */
+    public abstract function get(string $key): mixed;
+    /**
+     * Method to add value to the cache.
+     * Based on the implementation, state of the cache is updated.
+     * If cache is full, a key is removed based on the eviction policy
+     *
+     * @param string $key against which value has to be stored
+     * @param mixed $value to be stored in cache
+     * @return bool void if value is set, false otherwise
+     */
+    public abstract function set(string $key, mixed $value): bool;
+    /**
+     * Returns the calculated performance metrics of the cache.
+     * Cache implementations can add additional metrics using
+     * this method
+     * @return array containing performance metrics of cache
+     */
+    public function getPerformanceMetrics(): array
+    {
+        return [];
+    }
+    /**
+     * Either a wrapper for file_get_contents, or if a WebSite object is being
+     * used to serve pages, it reads it in using blocking I/O
+     * file_get_contents() and caches it before return its string contents.
+     * Note this function assumes that only the web server is performing I/O
+     * with this file. filemtime() can be used to see if a file on disk has
+     * been changed and then you can use $force_read = true below to force re-
+     * reading the file into the cache
+     *
+     * @param string $filename name of file to get contents of
+     * @param bool $force_read whether to force the file to be read from
+     * persistent storage rather than the cache
+     * @return string contents of the file given by $filename
+     */
+    public function fileGetContents(string $filename,
+                                    bool $force_read = false): string
+    {
+        if (!empty($this->web_site)) {
+            return $this->web_site->fileGetContents($filename, $force_read);
+        }
+        return file_get_contents($filename);
+    }
+    /**
+     * Either a wrapper for file_put_contents, or if a WebSite object is being
+     * used to serve pages, writes $data to the persistent file with name
+     * $filename. Saves a copy in the RAM cache if there is a copy already
+     * there.
+     *
+     * @param string $filename name of file to write to persistent storages
+     * @param string $data string of data to store in file
+     */
+    public function filePutContents(string $filename, string $data): false|int
+    {
+        if (!empty($this->web_site)) {
+            return $this->web_site->filePutContents($filename, $data);
+        }
+        return file_put_contents($filename, $data);
+    }
+    /**
+     * Deletes cache key value files and ram copies of key values stored in the
+     * this file cache
+     */
+    public function clear(): void
+    {
+        if (!empty($this->web_site)) {
+            $this->web_site->clearFileCache();
+        }
+        if (is_dir($this->dir_name)) {
+            $db_class = C\NS_DATASOURCES . ucfirst(C\DBMS)."Manager";
+            $db = new $db_class();
+            $db->unlinkRecursive($this->dir_name, false);
+        }
+    }
+    /**
+     * Makes a 0 - self::NUMBER_OF_BINS value out of the provided key
+     *
+     * @param string $key to convert to a random value between
+     *     0 - self::NUMBER_OF_BINS
+     * @return int value between 0 and self::NUMBER_OF_BINS
+     */
+    public function checksum(string $key): int
+    {
+        $len = strlen($key);
+        $value = 0;
+        for ($i = 0; $i < $len; $i++) {
+            $value += ord($key[$i]);
+        }
+        return ($value % self::NUMBER_OF_BINS);
+    }
+    /**
+     * Adds file with encoded key name and content as value in cache dir
+     * @param string $key cache key
+     * @param mixed $value cache value
+     */
+    protected function addToStore(string $key, mixed $value): void
+    {
+        $checksum_block = $this->checksum($key);
+        $checksum_dir = $this->dir_name . "/$checksum_block";
+        if (!file_exists($checksum_dir)) {
+            mkdir($checksum_dir);
+            chmod($checksum_dir, 0777);
+        }
+        $cache_file = "$checksum_dir/c" . webencode($key);
+        $this->filePutContents($cache_file, serialize($value));
+    }
+    /**
+     * Returns value from cache file
+     * @param string $key cache key
+     * @return false|mixed the data associated with the key if it exists, false
+     * otherwise
+     */
+    protected function getFromStore(string $key): mixed
+    {
+        $checksum_block = $this->checksum($key);
+        $checksum_dir = $this->dir_name . "/$checksum_block";
+        $this->cache_file = $checksum_dir . "/c" . webencode($key);
+        if (file_exists($this->cache_file)) {
+            return unserialize($this->fileGetContents($this->cache_file));
+        }
+        return false;
+    }
+    /**
+     * Removes key from the cache
+     * @param string $key cache key
+     */
+    protected function removeFromStore(string $key): void
+    {
+        $checksum_block = $this->checksum($key);
+        $checksum_dir = $this->dir_name . "/$checksum_block";
+        $delete_file = $checksum_dir . "/c" . webencode($key);
+        if (file_exists($delete_file)) {
+            unlink($delete_file);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/library/cache/CacheMetricsWrapper.php b/src/library/cache/CacheMetricsWrapper.php
new file mode 100644
index 000000000..ded7d5a98
--- /dev/null
+++ b/src/library/cache/CacheMetricsWrapper.php
@@ -0,0 +1,123 @@
+<?php
+/**
+ * SeekQuarry/Yioop --
+ * Open Source Pure PHP Search Engine, Crawler, and Indexer
+ *
+ * Copyright (C) 2009 - 2023  Chris Pollett chris@pollett.org
+ *
+ * LICENSE:
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * END LICENSE
+ *
+ *
+ * @author Rushikesh Padia padiarushi3012@gmail.com
+ * @license https://www.gnu.org/licenses/ GPL3
+ * @link https://www.seekquarry.com/
+ * @copyright 2009 - 2023
+ * @filesource
+ */
+namespace seekquarry\yioop\library\cache;
+
+/**
+ * A wrapper class to collect performance metrics of cache implementations
+ *
+ * @author Rushikesh Padia
+ */
+class CacheMetricsWrapper extends Cache
+{
+    /**
+     * @var Cache Any implementation of cache
+     */
+    private Cache $cache;
+    /**
+     * @var int Number of hits
+     */
+    private int $hits;
+    /**
+     * @var int|float Hit rate of cache
+     */
+    private int|float $hit_rate;
+    /**
+     * @var int Number of misses
+     */
+    private int $misses;
+    /**
+     * @var int Total number of requests
+     */
+    private int $total;
+
+    /**
+     * @param Cache $cache Any implementation of cache
+     */
+    public function __construct(Cache $cache)
+    {
+        parent::__construct();
+        $this->cache = $cache;
+        $this->hits = 0;
+        $this->misses = 0;
+        $this->total = 0;
+    }
+    /**
+     * Calculates hits and misses based on the return value from the
+     * wrapped class
+     * @param string $key cache key
+     * @return mixed|false cache value
+     */
+    public function get(string $key): mixed
+    {
+        $value = $this->cache->get($key);
+        if ($value !== false) {
+            $this->hits++;
+        } else {
+            $this->misses++;
+        }
+        $this->total++;
+        $this->hit_rate = ($this->hits * 100) / ($this->total + 1);
+        return $value;
+    }
+    /**
+     * Delegates call to the wrapped class which sets value in cache
+     * @param string $key cache key
+     * @param mixed $value cache value
+     */
+    public function set(string $key, mixed $value): bool
+    {
+        return $this->cache->set($key, $value);
+    }
+    /**
+     * Delegates call to the wrapped class which clears the cache
+     */
+    public function clear(): void
+    {
+        $this->cache->clear();
+    }
+    /**
+     * Returns the calculated performance metrics of the cache.
+     *
+     * @return array an associative array containing 'total', 'hits',
+     * and 'misses' representing total number of queries,
+     * total number of hits and misses respectively.
+     */
+    public function getPerformanceMetrics(): array
+    {
+        $performance_metrics = $this->cache->getPerformanceMetrics();
+        $performance_metrics['total'] =  $this->total;
+        $performance_metrics['hits'] =  $this->hits;
+        $performance_metrics['misses'] =  $this->misses;
+        $performance_metrics['hit rate'] =  $this->hit_rate;
+        return $performance_metrics;
+    }
+}
\ No newline at end of file
diff --git a/src/library/cache/LRUCache.php b/src/library/cache/LRUCache.php
new file mode 100644
index 000000000..86086352f
--- /dev/null
+++ b/src/library/cache/LRUCache.php
@@ -0,0 +1,285 @@
+<?php
+/**
+ * SeekQuarry/Yioop --
+ * Open Source Pure PHP Search Engine, Crawler, and Indexer
+ *
+ * Copyright (C) 2009 - 2023  Chris Pollett chris@pollett.org
+ *
+ * LICENSE:
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * END LICENSE
+ *
+ *
+ * @author Rushikesh Padia padiarushi3012@gmail.com
+ * @license https://www.gnu.org/licenses/ GPL3
+ * @link https://www.seekquarry.com/
+ * @copyright 2009 - 2023
+ * @filesource
+ */
+namespace seekquarry\yioop\library\cache;
+use seekquarry\yioop\library\WebSite;
+
+/**
+ * Implementation of Least Recently Used caching algorithm
+ *
+ * @author Rushikesh Padia
+ */
+class LRUCache extends Cache
+{
+    /**
+     * Relative path to the file containing persistent state of LRU cache
+     */
+    const LRU_STATE_RELATIVE_PATH = "/lru_state.txt";
+    /**
+     * @var Node head of the linked list
+     */
+    private Node $head;
+    /**
+     * @var Node tail of the linked list
+     */
+    private Node $tail;
+    /**
+     * @var array associated array to store mapping key => Node
+     */
+    private array $map;
+    /**
+     * @var int portion of maximum capacity to be used
+     */
+    private int $capacity;
+
+    /**
+     * Creates the directory for the file cache, sets how frequently
+     * all items in the cache expire
+     *
+     * @param string $dir_name folder name of where to put the file cache
+     * @param WebSite|null $web_site an optional object that might be
+     * used to serve webpages when Yioop run in CLI mode. This object has
+     * fileGetContents and filePutContents methods which allow RAM caching
+     * of files.
+     * @param int $load_factor portion of maximum capacity to be used
+     */
+    public function __construct(string $dir_name, WebSite $web_site = null,
+                                int $load_factor = 1)
+    {
+        parent::__construct($dir_name, $web_site);
+        $this->capacity = $load_factor * self::MAX_FILES_IN_A_BIN
+            * self::NUMBER_OF_BINS;
+        $this->loadState();
+    }
+    /**
+     * Method to get the value of given key from the cache.
+     * If key is present, corresponding node is removed from
+     * the linked list and is attached to the first place
+     *
+     * @param string $key for which value is required
+     * @return mixed|false returns value if key is present in the cache,
+     * else false
+     */
+    public function get(string $key): mixed
+    {
+        if (!isset($this->map[$key])) {
+            return false;
+        }
+        $node = $this->map[$key];
+        if (count($this->map) == 1) {
+            $this->saveState();
+            return $this->getFromStore($key);
+        }
+        $this->detach($node);
+        $this->attach($this->head, $node);
+        $this->saveState();
+        return $this->getFromStore($key);
+    }
+    /**
+     * Sets value in the cache. If key is present, it is attached
+     * to the first place. If capacity is full, node is deleted from last
+     *
+     * @param string $key against which value has to be stored
+     * @param mixed $value to be stored in cache
+     * @return bool void if value is set, false otherwise
+     */
+    public function set(string $key, mixed $value): bool
+    {
+        if ($this->capacity <= 0) {
+            return false;
+        }
+        if (isset($this->map[$key]) && !empty($this->map[$key])) {
+            $node = $this->map[$key];
+            $this->detach($node);
+            $this->attach($this->head, $node);
+            $this->addToStore($key, $value);
+        } else {
+            $node = new Node($key);
+            $this->addToStore($key, $value);
+            $this->map[$key] = $node;
+            $this->attach($this->head, $node);
+            if (count($this->map) > $this->capacity) {
+                $node_to_remove = $this->tail->getPrevious();
+                $this->detach($node_to_remove);
+                unset($this->map[$node_to_remove->getKey()]);
+                $this->removeFromStore($key);
+            }
+        }
+        $this->saveState();
+        return true;
+    }
+    /**
+     * Adds node in the front of linked-list
+     *
+     * @param Node $head head not of the linked-list
+     * @param Node $node node to be attached
+     */
+    private function attach(Node $head, Node $node): void
+    {
+        $node->setPrevious($head);
+        $node->setNext($head->getNext());
+        $node->getNext()->setPrevious($node);
+        $node->getPrevious()->setNext($node);
+    }
+    /**
+     * Deletes node from linked-list
+     *
+     * @param Node $node node to be deleted
+     */
+    private function detach(Node $node): void
+    {
+        $node->getPrevious()->setNext($node->getNext());
+        $node->getNext()->setPrevious($node->getPrevious());
+    }
+    /**
+     * Clears cache and local data structures
+     */
+    public function clear(): void
+    {
+        parent::clear();
+        $this->clearLocalData();
+    }
+    /**
+     * Re-initializes local data structures for maintaining cache.
+     * Creates empty map and linked-list used for cache.
+     */
+    public function clearLocalData(): void
+    {
+        $this->map = [];
+        $this->head = new Node(null);
+        $this->tail = new Node(null);
+        $this->head->setNext($this->tail);
+        $this->tail->setPrevious($this->head);
+    }
+    /**
+     * Loads the saved state of cache
+     */
+    private function loadState(): void
+    {
+        $this->clearLocalData();
+        $lru_file = $this->dir_name . self::LRU_STATE_RELATIVE_PATH;
+        if (file_exists($lru_file)) {
+            $keys = unserialize($this->fileGetContents($lru_file));
+        } else {
+            $keys = [];
+        }
+        $key_count = min(count($keys), $this->capacity);
+        for($i = $key_count - 1; $i >= 0; $i--){
+            $node = new Node($keys[$i]);
+            $this->map[$keys[$i]] = $node;
+            $this->attach($this->head, $node);
+        }
+    }
+    /**
+     * Saves the state of cache so that it does not
+     * lose order of queries after restart
+     */
+    private function saveState(): void
+    {
+        $keys = [];
+        $node = $this->head->getNext();
+        while($node->getKey() !== null){
+            $keys[] = $node->getKey();
+            $node = $node->getNext();
+        }
+        $lru_file = $this->dir_name . self::LRU_STATE_RELATIVE_PATH;
+        $this->filePutContents($lru_file, serialize($keys));
+    }
+}
+
+/**
+ * Class represents node in the linked list
+ */
+class Node {
+    /**
+     * @var string|null cache key
+     */
+    private ?string $key;
+    /**
+     * @var Node|null next node in the linked list
+     */
+    private ?Node $next;
+    /**
+     * @var Node|null previous node in the linked list
+     */
+    private ?Node $previous;
+
+    /**
+     * Initializes Node object
+     * @param string|null $key
+     */
+    public function __construct(?string $key) {
+        $this->key = $key;
+    }
+    /**
+     * Sets next node
+     * @param Node|null $next next node
+     * @return void
+     */
+    public function setNext(?Node $next): void
+    {
+        $this->next = $next;
+    }
+
+    /**
+     * Sets previous node
+     * @param Node|null $previous previous node
+     * @return void
+     */
+    public function setPrevious(?Node $previous): void
+    {
+        $this->previous = $previous;
+    }
+    /**
+     * Returns key
+     * @return string|null key
+     */
+    public function getKey(): ?string
+    {
+        return $this->key;
+    }
+    /**
+     * Returns next node
+     * @return Node|null next node
+     */
+    public function getNext(): ?Node
+    {
+        return $this->next;
+    }
+    /**
+     * Returns previous node
+     * @return Node|null previous node
+     */
+    public function getPrevious(): ?Node
+    {
+        return $this->previous;
+    }
+}
\ No newline at end of file
diff --git a/src/library/cache/MarkersCache.php b/src/library/cache/MarkersCache.php
new file mode 100644
index 000000000..91d37730a
--- /dev/null
+++ b/src/library/cache/MarkersCache.php
@@ -0,0 +1,161 @@
+<?php
+/**
+ * SeekQuarry/Yioop --
+ * Open Source Pure PHP Search Engine, Crawler, and Indexer
+ *
+ * Copyright (C) 2009 - 2023  Chris Pollett chris@pollett.org
+ *
+ * LICENSE:
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * END LICENSE
+ *
+ *
+ * @author Chris Pollett chris@pollett.org
+ * @author Rushikesh Padia padiarushi3012@gmail.com
+ * @license https://www.gnu.org/licenses/ GPL3
+ * @link https://www.seekquarry.com/
+ * @copyright 2009 - 2023
+ * @filesource
+ */
+namespace seekquarry\yioop\library\cache;
+
+use seekquarry\yioop\library\WebSite;
+use seekquarry\yioop\configs as C;
+use function seekquarry\yioop\library\webencode;
+
+/** For Yioop global defines */
+require_once __DIR__ . "/../../configs/Config.php";
+
+class MarkersCache extends Cache
+{
+    /**
+     * Creates the directory for the file cache, sets how frequently
+     * all items in the cache expire
+     *
+     * @param string $dir_name folder name of where to put the file cache
+     * @param WebSite|null $web_site an optional object that might be used to serve
+     *      webpages when Yioop run in CLI mode. This object has
+     *      fileGetContents and filePutContents methods which allow
+     *      RAM caching of files.
+     */
+    public function __construct(string $dir_name, WebSite $web_site = null)
+    {
+        parent::__construct($dir_name, $web_site);
+    }
+    /**
+     * Retrieve data associated with a key that has been put in the cache
+     *
+     * @param string $key the key to look up
+     * @return mixed|false the data associated with the key if it exists, false
+     *     otherwise
+     */
+    public function get(string $key): mixed
+    {
+        $result = $this->getFromStore($key);
+        if($result){
+            $this->updateCache($key);
+        }
+        return $result;
+    }
+    /**
+     * Stores in the file cache a key-value pair
+     *
+     * @param string $key to associate with value
+     * @param mixed $value to store
+     */
+    public function set(string $key, mixed $value): bool
+    {
+        $this->addToStore($key, $value);
+        $this->updateCache($key);
+        return true;
+    }
+    /**
+     * Used to mark a cache item, and keep track of rounds according to the
+     * marker algorithm. This function determine if the cache is too full
+     * and if so eject an item.
+     *
+     * @param string $key that was just read from or written to. Might need
+     *  to be marked according to Marker algorithm
+     */
+    protected function updateCache(string $key): void
+    {
+        $checksum_block = $this->checksum($key);
+        $checksum_dir = $this->dir_name . "/$checksum_block";
+        $marker_file = "$checksum_dir/cache_markers.txt";
+        if (!file_exists($checksum_dir)) {
+            mkdir($checksum_dir);
+            chmod($checksum_dir, 0777);
+        }
+        if (file_exists($marker_file)) {
+            $data = unserialize($this->fileGetContents($marker_file));
+        } else {
+            $data = [];
+        }
+        if (empty($data['MARKED'])) {
+            $data['MARKED'] = [];
+        }
+        if (empty($data['UNMARKED'])) {
+            $data['UNMARKED'] = [];
+        }
+        $now = time();
+        if (empty($data['TIME'])) {
+            $data['TIME'] = $now;
+        }
+        if (empty($data['UNMARKED'][$key]) && empty($data['MARKED'][$key])) {
+            $data['UNMARKED'][$key] = true;
+        }
+        if (!empty($data['UNMARKED'][$key])) {
+            $data['MARKED'][$key] = true;
+            unset($data['UNMARKED'][$key]);
+        }
+        $num_marked = count($data['MARKED']);
+        if ($num_marked > self::MAX_FILES_IN_A_BIN) {
+            $data['UNMARKED'] = array_merge($data['UNMARKED'], $data['MARKED']);
+            $data['MARKED'] = [];
+        }
+        $num_unmarked = count($data['UNMARKED']);
+        $total_in_cache = $num_marked + $num_unmarked;
+        if ($total_in_cache > self::MAX_FILES_IN_A_BIN) {
+            $num_delete = $total_in_cache - self::MAX_FILES_IN_A_BIN;
+            $num_unmarked_delete = min($num_unmarked, $num_delete);
+            for ($i = 0; $i < $num_unmarked_delete; $i++) {
+                $keys = array_keys($data['UNMARKED']);
+                $eject_key = mt_rand(0, $num_unmarked - 1);
+                unset($data['UNMARKED'][$eject_key]);
+                $num_unmarked--;
+                $delete_file = $checksum_dir . "/c" . webencode($keys[$eject_key]);
+                if (file_exists($delete_file)) {
+                    unlink($delete_file);
+                }
+            }
+            if ($now - $data["TIME"] > C\MIN_QUERY_CACHE_TIME) {
+                $in_cache_files = array_flip(glob($checksum_dir . "/c*"));
+                $keys = array_keys($data['UNMARKED']);
+                foreach ($keys as $check_key) {
+                    $check_file = $checksum_dir . "/c" . webencode($check_key);
+                    if (isset($in_cache_files[$check_file])) {
+                        unset($in_cache_files[$check_file]);
+                    }
+                }
+                foreach ($in_cache_files as $to_delete => $num) {
+                    unlink($to_delete);
+                }
+                $data["TIME"] = $now;
+            }
+        }
+        $this->filePutContents($marker_file, serialize($data));
+    }
+}
\ No newline at end of file
diff --git a/src/library/cache/StaticCache.php b/src/library/cache/StaticCache.php
new file mode 100644
index 000000000..68bb7fe54
--- /dev/null
+++ b/src/library/cache/StaticCache.php
@@ -0,0 +1,92 @@
+<?php
+/**
+ * SeekQuarry/Yioop --
+ * Open Source Pure PHP Search Engine, Crawler, and Indexer
+ *
+ * Copyright (C) 2009 - 2023  Chris Pollett chris@pollett.org
+ *
+ * LICENSE:
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * END LICENSE
+ *
+ *
+ * @author Rushikesh Padia padiarushi3012@gmail.com
+ * @license https://www.gnu.org/licenses/ GPL3
+ * @link https://www.seekquarry.com/
+ * @copyright 2009 - 2023
+ * @filesource
+ */
+namespace seekquarry\yioop\library\cache;
+
+use seekquarry\yioop\controllers\SearchController;
+use seekquarry\yioop\library\CrawlConstants;
+use seekquarry\yioop\library\WebSite;
+use seekquarry\yioop\configs as C;
+/**
+ * A simple read-only cache that does not allow set operation.
+ * Values can be populated using load() method
+ *
+ * @author Rushikesh Padia
+ */
+class StaticCache extends Cache
+{
+    /**
+     * Relative path to the directory to store file cache
+     */
+    const RELATIVE_DIR = "/static";
+
+    /**
+     * Creates the directory for the file cache, sets how frequently
+     * all items in the cache expire
+     *
+     * @param string $dir_name folder name of where to put the file cache
+     * @param WebSite|null $web_site an optional object that might be
+     * used to serve webpages when Yioop run in CLI mode. This object has
+     * fileGetContents and filePutContents methods which allow RAM caching
+     * of files.
+     */
+    public function __construct(string $dir_name, WebSite $web_site = null) {
+        parent::__construct($dir_name . self::RELATIVE_DIR, $web_site);
+    }
+    /**
+     * Returns value of cache against the key
+     * @param string $key cache key
+     * @return false|mixed cache value
+     */
+    public function get(string $key): mixed
+    {
+        return $this->getFromStore($key);
+    }
+    /**
+     * Ignores value to be set in the cache
+     * @param string $key cache key
+     * @param mixed $value cache value
+     */
+    public function set(string $key, mixed $value): bool
+    {
+        return false;
+    }
+    /**
+     * adds value to in the cache
+     * @param string $key cache key
+     * @param mixed $value cache value
+     */
+    public function forced_set(string $key, mixed $value): bool
+    {
+        $this->addToStore($key, $value);
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/src/library/cache/StaticDynamicCache.php b/src/library/cache/StaticDynamicCache.php
new file mode 100644
index 000000000..5f78d6c94
--- /dev/null
+++ b/src/library/cache/StaticDynamicCache.php
@@ -0,0 +1,100 @@
+<?php
+/**
+ * SeekQuarry/Yioop --
+ * Open Source Pure PHP Search Engine, Crawler, and Indexer
+ *
+ * Copyright (C) 2009 - 2023  Chris Pollett chris@pollett.org
+ *
+ * LICENSE:
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * END LICENSE
+ *
+ *
+ * @author Rushikesh Padia padiarushi3012@gmail.com
+ * @license https://www.gnu.org/licenses/ GPL3
+ * @link https://www.seekquarry.com/
+ * @copyright 2009 - 2023
+ * @filesource
+ */
+namespace seekquarry\yioop\library\cache;
+use seekquarry\yioop\library\WebSite;
+
+/**
+ * Static-Dynamic cache stores cache in two different sections.
+ * One is a static part to store all time popular queries. Other
+ * is a dynamic part where it stores short-term poplar queries.
+ *
+ * @author Rushikesh Padia
+ */
+class StaticDynamicCache extends Cache
+{
+    /**
+     * @var Cache static cache whose content does not change
+     */
+    private Cache $static_cache;
+    /**
+     * @var Cache any implementation of dynamic cache for e.g. LRU
+     */
+    private Cache $dynamic_cache;
+
+    /**
+     * Initializes static and dynamic cache
+     *
+     * @param string $dir_name folder name of where to put the file cache
+     * @param WebSite|null $web_site an optional object that might be
+     * used to serve webpages when Yioop run in CLI mode. This object has
+     * fileGetContents and filePutContents methods which allow RAM caching
+     * of files.
+     */
+    public function __construct(string $dir_name, WebSite $web_site = null)
+    {
+        parent::__construct($dir_name, $web_site);
+        $this->static_cache = new StaticCache($dir_name, $web_site);
+        $this->dynamic_cache = new LRUCache($dir_name, $web_site);
+    }
+    /**
+     * Checks whether key is present in static cache first.
+     * If not there checks in dynamic cache
+     * @param string $key cache key
+     * @return mixed|false value of cache if present, false otherwise
+     */
+    public function get(string $key): mixed
+    {
+        $value = $this->static_cache->get($key);
+        if ($value !== false) {
+            return $value;
+        } else {
+            return $this->dynamic_cache->get($key);
+        }
+    }
+    /**
+     * Sets value in dynamic cache
+     * @param string $key cache key
+     * @param mixed $value cache value
+     * @return bool returns true if value is set, false otherwise
+     */
+    public function set(string $key, mixed $value): bool
+    {
+        return $this->dynamic_cache->set($key, $value);
+    }
+    /**
+     * Clears the dynamic part of the cache
+     */
+    public function clear(): void
+    {
+        $this->dynamic_cache->clear();
+    }
+}
\ No newline at end of file
diff --git a/src/library/cache/StaticTopicDynamicCache.php b/src/library/cache/StaticTopicDynamicCache.php
new file mode 100644
index 000000000..219dcd973
--- /dev/null
+++ b/src/library/cache/StaticTopicDynamicCache.php
@@ -0,0 +1,107 @@
+<?php
+/**
+ * SeekQuarry/Yioop --
+ * Open Source Pure PHP Search Engine, Crawler, and Indexer
+ *
+ * Copyright (C) 2009 - 2023  Chris Pollett chris@pollett.org
+ *
+ * LICENSE:
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * END LICENSE
+ *
+ * @author Rushikesh Padia padiarushi3012@gmail.com
+ * @license https://www.gnu.org/licenses/ GPL3
+ * @link https://www.seekquarry.com/
+ * @copyright 2009 - 2023
+ * @filesource
+ */
+namespace seekquarry\yioop\library\cache;
+use seekquarry\yioop\library\WebSite;
+
+/**
+ * Adds topical layer to the @see StaticDynamicCache cache
+ * Stores cache in three different sections. One is a static part
+ * to store all time popular queries. Other is topical cache which
+ * stores topic specific cache. The last one is a dynamic part
+ * where it stores short-term poplar queries.
+ *
+ * @author Rushikesh Padia
+ */
+class StaticTopicDynamicCache extends Cache
+{
+    /**
+     * @var Cache Implementation of static cache
+     */
+    private Cache $static_cache;
+    /**
+     * @var Cache Implementation of dynamic cache for e.g. LRU
+     */
+    private Cache $dynamic_cache;
+    /**
+     * @var Cache Implementation of topical cache
+     */
+    private Cache $topical_cache;
+
+    /**
+     * Initializes static, dynamic, and topical cache
+     * @param string $dir_name
+     * @param WebSite|null $web_site
+     */
+    public function __construct(string $dir_name, WebSite $web_site = null)
+    {
+        parent::__construct();
+        $this->static_cache = new StaticCache($dir_name, $web_site);
+        $this->dynamic_cache = new LRUCache($dir_name, $web_site);
+        $this->topical_cache = new TopicalCache($dir_name, $web_site);
+    }
+    /**
+     * Searches through static, topical, and dynamic cache in the order.
+     * @param string $key cache key
+     * @return mixed|false value against the key, false otherwise
+     */
+    public function get(string $key): mixed
+    {
+        $value = $this->static_cache->get($key);
+        if ($value !== false) {
+            return $value;
+        } else {
+            $value = $this->topical_cache->get($key);
+            if ($value !== false) {
+                return $value;
+            } else {
+                return $this->dynamic_cache->get($key);
+            }
+        }
+    }
+    /**
+     * Sets value in the topical cache
+     * @param string $key cache key
+     * @param mixed $value cache value
+     */
+    public function set(string $key, mixed $value): bool
+    {
+        return $this->topical_cache->set($key, $value)
+            || $this->dynamic_cache->set($key, $value);
+    }
+    /**
+     * Clears dynamic and topical cache
+     */
+    public function clear(): void
+    {
+        $this->dynamic_cache->clear();
+        $this->topical_cache->clear();
+    }
+}
\ No newline at end of file
diff --git a/src/library/cache/TopicalCache.php b/src/library/cache/TopicalCache.php
new file mode 100644
index 000000000..f1cab4c39
--- /dev/null
+++ b/src/library/cache/TopicalCache.php
@@ -0,0 +1,136 @@
+<?php
+/**
+ * SeekQuarry/Yioop --
+ * Open Source Pure PHP Search Engine, Crawler, and Indexer
+ *
+ * Copyright (C) 2009 - 2023  Chris Pollett chris@pollett.org
+ *
+ * LICENSE:
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * END LICENSE
+ *
+ *
+ * @author Rushikesh Padia padiarushi3012@gmail.com
+ * @license https://www.gnu.org/licenses/ GPL3
+ * @link https://www.seekquarry.com/
+ * @copyright 2009 - 2023
+ * @filesource
+ */
+namespace seekquarry\yioop\library\cache;
+
+use seekquarry\yioop\library\CountVectorizer;
+use seekquarry\yioop\library\KMeansClustering;
+use seekquarry\yioop\library\WebSite;
+
+/**
+ * Topical Cache is a form of dynamic cache based on topic models.
+ * This cache uses topic models to find the topic of the query and stores
+ * the result in the cache of corresponding topic. Each topic has its own
+ * dynamic cache. In this implementation topic model is using KMeans clustering
+ * and dynamic cache for each cache topic is LRU.
+ *
+ * @author Rushikesh Padia
+ */
+class TopicalCache extends Cache
+{
+    /**
+     * Relative path to cache directory
+     */
+    const RELATIVE_PATH_TOPIC_CACHE = "/topical";
+    /**
+     * Relative path prefix to cache directory for individual
+     * topic cache
+     */
+    const RELATIVE_PATH_EACH_TOPIC =
+        self::RELATIVE_PATH_TOPIC_CACHE . "/topic";
+    /**
+     * Number of topics
+     */
+    private int $no_of_topics;
+    /**
+     * @var array containing dynamic cache for each topic
+     */
+    private array $cache;
+    /**
+     * @var KMeansClustering|mixed Topic model used for topic
+     * classification
+     */
+    private KMeansClustering $topic_model;
+    /**
+     * @var CountVectorizer|mixed vectorizer to convert word to vector
+     */
+    private CountVectorizer $vectorizer;
+
+    /**
+     * Initializes cache, topic model and vectorizer
+     */
+    public function __construct(string $dir_name, WebSite $web_site = null)
+    {
+        parent::__construct($dir_name . self::RELATIVE_PATH_TOPIC_CACHE,
+            $web_site);
+        $this->topic_model = new KMeansClustering();
+        $this->no_of_topics = count($this->topic_model->getCentroids());
+        $this->vectorizer = new CountVectorizer();
+        $this->cache = [];
+        for ($i = 0; $i < $this->no_of_topics; $i++) {
+            $this->cache[$i] = new LRUCache(
+                $dir_name . self::RELATIVE_PATH_EACH_TOPIC . $i,
+                $web_site, 1 / $this->no_of_topics);
+        }
+    }
+    /**
+     * Vectorizes and finds the topic from the key.
+     * Uses the predicted topic to search in the cache of corresponding topic
+     * @param string $key cache key
+     * @return mixed|false value against the key, false otherwise
+     */
+    public function get(string $key): mixed
+    {
+        $vec = $this->vectorizer->vectorizeOne($key);
+        $predicted_index = $this->topic_model->predict($vec);
+        if($predicted_index > -1) {
+            return $this->cache[$predicted_index]->get($key);
+        }else{
+            return false;
+        }
+    }
+    /**
+     * Vectorizes and finds the topic from the key.
+     * Uses the predicted topic to store in the cache of corresponding topic
+     * @param string $key cache key
+     * @param mixed $value cache value
+     * @return bool return true if added successfully, false otherwise
+     */
+    public function set(string $key, mixed $value): bool
+    {
+        $vec = $this->vectorizer->vectorizeOne($key);
+        $predicted_index = $this->topic_model->predict($vec);
+        if($predicted_index > -1) {
+            return $this->cache[$predicted_index]->set($key, $value);
+        }else{
+            return false;
+        }
+    }
+    /**
+     * Clears the cache of all topics
+     */
+    public function clear(): void
+    {
+        for ($i = 0; $i < $this->no_of_topics; $i++) {
+            $this->cache[$i]->clear();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/library/media_jobs/StaticCacheRefreshJob.php b/src/library/media_jobs/StaticCacheRefreshJob.php
new file mode 100644
index 000000000..dc0a19b24
--- /dev/null
+++ b/src/library/media_jobs/StaticCacheRefreshJob.php
@@ -0,0 +1,190 @@
+<?php
+/**
+ * SeekQuarry/Yioop --
+ * Open Source Pure PHP Search Engine, Crawler, and Indexer
+ *
+ * Copyright (C) 2009 - 2022  Chris Pollett chris@pollett.org
+ *
+ * LICENSE:
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * END LICENSE
+ *
+ * @author Rushikesh Padia padiarushi3012@gmail.com
+ * (Reworked so could scale for yioop.com by Chris Pollett)
+ * @license https://www.gnu.org/licenses/ GPL3
+ * @link https://www.seekquarry.com/
+ * @copyright 2009 - 2022
+ * @filesource
+ */
+namespace seekquarry\yioop\library\media_jobs;
+
+use seekquarry\yioop\configs as C;
+use seekquarry\yioop\controllers\SearchController;
+use seekquarry\yioop\library as L;
+use seekquarry\yioop\library\cache\StaticCache;
+use seekquarry\yioop\library\CrawlConstants;
+
+/**
+ * Refreshes caches by executing queries.
+ * Executed queries are cached by search engine.
+ * Queries to be cached are specified in files located at
+ * WORK_DIRECTORY/cache/cache_queries
+ */
+class StaticCacheRefreshJob extends MediaJob
+{
+    /**
+     * Capacity of static cache
+     */
+    private const CAPACITY = 5000;
+    /**
+     * Maximum number of queries to be analysed from the dataset
+     */
+    private const MAXIMUM_QUERIES = 10000;
+    /**
+     * Index of key column in csv file
+     */
+    private const KEY_COLUMN_INDEX = 0;
+    /**
+     * Maximum number of results in search query to be cached
+     */
+    private const MAXIMUM_SEARCH_RESULTS = 10000;
+    /**
+     * Time in current epoch when cache were last refreshed
+     * @var int
+     */
+    public int $update_time;
+    /**
+     * How long to wait to refresh the cache in seconds
+     */
+    const CACHE_REFRESH_INTERVAL = C\ONE_MONTH;
+
+    /**
+     * Sets up the database connection so can access tables related
+     * to recommendations. Initialize timing info related to job.
+     */
+    public function init(): void
+    {
+        $this->update_time = 0;
+        $this->name_server_does_client_tasks = true;
+        $this->name_server_does_client_tasks_only = true;
+    }
+    /**
+     * Only update if it's been more than a day since the last update
+     *
+     * @return bool whether it's been a day since the last update
+     */
+    public function checkPrerequisites(): bool
+    {
+        $time = time();
+        $delta = $time - $this->update_time;
+        if ($delta > self::CACHE_REFRESH_INTERVAL) {
+            $this->update_time = $time;
+            L\crawlLog("Prerequisites for StaticCacheRefresh Media Job met");
+            return true;
+        }
+        L\crawlLog("Time since last update not exceeded, skipping" .
+            " StaticCacheRefresh MediaJob $delta");
+        return false;
+    }
+    /**
+     * Run the same task as in the distributed setting
+     */
+    public function nondistributedTasks(): void
+    {
+        L\crawlLog("Name server is refreshing cache");
+        $this->doTasks(array());
+    }
+    /**
+     * Queries the search engine for each cache query.
+     * Executed queries are automatically cached by the search engine.
+     *
+     * @param $tasks array ignored
+     * @return void
+     */
+    public function doTasks($tasks): void
+    {
+        L\crawlLog("This media updater is refreshing Static cache");
+        $this->load();
+        L\crawlLog("Static cache is updated");
+    }
+    /**
+     * Loads key-value pairs in the cache
+     */
+    public function load(): void
+    {
+        $cache = new StaticCache(C\WORK_DIRECTORY . "/cache/queries", null);
+        $controller = new SearchController();
+        $cache->clear();
+        foreach ($this->getQueryList() as $key) {
+            $cache_result = $this->getQueryResults($controller, $key);
+            $cache->forced_set($key, $cache_result);
+        }
+    }
+    /**
+     * Fetches search results of a query
+     * @param SearchController $controller search controller to call query API
+     * @param string $key query string
+     * @return array search result
+     */
+    private function getQueryResults(SearchController $controller,
+                                     string $key): array
+    {
+        $data = $controller->queryRequest($key, self::MAXIMUM_SEARCH_RESULTS);
+        $cache_value = [];
+        $cache_value["PAGES"] = $data["PAGES"];
+        $cache_value["TOTAL_ROWS"] = count($data["PAGES"]);
+        $cache_value["TIME"] = time();
+        return $cache_value;
+    }
+    /**
+     * Finds top frequent queries
+     * @return array list of top frequent queries
+     */
+    private function getQueryList(): array
+    {
+        $queries = [];
+        $file = fopen(C\BASE_DIR . "/data/dataset.csv", 'r');
+        if ($file) {
+            $counter = 0;
+            while (($row = fgetcsv($file)) !== false
+                && $counter < self::MAXIMUM_QUERIES) {
+                $queries[] = $row[self::KEY_COLUMN_INDEX];
+                $counter++;
+            }
+            fclose($file);
+        }
+        $sorted_queries = self::sortQueriesByFrequency($queries);
+        $top_queries = [];
+        $min_count = min(self::CAPACITY, count($sorted_queries));
+        for($i = 0; $i < $min_count; $i++){
+            $top_queries[] = $sorted_queries[$i];
+        }
+        return $top_queries;
+    }
+    /**
+     * Sort queries by the frequency
+     * @param array $queries list of queries
+     * @return array queries sorted by frequency
+     */
+    public static function sortQueriesByFrequency(array $queries): array
+    {
+        $freq = array_count_values($queries);
+        uasort($freq, function($a, $b) {
+            return $b - $a;
+        });
+        return array_keys($freq);
+    }
+}
\ No newline at end of file
diff --git a/src/locale/en_US/configure.ini b/src/locale/en_US/configure.ini
index 61b04fc1c..885ab4c96 100644
--- a/src/locale/en_US/configure.ini
+++ b/src/locale/en_US/configure.ini
@@ -714,6 +714,11 @@ system_component_localestrings_updated = "Locale Strings Updated!"
 system_component_all_strings = "All Strings"
 system_component_missing_strings = "Missing Strings"
 system_component_edit_strings_only = "Edit Strings Only"
+system_component_filecache_none = "None"
+system_component_filecache_markers = "Markers"
+system_component_filecache_lru = "Least Recently Used"
+system_component_filecache_static_dynamic = "Static-Dynamic"
+system_component_filecache_static_topic_dynamic = "Static-Topic-Dynamic"
 system_component_cache_cleared = "File Cache Cleared!"
 system_component_server_restarted = "Server restarted!"
 system_component_configure_no_change_db = "Problem Updating Database!"
@@ -1463,7 +1468,7 @@ serversettings_element_name_server = "Name Server Set-up"
 serversettings_element_restart = "Restart Server"
 serversettings_element_name_server_key = "Server Key:"
 serversettings_element_name_server_url = "Name Server URL:"
-serversettings_element_use_filecache = "Use Filecache:"
+serversettings_element_filecache = "Cache Type:"
 serversettings_element_clear_cache = "Clear Cache"
 serversettings_element_database_setup = "Database Set-up"
 serversettings_element_database_system = "Database System:"
diff --git a/src/models/ProfileModel.php b/src/models/ProfileModel.php
index 946a5222a..bfe18be23 100755
--- a/src/models/ProfileModel.php
+++ b/src/models/ProfileModel.php
@@ -69,7 +69,7 @@ class ProfileModel extends Model
         'SEARCHBAR_PATH', 'SEND_MAIL_MEDIA_UPDATER', 'SERP_FAVICONS',
         'SESSION_NAME', 'SIDE_ADSCRIPT', 'SIDEBAR_COLOR', 'SIGNIN_LINK',
         'SIMILAR_LINK', 'SUBSEARCH_LINK', 'TITLE_BONUS', 'TIMEZONE',
-        'TOPBAR_COLOR', 'TOP_ADSCRIPT','TOR_PROXY', 'USE_FILECACHE',
+        'TOPBAR_COLOR', 'TOP_ADSCRIPT','TOR_PROXY', 'FILECACHE',
         'USE_MAIL_PHP', 'USE_PROXY', 'USER_AGENT_SHORT', 'USER_RANK_BONUS',
         'WEB_URI', 'WEB_ACCESS', 'WORD_CLOUD', 'WORD_SUGGEST'
         ];
diff --git a/src/views/elements/ServersettingsElement.php b/src/views/elements/ServersettingsElement.php
index cb65c9ce5..931b3f416 100644
--- a/src/views/elements/ServersettingsElement.php
+++ b/src/views/elements/ServersettingsElement.php
@@ -89,13 +89,13 @@ class ServersettingsElement extends Element
                         value="<?= $data['NAME_SERVER'] ?>"
                         class="extra-wide-field" >
                 </div>
-                <div id="filecache">
-                <div class="top-margin"><label for="use-filecache"><b><?=
-                    tl('serversettings_element_use_filecache')?></b></label>
-                    <input type="checkbox" id="use-filecache"
-                        name="USE_FILECACHE" value="true" <?=
-                        $data['USE_FILECACHE'] ? "checked='checked'" :
-                            "" ?> >
+                <div class="top-margin"><label for="filecache"><b><?=
+                    tl('serversettings_element_filecache')?></b></label>
+                    <?php $this->view->helper("options")->render(
+                        "filecache", "FILECACHE",
+                        $data['FILECACHES'],
+                        $data['FILECACHE']);
+                    ?>
                     [<a href="<?= $base_url . '&amp;arg=clearCache'
                     ?>"><?=tl('serversettings_element_clear_cache')
                     ?></a>]
diff --git a/tests/FileCacheTest.php b/tests/FileCacheTest.php
new file mode 100644
index 000000000..153dbd508
--- /dev/null
+++ b/tests/FileCacheTest.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * SeekQuarry/Yioop --
+ * Open Source Pure PHP Search Engine, Crawler, and Indexer
+ *
+ * Copyright (C) 2009 - 2023  Chris Pollett chris@pollett.org
+ *
+ * LICENSE:
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * END LICENSE
+ *
+ *
+ * @author Rushikesh Padia padiarushi3012@gmail.com
+ * @license https://www.gnu.org/licenses/ GPL3
+ * @link https://www.seekquarry.com/
+ * @copyright 2009 - 2023
+ * @filesource
+ */
+namespace seekquarry\yioop\tests;
+
+use seekquarry\yioop\configs as C;
+use seekquarry\yioop\library\FileCache;
+use seekquarry\yioop\library\UnitTest;
+
+/**
+ * Used to test that the FileCache class properly caches data
+ *
+ * @author Rushikesh Padia
+ */
+class FileCacheTest extends UnitTest
+{
+    /**
+     * We'll set up one Cache for testing purpose
+     */
+    public function setUp(): void
+    {
+        $dir = C\TEST_DIR . "/test_files/cache";
+        $this->test_objects["CACHE"] = new FileCache($dir);
+    }
+    /**
+     * Since a Trie is not a PersistentStructure we don't need to do
+     * anything to tear it down
+     */
+    public function tearDown()
+    {
+    }
+    /**
+     * Check if we add something into our Cache is retrieved with the key
+     */
+    public function addTestCase(): void
+    {
+        $this->test_objects['CACHE']->set("testKey", "testValue");
+        $this->assertEqual($this->test_objects['CACHE']->get("testKey"),
+            "testValue",
+            "Get key method should return correct value from the cache");
+    }
+    /**
+     * Check if we add something into our Cache is retrieved with the key
+     */
+    public function addMultipleTestCase(): void
+    {
+        for($i=0; $i<100; $i++) {
+            $this->test_objects['CACHE']->set("testKey$i", "testValue");
+        }
+        $this->assertEqual($this->test_objects['CACHE']->get("testKey1"),
+            "testValue",
+            "Get key method should return correct value from the cache");
+    }
+    /**
+     * Check that if cache is cleared
+     */
+    public function clearTestCase(): void
+    {
+        $this->test_objects['CACHE']->set("testKey1", "testValue1");
+        $this->test_objects['CACHE']->set("testKey2", "testValue2");
+        $this->test_objects['CACHE']->clear();
+        $this->assertFalse($this->test_objects['CACHE']->get("testKey1"),
+            "If key not in Cache should get false");
+        $this->assertFalse($this->test_objects['CACHE']->get("testKey2"),
+            "If key not in Cache should get false");
+    }
+}
\ No newline at end of file
-- 
2.25.1

