diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 181efdd37f7..646da7261fa 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 frappe.website.doctype.website_slideshow.website_slideshow import get_slide 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 @@ class WebsiteItem(WebsiteGenerator): 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): @@ -377,6 +387,9 @@ def invalidate_cache_for_web_item(doc): 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() @@ -403,6 +416,10 @@ def make_website_item(doc, save=True): 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 4f4d95bc473..131ddbebed8 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -7,8 +7,18 @@ from frappe.utils import cstr, nowdate, cint 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 @@ -48,3 +58,114 @@ def get_product_list(search=None, start=0, limit=12): 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 00000000000..e7d437ad5fc --- /dev/null +++ b/erpnext/www/all-products/search.html @@ -0,0 +1,14 @@ +{% extends "templates/web.html" %} + + +{% block title %}{{ _('Search') }}{% endblock %} +{% block header %} +