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