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