Implementes an advanced search feature, which allows for individual field terms

This commit is contained in:
Roberto Rosario
2011-06-22 00:47:37 -04:00
parent ec31f0ecce
commit 604ef90f79
6 changed files with 160 additions and 66 deletions

View File

@@ -323,5 +323,16 @@ class RecentDocument(models.Model):
# Register the fields that will be searchable # Register the fields that will be searchable
register(Document, _(u'document'), [u'document_type__name', u'file_mimetype', u'file_filename', u'file_extension', u'documentmetadata__value', u'documentpage__content', u'description', u'tags__name', u'comments__comment']) register('document', Document, _(u'document'), [
{'name': u'document_type__name', 'title': _(u'Document type')},
{'name': u'file_mimetype', 'title': _(u'MIME type')},
{'name': u'file_filename', 'title': _(u'Filename')},
{'name': u'file_extension', 'title': _(u'Filename extension')},
{'name': u'documentmetadata__value', 'title': _(u'Metadata value')},
{'name': u'documentpage__content', 'title': _(u'Content')},
{'name': u'description', 'title': _(u'Description')},
{'name': u'tags__name', 'title': _(u'Tags')},
{'name': u'comments__comment', 'title': _(u'Comments')},
]
)
#register(Document, _(u'document'), ['document_type__name', 'file_mimetype', 'file_extension', 'documentmetadata__value', 'documentpage__content', 'description', {'field_name':'file_filename', 'comparison':'iexact'}]) #register(Document, _(u'document'), ['document_type__name', 'file_mimetype', 'file_extension', 'documentmetadata__value', 'documentpage__content', 'description', {'field_name':'file_filename', 'comparison':'iexact'}])

View File

@@ -1,3 +1,10 @@
from navigation.api import register_sidebar_template from django.utils.translation import ugettext_lazy as _
register_sidebar_template(['search'], 'search_help.html') from navigation.api import register_sidebar_template, register_links
search = {'text': _(u'search'), 'view': 'search', 'famfam': 'zoom'}
search_advanced = {'text': _(u'advanced search'), 'view': 'search_advanced', 'famfam': 'zoom_in'}
register_sidebar_template(['search', 'search_advanced'], 'search_help.html')
register_links(['search', 'search_advanced'], [search, search_advanced], menu_name='form_header')

View File

