Repository: frappe/frappe

Issue:
Tree DocTypes: Display Group Node 1st in Link Field search list
Presently in Link Fields when you search tree doctype document name you will see that if the document is a group node it is shown last in the list after the child nodes.

![tdYsDN1](https://user-images.githubusercontent.com/36654812/118114929-e123fb80-b405-11eb-933b-c902d8aca583.png)

Search should generally show the best match first. The current implementation does the opposite.


Discussion:
Hi @gavindsouza , can you review the PR? Thanks!

Resolving patch:
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -168,7 +168,18 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
 				strict=False)
 
 			if doctype in UNTRANSLATED_DOCTYPES:
-				values = tuple([v for v in list(values) if re.search(re.escape(txt)+".*", (_(v.name) if as_dict else _(v[0])), re.IGNORECASE)])
+				# Filtering the values array so that query is included in very element
+				values = (
+					v for v in values
+					if re.search(
+						f"{re.escape(txt)}.*", _(v.name if as_dict else v[0]), re.IGNORECASE
+					)
+				)
+
+			# Sorting the values array so that relevant results always come first
+			# This will first bring elements on top in which query is a prefix of element
+			# Then it will bring the rest of the elements and sort them in lexicographical order
+			values = sorted(values, key=lambda x: relevance_sorter(x, txt, as_dict))
 
 			# remove _relevance from results
 			if as_dict:
@@ -208,6 +219,13 @@ def scrub_custom_query(query, key, txt):
 		query = query.replace('%s', ((txt or '') + '%'))
 	return query
 
+def relevance_sorter(key, query, as_dict):
+	value = _(key.name if as_dict else key[0])
+	return (
+		value.lower().startswith(query.lower()) is not True,
+		value
+	)
+
 @wrapt.decorator
 def validate_and_sanitize_search_inputs(fn, instance, args, kwargs):
 	kwargs.update(dict(zip(fn.__code__.co_varnames, args)))


Test patch:
diff --git a/frappe/tests/test_search.py b/frappe/tests/test_search.py
--- a/frappe/tests/test_search.py
+++ b/frappe/tests/test_search.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
 # MIT License. See license.txt
 
 import unittest
@@ -6,7 +6,16 @@
 from frappe.desk.search import search_link
 from frappe.desk.search import search_widget
 
+
 class TestSearch(unittest.TestCase):
+	def setUp(self):
+		if self._testMethodName == "test_link_field_order":
+			setup_test_link_field_order(self)
+
+	def tearDown(self):
+		if self._testMethodName == "test_link_field_order":
+			teardown_test_link_field_order(self)
+
 	def test_search_field_sanitizer(self):
 		# pass
 		search_link('DocType', 'User', query=None, filters=None, page_length=20, searchfield='name')
@@ -38,6 +47,18 @@ def test_search_field_sanitizer(self):
 			search_link, 'DocType', 'Customer', query=None, filters=None,
 			page_length=20, searchfield=';')
 
+	def test_link_field_order(self):
+		# Making a request to the search_link with the tree doctype
+		search_link(doctype=self.tree_doctype_name, txt='all', query=None,
+					filters=None, page_length=20, searchfield=None)
+		result = frappe.response['results']
+
+		# Check whether the result is sorted or not
+		self.assertEquals(self.parent_doctype_name, result[0]['value'])
+
+		# Check whether searching for parent also list out children
+		self.assertEquals(len(result), len(self.child_doctypes_names) + 1)
+
 	#Search for the word "pay", part of the word "pays" (country) in french.
 	def test_link_search_in_foreign_language(self):
 		try:
@@ -80,4 +101,58 @@ def test_validate_and_sanitize_search_inputs(self):
 
 @frappe.validate_and_sanitize_search_inputs
 def get_data(doctype, txt, searchfield, start, page_len, filters):
-	return [doctype, txt, searchfield, start, page_len, filters]
\ No newline at end of file
+	return [doctype, txt, searchfield, start, page_len, filters]
+
+def setup_test_link_field_order(TestCase):
+	TestCase.tree_doctype_name = 'Test Tree Order'
+	TestCase.child_doctype_list = []
+	TestCase.child_doctypes_names = ['USA', 'India', 'Russia', 'China']
+	TestCase.parent_doctype_name = 'All Territories'
+
+	# Create Tree doctype
+	TestCase.tree_doc = frappe.get_doc({
+		'doctype': 'DocType',
+		'name': TestCase.tree_doctype_name,
+		'module': 'Custom',
+		'custom': 1,
+		'is_tree': 1,
+		'autoname': 'field:random',
+		'fields': [{
+			'fieldname': 'random',
+			'label': 'Random',
+			'fieldtype': 'Data'
+		}]
+	}).insert()
+	TestCase.tree_doc.search_fields = 'parent_test_tree_order'
+	TestCase.tree_doc.save()
+
+	# Create root for the tree doctype
+	frappe.get_doc({
+		"doctype": TestCase.tree_doctype_name,
+		"random": TestCase.parent_doctype_name,
+		"is_group": 1
+	}).insert()
+
+	# Create children for the root
+	for child_name in TestCase.child_doctypes_names:
+		temp = frappe.get_doc({
+			"doctype": TestCase.tree_doctype_name,
+			"random": child_name,
+			"parent_test_tree_order": TestCase.parent_doctype_name
+		}).insert()
+		TestCase.child_doctype_list.append(temp)
+
+def teardown_test_link_field_order(TestCase):
+	# Deleting all the created doctype
+	for child_doctype in TestCase.child_doctype_list:
+		child_doctype.delete()
+
+	frappe.delete_doc(
+		TestCase.tree_doctype_name,
+		TestCase.parent_doctype_name,
+		ignore_permissions=True,
+		force=True,
+		for_reload=True,
+	)
+
+	TestCase.tree_doc.delete()