Return all results from the several methods, ordered by best match
parent
afa8e9042f
commit
aa9b06681c
|
@ -6,17 +6,18 @@
|
||||||
Improved Name Search
|
Improved Name Search
|
||||||
====================
|
====================
|
||||||
|
|
||||||
Extends the name search feature to use fuzzy matching methods, and
|
Extends the name search feature to use additional, more relaxed
|
||||||
allowing to search in additional related record attributes.
|
matching methods, and to allow searching into configurable additional
|
||||||
|
record fields.
|
||||||
|
|
||||||
The name search is the lookup feature to select a related record.
|
The name search is the lookup feature to select a related record.
|
||||||
For example, selecting a Customer on a new Sales order.
|
For example, selecting a Customer on a new Sales order.
|
||||||
|
|
||||||
For example, typing "john brown" doesn't match "John M. Brown".
|
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.
|
so "John M. Brown" would be a match.
|
||||||
It also tolerates words in a different order, so searching
|
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
|
.. image:: images/image0.png
|
||||||
|
|
||||||
|
@ -28,16 +29,19 @@ For example, Customers could be additionally searched by City or Phone number.
|
||||||
How it works:
|
How it works:
|
||||||
|
|
||||||
Regular name search is performed, and the additional search logic is only
|
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
|
triggered if not enough results are found.
|
||||||
on searches that would normally yield results.
|
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
|
But if not enough results are found, then additional search methods are tried.
|
||||||
some results are found. The sepcific methods used are:
|
The specific methods used are:
|
||||||
|
|
||||||
- Try regular search on each of the additional fields
|
- Try regular search on each of the additional fields
|
||||||
- Try ordered word search on each of the search fields
|
- Try ordered word search on each of the search fields
|
||||||
- Try unordered 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
|
Installation
|
||||||
============
|
============
|
||||||
|
@ -74,8 +78,10 @@ Just type into any related field, such as Customer on a Sale Order.
|
||||||
Known issues / Roadmap
|
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.
|
* 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
|
Bug Tracker
|
||||||
|
|
|
@ -4,6 +4,29 @@
|
||||||
|
|
||||||
from openerp import models, fields, api
|
from openerp import models, fields, api
|
||||||
from openerp import SUPERUSER_ID
|
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):
|
class ModelExtended(models.Model):
|
||||||
|
@ -20,35 +43,27 @@ class ModelExtended(models.Model):
|
||||||
@api.model
|
@api.model
|
||||||
def name_search(self, name='', args=None,
|
def name_search(self, name='', args=None,
|
||||||
operator='ilike', limit=100):
|
operator='ilike', limit=100):
|
||||||
# Regular name search
|
# Perform standard name search
|
||||||
res = name_search.origin(
|
res = name_search.origin(
|
||||||
self, name=name, args=args, operator=operator, limit=limit)
|
self, name=name, args=args, operator=operator, limit=limit)
|
||||||
|
enabled = self.env.context.get('name_search_extended', True)
|
||||||
allowed_ops = ['ilike', 'like', '=']
|
# Perform extended name search
|
||||||
if not res and operator in allowed_ops and self._rec_name:
|
if enabled and operator in ALLOWED_OPS:
|
||||||
# Support a list of fields to search on
|
# Support a list of fields to search on
|
||||||
model = self.env['ir.model'].search(
|
all_names = _get_rec_names(self)
|
||||||
[('model', '=', str(self._model))])
|
|
||||||
other_names = model.name_search_ids.mapped('name')
|
|
||||||
# Try regular search on each additional search field
|
# 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)]
|
domain = [(rec_name, operator, name)]
|
||||||
recs = self.search(domain, limit=limit)
|
res = _extend_name_results(self, domain, res, limit)
|
||||||
if recs:
|
|
||||||
return recs.name_get()
|
|
||||||
# Try ordered word search on each of the search fields
|
# 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(' ', '%'))]
|
domain = [(rec_name, operator, name.replace(' ', '%'))]
|
||||||
recs = self.search(domain, limit=limit)
|
res = _extend_name_results(self, domain, res, limit)
|
||||||
if recs:
|
|
||||||
return recs.name_get()
|
|
||||||
# Try unordered word search on each of the search fields
|
# 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)
|
domain = [(rec_name, operator, x)
|
||||||
for x in name.split() if x]
|
for x in name.split() if x]
|
||||||
recs = self.search(domain, limit=limit)
|
res = _extend_name_results(self, domain, res, limit)
|
||||||
if recs:
|
|
||||||
return recs.name_get()
|
|
||||||
return res
|
return res
|
||||||
return name_search
|
return name_search
|
||||||
|
|
||||||
|
|
|
@ -16,29 +16,29 @@ class NameSearchCase(TransactionCase):
|
||||||
model_partner.name_search_ids = phone_field
|
model_partner.name_search_ids = phone_field
|
||||||
self.Partner = self.env['res.partner']
|
self.Partner = self.env['res.partner']
|
||||||
self.partner1 = self.Partner.create(
|
self.partner1 = self.Partner.create(
|
||||||
{'name': 'Johann Gambolputty of Ulm',
|
|
||||||
'phone': '+351 555 777'})
|
|
||||||
self.partner2 = self.Partner.create(
|
|
||||||
{'name': 'Luigi Verconti',
|
{'name': 'Luigi Verconti',
|
||||||
'phone': '+351 777 555'})
|
'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_NameSearchSearchWithSpaces(self):
|
def test_RelevanceOrderedResults(self):
|
||||||
"""Name Search Match full string, honoring spaces"""
|
"""Return results ordered by relevance"""
|
||||||
res = self.Partner.name_search('777 555')
|
res = self.Partner.name_search('555 777')
|
||||||
self.assertEqual(res[0][0], self.partner2.id)
|
self.assertEqual(
|
||||||
|
res[0][0], self.partner1.id,
|
||||||
def test_NameSearchOrdered(self):
|
'Match full string honoring spaces')
|
||||||
"""Name Search Match by words, honoring order"""
|
self.assertEqual(
|
||||||
res = self.Partner.name_search('johann ulm')
|
res[1][0], self.partner2.id,
|
||||||
# res is a list of tuples (id, name)
|
'Match words honoring order of appearance')
|
||||||
self.assertEqual(res[0][0], self.partner1.id)
|
self.assertEqual(
|
||||||
|
res[2][0], self.partner3.id,
|
||||||
def test_NameSearchUnordered(self):
|
'Match all words, regardless of order of appearance')
|
||||||
"""Name Search Math by unordered words"""
|
|
||||||
res = self.Partner.name_search('ulm gambol')
|
|
||||||
self.assertEqual(res[0][0], self.partner1.id)
|
|
||||||
|
|
||||||
def test_NameSearchMustMatchAllWords(self):
|
def test_NameSearchMustMatchAllWords(self):
|
||||||
"""Name Search Must Match All Words"""
|
"""Must Match All Words"""
|
||||||
res = self.Partner.name_search('ulm 555 777')
|
res = self.Partner.name_search('ulm 555 777')
|
||||||
self.assertFalse(res)
|
self.assertFalse(res)
|
||||||
|
|
Loading…
Reference in New Issue