feat: Add basic autocomplete using redisearch
diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py
index 3cff8ec..bb0af52 100644
--- a/erpnext/e_commerce/doctype/website_item/website_item.py
+++ b/erpnext/e_commerce/doctype/website_item/website_item.py
@@ -16,6 +16,14 @@
 from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for)
 from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
 
+# SEARCH 
+from erpnext.templates.pages.product_search import (
+	insert_item_to_index, 
+	update_index_for_item, 
+	delete_item_from_index
+)
+# -----
+
 class WebsiteItem(WebsiteGenerator):
 	website = frappe._dict(
 		page_title_field="web_item_name",
@@ -49,6 +57,8 @@
 
 	def on_trash(self):
 		super(WebsiteItem, self).on_trash()
+		# Delete Item from search index
+		delete_item_from_index(self)
 		self.publish_unpublish_desk_item(publish=False)
 
 	def validate_duplicate_website_item(self):
@@ -376,6 +386,9 @@
 	for item_group in website_item_groups:
 		invalidate_cache_for(doc, item_group)
 
+	# Update Search Cache
+	update_index_for_item(doc)
+
 	invalidate_item_variants_cache_for_website(doc)
 
 @frappe.whitelist()
@@ -402,6 +415,10 @@
 		return website_item
 
 	website_item.save()
+
+	# Add to search cache
+	insert_item_to_index(website_item)
+	
 	return [website_item.name, website_item.web_item_name]
 
 def on_doctype_update():
diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py
index e5686f0..76b007a 100644
--- a/erpnext/templates/pages/product_search.py
+++ b/erpnext/templates/pages/product_search.py
@@ -9,8 +9,18 @@
 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 -------
+import redis
+from redisearch import Client, AutoCompleter, Suggestion, IndexDefinition, TextField, TagField
+
+WEBSITE_ITEM_INDEX = 'website_items_index'
+WEBSITE_ITEM_KEY_PREFIX = 'website_item:'
+WEBSITE_ITEM_NAME_AUTOCOMPLETE = 'website_items_name_dict'
+# -----------------
+
 no_cache = 1
 
+
 def get_context(context):
 	context.show_search = True
 
@@ -49,3 +59,115 @@
 		set_product_info_for_website(item)
 
 	return [get_item_for_list_in_html(r) for r in data]
+
+@frappe.whitelist(allow_guest=True)
+def search(query):
+	ac = AutoCompleter(WEBSITE_ITEM_NAME_AUTOCOMPLETE, port=13000)
+	suggestions = ac.get_suggestions(query, num=10)
+	print(suggestions)
+	return list([s.string for s in suggestions])
+
+def create_website_items_index():
+	'''Creates Index Definition'''
+	# DROP if already exists
+	try:
+		client.drop_index()
+	except:
+		pass
+
+	# CREATE index
+	client = Client(WEBSITE_ITEM_INDEX, port=13000)
+	idx_def = IndexDefinition([WEBSITE_ITEM_KEY_PREFIX])
+
+	client.create_index(
+		[TextField("web_item_name", sortable=True), TagField("tags")],
+		definition=idx_def
+	)
+
+	reindex_all_web_items()
+
+def insert_item_to_index(website_item_doc):
+	# Insert item to index
+	key = get_cache_key(website_item_doc.name)
+	r = redis.Redis("localhost", 13000)
+	web_item = create_web_item_map(website_item_doc)
+	r.hset(key, mapping=web_item)
+	insert_to_name_ac(website_item_doc.name)
+
+def insert_to_name_ac(name):
+	ac = AutoCompleter(WEBSITE_ITEM_NAME_AUTOCOMPLETE, port=13000)
+	ac.add_suggestions(Suggestion(name))
+
+def create_web_item_map(website_item_doc):
+	web_item = {}
+	web_item["web_item_name"] = website_item_doc.web_item_name
+	web_item["route"] = website_item_doc.route
+	web_item["thumbnail"] = website_item_doc.thumbnail or ''
+	web_item["description"] = website_item_doc.description or ''
+
+	return web_item
+
+def update_index_for_item(website_item_doc):
+	# Reinsert to Cache
+	insert_item_to_index(website_item_doc)
+	define_autocomplete_dictionary()
+	# TODO: Only reindex updated items
+	create_website_items_index()
+
+def delete_item_from_index(website_item_doc):
+	r = redis.Redis("localhost", 13000)
+	key = get_cache_key(website_item_doc.name)
+
+	try:
+		r.delete(key)
+	except:
+		return False
+
+	# TODO: Also delete autocomplete suggestion
+	return True
+
+def define_autocomplete_dictionary():
+	# AC for name
+	# TODO: AC for category
+
+	r = redis.Redis("localhost", 13000)
+	ac = AutoCompleter(WEBSITE_ITEM_NAME_AUTOCOMPLETE, port=13000)
+
+	try:
+		r.delete(WEBSITE_ITEM_NAME_AUTOCOMPLETE)
+	except:
+		return False
+
+	items = frappe.get_all(
+		'Website Item',
+		fields=['web_item_name'],
+		filters={"published": True}
+	)
+
+	for item in items:
+		print("adding suggestion: " + item.web_item_name)
+		ac.add_suggestions(Suggestion(item.web_item_name))
+
+	return True
+
+def reindex_all_web_items():
+	items = frappe.get_all(
+		'Website Item',
+		fields=['web_item_name', 'name', 'route', 'thumbnail', 'description'],
+		filters={"published": True}
+	)
+
+	r = redis.Redis("localhost", 13000)
+	for item in items:
+		web_item = create_web_item_map(item)
+		key = get_cache_key(item.name)
+		print(key, web_item)
+		r.hset(key, mapping=web_item)
+
+def get_cache_key(name):
+	name = frappe.scrub(name)
+	return f"{WEBSITE_ITEM_KEY_PREFIX}{name}"
+
+# TODO: Remove later
+define_autocomplete_dictionary()
+create_website_items_index()
diff --git a/erpnext/www/all-products/search.html b/erpnext/www/all-products/search.html
new file mode 100644
index 0000000..e7d437a
--- /dev/null
+++ b/erpnext/www/all-products/search.html
@@ -0,0 +1,14 @@
+{% extends "templates/web.html" %}
+
+
+{% block title %}{{ _('Search') }}{% endblock %}
+{% block header %}
+<div class="mb-6">{{ _('Search Products') }}</div>
+{% endblock header %}
+
+{% block page_content %}
+<input type="text" name="query" id="search-box">
+<ul id="results">
+
+</ul>
+{% endblock %}
\ No newline at end of file
diff --git a/erpnext/www/all-products/search.js b/erpnext/www/all-products/search.js
new file mode 100644
index 0000000..02678a2
--- /dev/null
+++ b/erpnext/www/all-products/search.js
@@ -0,0 +1,25 @@
+console.log("search.js loaded");
+
+const search_box = document.getElementById("search-box");
+const results = document.getElementById("results");
+
+function populateResults(data) {
+    html = ""
+    for (let res of data.message) {
+        html += `<li>${res}</li>`
+    }
+    console.log(html);
+    results.innerHTML = html;
+}
+
+search_box.addEventListener("input", (e) => {
+    frappe.call({
+        method: "erpnext.templates.pages.product_search.search", 
+        args: {
+            query: e.target.value 
+        },
+        callback: (data) => {
+            populateResults(data);
+        }
+    })
+});
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index f28906a..400e6a3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,3 +11,4 @@
 taxjar~=1.9.2
 tweepy~=3.10.0
 Unidecode~=1.2.0
+redisearch==2.0.0
\ No newline at end of file