You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bloodhound.apache.org by as...@apache.org on 2013/04/19 11:38:34 UTC
svn commit: r1469760 - in /bloodhound/trunk/bloodhound_search: ./ bhsearch/
bhsearch/templates/ bhsearch/tests/
Author: astaric
Date: Fri Apr 19 09:38:34 2013
New Revision: 1469760
URL: http://svn.apache.org/r1469760
Log:
Basic query suggestions - towards #505.
Added:
bloodhound/trunk/bloodhound_search/bhsearch/query_suggestion.py
bloodhound/trunk/bloodhound_search/bhsearch/tests/query_suggestion.py
Modified:
bloodhound/trunk/bloodhound_search/bhsearch/api.py
bloodhound/trunk/bloodhound_search/bhsearch/query_parser.py
bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html
bloodhound/trunk/bloodhound_search/bhsearch/tests/__init__.py
bloodhound/trunk/bloodhound_search/bhsearch/tests/security.py
bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py
bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py
bloodhound/trunk/bloodhound_search/setup.py
Modified: bloodhound/trunk/bloodhound_search/bhsearch/api.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/api.py?rev=1469760&r1=1469759&r2=1469760&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/api.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/api.py Fri Apr 19 09:38:34 2013
@@ -50,6 +50,7 @@ class QueryResult(object):
self.docs = []
self.highlighting = []
self.facets = None
+ self.query_suggestion = None
self.debug = {}
class SortInstruction(object):
@@ -298,6 +299,7 @@ class BloodhoundSearchApi(Component):
query_parameters = dict(
query = parsed_query,
+ query_string = query,
sort = sort,
fields = fields,
filter = parsed_filters,
Modified: bloodhound/trunk/bloodhound_search/bhsearch/query_parser.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/query_parser.py?rev=1469760&r1=1469759&r2=1469760&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/query_parser.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/query_parser.py Fri Apr 19 09:38:34 2013
@@ -91,6 +91,7 @@ class DefaultQueryParser(Component):
content = 1,
changes = 1,
message = 1,
+ query_suggestion_basket = 0,
)
meta_keyword_parsers = ExtensionPoint(IMetaKeywordParser)
@@ -102,6 +103,7 @@ class DefaultQueryParser(Component):
return query.Every()
query_string = unicode(query_string)
parsed_query = parser.parse(query_string)
+ parsed_query.original_query_string = query_string
return parsed_query
def parse_filters(self, filters):
Added: bloodhound/trunk/bloodhound_search/bhsearch/query_suggestion.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/query_suggestion.py?rev=1469760&view=auto
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/query_suggestion.py (added)
+++ bloodhound/trunk/bloodhound_search/bhsearch/query_suggestion.py Fri Apr 19 09:38:34 2013
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from trac.core import Component, implements
+from bhsearch.api import IDocIndexPreprocessor, IndexFields
+
+
+class SuggestionFields(IndexFields):
+ SUMMARY = 'summary'
+ BASKET = 'query_suggestion_basket'
+
+
+class QuerySuggestionPreprocessor(Component):
+ implements(IDocIndexPreprocessor)
+
+ suggestion_fields = [
+ IndexFields.NAME,
+ IndexFields.CONTENT,
+ SuggestionFields.SUMMARY,
+ ]
+
+ # IDocIndexPreprocessor methods
+ def pre_process(self, doc):
+ basket = u' '.join(doc.get(field, '')
+ for field in self.suggestion_fields)
+ doc[SuggestionFields.BASKET] = basket
Modified: bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html?rev=1469760&r1=1469759&r2=1469760&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/templates/bhsearch.html Fri Apr 19 09:38:34 2013
@@ -84,6 +84,9 @@
<div class="${'span12' if not facet_counts else 'span9'}${
' search_results'}">
+ <div class='query_suggestion' py:if='query_suggestion'>
+ Did you mean <a href="${query_suggestion.href}">${query_suggestion.query}</a>?
+ </div>
<h2>
Results <small>(${results.displayed_items()})</small>
</h2>
@@ -175,6 +178,10 @@
py:if="query and not (results or quickjump)">
<p id="notfound" class="alert">
No matches found.
+ <py:if test='query_suggestion'>
+ Did you mean
+ <a href="${query_suggestion.href}">${query_suggestion.query}</a>?
+ </py:if>
</p>
</div>
Modified: bloodhound/trunk/bloodhound_search/bhsearch/tests/__init__.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/tests/__init__.py?rev=1469760&r1=1469759&r2=1469760&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/tests/__init__.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/tests/__init__.py Fri Apr 19 09:38:34 2013
@@ -19,7 +19,7 @@
# under the License.
import unittest
from bhsearch.tests import (whoosh_backend, index_with_whoosh, web_ui,
- api, query_parser, security)
+ api, query_parser, query_suggestion, security)
from bhsearch.tests.search_resources import (ticket_search, wiki_search,
milestone_search, base,
changeset_search)
@@ -32,6 +32,7 @@ def suite():
test_suite.addTest(web_ui.suite())
test_suite.addTest(api.suite())
test_suite.addTest(query_parser.suite())
+ test_suite.addTest(query_suggestion.suite())
test_suite.addTest(security.suite())
test_suite.addTest(ticket_search.suite())
test_suite.addTest(wiki_search.suite())
Added: bloodhound/trunk/bloodhound_search/bhsearch/tests/query_suggestion.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/tests/query_suggestion.py?rev=1469760&view=auto
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/tests/query_suggestion.py (added)
+++ bloodhound/trunk/bloodhound_search/bhsearch/tests/query_suggestion.py Fri Apr 19 09:38:34 2013
@@ -0,0 +1,93 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import unittest
+from bhsearch.tests.base import BaseBloodhoundSearchTest
+
+from bhsearch.api import BloodhoundSearchApi
+from bhsearch.query_suggestion import SuggestionFields
+from bhsearch.web_ui import RequestParameters, RequestContext
+from bhsearch.whoosh_backend import WhooshBackend
+
+
+class QuerySuggestionTestCase(BaseBloodhoundSearchTest):
+ def setUp(self):
+ super(QuerySuggestionTestCase, self).setUp(create_req=True)
+ self.whoosh_backend = WhooshBackend(self.env)
+ self.whoosh_backend.recreate_index()
+
+ self.search_api = BloodhoundSearchApi(self.env)
+
+ def test_fills_suggestion_field(self):
+ self.insert_ticket("test")
+ self.insert_milestone("test")
+ self.insert_wiki("name", "test")
+
+ results = self.search_api.query("%s:test" % SuggestionFields.BASKET)
+
+ self.assertEqual(results.hits, 3)
+
+ def test_provides_suggestions(self):
+ self.insert_ticket("test")
+ self.req.args[RequestParameters.QUERY] = "tesk"
+
+ data = self.process_request()
+
+ self.assertIn(RequestContext.DATA_QUERY_SUGGESTION, data)
+ suggestion = data[RequestContext.DATA_QUERY_SUGGESTION]
+ self.assertEqual(suggestion['query'], 'test')
+ self.assertIn('q=test', suggestion['href'])
+
+ def test_provides_suggestions_for_multi_term_queries(self):
+ self.insert_ticket("another test")
+ self.req.args[RequestParameters.QUERY] = "another tesk"
+
+ data = self.process_request()
+
+ suggestion = data[RequestContext.DATA_QUERY_SUGGESTION]
+ self.assertEqual(suggestion['query'], 'another test')
+
+ def test_provides_suggestions_for_queries_with_unknown_words(self):
+ self.insert_ticket("test")
+ self.req.args[RequestParameters.QUERY] = "another tesk"
+
+ data = self.process_request()
+
+ suggestion = data[RequestContext.DATA_QUERY_SUGGESTION]
+ self.assertEqual(suggestion['query'], 'another test')
+
+ def test_suggestion_href_contains_used_filters(self):
+ self.insert_ticket("test")
+ self.req.args[RequestParameters.QUERY] = "tesk"
+ self.req.args[RequestParameters.FILTER_QUERY] = ['filter']
+
+ data = self.process_request()
+
+ suggestion = data[RequestContext.DATA_QUERY_SUGGESTION]
+ self.assertIn('fq=filter', suggestion['href'])
+
+
+def suite():
+ test_suite = unittest.TestSuite()
+ test_suite.addTest(unittest.makeSuite(QuerySuggestionTestCase, 'test'))
+ return test_suite
+
+if __name__ == '__main__':
+ unittest.main()
Modified: bloodhound/trunk/bloodhound_search/bhsearch/tests/security.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/tests/security.py?rev=1469760&r1=1469759&r2=1469760&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/tests/security.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/tests/security.py Fri Apr 19 09:38:34 2013
@@ -26,8 +26,8 @@ import contextlib
import unittest
from sqlite3 import OperationalError
-from trac.perm import (PermissionSystem, DefaultPermissionPolicy,\
- PermissionCache)
+from trac.perm import (PermissionSystem, DefaultPermissionPolicy,
+ PermissionCache)
from multiproduct.api import MultiProductSystem, ProductEnvironment
Modified: bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py?rev=1469760&r1=1469759&r2=1469760&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/web_ui.py Fri Apr 19 09:38:34 2013
@@ -45,7 +45,6 @@ from bhsearch.api import (BloodhoundSear
from bhsearch.utils import get_global_env, using_multiproduct
from trac.wiki.formatter import extract_link
from multiproduct.env import ProductEnvironment
-from multiproduct.model import Product
from multiproduct.web_ui import ProductModule
SEARCH_PERMISSION = 'SEARCH_VIEW'
@@ -166,10 +165,12 @@ class RequestParameters(object):
skip_view=False,
sort=None,
skip_sort=False,
+ query=None,
skip_query=False,
product=None,
skip_product=False,
):
+ # pylint: disable=too-many-locals,too-many-branches
params = copy.deepcopy(self.params)
if skip_sort:
@@ -203,6 +204,8 @@ class RequestParameters(object):
if skip_query:
self._delete_if_exists(params, self.QUERY)
+ elif query is not None:
+ params[self.QUERY] = query
if skip_product:
self._delete_if_exists(params, self.PRODUCT)
@@ -292,6 +295,13 @@ class BloodhoundSearchModule(Component):
doc="""Quicksearch searches all products, even when used
in product env.""")
+ query_suggestions_enabled = BoolOption(
+ BHSEARCH_CONFIG_SECTION,
+ 'query_suggestions',
+ default=True,
+ doc="""Display query suggestions."""
+ )
+
# INavigationContributor methods
def get_active_navigation_item(self, req):
# pylint: disable=unused-argument
@@ -326,6 +336,7 @@ class BloodhoundSearchModule(Component):
self.all_grid_fields,
self.default_facets,
self.global_quicksearch,
+ self.query_suggestions_enabled,
)
if request_context.requires_redirect:
@@ -403,6 +414,7 @@ class RequestContext(object):
DATA_PRODUCT_LIST = 'search_product_list'
DATA_QUERY = 'query'
DATA_QUICK_JUMP = "quickjump"
+ DATA_QUERY_SUGGESTION = 'query_suggestion'
DATA_SEARCH_EXTRAS = 'extra_search_fields'
#bhsearch may support more pluggable views later
@@ -424,6 +436,7 @@ class RequestContext(object):
all_grid_fields,
default_facets,
global_quicksearch,
+ query_suggestions,
):
self.env = env
self.req = req
@@ -444,6 +457,7 @@ class RequestContext(object):
self.view = None
self.page = self.parameters.page
self.pagelen = self.parameters.pagelen
+ self.query_suggestions = query_suggestions
if self.parameters.sort:
self.sort = self.parameters.sort
@@ -750,6 +764,7 @@ class RequestContext(object):
self._prepare_results(docs, query_result.hits)
self._prepare_result_facet_counts(query_result.facets)
self._prepare_breadcrumbs()
+ self._prepare_query_suggestion(query_result.query_suggestion)
self.data[self.DATA_DEBUG] = query_result.debug
if self.parameters.debug:
self.data[self.DATA_DEBUG]['enabled'] = True
@@ -846,6 +861,7 @@ class RequestContext(object):
search_product_list = global_product + products + all_products
+ # pylint: disable=unused-variable
for prefix, name, url in search_product_list:
if prefix == self.active_product:
self.data[self.DATA_ACTIVE_PRODUCT] = name
@@ -891,3 +907,12 @@ class RequestContext(object):
self.data[self.DATA_ACTIVE_FILTER_QUERIES] = \
type_filters + active_filter_queries
self.data[self.DATA_ACTIVE_QUERY] = active_query
+
+ def _prepare_query_suggestion(self, suggestion):
+ if self.query_suggestions and suggestion is not None:
+ self.data[self.DATA_QUERY_SUGGESTION] = dict(
+ query=suggestion,
+ href=self.parameters.create_href(query=suggestion)
+ )
+ else:
+ self.data[self.DATA_QUERY_SUGGESTION] = None
Modified: bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py?rev=1469760&r1=1469759&r2=1469760&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py (original)
+++ bloodhound/trunk/bloodhound_search/bhsearch/whoosh_backend.py Fri Apr 19 09:38:34 2013
@@ -80,7 +80,9 @@ class WhooshBackend(Component):
analyzer=analysis.SimpleAnalyzer()),
security=ID(),
name=TEXT(stored=True,
- analyzer=analysis.SimpleAnalyzer())
+ analyzer=analysis.SimpleAnalyzer()),
+ query_suggestion_basket=TEXT(analyzer=analysis.SimpleAnalyzer(),
+ spelling=True),
)
max_fragment_size = IntOption('bhsearch', 'max_fragment_size', 240,
@@ -182,6 +184,7 @@ class WhooshBackend(Component):
def query(self,
query,
+ query_string=None,
sort = None,
fields = None,
filter = None,
@@ -237,6 +240,9 @@ class WhooshBackend(Component):
fields,
highlight_fields,
query_parameters)
+ if query_string is not None:
+ c = searcher.correct_query(query, query_string)
+ results.query_suggestion = c.string
try:
actual_query = unicode(query.simplify(searcher))
results.debug['actual_query'] = actual_query
Modified: bloodhound/trunk/bloodhound_search/setup.py
URL: http://svn.apache.org/viewvc/bloodhound/trunk/bloodhound_search/setup.py?rev=1469760&r1=1469759&r2=1469760&view=diff
==============================================================================
--- bloodhound/trunk/bloodhound_search/setup.py (original)
+++ bloodhound/trunk/bloodhound_search/setup.py Fri Apr 19 09:38:34 2013
@@ -134,6 +134,7 @@ ENTRY_POINTS = {
'bhsearch.search_resources.milestone_search = \
bhsearch.search_resources.milestone_search',
'bhsearch.query_parser = bhsearch.query_parser',
+ 'bhsearch.query_suggestion = bhsearch.query_suggestion',
'bhsearch.security = bhsearch.security',
'bhsearch.whoosh_backend = bhsearch.whoosh_backend',
],