feat: Search UI

- Search UI with dropdown results
- Client class to handle Product Search actions and results
- Integrated Search bar into all-products and item group pages
- Run db search without redisearch
- Cleanup: [Search] change decorator names and variables
- Sider fixes
diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py
index cca8df5..8d6cc74 100644
--- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py
+++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py
@@ -6,7 +6,7 @@
 from frappe.utils import cint, comma_and
 from frappe import _, msgprint
 from frappe.model.document import Document
-from frappe.utils import get_datetime, get_datetime_str, now_datetime, unique
+from frappe.utils import unique
 from erpnext.e_commerce.website_item_indexing import create_website_items_index, ALLOWED_INDEXABLE_FIELDS_SET, is_search_module_loaded
 
 class ShoppingCartSetupError(frappe.ValidationError): pass
@@ -59,11 +59,8 @@
 		if not self.search_index_fields:
 			return
 
-		# Clean up
-		# Remove whitespaces
 		fields = self.search_index_fields.replace(' ', '')
-		# Remove extra ',' and remove duplicates
-		fields = unique(fields.strip(',').split(','))
+		fields = unique(fields.strip(',').split(',')) # Remove extra ',' and remove duplicates
 
 		# All fields should be indexable
 		if not (set(fields).issubset(ALLOWED_INDEXABLE_FIELDS_SET)):
diff --git a/erpnext/e_commerce/product_grid.js b/erpnext/e_commerce/product_grid.js
index a716efa..bd7a568 100644
--- a/erpnext/e_commerce/product_grid.js
+++ b/erpnext/e_commerce/product_grid.js
@@ -144,6 +144,8 @@
 					${ __('Add to Cart') }
 				</div>
 			`;
+		} else {
+			return ``;
 		}
 	}
 };
\ No newline at end of file
diff --git a/erpnext/e_commerce/product_list.js b/erpnext/e_commerce/product_list.js
index 3aa6e8c..d5ba6f5 100644
--- a/erpnext/e_commerce/product_list.js
+++ b/erpnext/e_commerce/product_list.js
@@ -153,6 +153,8 @@
 					${ __('Add to Cart') }
 				</div>
 			`;
+		} else {
+			return ``;
 		}
 	}
 
