Browse Source

Return all results from the several methods, ordered by best match

pull/621/head
Daniel Reis 9 years ago
committed by Nicolas Mac Rouillon
parent
commit
7ab40f6a30
  1. 24
      base_name_search_improved/README.rst
  2. 53
      base_name_search_improved/models/ir_model.py
  3. 42
      base_name_search_improved/tests/test_name_search.py

24
base_name_search_improved/README.rst

@ -6,17 +6,18 @@
Improved Name Search 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. 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
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 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

53
base_name_search_improved/models/ir_model.py

@ -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)
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 # 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 # 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)
if recs:
return recs.name_get()
res = _extend_name_results(self, domain, res, limit)
# 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)
if recs:
return recs.name_get()
res = _extend_name_results(self, domain, res, limit)
# 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)
if recs:
return recs.name_get()
res = _extend_name_results(self, domain, res, limit)
return res return res
return name_search return name_search

42
base_name_search_improved/tests/test_name_search.py

@ -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'})
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): 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…
Cancel
Save