| # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors |
| # License: GNU General Public License v3. See license.txt |
| |
| import frappe |
| from frappe.utils.redis_wrapper import RedisWrapper |
| from redisearch import AutoCompleter, Client, IndexDefinition, Suggestion, TagField, TextField |
| |
| 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' |
| |
| def get_indexable_web_fields(): |
| "Return valid fields from Website Item that can be searched for." |
| web_item_meta = frappe.get_meta("Website Item", cached=True) |
| valid_fields = filter( |
| lambda df: df.fieldtype in ("Link", "Table MultiSelect", "Data", "Small Text", "Text Editor"), |
| web_item_meta.fields) |
| |
| return [df.fieldname for df in valid_fields] |
| |
| def is_search_module_loaded(): |
| try: |
| 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 |
| except Exception: |
| return False |
| |
| def if_redisearch_loaded(function): |
| "Decorator to check if Redisearch is loaded." |
| def wrapper(*args, **kwargs): |
| if is_search_module_loaded(): |
| func = function(*args, **kwargs) |
| return func |
| return |
| |
| return wrapper |
| |
| def make_key(key): |
| return "{0}|{1}".format(frappe.conf.db_name, key).encode('utf-8') |
| |
| @if_redisearch_loaded |
| def create_website_items_index(): |
| "Creates Index Definition." |
| |
| # CREATE index |
| client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache()) |
| |
| # DROP if already exists |
| try: |
| client.drop_index() |
| except Exception: |
| pass |
| |
| idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)]) |
| |
| # Based on e-commerce settings |
| idx_fields = frappe.db.get_single_value( |
| 'E Commerce Settings', |
| 'search_index_fields' |
| ) |
| idx_fields = idx_fields.split(',') if idx_fields else [] |
| |
| 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) |
| |
| @if_redisearch_loaded |
| def insert_item_to_index(website_item_doc): |
| # Insert item to index |
| key = get_cache_key(website_item_doc.name) |
| cache = frappe.cache() |
| web_item = create_web_item_map(website_item_doc) |
| |
| for k, v in web_item.items(): |
| super(RedisWrapper, cache).hset(make_key(key), k, v) |
| |
| insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name) |
| |
| @if_redisearch_loaded |
| def insert_to_name_ac(web_name, doc_name): |
| ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache()) |
| ac.add_suggestions(Suggestion(web_name, payload=doc_name)) |
| |
| def create_web_item_map(website_item_doc): |
| fields_to_index = get_fields_indexed() |
| web_item = {} |
| |
| for f in fields_to_index: |
| web_item[f] = website_item_doc.get(f) or '' |
| |
| return web_item |
| |
| @if_redisearch_loaded |
| def update_index_for_item(website_item_doc): |
| # Reinsert to Cache |
| insert_item_to_index(website_item_doc) |
| define_autocomplete_dictionary() |
| |
| @if_redisearch_loaded |
| def delete_item_from_index(website_item_doc): |
| cache = frappe.cache() |
| key = get_cache_key(website_item_doc.name) |
| |
| try: |
| cache.delete(key) |
| except Exception: |
| return False |
| |
| delete_from_ac_dict(website_item_doc) |
| return True |
| |
| @if_redisearch_loaded |
| def delete_from_ac_dict(website_item_doc): |
| '''Removes this items's name from autocomplete dictionary''' |
| cache = frappe.cache() |
| name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache) |
| name_ac.delete(website_item_doc.web_item_name) |
| |
| @if_redisearch_loaded |
| def define_autocomplete_dictionary(): |
| """Creates an autocomplete search dictionary for `name`. |
| Also creats autocomplete dictionary for `categories` if |
| checked in E Commerce Settings""" |
| |
| cache = frappe.cache() |
| name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache) |
| cat_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache) |
| |
| ac_categories = frappe.db.get_single_value( |
| 'E Commerce Settings', |
| 'show_categories_in_search_autocomplete' |
| ) |
| |
| # Delete both autocomplete dicts |
| try: |
| cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE)) |
| cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE)) |
| except Exception: |
| return False |
| |
| items = frappe.get_all( |
| 'Website Item', |
| fields=['web_item_name', 'item_group'], |
| filters={"published": 1} |
| ) |
| |
| for item in items: |
| 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 |
| |
| @if_redisearch_loaded |
| def reindex_all_web_items(): |
| items = frappe.get_all( |
| 'Website Item', |
| fields=get_fields_indexed(), |
| filters={"published": True} |
| ) |
| |
| cache = frappe.cache() |
| for item in items: |
| web_item = create_web_item_map(item) |
| key = make_key(get_cache_key(item.name)) |
| |
| for k, v in web_item.items(): |
| super(RedisWrapper, cache).hset(key, k, v) |
| |
| 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' |
| ) |
| fields_to_index = fields_to_index.split(',') if fields_to_index else [] |
| |
| mandatory_fields = ['name', 'web_item_name', 'route', 'thumbnail', 'ranking'] |
| 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() |