diff --git a/erpnext/e_commerce/product_search.js b/erpnext/e_commerce/product_search.js
new file mode 100644
index 0000000..4f8b028
--- /dev/null
+++ b/erpnext/e_commerce/product_search.js
@@ -0,0 +1,226 @@
+erpnext.ProductSearch = class {
+	constructor() {
+		this.MAX_RECENT_SEARCHES = 4;
+		this.searchBox = $("#search-box");
+
+		this.setupSearchDropDown();
+		this.bindSearchAction();
+	}
+
+	setupSearchDropDown() {
+		this.search_area = $("#dropdownMenuSearch");
+		this.setupSearchResultContainer();
+		this.setupProductsContainer();
+		this.setupCategoryRecentsContainer();
+		this.populateRecentSearches();
+	}
+
+	bindSearchAction() {
+		let me = this;
+
+		this.searchBox.on("focus", (e) => {
+			this.search_dropdown.removeClass("hidden");
+		});
+
+		this.searchBox.on("focusout", (e) => {
+			this.search_dropdown.addClass("hidden");
+		});
+
+		this.searchBox.on("input", (e) => {
+			let query = e.target.value;
+
+			if (query.length < 3 || !query.length) return;
+
+			// Populate recent search chips
+			me.setRecentSearches(query);
+
+			// Fetch and populate product results
+			frappe.call({
+				method: "erpnext.templates.pages.product_search.search",
+				args: {
+					query: query
+				},
+				callback: (data) => {
+					me.populateResults(data);
+				}
+			});
+
+			// Populate categories
+			if (me.category_container) {
+				frappe.call({
+					method: "erpnext.templates.pages.product_search.get_category_suggestions",
+					args: {
+						query: query
+					},
+					callback: (data) => {
+						me.populateCategoriesList(data)
+					}
+				});
+			}
+
+			this.search_dropdown.removeClass("hidden");
+		});
+	}
+
+	setupSearchResultContainer() {
+		this.search_dropdown = this.search_area.append(`
+			<div class="overflow-hidden shadow dropdown-menu w-100 hidden"
+				id="search-results-container"
+				aria-labelledby="dropdownMenuSearch"
+				style="display: flex;">
+			</div>
+		`).find("#search-results-container");
+	}
+
+	setupProductsContainer() {
+		let $products_section = this.search_dropdown.append(`
+			<div class="col-7 mr-2 mt-1"
+				id="product-results"
+				style="border-right: 1px solid var(--gray-200);">
+			</div>
+		`).find("#product-results");
+
+		this.products_container = $products_section.append(`
+			<div id="product-scroll" style="overflow: scroll; max-height: 300px">
+				<div class="mt-6 w-100 text-muted" style="font-weight: 400; text-align: center;">
+					${ __("Type something ...") }
+				</div>
+			</div>
+		`).find("#product-scroll");
+	}
+
+	setupCategoryRecentsContainer() {
+		let $category_recents_section = $("#search-results-container").append(`
+			<div id="category-recents-container"
+				class="col-5 mt-2 h-100"
+				style="margin-left: -15px;">
+			</div>
+		`).find("#category-recents-container");
+
+		this.category_container = $category_recents_section.append(`
+			<div class="category-container">
+				<div class="mb-2"
+					style="border-bottom: 1px solid var(--gray-200);">
+					${ __("Categories") }
+				</div>
+				<div class="categories">
+					<span class="text-muted" style="font-weight: 400;"> ${ __('No results') } <span>
+				</div>
+			</div>
+		`).find(".categories");
+
+		let $recents_section = $("#category-recents-container").append(`
+			<div class="mb-2 mt-4 recent-searches">
+				<div style="border-bottom: 1px solid var(--gray-200);">
+					${ __("Recent") }
+				</div>
+			</div>
+		`).find(".recent-searches");
+
+		this.recents_container = $recents_section.append(`
+			<div id="recent-chips" style="padding: 1rem 0;">
+			</div>
+		`).find("#recent-chips");
+	}
+
+	getRecentSearches() {
+		return JSON.parse(localStorage.getItem("recent_searches") || "[]");
+	}
+
+	attachEventListenersToChips() {
+		let me  = this;
+		const chips = $(".recent-chip");
+		window.chips = chips;
+
+		for (let chip of chips) {
+			chip.addEventListener("click", () => {
+				me.searchBox[0].value = chip.innerText;
+
+				// Start search with `recent query`
+				me.searchBox.trigger("input");
+				me.searchBox.focus();
+			});
+		}
+	}
+
+	setRecentSearches(query) {
+		let recents = this.getRecentSearches();
+		if (recents.length >= this.MAX_RECENT_SEARCHES) {
+			// Remove the `first` query
+			recents.splice(0, 1);
+		}
+
+		if (recents.indexOf(query) >= 0) {
+			return;
+		}
+
+		recents.push(query);
+		localStorage.setItem("recent_searches", JSON.stringify(recents));
+
+		this.populateRecentSearches();
+	}
+
+	populateRecentSearches() {
+		let recents = this.getRecentSearches();
+
+		if (!recents.length) {
+			return;
+		}
+
+		let html = "";
+		recents.forEach((key) => {
+			html += `<button class="btn btn-sm recent-chip mr-1 mb-2">${ key }</button>`;
+		});
+
+		this.recents_container.html(html);
+		this.attachEventListenersToChips();
+	}
+
+	populateResults(data) {
+		if (data.message.results.length === 0) {
+			this.products_container.html('No results');
+			return;
+		}
+
+		let html = "";
+		let search_results = data.message.results;
+
+		search_results.forEach((res) => {
+			html += `
+				<div class="dropdown-item" style="display: flex;">
+					<img class="item-thumb col-2" src=${res.thumbnail || 'img/placeholder.png'} />
+					<div class="col-9" style="white-space: normal;">
+						<a href="/${res.route}">${res.web_item_name}</a><br>
+						<span class="brand-line">${res.brand ? "by " + res.brand : ""}</span>
+					</div>
+				</div>
+			`;
+		});
+
+		this.products_container.html(html);
+	}
+
+	populateCategoriesList(data) {
+		if (data.message.results.length === 0) {
+			let empty_html = `
+				<span class="text-muted" style="font-weight: 400;">
+					${__('No results')}
+				</span>
+			`;
+			this.category_container.html(empty_html);
+			return;
+		}
+
+		let html = ""
+		let search_results = data.message.results
+		search_results.forEach((category) => {
+			html += `
+				<div class="mb-2" style="font-weight: 400;">
+					<a href="/${category.route}">${category.name}</a>
+				</div>
+			`;
+		})
+
+		this.category_container.html(html);
+	}
+}
\ No newline at end of file
diff --git a/erpnext/e_commerce/product_view.js b/erpnext/e_commerce/product_view.js
index 4e1c23c..3a2cdae 100644
--- a/erpnext/e_commerce/product_view.js
+++ b/erpnext/e_commerce/product_view.js
@@ -402,4 +402,4 @@
 		}
 		return exists ? obj : undefined;
 	}
