marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 1 | # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 2 | # License: GNU General Public License v3. See license.txt |
| 3 | |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 4 | import frappe |
Hussain Nagaria | ffc8616 | 2021-04-29 20:47:32 +0530 | [diff] [blame] | 5 | from frappe.utils.redis_wrapper import RedisWrapper |
marination | 9fb61ef | 2022-02-01 00:39:14 +0530 | [diff] [blame] | 6 | from redisearch import AutoCompleter, Client, IndexDefinition, Suggestion, TagField, TextField |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 7 | |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 8 | WEBSITE_ITEM_INDEX = 'website_items_index' |
| 9 | WEBSITE_ITEM_KEY_PREFIX = 'website_item:' |
| 10 | WEBSITE_ITEM_NAME_AUTOCOMPLETE = 'website_items_name_dict' |
Hussain Nagaria | f342155 | 2021-04-26 11:15:20 +0530 | [diff] [blame] | 11 | WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE = 'website_items_category_dict' |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 12 | |
marination | bbcbcf7 | 2021-09-02 14:07:59 +0530 | [diff] [blame] | 13 | def get_indexable_web_fields(): |
| 14 | "Return valid fields from Website Item that can be searched for." |
| 15 | web_item_meta = frappe.get_meta("Website Item", cached=True) |
| 16 | valid_fields = filter( |
| 17 | lambda df: df.fieldtype in ("Link", "Table MultiSelect", "Data", "Small Text", "Text Editor"), |
| 18 | web_item_meta.fields) |
| 19 | |
| 20 | return [df.fieldname for df in valid_fields] |
| 21 | |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 22 | def is_search_module_loaded(): |
Marica | d637f79 | 2021-09-18 14:23:44 +0530 | [diff] [blame] | 23 | try: |
| 24 | cache = frappe.cache() |
| 25 | out = cache.execute_command('MODULE LIST') |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 26 | |
Marica | d637f79 | 2021-09-18 14:23:44 +0530 | [diff] [blame] | 27 | parsed_output = " ".join( |
| 28 | (" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out) |
| 29 | ) |
| 30 | return "search" in parsed_output |
| 31 | except Exception: |
| 32 | return False |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 33 | |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 34 | def if_redisearch_loaded(function): |
marination | bbcbcf7 | 2021-09-02 14:07:59 +0530 | [diff] [blame] | 35 | "Decorator to check if Redisearch is loaded." |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 36 | def wrapper(*args, **kwargs): |
| 37 | if is_search_module_loaded(): |
| 38 | func = function(*args, **kwargs) |
| 39 | return func |
| 40 | return |
| 41 | |
| 42 | return wrapper |
| 43 | |
| 44 | def make_key(key): |
| 45 | return "{0}|{1}".format(frappe.conf.db_name, key).encode('utf-8') |
| 46 | |
| 47 | @if_redisearch_loaded |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 48 | def create_website_items_index(): |
marination | bbcbcf7 | 2021-09-02 14:07:59 +0530 | [diff] [blame] | 49 | "Creates Index Definition." |
| 50 | |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 51 | # CREATE index |
Hussain Nagaria | ffc8616 | 2021-04-29 20:47:32 +0530 | [diff] [blame] | 52 | client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache()) |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 53 | |
| 54 | # DROP if already exists |
| 55 | try: |
| 56 | client.drop_index() |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 57 | except Exception: |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 58 | pass |
| 59 | |
Hussain Nagaria | ffc8616 | 2021-04-29 20:47:32 +0530 | [diff] [blame] | 60 | idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)]) |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 61 | |
| 62 | # Based on e-commerce settings |
| 63 | idx_fields = frappe.db.get_single_value( |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 64 | 'E Commerce Settings', |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 65 | 'search_index_fields' |
marination | 24ba06c | 2021-06-08 14:47:11 +0530 | [diff] [blame] | 66 | ) |
| 67 | idx_fields = idx_fields.split(',') if idx_fields else [] |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 68 | |
| 69 | if 'web_item_name' in idx_fields: |
| 70 | idx_fields.remove('web_item_name') |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 71 | |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 72 | idx_fields = list(map(to_search_field, idx_fields)) |
| 73 | |
| 74 | client.create_index( |
| 75 | [TextField("web_item_name", sortable=True)] + idx_fields, |
Hussain Nagaria | ffc8616 | 2021-04-29 20:47:32 +0530 | [diff] [blame] | 76 | definition=idx_def, |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 77 | ) |
| 78 | |
| 79 | reindex_all_web_items() |
| 80 | define_autocomplete_dictionary() |
| 81 | |
| 82 | def to_search_field(field): |
| 83 | if field == "tags": |
| 84 | return TagField("tags", separator=",") |
| 85 | |
| 86 | return TextField(field) |
| 87 | |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 88 | @if_redisearch_loaded |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 89 | def insert_item_to_index(website_item_doc): |
| 90 | # Insert item to index |
| 91 | key = get_cache_key(website_item_doc.name) |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 92 | cache = frappe.cache() |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 93 | web_item = create_web_item_map(website_item_doc) |
Hussain Nagaria | ffc8616 | 2021-04-29 20:47:32 +0530 | [diff] [blame] | 94 | |
| 95 | for k, v in web_item.items(): |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 96 | super(RedisWrapper, cache).hset(make_key(key), k, v) |
Hussain Nagaria | ffc8616 | 2021-04-29 20:47:32 +0530 | [diff] [blame] | 97 | |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 98 | insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name) |
| 99 | |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 100 | @if_redisearch_loaded |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 101 | def insert_to_name_ac(web_name, doc_name): |
Hussain Nagaria | ffc8616 | 2021-04-29 20:47:32 +0530 | [diff] [blame] | 102 | ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache()) |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 103 | ac.add_suggestions(Suggestion(web_name, payload=doc_name)) |
| 104 | |
| 105 | def create_web_item_map(website_item_doc): |
| 106 | fields_to_index = get_fields_indexed() |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 107 | web_item = {} |
| 108 | |
| 109 | for f in fields_to_index: |
| 110 | web_item[f] = website_item_doc.get(f) or '' |
| 111 | |
| 112 | return web_item |
Hussain Nagaria | bd0d0ad | 2021-05-26 20:26:34 +0530 | [diff] [blame] | 113 | |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 114 | @if_redisearch_loaded |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 115 | def update_index_for_item(website_item_doc): |
| 116 | # Reinsert to Cache |
| 117 | insert_item_to_index(website_item_doc) |
| 118 | define_autocomplete_dictionary() |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 119 | |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 120 | @if_redisearch_loaded |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 121 | def delete_item_from_index(website_item_doc): |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 122 | cache = frappe.cache() |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 123 | key = get_cache_key(website_item_doc.name) |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 124 | |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 125 | try: |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 126 | cache.delete(key) |
marination | ea30ce4 | 2021-06-02 13:24:06 +0530 | [diff] [blame] | 127 | except Exception: |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 128 | return False |
| 129 | |
Hussain Nagaria | 34f3a66 | 2021-05-05 13:47:43 +0530 | [diff] [blame] | 130 | delete_from_ac_dict(website_item_doc) |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 131 | return True |
| 132 | |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 133 | @if_redisearch_loaded |
Hussain Nagaria | 34f3a66 | 2021-05-05 13:47:43 +0530 | [diff] [blame] | 134 | def delete_from_ac_dict(website_item_doc): |
| 135 | '''Removes this items's name from autocomplete dictionary''' |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 136 | cache = frappe.cache() |
| 137 | name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache) |
Hussain Nagaria | 34f3a66 | 2021-05-05 13:47:43 +0530 | [diff] [blame] | 138 | name_ac.delete(website_item_doc.web_item_name) |
| 139 | |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 140 | @if_redisearch_loaded |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 141 | def define_autocomplete_dictionary(): |
Hussain Nagaria | f342155 | 2021-04-26 11:15:20 +0530 | [diff] [blame] | 142 | """Creates an autocomplete search dictionary for `name`. |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 143 | Also creats autocomplete dictionary for `categories` if |
| 144 | checked in E Commerce Settings""" |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 145 | |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 146 | cache = frappe.cache() |
| 147 | name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache) |
| 148 | cat_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache) |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 149 | |
Hussain Nagaria | f342155 | 2021-04-26 11:15:20 +0530 | [diff] [blame] | 150 | ac_categories = frappe.db.get_single_value( |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 151 | 'E Commerce Settings', |
Hussain Nagaria | f342155 | 2021-04-26 11:15:20 +0530 | [diff] [blame] | 152 | 'show_categories_in_search_autocomplete' |
| 153 | ) |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 154 | |
Hussain Nagaria | f342155 | 2021-04-26 11:15:20 +0530 | [diff] [blame] | 155 | # Delete both autocomplete dicts |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 156 | try: |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 157 | cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE)) |
| 158 | cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE)) |
marination | ea30ce4 | 2021-06-02 13:24:06 +0530 | [diff] [blame] | 159 | except Exception: |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 160 | return False |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 161 | |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 162 | items = frappe.get_all( |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 163 | 'Website Item', |
| 164 | fields=['web_item_name', 'item_group'], |
| 165 | filters={"published": 1} |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 166 | ) |
| 167 | |
| 168 | for item in items: |
Hussain Nagaria | f342155 | 2021-04-26 11:15:20 +0530 | [diff] [blame] | 169 | name_ac.add_suggestions(Suggestion(item.web_item_name)) |
| 170 | if ac_categories and item.item_group: |
| 171 | cat_ac.add_suggestions(Suggestion(item.item_group)) |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 172 | |
| 173 | return True |
| 174 | |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 175 | @if_redisearch_loaded |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 176 | def reindex_all_web_items(): |
| 177 | items = frappe.get_all( |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 178 | 'Website Item', |
| 179 | fields=get_fields_indexed(), |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 180 | filters={"published": True} |
| 181 | ) |
| 182 | |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 183 | cache = frappe.cache() |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 184 | for item in items: |
| 185 | web_item = create_web_item_map(item) |
Hussain Nagaria | ffc8616 | 2021-04-29 20:47:32 +0530 | [diff] [blame] | 186 | key = make_key(get_cache_key(item.name)) |
| 187 | |
| 188 | for k, v in web_item.items(): |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 189 | super(RedisWrapper, cache).hset(key, k, v) |
Hussain Nagaria | ffc8616 | 2021-04-29 20:47:32 +0530 | [diff] [blame] | 190 | |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 191 | def get_cache_key(name): |
| 192 | name = frappe.scrub(name) |
| 193 | return f"{WEBSITE_ITEM_KEY_PREFIX}{name}" |
| 194 | |
| 195 | def get_fields_indexed(): |
| 196 | fields_to_index = frappe.db.get_single_value( |
marination | b0d7e32 | 2021-06-01 12:44:49 +0530 | [diff] [blame] | 197 | 'E Commerce Settings', |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 198 | 'search_index_fields' |
marination | 24ba06c | 2021-06-08 14:47:11 +0530 | [diff] [blame] | 199 | ) |
| 200 | fields_to_index = fields_to_index.split(',') if fields_to_index else [] |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 201 | |
marination | bbcbcf7 | 2021-09-02 14:07:59 +0530 | [diff] [blame] | 202 | mandatory_fields = ['name', 'web_item_name', 'route', 'thumbnail', 'ranking'] |
Hussain Nagaria | 8e55c95 | 2021-04-26 07:01:06 +0530 | [diff] [blame] | 203 | fields_to_index = fields_to_index + mandatory_fields |
| 204 | |
| 205 | return fields_to_index |
| 206 | |
| 207 | # TODO: Remove later |
| 208 | # # Figure out a way to run this at startup |
| 209 | define_autocomplete_dictionary() |
| 210 | create_website_items_index() |