blob: 61b4b9ee1f421fc74420dadb55abdaee49c3630d [file] [log] [blame]
marinationea036e42022-04-04 11:07:53 +05301# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
Hussain Nagaria8e55c952021-04-26 07:01:06 +05302# License: GNU General Public License v3. See license.txt
3
marination97e3a852022-04-04 11:32:49 +05304import json
5
Hussain Nagaria8e55c952021-04-26 07:01:06 +05306import frappe
marinationea036e42022-04-04 11:07:53 +05307from frappe import _
Hussain Nagariaffc86162021-04-29 20:47:32 +05308from frappe.utils.redis_wrapper import RedisWrapper
marinationea036e42022-04-04 11:07:53 +05309from redis import ResponseError
marination9fb61ef2022-02-01 00:39:14 +053010from redisearch import AutoCompleter, Client, IndexDefinition, Suggestion, TagField, TextField
Hussain Nagaria8e55c952021-04-26 07:01:06 +053011
Ankush Menat494bd9e2022-03-28 18:52:46 +053012WEBSITE_ITEM_INDEX = "website_items_index"
13WEBSITE_ITEM_KEY_PREFIX = "website_item:"
14WEBSITE_ITEM_NAME_AUTOCOMPLETE = "website_items_name_dict"
15WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE = "website_items_category_dict"
16
Hussain Nagaria8e55c952021-04-26 07:01:06 +053017
marinationbbcbcf72021-09-02 14:07:59 +053018def get_indexable_web_fields():
19 "Return valid fields from Website Item that can be searched for."
20 web_item_meta = frappe.get_meta("Website Item", cached=True)
21 valid_fields = filter(
22 lambda df: df.fieldtype in ("Link", "Table MultiSelect", "Data", "Small Text", "Text Editor"),
Ankush Menat494bd9e2022-03-28 18:52:46 +053023 web_item_meta.fields,
24 )
marinationbbcbcf72021-09-02 14:07:59 +053025
26 return [df.fieldname for df in valid_fields]
27
Ankush Menat494bd9e2022-03-28 18:52:46 +053028
marination7e207c82022-03-31 16:29:18 +053029def is_redisearch_enabled():
30 "Return True only if redisearch is loaded and enabled."
31 is_redisearch_enabled = frappe.db.get_single_value("E Commerce Settings", "is_redisearch_enabled")
32 return is_search_module_loaded() and is_redisearch_enabled
33
34
marinationb0d7e322021-06-01 12:44:49 +053035def is_search_module_loaded():
Maricad637f792021-09-18 14:23:44 +053036 try:
37 cache = frappe.cache()
Ankush Menat494bd9e2022-03-28 18:52:46 +053038 out = cache.execute_command("MODULE LIST")
marinationb0d7e322021-06-01 12:44:49 +053039
Maricad637f792021-09-18 14:23:44 +053040 parsed_output = " ".join(
41 (" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out)
42 )
43 return "search" in parsed_output
44 except Exception:
marinationea036e42022-04-04 11:07:53 +053045 return False # handling older redis versions
marinationb0d7e322021-06-01 12:44:49 +053046
Ankush Menat494bd9e2022-03-28 18:52:46 +053047
marination7e207c82022-03-31 16:29:18 +053048def if_redisearch_enabled(function):
49 "Decorator to check if Redisearch is enabled."
Ankush Menat494bd9e2022-03-28 18:52:46 +053050
marinationb0d7e322021-06-01 12:44:49 +053051 def wrapper(*args, **kwargs):
marination7e207c82022-03-31 16:29:18 +053052 if is_redisearch_enabled():
marinationb0d7e322021-06-01 12:44:49 +053053 func = function(*args, **kwargs)
54 return func
55 return
56
57 return wrapper
58
Ankush Menat494bd9e2022-03-28 18:52:46 +053059
marinationb0d7e322021-06-01 12:44:49 +053060def make_key(key):
Ankush Menat494bd9e2022-03-28 18:52:46 +053061 return "{0}|{1}".format(frappe.conf.db_name, key).encode("utf-8")
62
marinationb0d7e322021-06-01 12:44:49 +053063
marination7e207c82022-03-31 16:29:18 +053064@if_redisearch_enabled
Hussain Nagaria8e55c952021-04-26 07:01:06 +053065def create_website_items_index():
marinationbbcbcf72021-09-02 14:07:59 +053066 "Creates Index Definition."
67
Hussain Nagaria8e55c952021-04-26 07:01:06 +053068 # CREATE index
Hussain Nagariaffc86162021-04-29 20:47:32 +053069 client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache())
Hussain Nagaria8e55c952021-04-26 07:01:06 +053070
Hussain Nagaria8e55c952021-04-26 07:01:06 +053071 try:
marinationea036e42022-04-04 11:07:53 +053072 client.drop_index() # drop if already exists
73 except ResponseError:
74 # will most likely raise a ResponseError if index does not exist
75 # ignore and create index
Hussain Nagaria8e55c952021-04-26 07:01:06 +053076 pass
marinationea036e42022-04-04 11:07:53 +053077 except Exception:
78 raise_redisearch_error()
Hussain Nagaria8e55c952021-04-26 07:01:06 +053079
Hussain Nagariaffc86162021-04-29 20:47:32 +053080 idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)])
Hussain Nagaria8e55c952021-04-26 07:01:06 +053081
marinationea036e42022-04-04 11:07:53 +053082 # Index fields mentioned in e-commerce settings
Ankush Menat494bd9e2022-03-28 18:52:46 +053083 idx_fields = frappe.db.get_single_value("E Commerce Settings", "search_index_fields")
84 idx_fields = idx_fields.split(",") if idx_fields else []
Hussain Nagaria8e55c952021-04-26 07:01:06 +053085
Ankush Menat494bd9e2022-03-28 18:52:46 +053086 if "web_item_name" in idx_fields:
87 idx_fields.remove("web_item_name")
marinationb0d7e322021-06-01 12:44:49 +053088
Hussain Nagaria8e55c952021-04-26 07:01:06 +053089 idx_fields = list(map(to_search_field, idx_fields))
90
91 client.create_index(
92 [TextField("web_item_name", sortable=True)] + idx_fields,
Hussain Nagariaffc86162021-04-29 20:47:32 +053093 definition=idx_def,
Hussain Nagaria8e55c952021-04-26 07:01:06 +053094 )
95
96 reindex_all_web_items()
97 define_autocomplete_dictionary()
98
Ankush Menat494bd9e2022-03-28 18:52:46 +053099
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530100def to_search_field(field):
101 if field == "tags":
102 return TagField("tags", separator=",")
103
104 return TextField(field)
105
Ankush Menat494bd9e2022-03-28 18:52:46 +0530106
marination7e207c82022-03-31 16:29:18 +0530107@if_redisearch_enabled
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530108def insert_item_to_index(website_item_doc):
109 # Insert item to index
110 key = get_cache_key(website_item_doc.name)
marinationb0d7e322021-06-01 12:44:49 +0530111 cache = frappe.cache()
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530112 web_item = create_web_item_map(website_item_doc)
Hussain Nagariaffc86162021-04-29 20:47:32 +0530113
marinationea036e42022-04-04 11:07:53 +0530114 for field, value in web_item.items():
115 super(RedisWrapper, cache).hset(make_key(key), field, value)
Hussain Nagariaffc86162021-04-29 20:47:32 +0530116
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530117 insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name)
118
Ankush Menat494bd9e2022-03-28 18:52:46 +0530119
marination7e207c82022-03-31 16:29:18 +0530120@if_redisearch_enabled
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530121def insert_to_name_ac(web_name, doc_name):
Hussain Nagariaffc86162021-04-29 20:47:32 +0530122 ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache())
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530123 ac.add_suggestions(Suggestion(web_name, payload=doc_name))
124
Ankush Menat494bd9e2022-03-28 18:52:46 +0530125
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530126def create_web_item_map(website_item_doc):
127 fields_to_index = get_fields_indexed()
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530128 web_item = {}
129
marinationea036e42022-04-04 11:07:53 +0530130 for field in fields_to_index:
131 web_item[field] = website_item_doc.get(field) or ""
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530132
133 return web_item
Hussain Nagariabd0d0ad2021-05-26 20:26:34 +0530134
Ankush Menat494bd9e2022-03-28 18:52:46 +0530135
marination7e207c82022-03-31 16:29:18 +0530136@if_redisearch_enabled
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530137def update_index_for_item(website_item_doc):
138 # Reinsert to Cache
139 insert_item_to_index(website_item_doc)
140 define_autocomplete_dictionary()
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530141
Ankush Menat494bd9e2022-03-28 18:52:46 +0530142
marination7e207c82022-03-31 16:29:18 +0530143@if_redisearch_enabled
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530144def delete_item_from_index(website_item_doc):
marinationb0d7e322021-06-01 12:44:49 +0530145 cache = frappe.cache()
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530146 key = get_cache_key(website_item_doc.name)
marinationb0d7e322021-06-01 12:44:49 +0530147
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530148 try:
marinationb0d7e322021-06-01 12:44:49 +0530149 cache.delete(key)
marinationea30ce42021-06-02 13:24:06 +0530150 except Exception:
marinationea036e42022-04-04 11:07:53 +0530151 raise_redisearch_error()
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530152
Hussain Nagaria34f3a662021-05-05 13:47:43 +0530153 delete_from_ac_dict(website_item_doc)
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530154 return True
155
Ankush Menat494bd9e2022-03-28 18:52:46 +0530156
marination7e207c82022-03-31 16:29:18 +0530157@if_redisearch_enabled
Hussain Nagaria34f3a662021-05-05 13:47:43 +0530158def delete_from_ac_dict(website_item_doc):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530159 """Removes this items's name from autocomplete dictionary"""
marinationb0d7e322021-06-01 12:44:49 +0530160 cache = frappe.cache()
161 name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
Hussain Nagaria34f3a662021-05-05 13:47:43 +0530162 name_ac.delete(website_item_doc.web_item_name)
163
Ankush Menat494bd9e2022-03-28 18:52:46 +0530164
marination7e207c82022-03-31 16:29:18 +0530165@if_redisearch_enabled
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530166def define_autocomplete_dictionary():
marination07f17452022-04-01 18:47:01 +0530167 """
168 Defines/Redefines an autocomplete search dictionary for Website Item Name.
169 Also creats autocomplete dictionary for Published Item Groups.
170 """
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530171
marinationb0d7e322021-06-01 12:44:49 +0530172 cache = frappe.cache()
marination07f17452022-04-01 18:47:01 +0530173 item_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
174 item_group_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache)
marinationb0d7e322021-06-01 12:44:49 +0530175
Hussain Nagariaf3421552021-04-26 11:15:20 +0530176 # Delete both autocomplete dicts
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530177 try:
marinationb0d7e322021-06-01 12:44:49 +0530178 cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE))
179 cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE))
marinationea30ce42021-06-02 13:24:06 +0530180 except Exception:
marinationea036e42022-04-04 11:07:53 +0530181 raise_redisearch_error()
marinationb0d7e322021-06-01 12:44:49 +0530182
marination07f17452022-04-01 18:47:01 +0530183 create_items_autocomplete_dict(autocompleter=item_ac)
184 create_item_groups_autocomplete_dict(autocompleter=item_group_ac)
185
186
187@if_redisearch_enabled
188def create_items_autocomplete_dict(autocompleter):
189 "Add items as suggestions in Autocompleter."
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530190 items = frappe.get_all(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530191 "Website Item", fields=["web_item_name", "item_group"], filters={"published": 1}
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530192 )
193
194 for item in items:
marination07f17452022-04-01 18:47:01 +0530195 autocompleter.add_suggestions(Suggestion(item.web_item_name))
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530196
marination07f17452022-04-01 18:47:01 +0530197
198@if_redisearch_enabled
199def create_item_groups_autocomplete_dict(autocompleter):
200 "Add item groups with weightage as suggestions in Autocompleter."
201 published_item_groups = frappe.get_all(
202 "Item Group", fields=["name", "route", "weightage"], filters={"show_in_website": 1}
203 )
204 if not published_item_groups:
205 return
206
207 for item_group in published_item_groups:
marination34456822022-04-04 12:33:25 +0530208 payload = json.dumps({"name": item_group.name, "route": item_group.route})
marination07f17452022-04-01 18:47:01 +0530209 autocompleter.add_suggestions(
210 Suggestion(
211 string=item_group.name,
marination7ef1ccb2022-04-04 12:04:35 +0530212 score=frappe.utils.flt(item_group.weightage) or 1.0,
marination07f17452022-04-01 18:47:01 +0530213 payload=payload, # additional info that can be retrieved later
214 )
215 )
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530216
Ankush Menat494bd9e2022-03-28 18:52:46 +0530217
marination7e207c82022-03-31 16:29:18 +0530218@if_redisearch_enabled
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530219def reindex_all_web_items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530220 items = frappe.get_all("Website Item", fields=get_fields_indexed(), filters={"published": True})
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530221
marinationb0d7e322021-06-01 12:44:49 +0530222 cache = frappe.cache()
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530223 for item in items:
224 web_item = create_web_item_map(item)
Hussain Nagariaffc86162021-04-29 20:47:32 +0530225 key = make_key(get_cache_key(item.name))
226
marinationea036e42022-04-04 11:07:53 +0530227 for field, value in web_item.items():
228 super(RedisWrapper, cache).hset(key, field, value)
Hussain Nagariaffc86162021-04-29 20:47:32 +0530229
Ankush Menat494bd9e2022-03-28 18:52:46 +0530230
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530231def get_cache_key(name):
232 name = frappe.scrub(name)
233 return f"{WEBSITE_ITEM_KEY_PREFIX}{name}"
234
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530235
Ankush Menat494bd9e2022-03-28 18:52:46 +0530236def get_fields_indexed():
237 fields_to_index = frappe.db.get_single_value("E Commerce Settings", "search_index_fields")
238 fields_to_index = fields_to_index.split(",") if fields_to_index else []
239
240 mandatory_fields = ["name", "web_item_name", "route", "thumbnail", "ranking"]
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530241 fields_to_index = fields_to_index + mandatory_fields
242
243 return fields_to_index
marinationea036e42022-04-04 11:07:53 +0530244
245
246def raise_redisearch_error():
247 "Create an Error Log and raise error."
Rushabh Mehta548afba2022-05-02 15:04:26 +0530248 log = frappe.log_error("Redisearch Error")
marinationea036e42022-04-04 11:07:53 +0530249 log_link = frappe.utils.get_link_to_form("Error Log", log.name)
250
251 frappe.throw(
252 msg=_("Something went wrong. Check {0}").format(log_link), title=_("Redisearch Error")
253 )