-}
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/erpnext/e_commerce/website_item_indexing.py b/erpnext/e_commerce/website_item_indexing.py
index 3b82a32..3270101 100644
--- a/erpnext/e_commerce/website_item_indexing.py
+++ b/erpnext/e_commerce/website_item_indexing.py
@@ -1,39 +1,10 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
 import frappe
 from frappe.utils.redis_wrapper import RedisWrapper
+from redisearch import (Client, AutoCompleter, Suggestion, IndexDefinition, TextField, TagField)
 
-from redisearch import (
-        Client, AutoCompleter,
-        Suggestion, IndexDefinition, 
-        TextField, TagField
-	)
-
-def is_search_module_loaded():
-    cache = frappe.cache()
-    out = cache.execute_command('MODULE LIST')
-
-    parsed_output = " ".join(
-        (" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out)
-    )
-
-    return "search" in parsed_output
-
-# Decorator for checking wether Redisearch is there or not
-def redisearch_decorator(function):
-    def wrapper(*args, **kwargs):
-        if is_search_module_loaded():
-            func = function(*args, **kwargs)
-            return func
-        return
-
-    return wrapper 
-
-def make_key(key):
-	return "{0}|{1}".format(frappe.conf.db_name, key).encode('utf-8')
-
-# GLOBAL CONSTANTS
 WEBSITE_ITEM_INDEX = 'website_items_index'
 WEBSITE_ITEM_KEY_PREFIX = 'website_item:'
 WEBSITE_ITEM_NAME_AUTOCOMPLETE = 'website_items_name_dict'
@@ -48,7 +19,30 @@
 	'web_long_description'
 }
 
-@redisearch_decorator
+def is_search_module_loaded():
+	cache = frappe.cache()
+	out = cache.execute_command('MODULE LIST')
+
+	parsed_output = " ".join(
+		(" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out)
+	)
+
+	return "search" in parsed_output
+
+# Decorator for checking wether Redisearch is there or not
+def if_redisearch_loaded(function):
+	def wrapper(*args, **kwargs):
+		if is_search_module_loaded():
+			func = function(*args, **kwargs)
+			return func
+		return
+
+	return wrapper
+
+def make_key(key):
+	return "{0}|{1}".format(frappe.conf.db_name, key).encode('utf-8')
+
+@if_redisearch_loaded
 def create_website_items_index():
 	'''Creates Index Definition'''
 	# CREATE index
@@ -57,21 +51,20 @@
 	# DROP if already exists
 	try:
 		client.drop_index()
-	except:
+	except Exception:
 		pass
 
-	
 	idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)])
 
 	# Based on e-commerce settings
 	idx_fields = frappe.db.get_single_value(
-		'E Commerce Settings', 
+		'E Commerce Settings',
 		'search_index_fields'
 	).split(',')
 
 	if 'web_item_name' in idx_fields:
 		idx_fields.remove('web_item_name')
-	
+
 	idx_fields = list(map(to_search_field, idx_fields))
 
 	client.create_index(
@@ -88,26 +81,25 @@
 
 	return TextField(field)
 
-@redisearch_decorator
+@if_redisearch_loaded
 def insert_item_to_index(website_item_doc):
 	# Insert item to index
 	key = get_cache_key(website_item_doc.name)
-	r = frappe.cache()
+	cache = frappe.cache()
 	web_item = create_web_item_map(website_item_doc)
 
 	for k, v in web_item.items():
-		super(RedisWrapper, r).hset(make_key(key), k, v)
+		super(RedisWrapper, cache).hset(make_key(key), k, v)
 
 	insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name)
 
