441 lines
19 KiB
Python
441 lines
19 KiB
Python
import datetime
|
|
|
|
from django.conf import settings
|
|
from django.test import TestCase
|
|
|
|
from haystack import connections, reset_search_queries
|
|
from haystack.models import SearchResult
|
|
from haystack.query import SearchQuerySet, SQ
|
|
|
|
from ...mocks import MockSearchResult
|
|
|
|
from ..models import MockModel, AnotherMockModel, AFourthMockModel
|
|
from ..search_indexes import MockQueryIndex, MockSearchIndex, BoostMockSearchIndex
|
|
from ..tests.test_backend import HaystackBackendTestCase
|
|
|
|
|
|
class XapianSearchQueryTestCase(HaystackBackendTestCase, TestCase):
|
|
"""
|
|
Tests the XapianSearchQuery, the class that converts SearchQuerySet queries
|
|
using the `__` notation to XapianQueries.
|
|
"""
|
|
fixtures = ['base_data.json']
|
|
|
|
def get_index(self):
|
|
return MockQueryIndex()
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.sq = connections['default'].get_query()
|
|
|
|
def test_all(self):
|
|
self.assertExpectedQuery(self.sq.build_query(), '<alldocuments>')
|
|
|
|
def test_single_word(self):
|
|
self.sq.add_filter(SQ(content='hello'))
|
|
self.assertExpectedQuery(self.sq.build_query(), '(Zhello OR hello)')
|
|
|
|
def test_single_word_not(self):
|
|
self.sq.add_filter(~SQ(content='hello'))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'(<alldocuments> AND_NOT (Zhello OR hello))')
|
|
|
|
def test_single_word_field_exact(self):
|
|
self.sq.add_filter(SQ(foo__exact='hello'))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'(XFOO^ PHRASE 3 XFOOhello PHRASE 3 XFOO$)')
|
|
|
|
def test_single_word_field_exact_not(self):
|
|
self.sq.add_filter(~SQ(foo='hello'))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'(<alldocuments> AND_NOT '
|
|
'(XFOO^ PHRASE 3 XFOOhello PHRASE 3 XFOO$))')
|
|
|
|
def test_boolean(self):
|
|
self.sq.add_filter(SQ(content=True))
|
|
self.assertExpectedQuery(self.sq.build_query(), '(Ztrue OR true)')
|
|
|
|
def test_date(self):
|
|
self.sq.add_filter(SQ(content=datetime.date(2009, 5, 8)))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'(Z2009-05-08 OR 2009-05-08)')
|
|
|
|
def test_date_not(self):
|
|
self.sq.add_filter(~SQ(content=datetime.date(2009, 5, 8)))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'(<alldocuments> AND_NOT '
|
|
'(Z2009-05-08 OR 2009-05-08))')
|
|
|
|
def test_datetime(self):
|
|
self.sq.add_filter(SQ(content=datetime.datetime(2009, 5, 8, 11, 28)))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'((Z2009-05-08 OR 2009-05-08) OR'
|
|
' (Z11:28:00 OR 11:28:00))')
|
|
|
|
def test_datetime_not(self):
|
|
self.sq.add_filter(~SQ(content=datetime.datetime(2009, 5, 8, 11, 28)))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'(<alldocuments> AND_NOT ((Z2009-05-08 OR 2009-05-08) OR (Z11:28:00 OR 11:28:00)))')
|
|
|
|
def test_float(self):
|
|
self.sq.add_filter(SQ(content=25.52))
|
|
self.assertExpectedQuery(self.sq.build_query(), '(Z25.52 OR 25.52)')
|
|
|
|
def test_multiple_words_and(self):
|
|
self.sq.add_filter(SQ(content='hello'))
|
|
self.sq.add_filter(SQ(content='world'))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'((Zhello OR hello) AND (Zworld OR world))')
|
|
|
|
def test_multiple_words_not(self):
|
|
self.sq.add_filter(~SQ(content='hello'))
|
|
self.sq.add_filter(~SQ(content='world'))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'((<alldocuments> AND_NOT (Zhello OR hello)) AND'
|
|
' (<alldocuments> AND_NOT (Zworld OR world)))')
|
|
|
|
def test_multiple_words_or(self):
|
|
self.sq.add_filter(SQ(content='hello') | SQ(content='world'))
|
|
self.assertExpectedQuery(
|
|
self.sq.build_query(),
|
|
'((Zhello OR hello) OR (Zworld OR world))')
|
|
|
|
def test_multiple_words_or_not(self):
|
|
self.sq.add_filter(~SQ(content='hello') | ~SQ(content='world'))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'((<alldocuments> AND_NOT (Zhello OR hello)) OR'
|
|
' (<alldocuments> AND_NOT (Zworld OR world)))')
|
|
|
|
def test_multiple_words_mixed(self):
|
|
self.sq.add_filter(SQ(content='why') | SQ(content='hello'))
|
|
self.sq.add_filter(~SQ(content='world'))
|
|
self.assertExpectedQuery(
|
|
self.sq.build_query(),
|
|
'(((Zwhi OR why) OR (Zhello OR hello)) AND '
|
|
'(<alldocuments> AND_NOT (Zworld OR world)))')
|
|
|
|
def test_multiple_word_field_exact(self):
|
|
self.sq.add_filter(SQ(foo='hello'))
|
|
self.sq.add_filter(SQ(title='world'))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'((XFOO^ PHRASE 3 XFOOhello PHRASE 3 XFOO$) AND'
|
|
' (XTITLE^ PHRASE 3 XTITLEworld PHRASE 3 XTITLE$))')
|
|
|
|
def test_multiple_word_field_exact_not(self):
|
|
self.sq.add_filter(~SQ(foo='hello'))
|
|
self.sq.add_filter(~SQ(title='world'))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'((<alldocuments> AND_NOT (XFOO^ PHRASE 3 XFOOhello PHRASE 3 XFOO$)) AND'
|
|
' (<alldocuments> AND_NOT (XTITLE^ PHRASE 3 XTITLEworld PHRASE 3 XTITLE$)))')
|
|
|
|
def test_or(self):
|
|
self.sq.add_filter(SQ(content='hello world'))
|
|
self.assertExpectedQuery(
|
|
self.sq.build_query(), '((Zhello OR hello) OR (Zworld OR world))')
|
|
|
|
def test_not_or(self):
|
|
self.sq.add_filter(~SQ(content='hello world'))
|
|
self.assertExpectedQuery(
|
|
self.sq.build_query(),
|
|
'(<alldocuments> AND_NOT ((Zhello OR hello) OR (Zworld OR world)))')
|
|
|
|
def test_boost(self):
|
|
self.sq.add_filter(SQ(content='hello'))
|
|
self.sq.add_boost('world', 5)
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'((Zhello OR hello) AND_MAYBE'
|
|
' 5 * (Zworld OR world))')
|
|
|
|
def test_not_in_filter_single_words(self):
|
|
self.sq.add_filter(SQ(content='why'))
|
|
self.sq.add_filter(~SQ(title__in=["Dune", "Jaws"]))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'((Zwhi OR why) AND '
|
|
'(<alldocuments> AND_NOT ('
|
|
'(XTITLE^ PHRASE 3 XTITLEdune PHRASE 3 XTITLE$) OR '
|
|
'(XTITLE^ PHRASE 3 XTITLEjaws PHRASE 3 XTITLE$))))')
|
|
|
|
def test_in_filter_multiple_words(self):
|
|
self.sq.add_filter(SQ(content='why'))
|
|
self.sq.add_filter(SQ(title__in=["A Famous Paper", "An Infamous Article"]))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'((Zwhi OR why) AND ((XTITLE^ PHRASE 5 XTITLEa PHRASE 5 '
|
|
'XTITLEfamous PHRASE 5 XTITLEpaper PHRASE 5 XTITLE$) OR '
|
|
'(XTITLE^ PHRASE 5 XTITLEan PHRASE 5 XTITLEinfamous PHRASE 5 '
|
|
'XTITLEarticle PHRASE 5 XTITLE$)))')
|
|
|
|
def test_in_filter_multiple_words_with_punctuation(self):
|
|
self.sq.add_filter(SQ(title__in=["A Famous Paper", "An Infamous Article", "My Store Inc."]))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'((XTITLE^ PHRASE 5 XTITLEa PHRASE 5 XTITLEfamous PHRASE 5'
|
|
' XTITLEpaper PHRASE 5 XTITLE$) OR '
|
|
'(XTITLE^ PHRASE 5 XTITLEan PHRASE 5 XTITLEinfamous PHRASE 5'
|
|
' XTITLEarticle PHRASE 5 XTITLE$) OR '
|
|
'(XTITLE^ PHRASE 5 XTITLEmy PHRASE 5 XTITLEstore PHRASE 5'
|
|
' XTITLEinc. PHRASE 5 XTITLE$))')
|
|
|
|
def test_not_in_filter_multiple_words(self):
|
|
self.sq.add_filter(SQ(content='why'))
|
|
self.sq.add_filter(~SQ(title__in=["A Famous Paper", "An Infamous Article"]))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'((Zwhi OR why) AND (<alldocuments> AND_NOT '
|
|
'((XTITLE^ PHRASE 5 XTITLEa PHRASE 5 XTITLEfamous PHRASE 5 '
|
|
'XTITLEpaper PHRASE 5 XTITLE$) OR (XTITLE^ PHRASE 5 '
|
|
'XTITLEan PHRASE 5 XTITLEinfamous PHRASE 5 '
|
|
'XTITLEarticle PHRASE 5 XTITLE$))))')
|
|
|
|
def test_in_filter_datetime(self):
|
|
self.sq.add_filter(SQ(content='why'))
|
|
self.sq.add_filter(SQ(pub_date__in=[datetime.datetime(2009, 7, 6, 1, 56, 21)]))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'((Zwhi OR why) AND '
|
|
'(XPUB_DATE2009-07-06 AND_MAYBE XPUB_DATE01:56:21))')
|
|
|
|
def test_clean(self):
|
|
self.assertEqual(self.sq.clean('hello world'), 'hello world')
|
|
self.assertEqual(self.sq.clean('hello AND world'), 'hello AND world')
|
|
self.assertEqual(self.sq.clean('hello AND OR NOT TO + - && || ! ( ) { } [ ] ^ " ~ * ? : \ world'),
|
|
'hello AND OR NOT TO + - && || ! ( ) { } [ ] ^ " ~ * ? : \ world')
|
|
self.assertEqual(self.sq.clean('so please NOTe i am in a bAND and bORed'),
|
|
'so please NOTe i am in a bAND and bORed')
|
|
|
|
def test_with_models(self):
|
|
self.sq.add_filter(SQ(content='hello'))
|
|
self.sq.add_model(MockModel)
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'((Zhello OR hello) AND '
|
|
'0 * CONTENTTYPEcore.mockmodel)')
|
|
|
|
self.sq.add_model(AnotherMockModel)
|
|
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
['((Zhello OR hello) AND '
|
|
'(0 * CONTENTTYPEcore.mockmodel OR'
|
|
' 0 * CONTENTTYPEcore.anothermockmodel))',
|
|
'((Zhello OR hello) AND '
|
|
'(0 * CONTENTTYPEcore.anothermockmodel OR'
|
|
' 0 * CONTENTTYPEcore.mockmodel))'])
|
|
|
|
def test_with_punctuation(self):
|
|
self.sq.add_filter(SQ(content='http://www.example.com'))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'(Zhttp://www.example.com OR'
|
|
' http://www.example.com)')
|
|
|
|
def test_in_filter_values_list(self):
|
|
self.sq.add_filter(SQ(content='why'))
|
|
self.sq.add_filter(SQ(title__in=MockModel.objects.values_list('id',
|
|
flat=True)))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'((Zwhi OR why) AND ('
|
|
'(XTITLE^ PHRASE 3 XTITLE1 PHRASE 3 XTITLE$) OR '
|
|
'(XTITLE^ PHRASE 3 XTITLE2 PHRASE 3 XTITLE$) OR '
|
|
'(XTITLE^ PHRASE 3 XTITLE3 PHRASE 3 XTITLE$)))')
|
|
|
|
def test_content_type(self):
|
|
self.sq.add_filter(SQ(django_ct='time'))
|
|
self.assertExpectedQuery(self.sq.build_query(), 'CONTENTTYPEtime')
|
|
|
|
def test_unphrased_id(self):
|
|
'An internal ID should NOT be phrased so one can exclude IDs.'
|
|
self.sq.add_filter(SQ(id__in=['testing123', 'testing456']))
|
|
expected = '(Qtesting123 OR Qtesting456)'
|
|
self.assertExpectedQuery(
|
|
query=self.sq.build_query(), string_or_list=expected)
|
|
|
|
|
|
class SearchQueryTestCase(HaystackBackendTestCase, TestCase):
|
|
"""
|
|
Tests expected behavior of
|
|
SearchQuery.
|
|
"""
|
|
fixtures = ['base_data.json']
|
|
|
|
def get_index(self):
|
|
return MockSearchIndex()
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
self.backend.update(self.index, MockModel.objects.all())
|
|
|
|
self.sq = connections['default'].get_query()
|
|
|
|
def test_get_spelling(self):
|
|
self.sq.add_filter(SQ(content='indxd'))
|
|
self.assertEqual(self.sq.get_spelling_suggestion(), 'indexed')
|
|
self.assertEqual(self.sq.get_spelling_suggestion('indxd'), 'indexed')
|
|
|
|
def test_contains(self):
|
|
self.sq.add_filter(SQ(content='circular'))
|
|
self.sq.add_filter(SQ(title__contains='haystack'))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'((Zcircular OR circular) AND '
|
|
'(ZXTITLEhaystack OR XTITLEhaystack))')
|
|
|
|
def test_startswith(self):
|
|
self.sq.add_filter(SQ(name__startswith='da'))
|
|
self.assertEqual([result.pk for result in self.sq.get_results()], [1, 2, 3])
|
|
|
|
def test_endswith(self):
|
|
with self.assertRaises(NotImplementedError):
|
|
self.sq.add_filter(SQ(name__endswith='el2'))
|
|
self.sq.get_results()
|
|
|
|
def test_gt(self):
|
|
self.sq.add_filter(SQ(name__gt='m'))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'(<alldocuments> AND_NOT VALUE_RANGE 3 a m)')
|
|
|
|
def test_gte(self):
|
|
self.sq.add_filter(SQ(name__gte='m'))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'VALUE_RANGE 3 m zzzzzzzzzzzzzzzzzzzzzzzzzzzz'
|
|
'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'
|
|
'zzzzzzzzzzzzzzzzzzzzzzzzzzzz')
|
|
|
|
def test_lt(self):
|
|
self.sq.add_filter(SQ(name__lt='m'))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'(<alldocuments> AND_NOT VALUE_RANGE 3 m '
|
|
'zzzzzzzzzzzzzzzzzzzzzzzzzzzz'
|
|
'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'
|
|
'zzzzzzzzzzzzzzzzzzzzzzzzzzzz)')
|
|
|
|
def test_lte(self):
|
|
self.sq.add_filter(SQ(name__lte='m'))
|
|
self.assertExpectedQuery(self.sq.build_query(), 'VALUE_RANGE 3 a m')
|
|
|
|
def test_range(self):
|
|
self.sq.add_filter(SQ(django_id__range=[2, 4]))
|
|
self.assertExpectedQuery(self.sq.build_query(), 'VALUE_RANGE 1 000000000002 000000000004')
|
|
self.sq.add_filter(~SQ(django_id__range=[0, 2]))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'(VALUE_RANGE 1 000000000002 000000000004 AND '
|
|
'(<alldocuments> AND_NOT VALUE_RANGE 1 000000000000 000000000002))')
|
|
self.assertEqual([result.pk for result in self.sq.get_results()], [3])
|
|
|
|
def test_multiple_filter_types(self):
|
|
self.sq.add_filter(SQ(content='why'))
|
|
self.sq.add_filter(SQ(pub_date__lte=datetime.datetime(2009, 2, 10, 1, 59, 0)))
|
|
self.sq.add_filter(SQ(name__gt='david'))
|
|
self.sq.add_filter(SQ(title__gte='B'))
|
|
self.sq.add_filter(SQ(django_id__in=[1, 2, 3]))
|
|
self.assertExpectedQuery(self.sq.build_query(),
|
|
'((Zwhi OR why) AND'
|
|
' VALUE_RANGE 5 00010101000000 20090210015900 AND'
|
|
' (<alldocuments> AND_NOT VALUE_RANGE 3 a david)'
|
|
' AND VALUE_RANGE 7 b zzzzzzzzzzzzzzzzzzzzzzzzzzz'
|
|
'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'
|
|
'zzzzzzzzzzzzzzzzzzzzzzzzz AND'
|
|
' (QQ000000000001 OR QQ000000000002 OR QQ000000000003))')
|
|
|
|
def test_log_query(self):
|
|
reset_search_queries()
|
|
self.assertEqual(len(connections['default'].queries), 0)
|
|
|
|
# Stow.
|
|
old_debug = settings.DEBUG
|
|
settings.DEBUG = False
|
|
|
|
len(self.sq.get_results())
|
|
self.assertEqual(len(connections['default'].queries), 0)
|
|
|
|
settings.DEBUG = True
|
|
# Redefine it to clear out the cached results.
|
|
self.sq = connections['default'].get_query()
|
|
self.sq.add_filter(SQ(name='bar'))
|
|
len(self.sq.get_results())
|
|
self.assertEqual(len(connections['default'].queries), 1)
|
|
self.assertExpectedQuery(connections['default'].queries[0]['query_string'],
|
|
'(XNAME^ PHRASE 3 XNAMEbar PHRASE 3 XNAME$)')
|
|
|
|
# And again, for good measure.
|
|
self.sq = connections['default'].get_query()
|
|
self.sq.add_filter(SQ(name='bar'))
|
|
self.sq.add_filter(SQ(text='moof'))
|
|
len(self.sq.get_results())
|
|
self.assertEqual(len(connections['default'].queries), 2)
|
|
self.assertExpectedQuery(connections['default'].queries[0]['query_string'],
|
|
'(XNAME^ PHRASE 3 XNAMEbar PHRASE 3 XNAME$)')
|
|
self.assertExpectedQuery(connections['default'].queries[1]['query_string'],
|
|
'((XNAME^ PHRASE 3 XNAMEbar PHRASE 3 XNAME$) AND'
|
|
' (XTEXT^ PHRASE 3 XTEXTmoof PHRASE 3 XTEXT$))')
|
|
|
|
# Restore.
|
|
settings.DEBUG = old_debug
|
|
|
|
|
|
class LiveSearchQuerySetTestCase(HaystackBackendTestCase, TestCase):
|
|
"""
|
|
SearchQuerySet specific tests
|
|
"""
|
|
fixtures = ['base_data.json']
|
|
|
|
def get_index(self):
|
|
return MockSearchIndex()
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
self.backend.update(self.index, MockModel.objects.all())
|
|
self.sq = connections['default'].get_query()
|
|
self.sqs = SearchQuerySet()
|
|
|
|
def test_result_class(self):
|
|
# Assert that we're defaulting to ``SearchResult``.
|
|
sqs = self.sqs.all()
|
|
self.assertTrue(isinstance(sqs[0], SearchResult))
|
|
|
|
# Custom class.
|
|
sqs = self.sqs.result_class(MockSearchResult).all()
|
|
self.assertTrue(isinstance(sqs[0], MockSearchResult))
|
|
|
|
# Reset to default.
|
|
sqs = self.sqs.result_class(None).all()
|
|
self.assertTrue(isinstance(sqs[0], SearchResult))
|
|
|
|
def test_facet(self):
|
|
self.assertEqual(len(self.sqs.facet('name').facet_counts()['fields']['name']), 3)
|
|
|
|
|
|
class BoostFieldTestCase(HaystackBackendTestCase, TestCase):
|
|
"""
|
|
Tests boosted fields.
|
|
"""
|
|
|
|
def get_index(self):
|
|
return BoostMockSearchIndex()
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
self.sample_objs = []
|
|
for i in range(1, 5):
|
|
mock = AFourthMockModel()
|
|
mock.id = i
|
|
if i % 2:
|
|
mock.author = 'daniel'
|
|
mock.editor = 'david'
|
|
else:
|
|
mock.author = 'david'
|
|
mock.editor = 'daniel'
|
|
mock.pub_date = datetime.date(2009, 2, 25) - datetime.timedelta(days=i)
|
|
self.sample_objs.append(mock)
|
|
|
|
self.backend.update(self.index, self.sample_objs)
|
|
|
|
def test_boost(self):
|
|
sqs = SearchQuerySet()
|
|
|
|
self.assertEqual(len(sqs.all()), 4)
|
|
|
|
results = sqs.filter(SQ(author='daniel') | SQ(editor='daniel'))
|
|
|
|
self.assertEqual([result.id for result in results], [
|
|
'core.afourthmockmodel.1',
|
|
'core.afourthmockmodel.3',
|
|
'core.afourthmockmodel.2',
|
|
'core.afourthmockmodel.4'
|
|
])
|