diff --git a/apps/search/clients.py b/apps/search/clients.py
index 159e215e5d8..4352424ad0e 100644
--- a/apps/search/clients.py
+++ b/apps/search/clients.py
@@ -1,3 +1,6 @@
+import logging
+import socket
+
from django.conf import settings
from .sphinxapi import SphinxClient
@@ -33,6 +36,12 @@
(r'\s+',),
)
+log = logging.getLogger('k.search')
+
+
+class SearchError(Exception):
+ pass
+
class SearchClient(object):
"""
@@ -58,7 +67,44 @@ def __init__(self):
self.compiled_patterns.append(p)
- def query(self, query, filters): abstract
+ def query(self, query, filters=None):
+ """
+ Query the search index.
+ """
+
+ if filters is None:
+ filters = []
+
+ sc = self.sphinx
+ sc.ResetFilters()
+
+ sc.SetFieldWeights(self.weights)
+
+ for f in filters:
+ if f.get('range', False):
+ sc.SetFilterRange(f['filter'], f['min'],
+ f['max'], f.get('exclude', False))
+ else:
+ sc.SetFilter(f['filter'], f['value'],
+ f.get('exclude', False))
+
+
+ try:
+ result = sc.Query(query, self.index)
+ except socket.timeout:
+ log.error("Query has timed out!")
+ raise SearchError("Query has timed out!")
+ except socket.error, msg:
+ log.error("Query socket error: %s" % msg)
+ raise SearchError("Could not execute your search!")
+ except Exception, e:
+ log.error("Sphinx threw an unknown exception: %s" % e)
+ raise SearchError("Sphinx threw an unknown exception!")
+
+ if result:
+ return result['matches']
+ else:
+ return []
def excerpt(self, result, query):
"""
@@ -97,35 +143,7 @@ class ForumClient(SearchClient):
Search the forum
"""
index = 'forum_threads'
-
- def query(self, query, filters=None):
- """
- Search through forum threads
- """
-
- if filters is None:
- filters = []
-
- sc = self.sphinx
- sc.ResetFilters()
-
- sc.SetFieldWeights({'title': 4, 'content': 3})
-
- for f in filters:
- if f.get('range', False):
- sc.SetFilterRange(f['filter'], f['min'],
- f['max'], f.get('exclude', False))
- else:
- sc.SetFilter(f['filter'], f['value'],
- f.get('exclude', False))
-
-
- result = sc.Query(query, 'forum_threads')
-
- if result:
- return result['matches']
- else:
- return []
+ weights = {'title': 4, 'content': 3}
class WikiClient(SearchClient):
@@ -133,31 +151,4 @@ class WikiClient(SearchClient):
Search the knowledge base
"""
index = 'wiki_pages'
-
- def query(self, query, filters=None):
- """
- Search through the wiki (ie KB)
- """
-
- if filters is None:
- filters = []
-
- sc = self.sphinx
- sc.ResetFilters()
-
- sc.SetFieldWeights({'title': 4, 'keywords': 3})
-
- for f in filters:
- if f.get('range', False):
- sc.SetFilterRange(f['filter'], f['min'],
- f['max'], f.get('exclude', False))
- else:
- sc.SetFilter(f['filter'], f['value'],
- f.get('exclude', False))
-
- result = sc.Query(query, self.index)
-
- if result:
- return result['matches']
- else:
- return []
+ weights = {'title': 4, 'content': 3}
diff --git a/apps/search/helpers.py b/apps/search/helpers.py
index d0f0f96621a..b85fe8c889b 100644
--- a/apps/search/helpers.py
+++ b/apps/search/helpers.py
@@ -1,4 +1,5 @@
from django.conf import settings
+from django.utils.encoding import force_unicode
import jinja2
from jingo import register
@@ -44,4 +45,5 @@ def suggestions(context, string, locale='en-US'):
url = u'%s?%s' % (reverse('search'), query_string)
- return jinja2.Markup(markup.format(url=jinja2.escape(url), text=text))
+ return jinja2.Markup(markup.format(url=jinja2.escape(url),
+ text=text))
diff --git a/apps/search/sphinxapi.py b/apps/search/sphinxapi.py
index ec0f4ead259..f7d104d6015 100644
--- a/apps/search/sphinxapi.py
+++ b/apps/search/sphinxapi.py
@@ -19,6 +19,8 @@
import re
from struct import *
+# Kitsune customizations
+K_TIMEOUT = 1 # Socket timeout in seconds
# known searchd commands
SEARCHD_COMMAND_SEARCH = 0
@@ -195,12 +197,13 @@ def _Connect (self):
addr = ( self._host, self._port )
desc = '%s;%s' % addr
sock = socket.socket ( af, socket.SOCK_STREAM )
+ sock.settimeout(K_TIMEOUT)
sock.connect ( addr )
except socket.error, msg:
if sock:
sock.close()
self._error = 'connection to %s failed (%s)' % ( desc, msg )
- return
+ raise socket.error
v = unpack('>L', sock.recv(4))
if v<1:
diff --git a/apps/search/tests/test_json.py b/apps/search/tests/test_json.py
index 03fee415424..546616cbba4 100644
--- a/apps/search/tests/test_json.py
+++ b/apps/search/tests/test_json.py
@@ -4,48 +4,51 @@
from sumo.urlresolvers import reverse
+from .test_search import SphinxTestCase
-def test_json_format():
- """JSON without callback should return application/json"""
- c = Client()
- response = c.get(reverse('search'), {
- 'q': 'bookmarks',
- 'format': 'json',
- })
- eq_(response['Content-Type'], 'application/json')
-
-
-def test_json_callback_validation():
- """Various json callbacks -- validation"""
- c = Client()
- q = 'bookmarks'
- format = 'json'
-
- callbacks = (
- ('callback', 200),
- ('validCallback', 200),
- ('obj.method', 200),
- ('obj.someMethod', 200),
- ('arr[1]', 200),
- ('arr[12]', 200),
- ("alert('xss');foo", 400),
- ("eval('nastycode')", 400),
- ("someFunc()", 400),
- ('x', 200),
- ('x123', 200),
- ('$', 200),
- ('_func', 200),
- ('">', 400),
- ('">', 400),
- ('var x=something;foo', 400),
- ('var x=', 400),
- )
-
- for callback, status in callbacks:
+
+class JSONTest(SphinxTestCase):
+ def test_json_format(self):
+ """JSON without callback should return application/json"""
+ c = Client()
response = c.get(reverse('search'), {
- 'q': q,
- 'format': format,
- 'callback': callback,
+ 'q': 'bookmarks',
+ 'format': 'json',
})
- eq_(response['Content-Type'], 'application/x-javascript')
- eq_(response.status_code, status)
+ eq_(response['Content-Type'], 'application/json')
+
+
+ def test_json_callback_validation(self):
+ """Various json callbacks -- validation"""
+ c = Client()
+ q = 'bookmarks'
+ format = 'json'
+
+ callbacks = (
+ ('callback', 200),
+ ('validCallback', 200),
+ ('obj.method', 200),
+ ('obj.someMethod', 200),
+ ('arr[1]', 200),
+ ('arr[12]', 200),
+ ("alert('xss');foo", 400),
+ ("eval('nastycode')", 400),
+ ("someFunc()", 400),
+ ('x', 200),
+ ('x123', 200),
+ ('$', 200),
+ ('_func', 200),
+ ('">', 400),
+ ('">', 400),
+ ('var x=something;foo', 400),
+ ('var x=', 400),
+ )
+
+ for callback, status in callbacks:
+ response = c.get(reverse('search'), {
+ 'q': q,
+ 'format': format,
+ 'callback': callback,
+ })
+ eq_(response['Content-Type'], 'application/x-javascript')
+ eq_(response.status_code, status)
diff --git a/apps/search/tests/test_search.py b/apps/search/tests/test_search.py
index d91be2bb70a..e5d962a1cb7 100644
--- a/apps/search/tests/test_search.py
+++ b/apps/search/tests/test_search.py
@@ -9,13 +9,14 @@
from django.db import connection
from nose import SkipTest
+from nose.tools import assert_raises
import test_utils
import json
from manage import settings
from sumo.urlresolvers import reverse
from search.utils import start_sphinx, stop_sphinx, reindex
-from search.clients import WikiClient
+from search.clients import WikiClient, SearchError
def create_extra_tables():
@@ -174,3 +175,11 @@ def test_category_exclude(self):
{'q': 'audio', 'category': -13,
'format': 'json', 'w': 1})
self.assertEquals(0, json.loads(response.content)['total'])
+
+
+def test_sphinx_down():
+ """
+ Tests that the client times out when Sphinx is down.
+ """
+ wc = WikiClient()
+ assert_raises(SearchError, wc.query, 'test')
diff --git a/log_settings.py b/log_settings.py
index a58895aeae1..f063df809ff 100644
--- a/log_settings.py
+++ b/log_settings.py
@@ -6,7 +6,7 @@
# Loggers created under the "z" namespace, e.g. "z.caching", will inherit the
# configuration from the base z logger.
-log = logging.getLogger('dj')
+log = logging.getLogger('k')
fmt = '%(asctime)s %(name)s:%(levelname)s %(message)s :%(pathname)s:%(lineno)s'
fmt = getattr(settings, 'LOG_FORMAT', fmt)