@@ -9,26 +9,23 @@ from django.db.models import Q
from dynamic_search.conf.settings import LIMIT from dynamic_search.conf.settings import LIMIT
search_list = {} registered_search_dict = {}
def register(model, text, field_list): def register(model_name, model, title, fields):
if model in search_list: registered_search_dict.setdefault(model_name, {'model': model, 'fields': [], 'title': title})
search_list[model]['fields'].append(field_list) registered_search_dict[model_name]['fields'].extend(fields)
else:
search_list[model] = {'fields': field_list, 'text': text}
def normalize_query(query_string, def normalize_query(query_string,
findterms=re.compile(r'"([^"]+)"|(\S+)').findall, findterms=re.compile(r'"([^"]+)"|(\S+)').findall,
normspace=re.compile(r'\s{2,}').sub): normspace=re.compile(r'\s{2,}').sub):
""" Splits the query string in invidual keywords, getting rid of unecessary spaces """
Splits the query string in invidual keywords, getting rid of unecessary spaces
and grouping quoted words together. and grouping quoted words together.
Example: Example:
>>> normalize_query(' some random words "with quotes " and spaces') >>> normalize_query(' some random words "with quotes " and spaces')
['some', 'random', 'words', 'with quotes', 'and', 'spaces'] ['some', 'random', 'words', 'with quotes', 'and', 'spaces']
""" """
return [normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)] return [normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)]
@@ -60,19 +57,53 @@ def get_query(terms, search_fields):
return queries return queries
def perform_search(query_string): def perform_search(query_string, field_list=None):
model_list = {} model_list = {}
flat_list = [] flat_list = []
result_count = 0 result_count = 0
shown_result_count = 0 shown_result_count = 0
elapsed_time = 0 elapsed_time = 0
start_time = datetime.datetime.now()
search_dict = {}
if query_string: if query_string:
start_time = datetime.datetime.now() simple_query_string = query_string.get('q', u'').strip()
terms = normalize_query(query_string) if simple_query_string:
for model, values in registered_search_dict.items():
search_dict.setdefault(values['model'], {'query_entries': [], 'title': values['title']})
field_names = [field['name'] for field in values['fields']]
# One entry, single set of terms for all fields names
search_dict[values['model']]['query_entries'].append(
{
'field_name': field_names,
'terms': normalize_query(simple_query_string)
}
)
else:
for key, value in query_string.items():
try:
model, field_name = key.split('__', 1)
model_entry = registered_search_dict.get(model, {})
if model_entry:
for model_field in model_entry.get('fields', [{}]):
if model_field.get('name') == field_name:
search_dict.setdefault(model_entry['model'], {'query_entries': [], 'title': model_entry['title']})
search_dict[model_entry['model']]['query_entries'].append(
{
'field_name': [field_name],
'terms': normalize_query(value.strip())
}
)
except ValueError:
pass
for model, data in search_list.items(): for model, data in search_dict.items():
queries = get_query(terms, data['fields']) title = data['title']
queries = []
for query_entry in data['query_entries']:
queries.extend(get_query(query_entry['terms'], query_entry['field_name']))
model_result_ids = None model_result_ids = None
for query in queries: for query in queries:
@@ -84,14 +115,24 @@ def perform_search(query_string):
else: else:
model_result_ids &= single_result_ids model_result_ids &= single_result_ids
if model_result_ids == None:
model_result_ids = []
result_count += len(model_result_ids) result_count += len(model_result_ids)
results = model.objects.in_bulk(list(model_result_ids)[: LIMIT]).values() results = model.objects.in_bulk(list(model_result_ids)[: LIMIT]).values()
shown_result_count += len(results) shown_result_count += len(results)
if results: if results:
model_list[data['text']] = results model_list[title] = results
for result in results: for result in results:
if result not in flat_list: if result not in flat_list:
flat_list.append(result) flat_list.append(result)
elapsed_time = unicode(datetime.datetime.now() - start_time).split(':')[2] elapsed_time = unicode(datetime.datetime.now() - start_time).split(':')[2]
return model_list, flat_list, shown_result_count, result_count, elapsed_time return {
'model_list': model_list,
'flat_list': flat_list,
'shown_result_count': shown_result_count,
'result_count': result_count,
'elapsed_time': elapsed_time
}

View File

@@ -3,4 +3,17 @@ from django.utils.translation import ugettext_lazy as _
class SearchForm(forms.Form): class SearchForm(forms.Form):
q = forms.CharField(max_length=128, label=_(u'Search term')) q = forms.CharField(max_length=128, label=_(u'Search terms'))
class AdvancedSearchForm(forms.Form):
def __init__(self, *args, **kwargs):
search_fields = kwargs.pop('search_fields')
super(AdvancedSearchForm, self).__init__(*args, **kwargs)
#Set form fields initial values
for search_field in search_fields:
self.fields[search_field['name']] = forms.CharField(
label=search_field['title'],
required=False
)

View File

@@ -2,5 +2,6 @@ from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('dynamic_search.views', urlpatterns = patterns('dynamic_search.views',
url(r'^search/$', 'search', (), 'search'), url(r'^search/$', 'search', (), 'search'),
url(r'^search/advanced/$', 'search', { 'advanced': True }, 'search_advanced'),
url(r'^results/$', 'results', (), 'results'), url(r'^results/$', 'results', (), 'results'),
) )

View File

