Return all results from the several methods, ordered by best match
parent
afa8e9042f
commit
aa9b06681c
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'})
|
||||
'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):
|
||||
"""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)
|
||||
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)
|
||||
|
|
Loading…
Reference in New Issue