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',
     ],