-@redisearch_decorator
+@if_redisearch_loaded
 def insert_to_name_ac(web_name, doc_name):
 	ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache())
 	ac.add_suggestions(Suggestion(web_name, payload=doc_name))
 
 def create_web_item_map(website_item_doc):
 	fields_to_index = get_fields_indexed()
-
 	web_item = {}
 
 	for f in fields_to_index:
@@ -115,59 +107,58 @@
 
 	return web_item
 
-@redisearch_decorator
+@if_redisearch_loaded
 def update_index_for_item(website_item_doc):
 	# Reinsert to Cache
 	insert_item_to_index(website_item_doc)
 	define_autocomplete_dictionary()
 
-@redisearch_decorator
+@if_redisearch_loaded
 def delete_item_from_index(website_item_doc):
-	r = frappe.cache()
+	cache = frappe.cache()
 	key = get_cache_key(website_item_doc.name)
-	
+
 	try:
-		r.delete(key)
+		cache.delete(key)
 	except:
 		return False
 
 	delete_from_ac_dict(website_item_doc)
-
 	return True
 
-@redisearch_decorator
+@if_redisearch_loaded
 def delete_from_ac_dict(website_item_doc):
 	'''Removes this items's name from autocomplete dictionary'''
-	r = frappe.cache()
-	name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=r)
+	cache = frappe.cache()
+	name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
 	name_ac.delete(website_item_doc.web_item_name)
 
