From cc402a5d0c9765158d4e1aebc67ed8f72b80334d Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Wed, 21 Apr 2021 13:52:23 +0530 Subject: [PATCH 01/18] feat: Add basic autocomplete using redisearch --- .../doctype/website_item/website_item.py | 17 +++ erpnext/templates/pages/product_search.py | 121 ++++++++++++++++++ erpnext/www/all-products/search.html | 14 ++ erpnext/www/all-products/search.js | 25 ++++ requirements.txt | 1 + 5 files changed, 178 insertions(+) create mode 100644 erpnext/www/all-products/search.html create mode 100644 erpnext/www/all-products/search.js 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 %} +
{{ _('Search Products') }}
+{% endblock header %} + +{% block page_content %} + + +{% 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 00000000000..02678a2c1ac --- /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 += `
  • ${res}
  • ` + } + 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 5a352364b6e..2eb78d79773 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ tweepy==3.8.0 Unidecode==1.1.1 WooCommerce==2.1.1 pycryptodome==3.9.8 +redisearch==2.0.0 \ No newline at end of file From 4a0136b524ef453b2eb87badfd3043ebd315f006 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Thu, 22 Apr 2021 14:21:29 +0530 Subject: [PATCH 02/18] feat: Basic Query + Autocomplete --- erpnext/templates/pages/product_search.py | 47 +++++++++++++++++++---- erpnext/www/all-products/search.css | 5 +++ erpnext/www/all-products/search.html | 6 ++- erpnext/www/all-products/search.js | 8 +++- 4 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 erpnext/www/all-products/search.css diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index 131ddbebed8..2dd76f3d9e8 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -9,7 +9,12 @@ from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_w # For SEARCH ------- import redis -from redisearch import Client, AutoCompleter, Suggestion, IndexDefinition, TextField, TagField +from redisearch import ( + Client, AutoCompleter, Query, + Suggestion, IndexDefinition, + TextField, TagField, + Document + ) WEBSITE_ITEM_INDEX = 'website_items_index' WEBSITE_ITEM_KEY_PREFIX = 'website_item:' @@ -60,21 +65,46 @@ def get_product_list(search=None, start=0, limit=12): @frappe.whitelist(allow_guest=True) def search(query): + if not query: + # TODO: return top/recent searches + return [] + ac = AutoCompleter(WEBSITE_ITEM_NAME_AUTOCOMPLETE, port=13000) + client = Client(WEBSITE_ITEM_INDEX, port=13000) suggestions = ac.get_suggestions(query, num=10) - print(suggestions) - return list([s.string for s in suggestions]) + + # Build a query + query_string = query + + for s in suggestions: + query_string += f"|({s.string})" + + q = Query(query_string) + + print(f"Executing query: {q.query_string()}") + + results = client.search(q) + results = list(map(convert_to_dict, results.docs)) + + print("SEARCH RESULTS ------------------\n ", results) + + return results + +def convert_to_dict(redis_search_doc): + return redis_search_doc.__dict__ def create_website_items_index(): '''Creates Index Definition''' + # CREATE index + client = Client(WEBSITE_ITEM_INDEX, port=13000) + # 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( @@ -90,11 +120,11 @@ def insert_item_to_index(website_item_doc): 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) + insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name) -def insert_to_name_ac(name): +def insert_to_name_ac(web_name, doc_name): ac = AutoCompleter(WEBSITE_ITEM_NAME_AUTOCOMPLETE, port=13000) - ac.add_suggestions(Suggestion(name)) + ac.add_suggestions(Suggestion(web_name, payload=doc_name)) def create_web_item_map(website_item_doc): web_item = {} @@ -167,5 +197,6 @@ def get_cache_key(name): return f"{WEBSITE_ITEM_KEY_PREFIX}{name}" # TODO: Remove later +# Figure out a way to run this at startup define_autocomplete_dictionary() create_website_items_index() diff --git a/erpnext/www/all-products/search.css b/erpnext/www/all-products/search.css new file mode 100644 index 00000000000..ff0ca6ac9fe --- /dev/null +++ b/erpnext/www/all-products/search.css @@ -0,0 +1,5 @@ +.item-thumb { + height: 50px; + width: 50px; + object-fit: cover; +} \ No newline at end of file diff --git a/erpnext/www/all-products/search.html b/erpnext/www/all-products/search.html index e7d437ad5fc..ef74536286d 100644 --- a/erpnext/www/all-products/search.html +++ b/erpnext/www/all-products/search.html @@ -1,7 +1,11 @@ {% extends "templates/web.html" %} - {% block title %}{{ _('Search') }}{% endblock %} + +{%- block head_include %} + +{% endblock -%} + {% block header %}
    {{ _('Search Products') }}
    {% endblock header %} diff --git a/erpnext/www/all-products/search.js b/erpnext/www/all-products/search.js index 02678a2c1ac..2080f35c628 100644 --- a/erpnext/www/all-products/search.js +++ b/erpnext/www/all-products/search.js @@ -1,12 +1,16 @@ -console.log("search.js loaded"); +console.log("search.js reloaded"); const search_box = document.getElementById("search-box"); const results = document.getElementById("results"); function populateResults(data) { + console.log(data); html = "" for (let res of data.message) { - html += `
  • ${res}
  • ` + html += `
  • + + ${res.web_item_name} +
  • ` } console.log(html); results.innerHTML = html; From fdcfa41a6f226eadc288b822924f21c55f7d1f7d Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Fri, 23 Apr 2021 12:08:58 +0530 Subject: [PATCH 03/18] feat: Add search fields field --- .../e_commerce_settings.json | 17 +++++++++++++++-- .../e_commerce_settings/e_commerce_settings.py | 11 +++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json index 805a530d8d3..76bf283e040 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json @@ -44,7 +44,9 @@ "enable_attribute_filters", "filter_attributes", "shop_by_category_section", - "slideshow" + "slideshow", + "item_search_settings_section", + "search_index_fields" ], "fields": [ { @@ -300,12 +302,23 @@ "fieldname": "enable_reviews", "fieldtype": "Check", "label": "Enable Reviews and Ratings" + }, + { + "fieldname": "search_index_fields", + "fieldtype": "Small Text", + "label": "Search Index Fields" + }, + { + "collapsible": 1, + "fieldname": "item_search_settings_section", + "fieldtype": "Section Break", + "label": "Item Search Settings" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-03-23 17:15:01.956630", + "modified": "2021-04-23 13:30:50.286088", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", 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 9d3ca37abbf..186ec03d425 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 @@ -24,6 +24,7 @@ class ECommerceSettings(Document): self.validate_field_filters() self.validate_attribute_filters() self.validate_checkout() + self.validate_search_index_fields() if self.enabled: self.validate_exchange_rates_exist() @@ -51,6 +52,16 @@ class ECommerceSettings(Document): if self.enable_checkout and not self.payment_gateway_account: self.enable_checkout = 0 + def validate_search_index_fields(self): + if not self.search_index_fields: + return + + # Clean up + fields = self.search_index_fields.replace(' ', '') + fields = fields.strip(',') + + self.search_index_fields = fields + def validate_exchange_rates_exist(self): """check if exchange rates exist for all Price List currencies (to company's currency)""" company_currency = frappe.get_cached_value('Company', self.company, "default_currency") From c376b67725aae1de35c96ae9402de7f5c12a5b76 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Mon, 26 Apr 2021 07:01:06 +0530 Subject: [PATCH 04/18] feat: Make search index fields configurable - Move indexing logic to separate file - Add more validation logic for 'search index fields' field --- .../e_commerce_settings.py | 29 ++- .../doctype/website_item/website_item.py | 2 +- erpnext/e_commerce/website_item_indexing.py | 167 ++++++++++++++++++ erpnext/templates/pages/product_search.py | 121 +------------ 4 files changed, 196 insertions(+), 123 deletions(-) create mode 100644 erpnext/e_commerce/website_item_indexing.py 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 186ec03d425..615fa04aa0f 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 @@ -7,7 +7,8 @@ import frappe 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 +from frappe.utils import get_datetime, get_datetime_str, now_datetime, unique +from erpnext.e_commerce.website_item_indexing import create_website_items_index, ALLOWED_INDEXABLE_FIELDS_SET class ShoppingCartSetupError(frappe.ValidationError): pass @@ -57,10 +58,23 @@ class ECommerceSettings(Document): return # Clean up + # Remove whitespaces fields = self.search_index_fields.replace(' ', '') - fields = fields.strip(',') + # Remove extra ',' and remove duplicates + fields = unique(fields.strip(',').split(',')) - self.search_index_fields = fields + # All fields should be indexable + if not (set(fields).issubset(ALLOWED_INDEXABLE_FIELDS_SET)): + invalid_fields = list(set(fields).difference(ALLOWED_INDEXABLE_FIELDS_SET)) + num_invalid_fields = len(invalid_fields) + invalid_fields = comma_and(invalid_fields) + + if num_invalid_fields > 1: + frappe.throw(_("{0} are not valid options for Search Index Field.").format(frappe.bold(invalid_fields))) + else: + frappe.throw(_("{0} is not a valid option for Search Index Field.").format(frappe.bold(invalid_fields))) + + self.search_index_fields = ','.join(fields) def validate_exchange_rates_exist(self): """check if exchange rates exist for all Price List currencies (to company's currency)""" @@ -113,6 +127,15 @@ class ECommerceSettings(Document): def get_shipping_rules(self, shipping_territory): return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule") + def on_change(self): + old_doc = self.get_doc_before_save() + old_fields = old_doc.search_index_fields + new_fields = self.search_index_fields + + # if search index fields get changed + if not (new_fields == old_fields): + create_website_items_index() + def validate_cart_settings(doc, method): frappe.get_doc("E Commerce Settings", "E Commerce Settings").run_method("validate") diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 646da7261fa..65c8782877e 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -17,7 +17,7 @@ from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews # SEARCH -from erpnext.templates.pages.product_search import ( +from erpnext.e_commerce.website_item_indexing import ( insert_item_to_index, update_index_for_item, delete_item_from_index diff --git a/erpnext/e_commerce/website_item_indexing.py b/erpnext/e_commerce/website_item_indexing.py new file mode 100644 index 00000000000..0e48a2d7e68 --- /dev/null +++ b/erpnext/e_commerce/website_item_indexing.py @@ -0,0 +1,167 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe +import redis + +from redisearch import ( + Client, AutoCompleter, Query, + Suggestion, IndexDefinition, + TextField, TagField, + Document + ) + +# GLOBAL CONSTANTS +WEBSITE_ITEM_INDEX = 'website_items_index' +WEBSITE_ITEM_KEY_PREFIX = 'website_item:' +WEBSITE_ITEM_NAME_AUTOCOMPLETE = 'website_items_name_dict' + +ALLOWED_INDEXABLE_FIELDS_SET = { + 'item_code', + 'item_name', + 'item_group', + 'brand', + 'description', + 'web_long_description' +} + +def create_website_items_index(): + '''Creates Index Definition''' + # CREATE index + client = Client(WEBSITE_ITEM_INDEX, port=13000) + + # DROP if already exists + try: + client.drop_index() + except: + pass + + + idx_def = IndexDefinition([WEBSITE_ITEM_KEY_PREFIX]) + + # Based on e-commerce settings + idx_fields = frappe.db.get_single_value( + '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( + [TextField("web_item_name", sortable=True)] + idx_fields, + definition=idx_def + ) + + reindex_all_web_items() + define_autocomplete_dictionary() + +def to_search_field(field): + if field == "tags": + return TagField("tags", separator=",") + + return TextField(field) + +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.web_item_name, website_item_doc.name) + +def insert_to_name_ac(web_name, doc_name): + ac = AutoCompleter(WEBSITE_ITEM_NAME_AUTOCOMPLETE, port=13000) + 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: + web_item[f] = website_item_doc.get(f) 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(): + print("Defining ac dict...") + # 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=get_fields_indexed(), + 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}" + +def get_fields_indexed(): + fields_to_index = frappe.db.get_single_value( + 'E Commerce Settings', + 'search_index_fields' + ).split(',') + + mandatory_fields = ['name', 'web_item_name', 'route', 'thumbnail'] + fields_to_index = fields_to_index + mandatory_fields + + return fields_to_index + +# TODO: Remove later +# # Figure out a way to run this at startup +define_autocomplete_dictionary() +create_website_items_index() diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index 2dd76f3d9e8..c04e5a3c519 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -8,17 +8,8 @@ from erpnext.setup.doctype.item_group.item_group import get_item_for_list_in_htm from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_website # For SEARCH ------- -import redis -from redisearch import ( - Client, AutoCompleter, Query, - Suggestion, IndexDefinition, - TextField, TagField, - Document - ) - -WEBSITE_ITEM_INDEX = 'website_items_index' -WEBSITE_ITEM_KEY_PREFIX = 'website_item:' -WEBSITE_ITEM_NAME_AUTOCOMPLETE = 'website_items_name_dict' +from redisearch import AutoCompleter, Client, Query +from erpnext.e_commerce.website_item_indexing import WEBSITE_ITEM_INDEX, WEBSITE_ITEM_NAME_AUTOCOMPLETE # ----------------- no_cache = 1 @@ -92,111 +83,3 @@ def search(query): def convert_to_dict(redis_search_doc): return redis_search_doc.__dict__ - -def create_website_items_index(): - '''Creates Index Definition''' - # CREATE index - client = Client(WEBSITE_ITEM_INDEX, port=13000) - - # DROP if already exists - try: - client.drop_index() - except: - pass - - - 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.web_item_name, website_item_doc.name) - -def insert_to_name_ac(web_name, doc_name): - ac = AutoCompleter(WEBSITE_ITEM_NAME_AUTOCOMPLETE, port=13000) - ac.add_suggestions(Suggestion(web_name, payload=doc_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 -# Figure out a way to run this at startup -define_autocomplete_dictionary() -create_website_items_index() From d22951b0142aef962720f2d8a025355fc46daf3b Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Mon, 26 Apr 2021 11:15:20 +0530 Subject: [PATCH 05/18] feat: Add Category autocomplete with config in settings --- .../e_commerce_settings.json | 11 ++++- erpnext/e_commerce/website_item_indexing.py | 24 +++++++---- erpnext/templates/pages/product_search.py | 17 +++++++- erpnext/www/all-products/search.html | 16 +++++++- erpnext/www/all-products/search.js | 41 +++++++++++++++---- 5 files changed, 90 insertions(+), 19 deletions(-) diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json index 76bf283e040..bfdfb0d17a1 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json @@ -46,7 +46,8 @@ "shop_by_category_section", "slideshow", "item_search_settings_section", - "search_index_fields" + "search_index_fields", + "show_categories_in_search_autocomplete" ], "fields": [ { @@ -313,12 +314,18 @@ "fieldname": "item_search_settings_section", "fieldtype": "Section Break", "label": "Item Search Settings" + }, + { + "default": "1", + "fieldname": "show_categories_in_search_autocomplete", + "fieldtype": "Check", + "label": "Show Categories in Search Autocomplete" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-04-23 13:30:50.286088", + "modified": "2021-04-26 09:50:40.581354", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", diff --git a/erpnext/e_commerce/website_item_indexing.py b/erpnext/e_commerce/website_item_indexing.py index 0e48a2d7e68..faf5760a6aa 100644 --- a/erpnext/e_commerce/website_item_indexing.py +++ b/erpnext/e_commerce/website_item_indexing.py @@ -17,6 +17,7 @@ from redisearch import ( WEBSITE_ITEM_INDEX = 'website_items_index' WEBSITE_ITEM_KEY_PREFIX = 'website_item:' WEBSITE_ITEM_NAME_AUTOCOMPLETE = 'website_items_name_dict' +WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE = 'website_items_category_dict' ALLOWED_INDEXABLE_FIELDS_SET = { 'item_code', @@ -108,27 +109,36 @@ def delete_item_from_index(website_item_doc): return True def define_autocomplete_dictionary(): - print("Defining ac dict...") - # AC for name - # TODO: AC for category + """Creates an autocomplete search dictionary for `name`. + Also creats autocomplete dictionary for `categories` if + checked in E Commerce Settings""" r = redis.Redis("localhost", 13000) - ac = AutoCompleter(WEBSITE_ITEM_NAME_AUTOCOMPLETE, port=13000) + name_ac = AutoCompleter(WEBSITE_ITEM_NAME_AUTOCOMPLETE, port=13000) + cat_ac = AutoCompleter(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, port=13000) + ac_categories = frappe.db.get_single_value( + 'E Commerce Settings', + 'show_categories_in_search_autocomplete' + ) + + # Delete both autocomplete dicts try: r.delete(WEBSITE_ITEM_NAME_AUTOCOMPLETE) + r.delete(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE) except: return False items = frappe.get_all( 'Website Item', - fields=['web_item_name'], + fields=['web_item_name', 'item_group'], filters={"published": True} ) for item in items: - print("adding suggestion: " + item.web_item_name) - ac.add_suggestions(Suggestion(item.web_item_name)) + name_ac.add_suggestions(Suggestion(item.web_item_name)) + if ac_categories and item.item_group: + cat_ac.add_suggestions(Suggestion(item.item_group)) return True diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index c04e5a3c519..8b7aeab066f 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -9,7 +9,11 @@ from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_w # For SEARCH ------- from redisearch import AutoCompleter, Client, Query -from erpnext.e_commerce.website_item_indexing import WEBSITE_ITEM_INDEX, WEBSITE_ITEM_NAME_AUTOCOMPLETE +from erpnext.e_commerce.website_item_indexing import ( + WEBSITE_ITEM_INDEX, + WEBSITE_ITEM_NAME_AUTOCOMPLETE, + WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE +) # ----------------- no_cache = 1 @@ -83,3 +87,14 @@ def search(query): def convert_to_dict(redis_search_doc): return redis_search_doc.__dict__ + +@frappe.whitelist(allow_guest=True) +def get_category_suggestions(query): + if not query: + # TODO: return top/recent searches + return [] + + ac = AutoCompleter(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, port=13000) + suggestions = ac.get_suggestions(query, num=10) + + return [s.string for s in suggestions] \ No newline at end of file diff --git a/erpnext/www/all-products/search.html b/erpnext/www/all-products/search.html index ef74536286d..1c58e65cc76 100644 --- a/erpnext/www/all-products/search.html +++ b/erpnext/www/all-products/search.html @@ -12,7 +12,19 @@ {% block page_content %} -
      -
    + +

    Products

    +
      Start typing...
    + +{% set show_categories = frappe.db.get_single_value('E Commerce Settings', 'show_categories_in_search_autocomplete') %} + +{% if show_categories %} +
    +

    Categories

    +
      +
    +
    +{% endif %} + {% endblock %} \ No newline at end of file diff --git a/erpnext/www/all-products/search.js b/erpnext/www/all-products/search.js index 2080f35c628..aa47a4d7e2a 100644 --- a/erpnext/www/all-products/search.js +++ b/erpnext/www/all-products/search.js @@ -1,10 +1,10 @@ -console.log("search.js reloaded"); +console.log("search.js loaded"); -const search_box = document.getElementById("search-box"); +const searchBox = document.getElementById("search-box"); const results = document.getElementById("results"); +const categoryList = document.getElementById("category-suggestions"); function populateResults(data) { - console.log(data); html = "" for (let res of data.message) { html += `
  • @@ -12,11 +12,24 @@ function populateResults(data) { ${res.web_item_name}
  • ` } - console.log(html); results.innerHTML = html; } -search_box.addEventListener("input", (e) => { +function populateCategoriesList(data) { + if (data.length === 0) { + categoryList.innerText = "No matches"; + return; + } + + html = "" + for (let category of data.message) { + html += `
  • ${category}
  • ` + } + + categoryList.innerHTML = html; +} + +searchBox.addEventListener("input", (e) => { frappe.call({ method: "erpnext.templates.pages.product_search.search", args: { @@ -25,5 +38,19 @@ search_box.addEventListener("input", (e) => { callback: (data) => { populateResults(data); } - }) -}); \ No newline at end of file + }); + + // 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); + } + }); + } +}); + From 58d08ee307a997e6fd608a9050ba0d0ace2fa75a Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Thu, 29 Apr 2021 20:01:31 +0530 Subject: [PATCH 06/18] chore: Make it a little beautiful --- erpnext/e_commerce/website_item_indexing.py | 1 - erpnext/templates/pages/product_search.py | 2 +- erpnext/www/all-products/search.html | 37 +++++++++++++-------- erpnext/www/all-products/search.js | 15 +++++++-- 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/erpnext/e_commerce/website_item_indexing.py b/erpnext/e_commerce/website_item_indexing.py index faf5760a6aa..a4c45cc3b80 100644 --- a/erpnext/e_commerce/website_item_indexing.py +++ b/erpnext/e_commerce/website_item_indexing.py @@ -153,7 +153,6 @@ def reindex_all_web_items(): 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): diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index 8b7aeab066f..523fd3e6433 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -72,7 +72,7 @@ def search(query): query_string = query for s in suggestions: - query_string += f"|({s.string})" + query_string += f"|('{s.string}')" q = Query(query_string) diff --git a/erpnext/www/all-products/search.html b/erpnext/www/all-products/search.html index 1c58e65cc76..c6c87089e80 100644 --- a/erpnext/www/all-products/search.html +++ b/erpnext/www/all-products/search.html @@ -11,20 +11,29 @@ {% endblock header %} {% block page_content %} - - - -

    Products

    -
      Start typing...
    - -{% set show_categories = frappe.db.get_single_value('E Commerce Settings', 'show_categories_in_search_autocomplete') %} - -{% if show_categories %} -
    -

    Categories

    -
      -
    +
    + +
    + +
    +
    + +
    + +
    +

    Products

    +
      +
      + + {% set show_categories = frappe.db.get_single_value('E Commerce Settings', 'show_categories_in_search_autocomplete') %} + + {% if show_categories %} +
      +

      Categories

      +
        +
      +
      + {% endif %}
      -{% endif %} {% endblock %} \ No newline at end of file diff --git a/erpnext/www/all-products/search.js b/erpnext/www/all-products/search.js index aa47a4d7e2a..f17f7a034cf 100644 --- a/erpnext/www/all-products/search.js +++ b/erpnext/www/all-products/search.js @@ -1,4 +1,4 @@ -console.log("search.js loaded"); +let loading = false; const searchBox = document.getElementById("search-box"); const results = document.getElementById("results"); @@ -7,7 +7,7 @@ const categoryList = document.getElementById("category-suggestions"); function populateResults(data) { html = "" for (let res of data.message) { - html += `
    • + html += `
    • ${res.web_item_name}
    • ` @@ -17,7 +17,7 @@ function populateResults(data) { function populateCategoriesList(data) { if (data.length === 0) { - categoryList.innerText = "No matches"; + categoryList.innerHTML = 'Type something...'; return; } @@ -29,7 +29,15 @@ function populateCategoriesList(data) { categoryList.innerHTML = html; } +function updateLoadingState() { + if (loading) { + results.innerHTML = `
      loading...
      `; + } +} + searchBox.addEventListener("input", (e) => { + loading = true; + updateLoadingState(); frappe.call({ method: "erpnext.templates.pages.product_search.search", args: { @@ -37,6 +45,7 @@ searchBox.addEventListener("input", (e) => { }, callback: (data) => { populateResults(data); + loading = false; } }); From a6deace37cc841ac62cc20cebd4805fa0bf15841 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Thu, 29 Apr 2021 20:47:32 +0530 Subject: [PATCH 07/18] refactor: Use global redis connection --- erpnext/e_commerce/website_item_indexing.py | 41 +++++++++++++-------- erpnext/templates/pages/product_search.py | 11 ++++-- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/erpnext/e_commerce/website_item_indexing.py b/erpnext/e_commerce/website_item_indexing.py index a4c45cc3b80..4e3af8bf1f4 100644 --- a/erpnext/e_commerce/website_item_indexing.py +++ b/erpnext/e_commerce/website_item_indexing.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -import redis +from frappe.utils.redis_wrapper import RedisWrapper from redisearch import ( Client, AutoCompleter, Query, @@ -13,6 +13,9 @@ from redisearch import ( Document ) +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:' @@ -31,7 +34,7 @@ ALLOWED_INDEXABLE_FIELDS_SET = { def create_website_items_index(): '''Creates Index Definition''' # CREATE index - client = Client(WEBSITE_ITEM_INDEX, port=13000) + client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache()) # DROP if already exists try: @@ -40,7 +43,7 @@ def create_website_items_index(): pass - idx_def = IndexDefinition([WEBSITE_ITEM_KEY_PREFIX]) + idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)]) # Based on e-commerce settings idx_fields = frappe.db.get_single_value( @@ -55,7 +58,7 @@ def create_website_items_index(): client.create_index( [TextField("web_item_name", sortable=True)] + idx_fields, - definition=idx_def + definition=idx_def, ) reindex_all_web_items() @@ -70,13 +73,16 @@ def to_search_field(field): 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) + r = frappe.cache() web_item = create_web_item_map(website_item_doc) - r.hset(key, mapping=web_item) + + for k, v in web_item.items(): + super(RedisWrapper, r).hset(make_key(key), k, v) + insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name) def insert_to_name_ac(web_name, doc_name): - ac = AutoCompleter(WEBSITE_ITEM_NAME_AUTOCOMPLETE, port=13000) + 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): @@ -97,7 +103,7 @@ def update_index_for_item(website_item_doc): create_website_items_index() def delete_item_from_index(website_item_doc): - r = redis.Redis("localhost", 13000) + r = frappe.cache() key = get_cache_key(website_item_doc.name) try: @@ -113,9 +119,9 @@ def define_autocomplete_dictionary(): Also creats autocomplete dictionary for `categories` if checked in E Commerce Settings""" - r = redis.Redis("localhost", 13000) - name_ac = AutoCompleter(WEBSITE_ITEM_NAME_AUTOCOMPLETE, port=13000) - cat_ac = AutoCompleter(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, port=13000) + 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) ac_categories = frappe.db.get_single_value( 'E Commerce Settings', @@ -124,8 +130,8 @@ def define_autocomplete_dictionary(): # Delete both autocomplete dicts try: - r.delete(WEBSITE_ITEM_NAME_AUTOCOMPLETE) - r.delete(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE) + r.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE)) + r.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE)) except: return False @@ -149,11 +155,14 @@ def reindex_all_web_items(): filters={"published": True} ) - r = redis.Redis("localhost", 13000) + r = frappe.cache() for item in items: web_item = create_web_item_map(item) - key = get_cache_key(item.name) - r.hset(key, mapping=web_item) + key = make_key(get_cache_key(item.name)) + + for k, v in web_item.items(): + super(RedisWrapper, r).hset(key, k, v) + def get_cache_key(name): name = frappe.scrub(name) diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index 523fd3e6433..9ec175cb838 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -12,7 +12,8 @@ from redisearch import AutoCompleter, Client, Query from erpnext.e_commerce.website_item_indexing import ( WEBSITE_ITEM_INDEX, WEBSITE_ITEM_NAME_AUTOCOMPLETE, - WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE + WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, + make_key ) # ----------------- @@ -64,8 +65,10 @@ def search(query): # TODO: return top/recent searches return [] - ac = AutoCompleter(WEBSITE_ITEM_NAME_AUTOCOMPLETE, port=13000) - client = Client(WEBSITE_ITEM_INDEX, port=13000) + red = frappe.cache() + + ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=red) + client = Client(make_key(WEBSITE_ITEM_INDEX), conn=red) suggestions = ac.get_suggestions(query, num=10) # Build a query @@ -94,7 +97,7 @@ def get_category_suggestions(query): # TODO: return top/recent searches return [] - ac = AutoCompleter(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, port=13000) + ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=frappe.cache()) suggestions = ac.get_suggestions(query, num=10) return [s.string for s in suggestions] \ No newline at end of file From 28c2f5d832370e2d07c6b11a1fb551ca282a5bec Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Mon, 3 May 2021 05:47:38 +0530 Subject: [PATCH 08/18] chore: Add query clean-up --- erpnext/templates/pages/product_search.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index 9ec175cb838..99d420de19b 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -67,6 +67,8 @@ def search(query): red = frappe.cache() + query = clean_up_query(query) + ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=red) client = Client(make_key(WEBSITE_ITEM_INDEX), conn=red) suggestions = ac.get_suggestions(query, num=10) @@ -75,7 +77,7 @@ def search(query): query_string = query for s in suggestions: - query_string += f"|('{s.string}')" + query_string += f"|('{clean_up_query(s.string)}')" q = Query(query_string) @@ -88,6 +90,9 @@ def search(query): return results +def clean_up_query(query): + return ''.join(c for c in query if c.isalnum() or c.isspace()) + def convert_to_dict(redis_search_doc): return redis_search_doc.__dict__ From eb955c7e99c2fefe2a44af762e14715cbf92a0de Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 4 May 2021 11:08:45 +0530 Subject: [PATCH 09/18] chore: Add placeholder image --- erpnext/www/all-products/img/placeholder.png | Bin 0 -> 3985 bytes erpnext/www/all-products/search.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 erpnext/www/all-products/img/placeholder.png diff --git a/erpnext/www/all-products/img/placeholder.png b/erpnext/www/all-products/img/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..9780ad8e0572d612ed6c12a510934d29e074da28 GIT binary patch literal 3985 zcmds)c~nzZ9>?zsU;-|Ht)kYDP@T3eEG`2A4T)O=cEAV*kljH62?AE36qH1*TA>A8 zT4V`OQIKE=E-|u%q@q$=Hh}~iWpS{HB*tjNBSId@yhmp`|8&lr_8fcWG=JQDbKm#g z`}w`^J?Gw>WCYM>+bpsH0L=FD-4qA_4wX1SutdM2_ATBAfNiMXruEx16xxBV+K@EI zwwHzmzmv`@*ZVpVTtZ{kuh>Vn4FBx$mcZ2ct9&;son^_7mwCmJ?T##58n?<}OT?Ye za&gu+A&0G(M-YzGlDE6$A>)x zBs3gnDe!$Q4Jfb#e!tfA?(IEW_N-o5#vc&YmR;fs#W&=waK*+;o`#Kwf<)>#zD$+I zcj>Rz%0G3NRlQg$>OY*>QdS$;>F!^_g0~%Fs2=(QL|!eb%#)o`Xo5GNVTpZe42c&b z@lvz?_E&0#I1sOa;!nTxXlq*CV4dNi7jQRSaPNPaDgKH}2`dQJba)N4SV+a?-Bn)O z*gS6MLg!Ex{K1rPioj{2aF4=;nLQ2Xt+a_^c_%d7xpg;P%yjjlUmxbyQ1es{M=PP2 zb-~r@ZSf~?le~Z%^8jM>OR5Qi@v37-GOGh_q(BU2>qlIEe5t*SCXkq1tIwXhpMAKa zUf@zJ!DUss%rV}A7Uan%6v_z3u7_$Qb?pMq;|zY^#!ibn;|%_+wvBAO+hT!4HoTaT z!H-I=9s#I5NA${Pi_c+izb4I^}qsfKoYt}zp( z^}?32?^o%YfPa>W$;`hV9wfnKqzBMDn4Bsf^BheP1vIJBpV(FG>5{Z&#@R~iJRK>P z3E$~;^<_U;yto~aPsXhsnD{uf>lt*;Pz}hmATIj^-0sNH?x&t6bgxwn?WA18$7l$7 z@8H(n`PrC}@h1IEsCW?Zz8pX9YMEH+Sr@hF!k!KZPvHiy;JkdYoc2bE`B`|5S0kZd z0&l?qMeMZzmGu9}tur`p5xRG&*cL#7ZfaCXNI-HW-3lsJBW>ne*Q}8_09tL10d{14 z{tQee{LwPTq$JG-xb`8W5~6V?O0Xi_*wZx16w`e`mLeV0MZof$WV$lF2Zs212eBuS zR*WUSi|U9jOa*ovpvMCox-6ifs3br@pIQJ21qEaPDQ$rziV1Okx4Fj}n~D6@^fG_{ zfBM0hrg;w|`%}oR$q7r?9QWI#^F?#6e}|muVuK5BFwg|Sler9K`9YJBNSoB*+NoaH z&V$HU{GVdQW8vFj*>6Cjr(wwxu;86gQ$+^WpYs8M(2RRZ9v(6i)j=aY&R2|^vY|JYaWHXWwdzY z#`b@8qCExC-EbmuRVvj7KZG+lU?QU!at+k?660 zbGK=OG_-$B7Dz6Z*tyBE!~L(n*cyJZ#r@mnQu4FSq?LwiSo;36B_k!rNPE_pyrt~| zpx`$=LQlLZ_PkWSv$Pzl^;yk!iYfhm6q4f6m;CvDPomEi)`KVyua?no5#a*K%uqp_ z0@rMJNzpNR8~Jhhu#fR(TE@J+<>l?VB8gpUId}9UX79+xtP@0dGi7+}l>4Kta7I%L zIRt1kx%)R=G(L^UpD-9VRfve&_8iO|9TO8<10!p9)0qkedQaoKcF&`Q3p|L-Jx9eq z#G8nc2G)a!T4C?0FQ5Mq<}A`pzQ0}M@btP)J)0h^prwozv-@?=7rV>i;R}^3&>KE{ zz<8zqXc`j58IKh!pKdWc6=v&o$Q`maqCCXI?~L|un#tm#JQ0BPESzh9W!}{TZ7sZk zLs4LD;R+5fT^!)`U60_vRUM*wF~He-ad2PhMenhI@X@IPZVJTX9e0NPC?g5zP4}YW zVase#kr-N+E9|f@%pUz-(Hm@}K8t8^Z05@!PY$jsOg0=MmbD=&OB%C@;VNq!abe0b z#eFXAs;>)`a7waHeljsn{0~;zd9k+uA6+~mF&+>edKXS9zQ6L8(T+WPt^IkP^~V$}Fb!t1BlfIz=UKJdmFqEbvI;$Fn}x z^4?91-z(t5sg$Qly`{#lTWZ47uF2K<#^6FtZ#rv3$xnF^%RCJ7xnL - + ${res.web_item_name} ` } From 4356ffbfe8243c83ef50d6a56c68ecdfe50b6f8f Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Wed, 5 May 2021 13:09:46 +0530 Subject: [PATCH 10/18] feat: Add brand line display setting --- .../e_commerce_settings/e_commerce_settings.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json index bfdfb0d17a1..0f7b2cc4f02 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json @@ -47,7 +47,8 @@ "slideshow", "item_search_settings_section", "search_index_fields", - "show_categories_in_search_autocomplete" + "show_categories_in_search_autocomplete", + "show_brand_line" ], "fields": [ { @@ -320,12 +321,19 @@ "fieldname": "show_categories_in_search_autocomplete", "fieldtype": "Check", "label": "Show Categories in Search Autocomplete" + }, + { + "default": "1", + "description": "e.g. \"iPhone 12 by Apple\"", + "fieldname": "show_brand_line", + "fieldtype": "Check", + "label": "Show Brand Line" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-04-26 09:50:40.581354", + "modified": "2021-05-05 13:05:52.615719", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", From b52d2837397e830f211333e44067b91f124ce6aa Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Wed, 5 May 2021 13:47:43 +0530 Subject: [PATCH 11/18] feat: Show brand line in search results --- .../e_commerce_settings/e_commerce_settings.json | 4 ++-- .../e_commerce_settings/e_commerce_settings.py | 6 ++++++ erpnext/e_commerce/website_item_indexing.py | 12 ++++++++---- erpnext/www/all-products/search.css | 4 ++++ erpnext/www/all-products/search.html | 6 +++++- erpnext/www/all-products/search.js | 3 ++- 6 files changed, 27 insertions(+), 8 deletions(-) diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json index 0f7b2cc4f02..36177ff8f91 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json @@ -323,7 +323,7 @@ "label": "Show Categories in Search Autocomplete" }, { - "default": "1", + "default": "0", "description": "e.g. \"iPhone 12 by Apple\"", "fieldname": "show_brand_line", "fieldtype": "Check", @@ -333,7 +333,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-05-05 13:05:52.615719", + "modified": "2021-05-05 13:41:11.483232", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", 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 615fa04aa0f..d8a05d6a31b 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 @@ -25,7 +25,9 @@ class ECommerceSettings(Document): self.validate_field_filters() self.validate_attribute_filters() self.validate_checkout() + self.validate_brand_check() self.validate_search_index_fields() + if self.enabled: self.validate_exchange_rates_exist() @@ -76,6 +78,10 @@ class ECommerceSettings(Document): self.search_index_fields = ','.join(fields) + def validate_brand_check(self): + if self.show_brand_line and not ("brand" in self.search_index_fields): + self.search_index_fields += ",brand" + def validate_exchange_rates_exist(self): """check if exchange rates exist for all Price List currencies (to company's currency)""" company_currency = frappe.get_cached_value('Company', self.company, "default_currency") diff --git a/erpnext/e_commerce/website_item_indexing.py b/erpnext/e_commerce/website_item_indexing.py index 4e3af8bf1f4..8c0e074dc3e 100644 --- a/erpnext/e_commerce/website_item_indexing.py +++ b/erpnext/e_commerce/website_item_indexing.py @@ -99,8 +99,6 @@ 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 = frappe.cache() @@ -111,9 +109,16 @@ def delete_item_from_index(website_item_doc): except: return False - # TODO: Also delete autocomplete suggestion + delete_from_ac_dict(website_item_doc) + return True +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) + name_ac.delete(website_item_doc.web_item_name) + def define_autocomplete_dictionary(): """Creates an autocomplete search dictionary for `name`. Also creats autocomplete dictionary for `categories` if @@ -163,7 +168,6 @@ def reindex_all_web_items(): for k, v in web_item.items(): super(RedisWrapper, r).hset(key, k, v) - def get_cache_key(name): name = frappe.scrub(name) return f"{WEBSITE_ITEM_KEY_PREFIX}{name}" diff --git a/erpnext/www/all-products/search.css b/erpnext/www/all-products/search.css index ff0ca6ac9fe..687532d2eb4 100644 --- a/erpnext/www/all-products/search.css +++ b/erpnext/www/all-products/search.css @@ -2,4 +2,8 @@ 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 index c6c87089e80..a86a9c0e133 100644 --- a/erpnext/www/all-products/search.html +++ b/erpnext/www/all-products/search.html @@ -26,7 +26,6 @@
      {% set show_categories = frappe.db.get_single_value('E Commerce Settings', 'show_categories_in_search_autocomplete') %} - {% if show_categories %}

      Categories

      @@ -34,6 +33,11 @@
      {% endif %} + + {% set show_brand_line = frappe.db.get_single_value('E Commerce Settings', 'show_brand_line') %} + {% if show_brand_line %} + + {% endif %} {% endblock %} \ No newline at end of file diff --git a/erpnext/www/all-products/search.js b/erpnext/www/all-products/search.js index 9ed5b21099e..c97e48b8db0 100644 --- a/erpnext/www/all-products/search.js +++ b/erpnext/www/all-products/search.js @@ -3,13 +3,14 @@ let loading = false; const searchBox = document.getElementById("search-box"); const results = document.getElementById("results"); const categoryList = document.getElementById("category-suggestions"); +const showBrandLine = document.getElementById("show-brand-line"); function populateResults(data) { html = "" for (let res of data.message) { html += `
    • - ${res.web_item_name} + ${res.web_item_name} ${showBrandLine && res.brand ? "by " + res.brand : ""}
    • ` } results.innerHTML = html; From 1ebb1b822c5ea8123c62a014c873cff1acbe2e59 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Wed, 5 May 2021 16:04:22 +0530 Subject: [PATCH 12/18] feat: Show Recent Searches --- erpnext/templates/pages/product_search.py | 2 +- erpnext/www/all-products/search.html | 5 +- erpnext/www/all-products/search.js | 65 +++++++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index 99d420de19b..6a75edb2f71 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -71,7 +71,7 @@ def search(query): ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=red) client = Client(make_key(WEBSITE_ITEM_INDEX), conn=red) - suggestions = ac.get_suggestions(query, num=10) + suggestions = ac.get_suggestions(query, num=10, fuzzy=len(query) > 4) # Build a query query_string = query diff --git a/erpnext/www/all-products/search.html b/erpnext/www/all-products/search.html index a86a9c0e133..735822d7659 100644 --- a/erpnext/www/all-products/search.html +++ b/erpnext/www/all-products/search.html @@ -14,10 +14,13 @@
      - +
      + +
      +
      diff --git a/erpnext/www/all-products/search.js b/erpnext/www/all-products/search.js index c97e48b8db0..cb3f9afe9b2 100644 --- a/erpnext/www/all-products/search.js +++ b/erpnext/www/all-products/search.js @@ -1,9 +1,48 @@ 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 += ``; + } + + recentSearchArea.innerHTML = html; + attachEventListenersToChips(); +} function populateResults(data) { html = "" @@ -64,3 +103,29 @@ searchBox.addEventListener("input", (e) => { } }); +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 From 7a278208c8ae1f6614a5ca5d94977be41fea5986 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 11 May 2021 13:00:41 +0530 Subject: [PATCH 13/18] chores: Add function params and remove unused imports --- erpnext/e_commerce/website_item_indexing.py | 7 ++----- erpnext/templates/pages/product_search.py | 14 +++++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/erpnext/e_commerce/website_item_indexing.py b/erpnext/e_commerce/website_item_indexing.py index 8c0e074dc3e..134939cdbb6 100644 --- a/erpnext/e_commerce/website_item_indexing.py +++ b/erpnext/e_commerce/website_item_indexing.py @@ -1,16 +1,13 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals - import frappe from frappe.utils.redis_wrapper import RedisWrapper from redisearch import ( - Client, AutoCompleter, Query, + Client, AutoCompleter, Suggestion, IndexDefinition, - TextField, TagField, - Document + TextField, TagField ) def make_key(key): diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index 6a75edb2f71..dace896c03a 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -1,7 +1,6 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals import frappe from frappe.utils import cstr, nowdate, cint from erpnext.setup.doctype.item_group.item_group import get_item_for_list_in_html @@ -60,9 +59,9 @@ 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): +def search(query, limit=10, fuzzy_search=True): if not query: - # TODO: return top/recent searches + # TODO: return top searches return [] red = frappe.cache() @@ -71,7 +70,11 @@ def search(query): ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=red) client = Client(make_key(WEBSITE_ITEM_INDEX), conn=red) - suggestions = ac.get_suggestions(query, num=10, fuzzy=len(query) > 4) + suggestions = ac.get_suggestions( + query, + num=limit, + fuzzy= fuzzy_search and len(query) > 4 # Fuzzy on length < 3 can be real slow + ) # Build a query query_string = query @@ -86,6 +89,7 @@ def search(query): results = client.search(q) results = list(map(convert_to_dict, results.docs)) + # FOR DEBUGGING print("SEARCH RESULTS ------------------\n ", results) return results @@ -99,7 +103,7 @@ def convert_to_dict(redis_search_doc): @frappe.whitelist(allow_guest=True) def get_category_suggestions(query): if not query: - # TODO: return top/recent searches + # TODO: return top searches return [] ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=frappe.cache()) From 9499db8cd9553f38573775551ac96649d16ed131 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Wed, 26 May 2021 20:26:34 +0530 Subject: [PATCH 14/18] feat: Add decorator for redisearch --- .../e_commerce_settings.json | 20 +++++++++++-- erpnext/e_commerce/website_item_indexing.py | 30 ++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json index 36177ff8f91..94560eb482a 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json @@ -48,7 +48,9 @@ "item_search_settings_section", "search_index_fields", "show_categories_in_search_autocomplete", - "show_brand_line" + "show_brand_line", + "is_redisearch_loaded", + "redisearch" ], "fields": [ { @@ -328,12 +330,26 @@ "fieldname": "show_brand_line", "fieldtype": "Check", "label": "Show Brand Line" + }, + { + "default": "0", + "fieldname": "is_redisearch_loaded", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Redisearch Loaded" + }, + { + "depends_on": "eval:doc.is_search_module_loaded", + "fieldname": "redisearch", + "fieldtype": "Heading", + "label": "Redisearch", + "options": "Some heading" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-05-05 13:41:11.483232", + "modified": "2021-05-26 20:00:35.399936", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", diff --git a/erpnext/e_commerce/website_item_indexing.py b/erpnext/e_commerce/website_item_indexing.py index 134939cdbb6..3b82a324395 100644 --- a/erpnext/e_commerce/website_item_indexing.py +++ b/erpnext/e_commerce/website_item_indexing.py @@ -10,6 +10,26 @@ from redisearch import ( 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') @@ -28,6 +48,7 @@ ALLOWED_INDEXABLE_FIELDS_SET = { 'web_long_description' } +@redisearch_decorator def create_website_items_index(): '''Creates Index Definition''' # CREATE index @@ -67,6 +88,7 @@ def to_search_field(field): return TextField(field) +@redisearch_decorator def insert_item_to_index(website_item_doc): # Insert item to index key = get_cache_key(website_item_doc.name) @@ -78,6 +100,7 @@ def insert_item_to_index(website_item_doc): insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name) +@redisearch_decorator 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)) @@ -91,12 +114,14 @@ def create_web_item_map(website_item_doc): web_item[f] = website_item_doc.get(f) or '' return web_item - + +@redisearch_decorator def update_index_for_item(website_item_doc): # Reinsert to Cache insert_item_to_index(website_item_doc) define_autocomplete_dictionary() +@redisearch_decorator def delete_item_from_index(website_item_doc): r = frappe.cache() key = get_cache_key(website_item_doc.name) @@ -110,12 +135,14 @@ def delete_item_from_index(website_item_doc): return True +@redisearch_decorator 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) name_ac.delete(website_item_doc.web_item_name) +@redisearch_decorator def define_autocomplete_dictionary(): """Creates an autocomplete search dictionary for `name`. Also creats autocomplete dictionary for `categories` if @@ -150,6 +177,7 @@ def define_autocomplete_dictionary(): return True +@redisearch_decorator def reindex_all_web_items(): items = frappe.get_all( 'Website Item', From 50d7989e503753797c4064b7ef945b9a9f021b7a Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Wed, 26 May 2021 22:43:22 +0530 Subject: [PATCH 15/18] chore: Redisearch module load tracking --- .../e_commerce_settings/e_commerce_settings.json | 14 +++++++------- .../e_commerce_settings/e_commerce_settings.py | 3 ++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json index 94560eb482a..8b54c665c71 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json @@ -46,11 +46,11 @@ "shop_by_category_section", "slideshow", "item_search_settings_section", + "redisearch_warning", "search_index_fields", "show_categories_in_search_autocomplete", "show_brand_line", - "is_redisearch_loaded", - "redisearch" + "is_redisearch_loaded" ], "fields": [ { @@ -339,17 +339,17 @@ "label": "Is Redisearch Loaded" }, { - "depends_on": "eval:doc.is_search_module_loaded", - "fieldname": "redisearch", + "depends_on": "eval:!doc.is_redisearch_loaded", + "fieldname": "redisearch_warning", "fieldtype": "Heading", - "label": "Redisearch", - "options": "Some heading" + "label": "Redisearch Warning", + "options": "Redisearch is not loaded. Please install the redisearch module to enable more robust search features." } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-05-26 20:00:35.399936", + "modified": "2021-05-26 22:41:34.400589", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", 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 d8a05d6a31b..a795a11b19f 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 @@ -8,13 +8,14 @@ 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 erpnext.e_commerce.website_item_indexing import create_website_items_index, ALLOWED_INDEXABLE_FIELDS_SET +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 class ECommerceSettings(Document): def onload(self): self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series") + self.is_redisearch_loaded = is_search_module_loaded() def validate(self): if self.home_page_is_products: From 6799eb98db45b4b1f1b442853219912a8a323f7a Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Fri, 28 May 2021 06:25:49 +0530 Subject: [PATCH 16/18] feat: Add fallback when redisearch not loaded --- erpnext/templates/pages/product_search.py | 49 +++++++++++++++-------- erpnext/www/all-products/search.js | 25 ++++++++++-- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index dace896c03a..df8ba075946 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -9,6 +9,7 @@ from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_w # For SEARCH ------- from redisearch import AutoCompleter, Client, Query from erpnext.e_commerce.website_item_indexing import ( + is_search_module_loaded, WEBSITE_ITEM_INDEX, WEBSITE_ITEM_NAME_AUTOCOMPLETE, WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, @@ -24,8 +25,15 @@ def get_context(context): @frappe.whitelist(allow_guest=True) def get_product_list(search=None, start=0, limit=12): - # limit = 12 because we show 12 items in the grid view + data = get_product_data(search, start, limit) + for item in data: + set_product_info_for_website(item) + + return [get_item_for_list_in_html(r) for r in data] + +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, @@ -48,24 +56,25 @@ def get_product_list(search=None, start=0, limit=12): # order by query += """ order by I.weightage desc, in_stock desc, I.modified desc limit %s, %s""" % (cint(start), cint(limit)) - data = frappe.db.sql(query, { + return frappe.db.sql(query, { "search": search, "today": nowdate() }, as_dict=1) - for item in data: - 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, limit=10, fuzzy_search=True): + search_results = {"from_redisearch": True, "results": []} + + if not is_search_module_loaded(): + # Redisearch module not loaded + search_results["from_redisearch"] = False + search_results["results"] = get_product_data(query, 0, limit) + return search_results + if not query: - # TODO: return top searches - return [] + return search_results red = frappe.cache() - query = clean_up_query(query) ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=red) @@ -87,12 +96,12 @@ def search(query, limit=10, fuzzy_search=True): print(f"Executing query: {q.query_string()}") results = client.search(q) - results = list(map(convert_to_dict, results.docs)) + search_results['results'] = list(map(convert_to_dict, results.docs)) # FOR DEBUGGING - print("SEARCH RESULTS ------------------\n ", results) + print("SEARCH RESULTS ------------------\n ", search_results) - return results + return search_results def clean_up_query(query): return ''.join(c for c in query if c.isalnum() or c.isspace()) @@ -102,11 +111,19 @@ def convert_to_dict(redis_search_doc): @frappe.whitelist(allow_guest=True) def get_category_suggestions(query): + search_results = {"from_redisearch": True, "results": []} + + if not is_search_module_loaded(): + # Redisearch module not loaded + search_results["from_redisearch"] = False + return search_results + if not query: - # TODO: return top searches - return [] + return search_results ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=frappe.cache()) suggestions = ac.get_suggestions(query, num=10) - return [s.string for s in suggestions] \ No newline at end of file + search_results['results'] = [s.string for s in suggestions] + + return search_results \ No newline at end of file diff --git a/erpnext/www/all-products/search.js b/erpnext/www/all-products/search.js index cb3f9afe9b2..e88b576c789 100644 --- a/erpnext/www/all-products/search.js +++ b/erpnext/www/all-products/search.js @@ -45,8 +45,18 @@ function populateRecentSearches() { } function populateResults(data) { + if (!data.message.from_redisearch) { + // Data not from redisearch + } + + if (data.message.results.length === 0) { + results.innerHTML = 'No results'; + return; + } + html = "" - for (let res of data.message) { + search_results = data.message.results + for (let res of search_results) { html += `
    • ${res.web_item_name} ${showBrandLine && res.brand ? "by " + res.brand : ""} @@ -56,13 +66,20 @@ function populateResults(data) { } function populateCategoriesList(data) { - if (data.length === 0) { - categoryList.innerHTML = 'Type something...'; + 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 = "" - for (let category of data.message) { + search_results = data.message.results + for (let category of search_results) { html += `
    • ${category}
    • ` } From 8a25523ad77e8fc17e646047fee1aa954a6d0cc4 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Fri, 28 May 2021 06:36:16 +0530 Subject: [PATCH 17/18] chore: Redisearch warning in search settings --- .../e_commerce_settings/e_commerce_settings.json | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json index 8b54c665c71..347a1c5ffb2 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json @@ -310,7 +310,8 @@ { "fieldname": "search_index_fields", "fieldtype": "Small Text", - "label": "Search Index Fields" + "label": "Search Index Fields", + "read_only_depends_on": "eval:!doc.is_redisearch_loaded" }, { "collapsible": 1, @@ -322,14 +323,16 @@ "default": "1", "fieldname": "show_categories_in_search_autocomplete", "fieldtype": "Check", - "label": "Show Categories in Search Autocomplete" + "label": "Show Categories in Search Autocomplete", + "read_only_depends_on": "eval:!doc.is_redisearch_loaded" }, { "default": "0", "description": "e.g. \"iPhone 12 by Apple\"", "fieldname": "show_brand_line", "fieldtype": "Check", - "label": "Show Brand Line" + "label": "Show Brand Line", + "read_only_depends_on": "eval:!doc.is_redisearch_loaded" }, { "default": "0", @@ -341,15 +344,15 @@ { "depends_on": "eval:!doc.is_redisearch_loaded", "fieldname": "redisearch_warning", - "fieldtype": "Heading", + "fieldtype": "HTML", "label": "Redisearch Warning", - "options": "Redisearch is not loaded. Please install the redisearch module to enable more robust search features." + "options": "

      Redisearch module not loaded. If you want to use advanced product search features, refer documentation here.

      " } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-05-26 22:41:34.400589", + "modified": "2021-05-28 06:34:05.647427", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", From fd3ce1b573598a86dcc4d448f5f1e87ac2a89fff Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Fri, 28 May 2021 07:10:24 +0530 Subject: [PATCH 18/18] fix: Documentation link and open in new tab --- .../doctype/e_commerce_settings/e_commerce_settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json index 347a1c5ffb2..627677481aa 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json @@ -346,13 +346,13 @@ "fieldname": "redisearch_warning", "fieldtype": "HTML", "label": "Redisearch Warning", - "options": "

      Redisearch module not loaded. If you want to use advanced product search features, refer documentation here.

      " + "options": "

      Redisearch module not loaded. If you want to use advanced product search features, refer documentation here.

      " } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-05-28 06:34:05.647427", + "modified": "2021-05-28 07:09:32.639710", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings",