From 82771865cefc14d89beaf3266f996af1f9ba46da Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Thu, 9 Jun 2016 23:57:08 +0100 Subject: [PATCH] Return all results from the several methods, ordered by best match --- base_name_search_improved/README.rst | 24 +++++---- base_name_search_improved/models/ir_model.py | 53 ++++++++++++------- .../tests/test_name_search.py | 42 +++++++-------- 3 files changed, 70 insertions(+), 49 deletions(-) diff --git a/base_name_search_improved/README.rst b/base_name_search_improved/README.rst index 46d91e16d..07998cbb2 100644 --- a/base_name_search_improved/README.rst +++ b/base_name_search_improved/README.rst @@ -6,17 +6,18 @@ Improved Name Search ==================== -Extends the name search feature to use fuzzy matching methods, and -allowing to search in additional related record attributes. +Extends the name search feature to use additional, more relaxed +matching methods, and to allow searching into configurable additional +record fields. The name search is the lookup feature to select a related record. For example, selecting a Customer on a new Sales order. For example, typing "john brown" doesn't match "John M. Brown". -The fuzzy search looks up for record containing all the words, +The relaxed search also looks up for records containing all the words, so "John M. Brown" would be a match. It also tolerates words in a different order, so searching -for "brown john" would also works. +for "brown john" also works. .. image:: images/image0.png @@ -28,16 +29,19 @@ For example, Customers could be additionally searched by City or Phone number. How it works: Regular name search is performed, and the additional search logic is only -triggered if no results are found. This way, no significan overhead is added -on searches that would normally yield results. +triggered if not enough results are found. +This way, no overhead is added on searches that would normally yield results. -But if no results are found, then sdditional search methods are tried until -some results are found. The sepcific methods used are: +But if not enough results are found, then additional search methods are tried. +The specific methods used are: - Try regular search on each of the additional fields - Try ordered word search on each of the search fields - Try unordered word search on each of the search fields +All results found are presented in that order, +hopefully presenting them in order of relevance. + Installation ============ @@ -74,8 +78,10 @@ Just type into any related field, such as Customer on a Sale Order. Known issues / Roadmap ====================== +* Also use fuzzy search, such as the Levenshtein distance: + https://www.postgresql.org/docs/9.5/static/fuzzystrmatch.html * The list of additional fields to search could benefit from caching, for efficiency. -* This feature could be implemented for regular ``search`` on the ``name`` field. +* This feature could also be implemented for regular ``search`` on the ``name`` field. Bug Tracker diff --git a/base_name_search_improved/models/ir_model.py b/base_name_search_improved/models/ir_model.py index 14a8ba9d2..66b00b3ea 100644 --- a/base_name_search_improved/models/ir_model.py +++ b/base_name_search_improved/models/ir_model.py @@ -4,6 +4,29 @@ from openerp import models, fields, api from openerp import SUPERUSER_ID +from openerp import tools + + +# Extended name search is only used on some operators +ALLOWED_OPS = set(['ilike', 'like']) + + +@tools.ormcache(skiparg=0) +def _get_rec_names(self): + model = self.env['ir.model'].search( + [('model', '=', str(self._model))]) + rec_name = [self._rec_name] or [] + other_names = model.name_search_ids.mapped('name') + return rec_name + other_names + + +def _extend_name_results(self, domain, results, limit): + result_count = len(results) + if result_count < limit: + domain += [('id', 'not in', [x[0] for x in results])] + recs = self.search(domain, limit=limit - result_count) + results.extend(recs.name_get()) + return results class ModelExtended(models.Model): @@ -20,35 +43,27 @@ class ModelExtended(models.Model): @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): - # Regular name search + # Perform standard name search res = name_search.origin( self, name=name, args=args, operator=operator, limit=limit) - - allowed_ops = ['ilike', 'like', '='] - if not res and operator in allowed_ops and self._rec_name: + enabled = self.env.context.get('name_search_extended', True) + # Perform extended name search + if enabled and operator in ALLOWED_OPS: # Support a list of fields to search on - model = self.env['ir.model'].search( - [('model', '=', str(self._model))]) - other_names = model.name_search_ids.mapped('name') + all_names = _get_rec_names(self) # Try regular search on each additional search field - for rec_name in other_names: + for rec_name in all_names[1:]: domain = [(rec_name, operator, name)] - recs = self.search(domain, limit=limit) - if recs: - return recs.name_get() + res = _extend_name_results(self, domain, res, limit) # Try ordered word search on each of the search fields - for rec_name in [self._rec_name] + other_names: + for rec_name in all_names: domain = [(rec_name, operator, name.replace(' ', '%'))] - recs = self.search(domain, limit=limit) - if recs: - return recs.name_get() + res = _extend_name_results(self, domain, res, limit) # Try unordered word search on each of the search fields - for rec_name in [self._rec_name] + other_names: + for rec_name in all_names: domain = [(rec_name, operator, x) for x in name.split() if x] - recs = self.search(domain, limit=limit) - if recs: - return recs.name_get() + res = _extend_name_results(self, domain, res, limit) return res return name_search diff --git a/base_name_search_improved/tests/test_name_search.py b/base_name_search_improved/tests/test_name_search.py index 4a016affb..408db3eb5 100644 --- a/base_name_search_improved/tests/test_name_search.py +++ b/base_name_search_improved/tests/test_name_search.py @@ -16,29 +16,29 @@ class NameSearchCase(TransactionCase): model_partner.name_search_ids = phone_field self.Partner = self.env['res.partner'] self.partner1 = self.Partner.create( - {'name': 'Johann Gambolputty of Ulm', - 'phone': '+351 555 777'}) - self.partner2 = self.Partner.create( {'name': 'Luigi Verconti', - 'phone': '+351 777 555'}) - - def test_NameSearchSearchWithSpaces(self): - """Name Search Match full string, honoring spaces""" - res = self.Partner.name_search('777 555') - self.assertEqual(res[0][0], self.partner2.id) - - def test_NameSearchOrdered(self): - """Name Search Match by words, honoring order""" - res = self.Partner.name_search('johann ulm') - # res is a list of tuples (id, name) - self.assertEqual(res[0][0], self.partner1.id) - - def test_NameSearchUnordered(self): - """Name Search Math by unordered words""" - res = self.Partner.name_search('ulm gambol') - self.assertEqual(res[0][0], self.partner1.id) + 'phone': '+351 555 777 333'}) + self.partner2 = self.Partner.create( + {'name': 'Ken Shabby', + 'phone': '+351 555 333 777'}) + self.partner3 = self.Partner.create( + {'name': 'Johann Gambolputty of Ulm', + 'phone': '+351 777 333 555'}) + + def test_RelevanceOrderedResults(self): + """Return results ordered by relevance""" + res = self.Partner.name_search('555 777') + self.assertEqual( + res[0][0], self.partner1.id, + 'Match full string honoring spaces') + self.assertEqual( + res[1][0], self.partner2.id, + 'Match words honoring order of appearance') + self.assertEqual( + res[2][0], self.partner3.id, + 'Match all words, regardless of order of appearance') def test_NameSearchMustMatchAllWords(self): - """Name Search Must Match All Words""" + """Must Match All Words""" res = self.Partner.name_search('ulm 555 777') self.assertFalse(res)