blob: 59c7f32fd4672c9d6e4c51d4d2b112d87d5c7f4d [file] [log] [blame]
marinationb0d7e322021-06-01 12:44:49 +05301# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
Hussain Nagaria8e55c952021-04-26 07:01:06 +05302# License: GNU General Public License v3. See license.txt
3
Hussain Nagaria8e55c952021-04-26 07:01:06 +05304import frappe
Hussain Nagariaffc86162021-04-29 20:47:32 +05305from frappe.utils.redis_wrapper import RedisWrapper
marination9fb61ef2022-02-01 00:39:14 +05306from redisearch import AutoCompleter, Client, IndexDefinition, Suggestion, TagField, TextField
Hussain Nagaria8e55c952021-04-26 07:01:06 +05307
Hussain Nagaria8e55c952021-04-26 07:01:06 +05308WEBSITE_ITEM_INDEX = 'website_items_index'
9WEBSITE_ITEM_KEY_PREFIX = 'website_item:'
10WEBSITE_ITEM_NAME_AUTOCOMPLETE = 'website_items_name_dict'
Hussain Nagariaf3421552021-04-26 11:15:20 +053011WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE = 'website_items_category_dict'
Hussain Nagaria8e55c952021-04-26 07:01:06 +053012
marinationbbcbcf72021-09-02 14:07:59 +053013def 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
marinationb0d7e322021-06-01 12:44:49 +053022def is_search_module_loaded():
Maricad637f792021-09-18 14:23:44 +053023 try:
24 cache = frappe.cache()
25 out = cache.execute_command('MODULE LIST')
marinationb0d7e322021-06-01 12:44:49 +053026
Maricad637f792021-09-18 14:23:44 +053027 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
marinationb0d7e322021-06-01 12:44:49 +053033
marinationb0d7e322021-06-01 12:44:49 +053034def if_redisearch_loaded(function):
marinationbbcbcf72021-09-02 14:07:59 +053035 "Decorator to check if Redisearch is loaded."
marinationb0d7e322021-06-01 12:44:49 +053036 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
44def make_key(key):
45 return "{0}|{1}".format(frappe.conf.db_name, key).encode('utf-8')
46
47@if_redisearch_loaded
Hussain Nagaria8e55c952021-04-26 07:01:06 +053048def create_website_items_index():
marinationbbcbcf72021-09-02 14:07:59 +053049 "Creates Index Definition."
50
Hussain Nagaria8e55c952021-04-26 07:01:06 +053051 # CREATE index
Hussain Nagariaffc86162021-04-29 20:47:32 +053052 client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache())
Hussain Nagaria8e55c952021-04-26 07:01:06 +053053
54 # DROP if already exists
55 try:
56 client.drop_index()
marinationb0d7e322021-06-01 12:44:49 +053057 except Exception:
Hussain Nagaria8e55c952021-04-26 07:01:06 +053058 pass
59
Hussain Nagariaffc86162021-04-29 20:47:32 +053060 idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)])
Hussain Nagaria8e55c952021-04-26 07:01:06 +053061
62 # Based on e-commerce settings
63 idx_fields = frappe.db.get_single_value(
marinationb0d7e322021-06-01 12:44:49 +053064 'E Commerce Settings',
Hussain Nagaria8e55c952021-04-26 07:01:06 +053065 'search_index_fields'
marination24ba06c2021-06-08 14:47:11 +053066 )
67 idx_fields = idx_fields.split(',') if idx_fields else []
Hussain Nagaria8e55c952021-04-26 07:01:06 +053068
69 if 'web_item_name' in idx_fields:
70 idx_fields.remove('web_item_name')
marinationb0d7e322021-06-01 12:44:49 +053071
Hussain Nagaria8e55c952021-04-26 07:01:06 +053072 idx_fields = list(map(to_search_field, idx_fields))
73
74 client.create_index(
75 [TextField("web_item_name", sortable=True)] + idx_fields,
Hussain Nagariaffc86162021-04-29 20:47:32 +053076 definition=idx_def,
Hussain Nagaria8e55c952021-04-26 07:01:06 +053077 )
78
79 reindex_all_web_items()
80 define_autocomplete_dictionary()
81
82def to_search_field(field):
83 if field == "tags":
84 return TagField("tags", separator=",")
85
86 return TextField(field)
87
marinationb0d7e322021-06-01 12:44:49 +053088@if_redisearch_loaded
Hussain Nagaria8e55c952021-04-26 07:01:06 +053089def insert_item_to_index(website_item_doc):
90 # Insert item to index
91 key = get_cache_key(website_item_doc.name)
marinationb0d7e322021-06-01 12:44:49 +053092 cache = frappe.cache()
Hussain Nagaria8e55c952021-04-26 07:01:06 +053093 web_item = create_web_item_map(website_item_doc)
Hussain Nagariaffc86162021-04-29 20:47:32 +053094
95 for k, v in web_item.items():
marinationb0d7e322021-06-01 12:44:49 +053096 super(RedisWrapper, cache).hset(make_key(key), k, v)
Hussain Nagariaffc86162021-04-29 20:47:32 +053097
Hussain Nagaria8e55c952021-04-26 07:01:06 +053098 insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name)
99
marinationb0d7e322021-06-01 12:44:49 +0530100@if_redisearch_loaded
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530101def insert_to_name_ac(web_name, doc_name):
Hussain Nagariaffc86162021-04-29 20:47:32 +0530102 ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache())
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530103 ac.add_suggestions(Suggestion(web_name, payload=doc_name))
104
105def create_web_item_map(website_item_doc):
106 fields_to_index = get_fields_indexed()
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530107 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 Nagariabd0d0ad2021-05-26 20:26:34 +0530113
marinationb0d7e322021-06-01 12:44:49 +0530114@if_redisearch_loaded
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530115def update_index_for_item(website_item_doc):
116 # Reinsert to Cache
117 insert_item_to_index(website_item_doc)
118 define_autocomplete_dictionary()
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530119
marinationb0d7e322021-06-01 12:44:49 +0530120@if_redisearch_loaded
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530121def delete_item_from_index(website_item_doc):
marinationb0d7e322021-06-01 12:44:49 +0530122 cache = frappe.cache()
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530123 key = get_cache_key(website_item_doc.name)
marinationb0d7e322021-06-01 12:44:49 +0530124
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530125 try:
marinationb0d7e322021-06-01 12:44:49 +0530126 cache.delete(key)
marinationea30ce42021-06-02 13:24:06 +0530127 except Exception:
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530128 return False
129
Hussain Nagaria34f3a662021-05-05 13:47:43 +0530130 delete_from_ac_dict(website_item_doc)
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530131 return True
132
marinationb0d7e322021-06-01 12:44:49 +0530133@if_redisearch_loaded
Hussain Nagaria34f3a662021-05-05 13:47:43 +0530134def delete_from_ac_dict(website_item_doc):
135 '''Removes this items's name from autocomplete dictionary'''
marinationb0d7e322021-06-01 12:44:49 +0530136 cache = frappe.cache()
137 name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
Hussain Nagaria34f3a662021-05-05 13:47:43 +0530138 name_ac.delete(website_item_doc.web_item_name)
139
marinationb0d7e322021-06-01 12:44:49 +0530140@if_redisearch_loaded
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530141def define_autocomplete_dictionary():
Hussain Nagariaf3421552021-04-26 11:15:20 +0530142 """Creates an autocomplete search dictionary for `name`.
marinationb0d7e322021-06-01 12:44:49 +0530143 Also creats autocomplete dictionary for `categories` if
144 checked in E Commerce Settings"""
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530145
marinationb0d7e322021-06-01 12:44:49 +0530146 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 Nagaria8e55c952021-04-26 07:01:06 +0530149
Hussain Nagariaf3421552021-04-26 11:15:20 +0530150 ac_categories = frappe.db.get_single_value(
marinationb0d7e322021-06-01 12:44:49 +0530151 'E Commerce Settings',
Hussain Nagariaf3421552021-04-26 11:15:20 +0530152 'show_categories_in_search_autocomplete'
153 )
marinationb0d7e322021-06-01 12:44:49 +0530154
Hussain Nagariaf3421552021-04-26 11:15:20 +0530155 # Delete both autocomplete dicts
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530156 try:
marinationb0d7e322021-06-01 12:44:49 +0530157 cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE))
158 cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE))
marinationea30ce42021-06-02 13:24:06 +0530159 except Exception:
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530160 return False
marinationb0d7e322021-06-01 12:44:49 +0530161
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530162 items = frappe.get_all(
marinationb0d7e322021-06-01 12:44:49 +0530163 'Website Item',
164 fields=['web_item_name', 'item_group'],
165 filters={"published": 1}
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530166 )
167
168 for item in items:
Hussain Nagariaf3421552021-04-26 11:15:20 +0530169 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 Nagaria8e55c952021-04-26 07:01:06 +0530172
173 return True
174
marinationb0d7e322021-06-01 12:44:49 +0530175@if_redisearch_loaded
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530176def reindex_all_web_items():
177 items = frappe.get_all(
marinationb0d7e322021-06-01 12:44:49 +0530178 'Website Item',
179 fields=get_fields_indexed(),
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530180 filters={"published": True}
181 )
182
marinationb0d7e322021-06-01 12:44:49 +0530183 cache = frappe.cache()
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530184 for item in items:
185 web_item = create_web_item_map(item)
Hussain Nagariaffc86162021-04-29 20:47:32 +0530186 key = make_key(get_cache_key(item.name))
187
188 for k, v in web_item.items():
marinationb0d7e322021-06-01 12:44:49 +0530189 super(RedisWrapper, cache).hset(key, k, v)
Hussain Nagariaffc86162021-04-29 20:47:32 +0530190
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530191def get_cache_key(name):
192 name = frappe.scrub(name)
193 return f"{WEBSITE_ITEM_KEY_PREFIX}{name}"
194
195def get_fields_indexed():
196 fields_to_index = frappe.db.get_single_value(
marinationb0d7e322021-06-01 12:44:49 +0530197 'E Commerce Settings',
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530198 'search_index_fields'
marination24ba06c2021-06-08 14:47:11 +0530199 )
200 fields_to_index = fields_to_index.split(',') if fields_to_index else []
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530201
marinationbbcbcf72021-09-02 14:07:59 +0530202 mandatory_fields = ['name', 'web_item_name', 'route', 'thumbnail', 'ranking']
Hussain Nagaria8e55c952021-04-26 07:01:06 +0530203 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
209define_autocomplete_dictionary()
210create_website_items_index()