@@ -4,48 +4,18 @@ from django.utils.translation import ugettext_lazy as _
from django.contrib import messages from django.contrib import messages
from django.conf import settings from django.conf import settings
from dynamic_search.api import perform_search from dynamic_search.api import perform_search, registered_search_dict
from dynamic_search.forms import SearchForm from dynamic_search.forms import SearchForm, AdvancedSearchForm
from dynamic_search.conf.settings import SHOW_OBJECT_TYPE from dynamic_search.conf.settings import SHOW_OBJECT_TYPE
from dynamic_search.conf.settings import LIMIT from dynamic_search.conf.settings import LIMIT
def results(request, form=None): def results(request, extra_context=None):
query_string = '' query_string = ''
context = {} context = {}
if ('q' in request.GET) and request.GET['q'].strip():
query_string = request.GET['q']
try:
model_list, flat_list, shown_result_count, total_result_count, elapsed_time = perform_search(query_string)
if shown_result_count != total_result_count:
title = _(u'results with: %(query_string)s (showing only %(shown_result_count)s out of %(total_result_count)s)') % {
'query_string': query_string, 'shown_result_count': shown_result_count,
'total_result_count': total_result_count}
else:
title = _(u'results with: %s') % query_string
context.update({ context.update({
'found_entries': model_list, 'query_string': request.GET,
'object_list': flat_list,
'title': title,
'time_delta': elapsed_time,
})
except Exception, e:
if settings.DEBUG:
raise
elif request.user.is_staff or request.user.is_superuser:
messages.error(request, _(u'Search error: %s') % e)
else:
context.update({
'found_entries': [],
'object_list': [],
'title': _(u'results'),
})
context.update({
'query_string': query_string,
'form': form,
'form_title': _(u'Search'), 'form_title': _(u'Search'),
#'hide_header': True, #'hide_header': True,
'form_hide_required_text': True, 'form_hide_required_text': True,
@@ -56,6 +26,30 @@ def results(request, form=None):
'search_results_limit': LIMIT, 'search_results_limit': LIMIT,
}) })
if extra_context:
context.update(extra_context)
try:
response = perform_search(request.GET)
if response['shown_result_count'] != response['result_count']:
title = _(u'results, (showing only %(shown_result_count)s out of %(result_count)s)') % {
'shown_result_count': response['shown_result_count'],
'result_count': response['result_count']}
else:
title = _(u'results')
context.update({
'found_entries': response['model_list'],
'object_list': response['flat_list'],
'title': title,
'time_delta': response['elapsed_time'],
})
except Exception, e:
if settings.DEBUG:
raise
elif request.user.is_staff or request.user.is_superuser:
messages.error(request, _(u'Search error: %s') % e)
if SHOW_OBJECT_TYPE: if SHOW_OBJECT_TYPE:
context.update({'extra_columns': context.update({'extra_columns':
[{'name': _(u'type'), 'attribute': lambda x: x._meta.verbose_name[0].upper() + x._meta.verbose_name[1:]}]}) [{'name': _(u'type'), 'attribute': lambda x: x._meta.verbose_name[0].upper() + x._meta.verbose_name[1:]}]})
@@ -64,11 +58,38 @@ def results(request, form=None):
context_instance=RequestContext(request)) context_instance=RequestContext(request))
def search(request): def search(request, advanced=False):
if advanced:
search_fields = []
for model_name, values in registered_search_dict.items():
for field in values['fields']:
search_fields.append(
{
'title': field['title'],
'name': '%s__%s' % (model_name, field['name'])
}
)
form = AdvancedSearchForm(
search_fields=search_fields,
data=request.GET
)
return results(request, extra_context={
'form': form,
'form_title': _(u'advanced search')
}
)
else:
if ('q' in request.GET) and request.GET['q'].strip(): if ('q' in request.GET) and request.GET['q'].strip():
query_string = request.GET['q'] query_string = request.GET['q']
form = SearchForm(initial={'q': query_string}) form = SearchForm(initial={'q': query_string})
return results(request, form=form) return results(request, extra_context={
'form': form
}
)
else: else:
form = SearchForm() form = SearchForm()
return results(request, form=form) return results(request, extra_context={
'form': form
}
)