-@redisearch_decorator
+@if_redisearch_loaded
 def define_autocomplete_dictionary():
 	"""Creates an autocomplete search dictionary for `name`.
-	   Also creats autocomplete dictionary for `categories` if 
-	   checked in E Commerce Settings"""
+		Also creats autocomplete dictionary for `categories` if
+		checked in E Commerce Settings"""
 
-	r = frappe.cache()
-	name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=r)
-	cat_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=r)
+	cache = frappe.cache()
+	name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
+	cat_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache)
 
 	ac_categories = frappe.db.get_single_value(
-		'E Commerce Settings', 
+		'E Commerce Settings',
 		'show_categories_in_search_autocomplete'
 	)
-	
+
 	# Delete both autocomplete dicts
 	try:
-		r.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE))
-		r.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE))
+		cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE))
+		cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE))
 	except:
 		return False
-	
+
 	items = frappe.get_all(
-		'Website Item', 
-		fields=['web_item_name', 'item_group'], 
-		filters={"published": True}
+		'Website Item',
+		fields=['web_item_name', 'item_group'],
+		filters={"published": 1}
 	)
 
 	for item in items:
@@ -177,21 +168,21 @@
 
 	return True
 
-@redisearch_decorator
+@if_redisearch_loaded
 def reindex_all_web_items():
 	items = frappe.get_all(
-		'Website Item', 
-		fields=get_fields_indexed(), 
+		'Website Item',
+		fields=get_fields_indexed(),
 		filters={"published": True}
 	)
 
-	r = frappe.cache()
+	cache = frappe.cache()
 	for item in items:
 		web_item = create_web_item_map(item)
 		key = make_key(get_cache_key(item.name))
 
 		for k, v in web_item.items():
-			super(RedisWrapper, r).hset(key, k, v)
+			super(RedisWrapper, cache).hset(key, k, v)
 
 def get_cache_key(name):
 	name = frappe.scrub(name)
@@ -199,7 +190,7 @@
 
 def get_fields_indexed():
 	fields_to_index = frappe.db.get_single_value(
-		'E Commerce Settings', 
+		'E Commerce Settings',
 		'search_index_fields'
 	).split(',')
 
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index 51082bd..553082f 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -70,6 +70,7 @@
 	"js/e-commerce.min.js": [
 		"e_commerce/product_view.js",
 		"e_commerce/product_grid.js",
-		"e_commerce/product_list.js"
+		"e_commerce/product_list.js",
+		"e_commerce/product_search.js"
 	]
 }
diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js
index 6b42987..40d58d1 100644
--- a/erpnext/public/js/shopping_cart.js
+++ b/erpnext/public/js/shopping_cart.js
@@ -199,7 +199,7 @@
 	},
 
 	freeze() {
-		if (window.location.pathname !== "/cart") return
+		if (window.location.pathname !== "/cart") return;
 
 		if (!$('#freeze').length) {
 			let freeze = $('<div id="freeze" class="modal-backdrop fade"></div>')
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index 8048f76..7128c02 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -421,7 +421,7 @@
 
 .total-discount {
 	font-size: var(--text-base);
-	color: var(--primary-color);
+	color: var(--primary-color) !important;
 }
 
 #page-cart {
@@ -529,6 +529,7 @@
 				height: 22px;
 				background-color: var(--gray-200);
 				float: right;
+				cursor: pointer;
 			}
 
 			.remove-cart-item-logo {
@@ -865,3 +866,14 @@
 	background-color: var(--gray-100);
 	height: 100%;
 }
+
+.item-thumb {
+	height: 50px;
+	max-width: 80px;
+	min-width: 80px;
+	object-fit: cover;
+}
+
+.brand-line {
+	color: gray;
+}
diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html
index 20ad7fb..0b2fb03 100644
--- a/erpnext/templates/generators/item_group.html
+++ b/erpnext/templates/generators/item_group.html
@@ -2,7 +2,16 @@
 {% extends "templates/web.html" %}
 
 {% block header %}
-<!-- <h2>{{ title }}</h2> -->
+<div class="row mb-6" style="width: 65vw">
+	<div class="mb-6 col-4 order-1">{{ title }}</div>
+
+	<div class="input-group mb-6 col-8 order-2">
+		<div class="dropdown w-100" id="dropdownMenuSearch">
+			<input type="search" name="query" id="search-box" class="form-control" placeholder="Search for products..." aria-label="Product" aria-describedby="button-addon2">
+			<!-- Results dropdown rendered in product_search.js -->
+		</div>
+	</div>
+</div>
 {% endblock header %}
 
 {% block script %}
@@ -19,7 +28,7 @@
 <div class="item-group-content" itemscope itemtype="http://schema.org/Product"
 	data-item-group="{{ name }}">
 	<div class="item-group-slideshow">
-		{% if slideshow %}<!-- slideshow -->
+		{% if slideshow %} <!-- slideshow -->
 			{{ web_block(
 				"Hero Slider",
 				values=slideshow,
@@ -28,8 +37,8 @@
 				add_bottom_padding=0,
 			) }}
 		{% endif %}
-		<h2 class="mt-3">{{ title }}</h2>
-		{% if description %}<!-- description -->
+
+		{% if description %} <!-- description -->
 		<div class="item-group-description text-muted mb-5" itemprop="description">{{ description or ""}}</div>
 		{% endif %}
 	</div>
diff --git a/erpnext/templates/includes/cart.js b/erpnext/templates/includes/cart.js
index 28fe882..c766dfd 100644
--- a/erpnext/templates/includes/cart.js
+++ b/erpnext/templates/includes/cart.js
@@ -163,7 +163,7 @@
 				item_code: item_code,
 				qty: 0
 			});
-		})
+		});
 	},
 
 	render_tax_row: function($cart_taxes, doc, shipping_rules) {
diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py
index 12967fa..190abfd 100644
--- a/erpnext/templates/pages/product_search.py
+++ b/erpnext/templates/pages/product_search.py
@@ -7,7 +7,6 @@
 from erpnext.setup.doctype.item_group.item_group import get_item_for_list_in_html
 from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_website
 
-# For SEARCH -------
 from redisearch import AutoCompleter, Client, Query
 from erpnext.e_commerce.website_item_indexing import (
 	is_search_module_loaded,
@@ -16,7 +15,6 @@
 	WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE,
 	make_key
 )
-# -----------------
 
 no_cache = 1
 
@@ -36,30 +34,29 @@
 def get_product_data(search=None, start=0, limit=12):
 	# limit = 12 because we show 12 items in the grid view
 	# base query
-	query = """select I.name, I.item_name, I.item_code, I.route, I.image, I.website_image, I.thumbnail, I.item_group,
-			I.description, I.web_long_description as website_description, I.is_stock_item,
-			case when (S.actual_qty - S.reserved_qty) > 0 then 1 else 0 end as in_stock, I.website_warehouse,
-			I.has_batch_no
-		from `tabItem` I
-		left join tabBin S on I.item_code = S.item_code and I.website_warehouse = S.warehouse
-		where (I.show_in_website = 1)
-			and I.disabled = 0
-			and (I.end_of_life is null or I.end_of_life='0000-00-00' or I.end_of_life > %(today)s)"""
+	query = """
+		Select
+			web_item_name, item_name, item_code, brand, route,
+			website_image, thumbnail, item_group,
+			description, web_long_description as website_description,
+			website_warehouse, ranking
+		from `tabWebsite Item`
+		where published = 1
+		"""
 
 	# search term condition
 	if search:
-		query += """ and (I.web_long_description like %(search)s
-				or I.description like %(search)s
-				or I.item_name like %(search)s
-				or I.name like %(search)s)"""
+		query += """ and (item_name like %(search)s
+				or web_item_name like %(search)s
+				or brand like %(search)s
+				or web_long_description like %(search)s)"""
 		search = "%" + cstr(search) + "%"
 
 	# order by
-	query += """ order by I.weightage desc, in_stock desc, I.modified desc limit %s, %s""" % (cint(start), cint(limit))
+	query += """ order by ranking asc, modified desc limit %s, %s""" % (cint(start), cint(limit))
 
 	return frappe.db.sql(query, {
-		"search": search,
-		"today": nowdate()
+		"search": search
 	}, as_dict=1)
 
 @frappe.whitelist(allow_guest=True)
@@ -112,11 +109,19 @@
 
 @frappe.whitelist(allow_guest=True)
 def get_category_suggestions(query):
-	search_results = {"from_redisearch": True, "results": []}
+	search_results = {"results": []}
 
 	if not is_search_module_loaded():
-		# Redisearch module not loaded
-		search_results["from_redisearch"] = False
+		# Redisearch module not loaded, query db
+		categories = frappe.db.get_all(
+			"Item Group",
+			filters={
+				"name": ["like", "%{0}%".format(query)],
+				"show_in_website": 1
+			},
+			fields=["name", "route"]
+		)
+		search_results['results'] = categories
 		return search_results
 
 	if not query:
diff --git a/erpnext/www/all-products/index.html b/erpnext/www/all-products/index.html
index f89ee65..0a62bc4 100644
--- a/erpnext/www/all-products/index.html
+++ b/erpnext/www/all-products/index.html
@@ -3,35 +3,19 @@
 
 {% block title %}{{ _('Products') }}{% endblock %}
 {% block header %}
-<div class="mb-6">{{ _('Products') }}</div>
+<div class="row mb-6" style="width: 65vw">
+	<div class="mb-6 col-4 order-1">{{ _('Products') }}</div>
+
+	<div class="input-group mb-6 col-8 order-2">
+		<div class="dropdown w-100" id="dropdownMenuSearch">
+			<input type="search" name="query" id="search-box" class="form-control" placeholder="Search for products..." aria-label="Product" aria-describedby="button-addon2">
+			<!-- Results dropdown rendered in product_search.js -->
+		</div>
+	</div>
+</div>
 {% endblock header %}
 
 {% block page_content %}
-<!-- Old Search -->
-<!-- <div class="row" style="display: none;">
-	<div class="col-8">
-		<div class="input-group input-group-sm mb-3">
-			<input type="search" class="form-control" placeholder="{{_('Search')}}"
-				aria-label="{{_('Product Search')}}" aria-describedby="product-search"
-				value="{{ frappe.sanitize_html(frappe.form_dict.search) or '' }}"
-			>
-		</div>
-	</div>
-
-	<div class="col-4 pl-0">
-		<button class="btn btn-light btn-sm btn-block d-md-none"
-			type="button"
-			data-toggle="collapse"
-			data-target="#product-filters"
-			aria-expanded="false"
-			aria-controls="product-filters"
-			style="white-space: nowrap;"
-		>
-			{{ _('Toggle Filters') }}
-		</button>
-	</div>
-</div> -->
-
 <div class="row">
 	<!-- Items section -->
 	<div id="product-listing" class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
@@ -40,11 +24,6 @@
 
 	<!-- Filters Section -->
 	<div class="col-12 order-1 col-md-3 order-md-1">
-		{% if frappe.form_dict.start or frappe.form_dict.field_filters or frappe.form_dict.attribute_filters or frappe.form_dict.search %}
-
-
-		{% endif  %}
-
 		<div class="collapse d-md-block mr-4 filters-section" id="product-filters">
 			<div class="d-flex justify-content-between align-items-center mb-5 title-section">
 				<div class="mb-4 filters-title" > {{ _('Filters') }} </div>
diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js
index ef8c210..e38514a 100644
--- a/erpnext/www/all-products/index.js
+++ b/erpnext/www/all-products/index.js
@@ -7,8 +7,10 @@
 
 			let view_type = "List View";
 
-			// Render Product Views and setup Filters
+			// Render Product Views, Filters & Search
 			frappe.require('/assets/js/e-commerce.min.js', function() {
+				new erpnext.ProductSearch();
+
 				new erpnext.ProductView({
 					view_type: view_type,
 					products_section: $('#product-listing'),
diff --git a/erpnext/www/all-products/search.css b/erpnext/www/all-products/search.css
deleted file mode 100644
index 687532d..0000000
--- a/erpnext/www/all-products/search.css
+++ /dev/null
@@ -1,9 +0,0 @@
-.item-thumb {
-    height: 50px;
-    width: 50px;
-    object-fit: cover;
-}
-
-.brand-line {
-    color: gray;
-}
\ No newline at end of file
diff --git a/erpnext/www/all-products/search.html b/erpnext/www/all-products/search.html
deleted file mode 100644
index 735822d..0000000
--- a/erpnext/www/all-products/search.html
+++ /dev/null
@@ -1,46 +0,0 @@
-{% extends "templates/web.html" %}
-
-{% block title %}{{ _('Search') }}{% endblock %}
-
-{%- block head_include %}
-	<link rel="stylesheet" href="search.css">
-{% endblock -%}
-
-{% block header %}
-<div class="mb-6">{{ _('Search Products') }}</div>
-{% endblock header %}
-
-{% block page_content %}
-<div class="input-group mb-3">
-	<input type="text" name="query" id="search-box" class="form-control" placeholder="Search for products..." aria-label="Product" aria-describedby="button-addon2">
-	<div class="input-group-append">
-		<button class="btn btn-outline-secondary" type="button" id="search-button">Search</button>
-	</div>
-</div>
-
-<!-- To show recent searches -->
-<div class="my-2" id="recent-search-chips"></div>
-
-<div class="row mt-2">
-	<!-- Search Results -->
-	<div class="col-sm">
-		<h2>Products</h2>
-		<ul id="results" class="list-group"></ul>
-	</div>
-	
-	{% set show_categories = frappe.db.get_single_value('E Commerce Settings', 'show_categories_in_search_autocomplete') %}
-	{% if show_categories %}
-	<div id="categories" class="col-sm">
-		<h2>Categories</h2>
-		<ul id="category-suggestions">
-		</ul>
-	</div>
-	{% endif %}
-	
-	{% set show_brand_line = frappe.db.get_single_value('E Commerce Settings', 'show_brand_line') %}
-	{% if show_brand_line %}
-	<span id="show-brand-line"></span>
-	{% endif %}
-</div>
-
-{% endblock %}
\ No newline at end of file
diff --git a/erpnext/www/all-products/search.js b/erpnext/www/all-products/search.js
deleted file mode 100644
index e88b576..0000000
--- a/erpnext/www/all-products/search.js
+++ /dev/null
@@ -1,148 +0,0 @@
-let loading = false;
-
-const MAX_RECENT_SEARCHES = 4;
-
-const searchBox = document.getElementById("search-box");
-const searchButton = document.getElementById("search-button");
-const results = document.getElementById("results");
-const categoryList = document.getElementById("category-suggestions");
-const showBrandLine = document.getElementById("show-brand-line");
-const recentSearchArea = document.getElementById("recent-search-chips");
-
-function getRecentSearches() {
-    return JSON.parse(localStorage.getItem("recent_searches") || "[]");
-}
-
-function attachEventListenersToChips() {
-    const chips = document.getElementsByClassName("recent-chip");
-
-    for (let chip of chips) {
-        chip.addEventListener("click", () => {
-            searchBox.value = chip.innerText;
-
-            // Start search with `recent query`
-            const event = new Event("input");
-            searchBox.dispatchEvent(event);
-            searchBox.focus();
-        });
-    }
-}
-
-function populateRecentSearches() {
-    let recents = getRecentSearches();
-
-    if (!recents.length) {
-        return;
-    }
-
-    html = "Recent Searches: ";
-    for (let query of recents) {
-        html += `<button class="btn btn-secondary btn-sm recent-chip mr-1">${query}</button>`;
-    }
-
-    recentSearchArea.innerHTML = html;
-    attachEventListenersToChips();
-}
-
-function populateResults(data) {
-    if (!data.message.from_redisearch) {
-        // Data not from redisearch
-    }
-
-    if (data.message.results.length === 0) {
-        results.innerHTML = 'No results';
-        return;
-    }
-
-    html = ""
-    search_results = data.message.results
-    for (let res of search_results) {
-        html += `<li class="list-group-item list-group-item-action">
-        <img class="item-thumb" src="${res.thumbnail || 'img/placeholder.png'}" />
-        <a href="/${res.route}">${res.web_item_name} <span class="brand-line">${showBrandLine && res.brand ? "by " + res.brand : ""}</span></a>
-        </li>`
-    }
-    results.innerHTML = html;
-}
-
-function populateCategoriesList(data) {
-    if (!data.message.from_redisearch) {
-        // Data not from redisearch
-        categoryList.innerHTML = "Install Redisearch to enable autocompletions.";
-        return;
-    }
-
-    if (data.message.results.length === 0) {
-        categoryList.innerHTML = 'No results';
-        return;
-    }
-
-    html = ""
-    search_results = data.message.results
-    for (let category of search_results) {
-        html += `<li>${category}</li>`
-    }
-
-    categoryList.innerHTML = html;
-}
-
-function updateLoadingState() {
-    if (loading) {
-        results.innerHTML = `<div class="spinner-border"><span class="sr-only">loading...<span></div>`;
-    }
-}
-
-searchBox.addEventListener("input", (e) => {
-    loading = true;
-    updateLoadingState();
-    frappe.call({
-        method: "erpnext.templates.pages.product_search.search", 
-        args: {
-            query: e.target.value 
-        },
-        callback: (data) => {
-            populateResults(data);
-            loading = false;
-        }
-    });
-
-    // If there is a suggestion list node
-    if (categoryList) {
-        frappe.call({
-            method: "erpnext.templates.pages.product_search.get_category_suggestions",
-            args: {
-                query: e.target.value
-            },
-            callback: (data) => {
-                populateCategoriesList(data);
-            }
-        });
-    }
-});
-
-searchButton.addEventListener("click", (e) => {
-    let query = searchBox.value;
-    if (!query) {
-        return;
-    }
-
-    let recents = getRecentSearches();
-
-    if (recents.length >= MAX_RECENT_SEARCHES) {
-        // Remove the `First` query
-        recents.splice(0, 1);
-    }
-
-    if (recents.indexOf(query) >= 0) {
-        return;
-    }
-
-    recents.push(query);
-
-    localStorage.setItem("recent_searches", JSON.stringify(recents));
-
-    // Refresh recent searches
-    populateRecentSearches();
-});
-
-populateRecentSearches();
\ No newline at end of file
diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py
index 700d5b2..f94b33e 100644
--- a/erpnext/www/shop-by-category/index.py
+++ b/erpnext/www/shop-by-category/index.py
@@ -71,8 +71,12 @@
 				fields += ["image"]
 
 			categorical_data[category] = frappe.db.sql(f"""
-				Select {",".join(fields)}
-				from `tab{doctype}`""", as_dict=1)
+				Select
+					{",".join(fields)}
+				from
+					`tab{doctype}`""",
+				as_dict=1
+			)
 
 	return categorical_data