Merged branch 'issue123' - Adds compatibility to Haystack 2.X.

This commit is contained in:
Jorge C. Leitão 2014-05-21 07:16:12 +02:00
commit 85cf58064f
7 changed files with 1449 additions and 772 deletions

View file

@ -1,2 +1,12 @@
# Copyright (C) 2009, 2010, 2011, 2012 David Sauve
# Copyright (C) 2009, 2010 Trapeze
from django.db import models
class Document(models.Model):
type_name = models.CharField(max_length=50)
number = models.IntegerField()
name = models.CharField(max_length=200)
date = models.DateField()
summary = models.TextField()
text = models.TextField()

View file

@ -0,0 +1,26 @@
from haystack import indexes
from . import models
class DocumentIndex(indexes.SearchIndex):
text = indexes.CharField(document=True)
summary = indexes.CharField(model_attr='summary')
type_name = indexes.CharField(model_attr='type_name')
number = indexes.IntegerField(model_attr='number')
name = indexes.CharField(model_attr='name')
date = indexes.DateField(model_attr='date')
tags = indexes.MultiValueField()
def get_model(self):
return models.Document()
def prepare_tags(self, obj):
l = [['tag', 'tag-test', 'tag-test-test'],
['tag', 'tag-test'],
['tag']]
return l[obj.id % 3]

View file

@ -6,27 +6,29 @@ import xapian
import subprocess
import os
from django.conf import settings
from django.db import models
from django.test import TestCase
from haystack import connections, reset_search_queries
from haystack import connections
from haystack import indexes
from haystack.backends.xapian_backend import InvalidIndexError, _marshal_value
from haystack.models import SearchResult
from haystack.query import SearchQuerySet, SQ
from haystack.backends.xapian_backend import InvalidIndexError, _term_to_xapian_value
from haystack.utils.loading import UnifiedIndex
from core.models import MockTag, MockModel, AnotherMockModel, AFourthMockModel
from core.models import MockTag, MockModel, AnotherMockModel
from core.tests.mocks import MockSearchResult
def get_terms(backend, *args):
result = subprocess.check_output(['delve'] + list(args) + [backend.path], env=os.environ.copy())
result = subprocess.check_output(['delve'] + list(args) + [backend.path],
env=os.environ.copy()).decode('utf-8')
result = result.split(": ")[1].strip()
return result.split(" ")
def pks(results):
return [result.pk for result in results]
class XapianMockModel(models.Model):
"""
Same as tests.core.MockModel with a few extra fields for testing various
@ -74,7 +76,7 @@ class XapianMockSearchIndex(indexes.SearchIndex):
return XapianMockModel
def prepare_sites(self, obj):
return ['%d' % (i * obj.id) for i in xrange(1, 4)]
return ['%d' % (i * obj.id) for i in range(1, 4)]
def prepare_tags(self, obj):
if obj.id == 1:
@ -85,7 +87,7 @@ class XapianMockSearchIndex(indexes.SearchIndex):
return ['an', 'to', 'or']
def prepare_keys(self, obj):
return [i * obj.id for i in xrange(1, 4)]
return [i * obj.id for i in range(1, 4)]
def prepare_titles(self, obj):
if obj.id == 1:
@ -105,7 +107,17 @@ class XapianMockSearchIndex(indexes.SearchIndex):
class XapianSimpleMockIndex(indexes.SearchIndex):
text = indexes.CharField(document=True)
author = indexes.CharField(model_attr='author')
pub_date = indexes.DateTimeField(model_attr='pub_date')
url = indexes.CharField()
non_anscii = indexes.CharField()
datetime = indexes.DateTimeField(model_attr='pub_date')
date = indexes.DateField()
number = indexes.IntegerField()
float_number = indexes.FloatField()
decimal_number = indexes.DecimalField()
multi_value = indexes.MultiValueField()
def get_model(self):
return MockModel
@ -113,11 +125,38 @@ class XapianSimpleMockIndex(indexes.SearchIndex):
def prepare_text(self, obj):
return 'this_is_a_word'
def prepare_author(self, obj):
return 'david'
def prepare_url(self, obj):
return 'http://example.com/1/'
def prepare_non_anscii(self, obj):
return 'thsi sdas das corrup\xe7\xe3o das'
def prepare_datetime(self, obj):
return datetime.datetime(2009, 2, 25, 1, 1, 1)
def prepare_date(self, obj):
return datetime.date(2008, 8, 8)
def prepare_number(self, obj):
return 123456789
def prepare_float_number(self, obj):
return 123.123456789
def prepare_decimal_number(self, obj):
return '22.34'
def prepare_multi_value(self, obj):
return ['tag', 'tag-test', 'tag-test-test']
class HaystackBackendTestCase(object):
"""
An abstract TestCase that implements a hack to ensure connections
has the mock index
Abstract TestCase that implements an hack to ensure `connections`
has the right index
It has a method get_index() that returns a SearchIndex
that must be overwritten.
@ -141,40 +180,23 @@ class HaystackBackendTestCase(object):
connections['default']._index = self.old_ui
class XapianBackendTestCase(HaystackBackendTestCase, TestCase):
class BackendIndexationTestCase(HaystackBackendTestCase, TestCase):
"""
Tests indexation behavior.
Tests related to how the backend indexes terms,
values, and others go here.
"""
def get_index(self):
return XapianSimpleMockIndex()
def setUp(self):
super(XapianBackendTestCase, self).setUp()
super(BackendIndexationTestCase, self).setUp()
mock = XapianMockModel()
mock.id = 1
mock.author = 'david'
mock.pub_date = datetime.date(2009, 2, 25)
self.backend.update(self.index, [mock])
def test_fields(self):
"""
Tests that all fields are in the database
"""
terms = get_terms(self.backend, '-a')
for field in ['author', 'pub_date', 'text']:
is_inside = False
for term in terms:
if "X%s" % field.upper() in term:
is_inside = True
break
self.assertTrue(is_inside, field)
def test_text(self):
terms = get_terms(self.backend, '-a')
self.assertTrue('this_is_a_word' in terms)
self.assertTrue('Zthis_is_a_word' in terms)
def test_app_is_not_split(self):
"""
Tests that the app path is not split
@ -195,18 +217,117 @@ class XapianBackendTestCase(HaystackBackendTestCase, TestCase):
self.assertFalse('xapianmockmodel' in terms)
self.assertFalse('tests' in terms)
def test_fields_exist(self):
"""
Tests that all fields are in the database
"""
terms = get_terms(self.backend, '-a')
for field in ['author', 'datetime', 'text', 'url']:
is_inside = False
for term in terms:
if term.startswith("X%s" % field.upper()):
is_inside = True
break
self.assertTrue(is_inside, field)
class XapianSearchBackendTestCase(HaystackBackendTestCase, TestCase):
def test_text_field(self):
terms = get_terms(self.backend, '-a')
self.assertTrue('this_is_a_word' in terms)
self.assertTrue('Zthis_is_a_word' in terms)
self.assertTrue('ZXTEXTthis_is_a_word' in terms)
self.assertTrue('XTEXTthis_is_a_word' in terms)
def test_author_field(self):
terms = get_terms(self.backend, '-a')
self.assertTrue('XAUTHORdavid' in terms)
self.assertTrue('ZXAUTHORdavid' in terms)
self.assertTrue('Zdavid' in terms)
self.assertTrue('david' in terms)
def test_datetime_field(self):
terms = get_terms(self.backend, '-a')
self.assertFalse('XDATETIME20090225000000' in terms)
self.assertFalse('ZXDATETIME20090225000000' in terms)
self.assertFalse('20090225000000' in terms)
self.assertTrue('XDATETIME2009-02-25' in terms)
self.assertTrue('2009-02-25' in terms)
self.assertTrue('01:01:01' in terms)
self.assertTrue('XDATETIME01:01:01' in terms)
def test_date_field(self):
terms = get_terms(self.backend, '-a')
self.assertTrue('XDATE2008-08-08' in terms)
self.assertTrue('2008-08-08' in terms)
self.assertFalse('XDATE00:00:00' in terms)
self.assertFalse('00:00:00' in terms)
def test_url_field(self):
terms = get_terms(self.backend, '-a')
self.assertTrue('http://example.com/1/' in terms)
def test_integer_field(self):
terms = get_terms(self.backend, '-a')
self.assertTrue('123456789' in terms)
self.assertTrue('XNUMBER123456789' in terms)
self.assertFalse('ZXNUMBER123456789' in terms)
def test_float_field(self):
terms = get_terms(self.backend, '-a')
self.assertTrue('123.123456789' in terms)
self.assertTrue('XFLOAT_NUMBER123.123456789' in terms)
self.assertFalse('ZXFLOAT_NUMBER123.123456789' in terms)
def test_decimal_field(self):
terms = get_terms(self.backend, '-a')
self.assertTrue('22.34' in terms)
self.assertTrue('XDECIMAL_NUMBER22.34' in terms)
self.assertFalse('ZXDECIMAL_NUMBER22.34' in terms)
def test_multivalue_field(self):
"""
Regression test for #103
"""
terms = get_terms(self.backend, '-a')
self.assertTrue('tag' in terms)
self.assertTrue('tag-test' in terms)
self.assertTrue('tag-test-test' in terms)
self.assertTrue('XMULTI_VALUEtag' in terms)
self.assertTrue('XMULTI_VALUEtag-test' in terms)
self.assertTrue('XMULTI_VALUEtag-test-test' in terms)
# these and only these terms
# 3 for the exact term (^{term}$)
self.assertEqual(len([term for term in terms if term.startswith('XMULTI_VALUE')]), 6)
# no stem for exact multivalues.
self.assertEqual(len([term for term in terms if term.startswith('ZXMULTI_VALUE')]), 0)
def test_non_ascii_chars(self):
terms = get_terms(self.backend, '-a')
self.assertIn('corrup\xe7\xe3o', terms)
class BackendFeaturesTestCase(HaystackBackendTestCase, TestCase):
"""
Tests supported features on the backend side.
Tests to features implemented on the backend
go here.
"""
def get_index(self):
return XapianMockSearchIndex()
def setUp(self):
super(XapianSearchBackendTestCase, self).setUp()
super(BackendFeaturesTestCase, self).setUp()
self.sample_objs = []
for i in xrange(1, 4):
for i in range(1, 4):
mock = XapianMockModel()
mock.id = i
mock.author = 'david%s' % i
@ -225,19 +346,20 @@ class XapianSearchBackendTestCase(HaystackBackendTestCase, TestCase):
self.backend.update(self.index, self.sample_objs)
def test_update(self):
self.assertEqual(self.backend.document_count(), 3)
self.assertEqual([result.pk for result in self.backend.search(xapian.Query(''))['results']], [1, 2, 3])
self.assertEqual(pks(self.backend.search(xapian.Query(''))['results']),
[1, 2, 3])
def test_duplicate_update(self):
# Duplicates should be updated, not appended -- http://github.com/notanumber/xapian-haystack/issues/#issue/6
"""
Regression test for #6.
"""
self.backend.update(self.index, self.sample_objs)
self.assertEqual(self.backend.document_count(), 3)
def test_remove(self):
self.backend.remove(self.sample_objs[0])
self.assertEqual(self.backend.document_count(), 2)
self.assertEqual([result.pk for result in self.backend.search(xapian.Query(''))['results']], [2, 3])
self.assertEqual(pks(self.backend.search(xapian.Query(''))['results']),
[2, 3])
def test_clear(self):
self.backend.clear()
@ -259,18 +381,20 @@ class XapianSearchBackendTestCase(HaystackBackendTestCase, TestCase):
self.assertEqual(self.backend.document_count(), 0)
def test_search(self):
# no match query
self.assertEqual(self.backend.search(xapian.Query()), {'hits': 0, 'results': []})
self.assertEqual(self.backend.search(xapian.Query(''))['hits'], 3)
self.assertEqual([result.pk for result in self.backend.search(xapian.Query(''))['results']], [1, 2, 3])
self.assertEqual(self.backend.search(xapian.Query('indexed'))['hits'], 3)
self.assertEqual([result.pk for result in self.backend.search(xapian.Query(''))['results']], [1, 2, 3])
# all match query
self.assertEqual(pks(self.backend.search(xapian.Query(''))['results']),
[1, 2, 3])
# Ensure that swapping the ``result_class`` works.
self.assertTrue(isinstance(self.backend.search(xapian.Query('indexed'), result_class=MockSearchResult)['results'][0], MockSearchResult))
# Other `result_class`
self.assertTrue(isinstance(self.backend.search(xapian.Query('indexed'),
result_class=MockSearchResult)['results'][0],
MockSearchResult))
def test_search_field_with_punctuation(self):
# self.assertEqual(self.backend.search(xapian.Query('http://example.com/'))['hits'], 3)
self.assertEqual([result.pk for result in self.backend.search(xapian.Query('http://example.com/1/'))['results']], [1])
self.assertEqual(pks(self.backend.search(xapian.Query('http://example.com/1/'))['results']),
[1])
def test_search_by_mvf(self):
self.assertEqual(self.backend.search(xapian.Query('ab'))['hits'], 1)
@ -279,22 +403,39 @@ class XapianSearchBackendTestCase(HaystackBackendTestCase, TestCase):
self.assertEqual(self.backend.search(xapian.Query('one'))['hits'], 3)
def test_field_facets(self):
self.assertEqual(self.backend.search(xapian.Query(), facets=['name']), {'hits': 0, 'results': []})
self.assertEqual(self.backend.search(xapian.Query(), facets=['name']),
{'hits': 0, 'results': []})
results = self.backend.search(xapian.Query('indexed'), facets=['name'])
self.assertEqual(results['hits'], 3)
self.assertEqual(results['facets']['fields']['name'], [('david1', 1), ('david2', 1), ('david3', 1)])
self.assertEqual(results['facets']['fields']['name'],
[('david1', 1), ('david2', 1), ('david3', 1)])
results = self.backend.search(xapian.Query('indexed'), facets=['flag'])
self.assertEqual(results['hits'], 3)
self.assertEqual(results['facets']['fields']['flag'], [(False, 1), (True, 2)])
self.assertEqual(results['facets']['fields']['flag'],
[(False, 1), (True, 2)])
results = self.backend.search(xapian.Query('indexed'), facets=['sites'])
self.assertEqual(results['hits'], 3)
self.assertEqual(results['facets']['fields']['sites'], [('1', 1), ('3', 2), ('2', 2), ('4', 1), ('6', 2), ('9', 1)])
self.assertEqual(results['facets']['fields']['sites'],
[('1', 1), ('3', 2), ('2', 2), ('4', 1), ('6', 2), ('9', 1)])
def test_raise_index_error_on_wrong_field(self):
"""
Regression test for #109.
"""
self.assertRaises(InvalidIndexError, self.backend.search, xapian.Query(''), facets=['dsdas'])
def test_date_facets(self):
self.assertEqual(self.backend.search(xapian.Query(), date_facets={'pub_date': {'start_date': datetime.datetime(2008, 10, 26), 'end_date': datetime.datetime(2009, 3, 26), 'gap_by': 'month'}}), {'hits': 0, 'results': []})
results = self.backend.search(xapian.Query('indexed'), date_facets={'pub_date': {'start_date': datetime.datetime(2008, 10, 26), 'end_date': datetime.datetime(2009, 3, 26), 'gap_by': 'month'}})
facets = {'pub_date': {'start_date': datetime.datetime(2008, 10, 26),
'end_date': datetime.datetime(2009, 3, 26),
'gap_by': 'month'}}
self.assertEqual(self.backend.search(xapian.Query(), date_facets=facets),
{'hits': 0, 'results': []})
results = self.backend.search(xapian.Query('indexed'), date_facets=facets)
self.assertEqual(results['hits'], 3)
self.assertEqual(results['facets']['dates']['pub_date'], [
('2009-02-26T00:00:00', 0),
@ -304,7 +445,11 @@ class XapianSearchBackendTestCase(HaystackBackendTestCase, TestCase):
('2008-10-26T00:00:00', 0),
])
results = self.backend.search(xapian.Query('indexed'), date_facets={'pub_date': {'start_date': datetime.datetime(2009, 02, 01), 'end_date': datetime.datetime(2009, 3, 15), 'gap_by': 'day', 'gap_amount': 15}})
facets = {'pub_date': {'start_date': datetime.datetime(2009, 2, 1),
'end_date': datetime.datetime(2009, 3, 15),
'gap_by': 'day',
'gap_amount': 15}}
results = self.backend.search(xapian.Query('indexed'), date_facets=facets)
self.assertEqual(results['hits'], 3)
self.assertEqual(results['facets']['dates']['pub_date'], [
('2009-03-03T00:00:00', 0),
@ -313,111 +458,131 @@ class XapianSearchBackendTestCase(HaystackBackendTestCase, TestCase):
])
def test_query_facets(self):
self.assertEqual(self.backend.search(xapian.Query(), query_facets={'name': 'da*'}), {'hits': 0, 'results': []})
self.assertEqual(self.backend.search(xapian.Query(), query_facets={'name': 'da*'}),
{'hits': 0, 'results': []})
results = self.backend.search(xapian.Query('indexed'), query_facets={'name': 'da*'})
self.assertEqual(results['hits'], 3)
self.assertEqual(results['facets']['queries']['name'], ('da*', 3))
def test_narrow_queries(self):
self.assertEqual(self.backend.search(xapian.Query(), narrow_queries={'name:david1'}), {'hits': 0, 'results': []})
self.assertEqual(self.backend.search(xapian.Query(), narrow_queries={'name:david1'}),
{'hits': 0, 'results': []})
results = self.backend.search(xapian.Query('indexed'), narrow_queries={'name:david1'})
self.assertEqual(results['hits'], 1)
def test_highlight(self):
self.assertEqual(self.backend.search(xapian.Query(), highlight=True), {'hits': 0, 'results': []})
self.assertEqual(self.backend.search(xapian.Query(), highlight=True),
{'hits': 0, 'results': []})
self.assertEqual(self.backend.search(xapian.Query('indexed'), highlight=True)['hits'], 3)
self.assertEqual([result.highlighted['text'] for result in self.backend.search(xapian.Query('indexed'), highlight=True)['results']], ['<em>indexed</em>!\n1', '<em>indexed</em>!\n2', '<em>indexed</em>!\n3'])
results = self.backend.search(xapian.Query('indexed'), highlight=True)['results']
self.assertEqual([result.highlighted['text'] for result in results],
['<em>indexed</em>!\n1', '<em>indexed</em>!\n2', '<em>indexed</em>!\n3'])
def test_spelling_suggestion(self):
self.assertEqual(self.backend.search(xapian.Query('indxe'))['hits'], 0)
self.assertEqual(self.backend.search(xapian.Query('indxe'))['spelling_suggestion'], 'indexed')
self.assertEqual(self.backend.search(xapian.Query('indxe'))['spelling_suggestion'],
'indexed')
self.assertEqual(self.backend.search(xapian.Query('indxed'))['hits'], 0)
self.assertEqual(self.backend.search(xapian.Query('indxed'))['spelling_suggestion'], 'indexed')
self.assertEqual(self.backend.search(xapian.Query('indxed'))['spelling_suggestion'],
'indexed')
self.assertEqual(self.backend.search(xapian.Query('foo'))['hits'], 0)
self.assertEqual(self.backend.search(xapian.Query('foo'), spelling_query='indexy')['spelling_suggestion'], 'indexed')
self.assertEqual(self.backend.search(xapian.Query('foo'), spelling_query='indexy')['spelling_suggestion'],
'indexed')
self.assertEqual(self.backend.search(xapian.Query('XNAMEdavid'))['hits'], 0)
self.assertEqual(self.backend.search(xapian.Query('XNAMEdavid'))['spelling_suggestion'], 'david1')
self.assertEqual(self.backend.search(xapian.Query('XNAMEdavid'))['spelling_suggestion'],
'david1')
def test_more_like_this(self):
results = self.backend.more_like_this(self.sample_objs[0])
self.assertEqual(results['hits'], 2)
self.assertEqual([result.pk for result in results['results']], [3, 2])
results = self.backend.more_like_this(self.sample_objs[0], additional_query=xapian.Query('david3'))
self.assertEqual(results['hits'], 1)
self.assertEqual([result.pk for result in results['results']], [3])
self.assertEqual(pks(results['results']), [3, 2])
results = self.backend.more_like_this(self.sample_objs[0], limit_to_registered_models=True)
self.assertEqual(results['hits'], 2)
self.assertEqual([result.pk for result in results['results']], [3, 2])
results = self.backend.more_like_this(self.sample_objs[0],
additional_query=xapian.Query('david3'))
# Ensure that swapping the ``result_class`` works.
self.assertTrue(isinstance(self.backend.more_like_this(self.sample_objs[0], result_class=MockSearchResult)['results'][0], MockSearchResult))
self.assertEqual(pks(results['results']), [3])
results = self.backend.more_like_this(self.sample_objs[0],
limit_to_registered_models=True)
self.assertEqual(pks(results['results']), [3, 2])
# Other `result_class`
self.assertTrue(isinstance(self.backend.more_like_this(self.sample_objs[0],
result_class=MockSearchResult)['results'][0],
MockSearchResult))
def test_order_by(self):
results = self.backend.search(xapian.Query(''), sort_by=['pub_date'])
self.assertEqual([result.pk for result in results['results']], [3, 2, 1])
self.assertEqual(pks(results['results']), [3, 2, 1])
results = self.backend.search(xapian.Query(''), sort_by=['-pub_date'])
self.assertEqual([result.pk for result in results['results']], [1, 2, 3])
self.assertEqual(pks(results['results']), [1, 2, 3])
results = self.backend.search(xapian.Query(''), sort_by=['exp_date'])
self.assertEqual([result.pk for result in results['results']], [1, 2, 3])
self.assertEqual(pks(results['results']), [1, 2, 3])
results = self.backend.search(xapian.Query(''), sort_by=['-exp_date'])
self.assertEqual([result.pk for result in results['results']], [3, 2, 1])
self.assertEqual(pks(results['results']), [3, 2, 1])
results = self.backend.search(xapian.Query(''), sort_by=['id'])
self.assertEqual([result.pk for result in results['results']], [1, 2, 3])
self.assertEqual(pks(results['results']), [1, 2, 3])
results = self.backend.search(xapian.Query(''), sort_by=['-id'])
self.assertEqual([result.pk for result in results['results']], [3, 2, 1])
self.assertEqual(pks(results['results']), [3, 2, 1])
results = self.backend.search(xapian.Query(''), sort_by=['value'])
self.assertEqual([result.pk for result in results['results']], [1, 2, 3])
self.assertEqual(pks(results['results']), [1, 2, 3])
results = self.backend.search(xapian.Query(''), sort_by=['-value'])
self.assertEqual([result.pk for result in results['results']], [3, 2, 1])
self.assertEqual(pks(results['results']), [3, 2, 1])
results = self.backend.search(xapian.Query(''), sort_by=['popularity'])
self.assertEqual([result.pk for result in results['results']], [2, 1, 3])
self.assertEqual(pks(results['results']), [2, 1, 3])
results = self.backend.search(xapian.Query(''), sort_by=['-popularity'])
self.assertEqual([result.pk for result in results['results']], [3, 1, 2])
self.assertEqual(pks(results['results']), [3, 1, 2])
results = self.backend.search(xapian.Query(''), sort_by=['flag', 'id'])
self.assertEqual([result.pk for result in results['results']], [2, 1, 3])
self.assertEqual(pks(results['results']), [2, 1, 3])
results = self.backend.search(xapian.Query(''), sort_by=['flag', '-id'])
self.assertEqual([result.pk for result in results['results']], [2, 3, 1])
self.assertEqual(pks(results['results']), [2, 3, 1])
def test_verify_type(self):
self.assertEqual([result.month for result in self.backend.search(xapian.Query(''))['results']],
['02', '02', '02'])
def test__marshal_value(self):
self.assertEqual(_marshal_value('abc'), 'abc')
self.assertEqual(_marshal_value(1), '000000000001')
self.assertEqual(_marshal_value(2653), '000000002653')
self.assertEqual(_marshal_value(25.5), b'\xb2`')
self.assertEqual(_marshal_value([1, 2, 3]), '[1, 2, 3]')
self.assertEqual(_marshal_value((1, 2, 3)), '(1, 2, 3)')
self.assertEqual(_marshal_value({'a': 1, 'c': 3, 'b': 2}), "{u'a': 1, u'c': 3, u'b': 2}")
self.assertEqual(_marshal_value(datetime.datetime(2009, 5, 9, 16, 14)), '20090509161400')
self.assertEqual(_marshal_value(datetime.datetime(2009, 5, 9, 0, 0)), '20090509000000')
self.assertEqual(_marshal_value(datetime.datetime(1899, 5, 18, 0, 0)), '18990518000000')
self.assertEqual(_marshal_value(datetime.datetime(2009, 5, 18, 1, 16, 30, 250)), '20090518011630000250')
def test_term_to_xapian_value(self):
self.assertEqual(_term_to_xapian_value('abc', 'text'), 'abc')
self.assertEqual(_term_to_xapian_value(1, 'integer'), '000000000001')
self.assertEqual(_term_to_xapian_value(2653, 'integer'), '000000002653')
self.assertEqual(_term_to_xapian_value(25.5, 'float'), b'\xb2`')
self.assertEqual(_term_to_xapian_value([1, 2, 3], 'text'), '[1, 2, 3]')
self.assertEqual(_term_to_xapian_value((1, 2, 3), 'text'), '(1, 2, 3)')
self.assertEqual(_term_to_xapian_value({'a': 1, 'c': 3, 'b': 2}, 'text'),
"{u'a': 1, u'c': 3, u'b': 2}")
self.assertEqual(_term_to_xapian_value(datetime.datetime(2009, 5, 9, 16, 14), 'datetime'),
'20090509161400')
self.assertEqual(_term_to_xapian_value(datetime.datetime(2009, 5, 9, 0, 0), 'date'),
'20090509000000')
self.assertEqual(_term_to_xapian_value(datetime.datetime(1899, 5, 18, 0, 0), 'date'),
'18990518000000')
def test_build_schema(self):
(content_field_name, fields) = self.backend.build_schema(connections['default'].get_unified_index().all_searchfields())
search_fields = connections['default'].get_unified_index().all_searchfields()
(content_field_name, fields) = self.backend.build_schema(search_fields)
self.assertEqual(content_field_name, 'text')
self.assertEqual(len(fields), 14 + 3)
self.assertEqual(fields, [
{'column': 0, 'type': 'text', 'field_name': 'id', 'multi_valued': 'false'},
{'column': 1, 'type': 'long', 'field_name': 'django_id', 'multi_valued': 'false'},
{'column': 1, 'type': 'integer', 'field_name': 'django_id', 'multi_valued': 'false'},
{'column': 2, 'type': 'text', 'field_name': 'django_ct', 'multi_valued': 'false'},
{'column': 3, 'type': 'text', 'field_name': 'empty', 'multi_valued': 'false'},
{'column': 4, 'type': 'date', 'field_name': 'exp_date', 'multi_valued': 'false'},
@ -432,33 +597,46 @@ class XapianSearchBackendTestCase(HaystackBackendTestCase, TestCase):
{'column': 13, 'type': 'text', 'field_name': 'text', 'multi_valued': 'false'},
{'column': 14, 'type': 'text', 'field_name': 'titles', 'multi_valued': 'true'},
{'column': 15, 'type': 'text', 'field_name': 'url', 'multi_valued': 'false'},
{'column': 16, 'type': 'long', 'field_name': 'value', 'multi_valued': 'false'}
{'column': 16, 'type': 'integer', 'field_name': 'value', 'multi_valued': 'false'}
])
def test_parse_query(self):
self.assertEqual(str(self.backend.parse_query('indexed')), 'Xapian::Query(Zindex:(pos=1))')
self.assertEqual(str(self.backend.parse_query('name:david')), 'Xapian::Query(ZXNAMEdavid:(pos=1))')
self.assertEqual(str(self.backend.parse_query('indexed')),
'Xapian::Query(Zindex:(pos=1))')
self.assertEqual(str(self.backend.parse_query('name:david')),
'Xapian::Query(ZXNAMEdavid:(pos=1))')
if xapian.minor_version() >= 2:
self.assertEqual(str(self.backend.parse_query('name:da*')), 'Xapian::Query((XNAMEdavid1:(pos=1) SYNONYM XNAMEdavid2:(pos=1) SYNONYM XNAMEdavid3:(pos=1)))')
self.assertEqual(str(self.backend.parse_query('name:da*')),
'Xapian::Query(('
'XNAMEdavid1:(pos=1) SYNONYM '
'XNAMEdavid2:(pos=1) SYNONYM '
'XNAMEdavid3:(pos=1)))')
else:
self.assertEqual(str(self.backend.parse_query('name:da*')), 'Xapian::Query((XNAMEdavid1:(pos=1) OR XNAMEdavid2:(pos=1) OR XNAMEdavid3:(pos=1)))')
self.assertEqual(str(self.backend.parse_query('name:da*')),
'Xapian::Query(('
'XNAMEdavid1:(pos=1) OR '
'XNAMEdavid2:(pos=1) OR '
'XNAMEdavid3:(pos=1)))')
self.assertEqual(str(self.backend.parse_query('name:david1..david2')),
'Xapian::Query(VALUE_RANGE 7 david1 david2)')
self.assertEqual(str(self.backend.parse_query('value:0..10')),
'Xapian::Query(VALUE_RANGE 16 000000000000 000000000010)')
self.assertEqual(str(self.backend.parse_query('value:..10')),
'Xapian::Query(VALUE_RANGE 16 %012d 000000000010)' % (-sys.maxint - 1))
'Xapian::Query(VALUE_RANGE 16 %012d 000000000010)' % (-sys.maxsize - 1))
self.assertEqual(str(self.backend.parse_query('value:10..*')),
'Xapian::Query(VALUE_RANGE 16 000000000010 %012d)' % sys.maxint)
'Xapian::Query(VALUE_RANGE 16 000000000010 %012d)' % sys.maxsize)
self.assertEqual(str(self.backend.parse_query('popularity:25.5..100.0')),
b'Xapian::Query(VALUE_RANGE 9 \xb2` \xba@)')
def test_order_by_django_id(self):
self.backend.clear()
"""
We need this test because ordering on more than
10 entries was not correct at some point.
"""
self.sample_objs = []
number_list = range(1, 101)
number_list = list(range(1, 101))
for i in number_list:
mock = XapianMockModel()
mock.id = i
@ -476,8 +654,7 @@ class XapianSearchBackendTestCase(HaystackBackendTestCase, TestCase):
self.backend.update(self.index, self.sample_objs)
results = self.backend.search(xapian.Query(''), sort_by=['-django_id'])
self.assertEqual(results['hits'], len(number_list))
self.assertEqual([result.pk for result in results['results']], list(reversed(number_list)))
self.assertEqual(pks(results['results']), list(reversed(number_list)))
def test_more_like_this_with_unindexed_model(self):
"""
@ -496,187 +673,3 @@ class XapianSearchBackendTestCase(HaystackBackendTestCase, TestCase):
self.backend.silently_fail = False
self.assertRaises(InvalidIndexError, self.backend.more_like_this, mock)
class LiveXapianMockSearchIndex(indexes.SearchIndex):
text = indexes.CharField(document=True, use_template=True)
name = indexes.CharField(model_attr='author', faceted=True)
pub_date = indexes.DateField(model_attr='pub_date')
created = indexes.DateField()
title = indexes.CharField()
def get_model(self):
return MockModel
class LiveXapianSearchQueryTestCase(HaystackBackendTestCase, TestCase):
"""
SearchQuery specific tests
"""
fixtures = ['initial_data.json']
def get_index(self):
return LiveXapianMockSearchIndex()
def setUp(self):
super(LiveXapianSearchQueryTestCase, self).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_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_build_query_gt(self):
self.sq.add_filter(SQ(name__gt='m'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query((<alldocuments> AND_NOT VALUE_RANGE 4 a m))')
def test_build_query_gte(self):
self.sq.add_filter(SQ(name__gte='m'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query(VALUE_RANGE 4 m zzzzzzzzzzzzzzzzzzzzzzzzzzzz'
'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'
'zzzzzzzzzzzzzz)')
def test_build_query_lt(self):
self.sq.add_filter(SQ(name__lt='m'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query((<alldocuments> AND_NOT VALUE_RANGE 4 m zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz))')
def test_build_query_lte(self):
self.sq.add_filter(SQ(name__lte='m'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query(VALUE_RANGE 4 a m)')
def test_build_query_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(created__lt=datetime.datetime(2009, 2, 12, 12, 13, 0)))
self.sq.add_filter(SQ(title__gte='B'))
self.sq.add_filter(SQ(id__in=[1, 2, 3]))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query(((Zwhi OR why) AND VALUE_RANGE 6 00010101000000 20090210015900 AND '
'(<alldocuments> AND_NOT VALUE_RANGE 4 a david) AND '
'(<alldocuments> AND_NOT VALUE_RANGE 3 20090212121300 99990101000000) AND '
'VALUE_RANGE 8 b zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz AND '
'(Q1 OR Q2 OR Q3)))')
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.assertEqual(str(connections['default'].queries[0]['query_string']), 'Xapian::Query((ZXNAMEbar OR XNAMEbar))')
# 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.assertEqual(str(connections['default'].queries[0]['query_string']), 'Xapian::Query((ZXNAMEbar OR XNAMEbar))')
self.assertEqual(str(connections['default'].queries[1]['query_string']), 'Xapian::Query(((ZXNAMEbar OR XNAMEbar) AND (ZXTEXTmoof OR XTEXTmoof)))')
# Restore.
settings.DEBUG = old_debug
class LiveXapianSearchQuerySetTestCase(HaystackBackendTestCase, TestCase):
"""
SearchQuerySet specific tests
"""
fixtures = ['initial_data.json']
def get_index(self):
return LiveXapianMockSearchIndex()
def setUp(self):
super(LiveXapianSearchQuerySetTestCase, self).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 XapianBoostMockSearchIndex(indexes.SearchIndex):
text = indexes.CharField(
document=True, use_template=True,
template_name='search/indexes/core/mockmodel_template.txt'
)
author = indexes.CharField(model_attr='author', weight=2.0)
editor = indexes.CharField(model_attr='editor')
pub_date = indexes.DateField(model_attr='pub_date')
def get_model(self):
return AFourthMockModel
class XapianBoostBackendTestCase(HaystackBackendTestCase, TestCase):
def get_index(self):
return XapianBoostMockSearchIndex()
def setUp(self):
super(XapianBoostBackendTestCase, self).setUp()
self.sample_objs = []
for i in xrange(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'
])

View file

@ -0,0 +1,203 @@
from __future__ import unicode_literals
import datetime
from django.db.models import Q
from django.test import TestCase
from haystack import connections
from haystack.inputs import AutoQuery
from haystack.query import SearchQuerySet
from xapian_tests.models import Document
from xapian_tests.search_indexes import DocumentIndex
from xapian_tests.tests.test_backend import pks
class InterfaceTestCase(TestCase):
"""
Tests the interface of Xapian-Haystack.
Tests related to usability and expected behavior
go here.
"""
def setUp(self):
super(InterfaceTestCase, self).setUp()
types_names = ['book', 'magazine', 'article']
texts = ['This is a huge text',
'This is a medium text',
'This is a small text']
dates = [datetime.date(year=2010, month=1, day=1),
datetime.date(year=2010, month=2, day=1),
datetime.date(year=2010, month=3, day=1)]
summaries = ['This is a huge corrup\xe7\xe3o summary',
'This is a medium summary',
'This is a small summary']
for i in range(1, 13):
doc = Document()
doc.type_name = types_names[i % 3]
doc.number = i * 2
doc.name = "%s %d" % (doc.type_name, doc.number)
doc.date = dates[i % 3]
doc.summary = summaries[i % 3]
doc.text = texts[i % 3]
doc.save()
self.index = DocumentIndex()
self.ui = connections['default'].get_unified_index()
self.ui.build(indexes=[self.index])
self.backend = connections['default'].get_backend()
self.backend.update(self.index, Document.objects.all())
self.queryset = SearchQuerySet()
def tearDown(self):
Document.objects.all().delete()
self.backend.clear()
super(InterfaceTestCase, self).tearDown()
def test_count(self):
self.assertEqual(self.queryset.count(), Document.objects.count())
def test_content_search(self):
result = self.queryset.filter(content='medium this')
self.assertEqual(sorted(pks(result)),
pks(Document.objects.all()))
# documents with "medium" AND "this" have higher score
self.assertEqual(pks(result)[:4], [1, 4, 7, 10])
def test_field_search(self):
self.assertEqual(pks(self.queryset.filter(name='8')), [4])
self.assertEqual(pks(self.queryset.filter(type_name='book')),
pks(Document.objects.filter(type_name='book')))
self.assertEqual(pks(self.queryset.filter(text='text huge')),
pks(Document.objects.filter(text__contains='text huge')))
def test_field_contains(self):
self.assertEqual(pks(self.queryset.filter(summary='huge')),
pks(Document.objects.filter(summary__contains='huge')))
result = self.queryset.filter(summary='huge summary')
self.assertEqual(sorted(pks(result)),
pks(Document.objects.all()))
# documents with "huge" AND "summary" have higher score
self.assertEqual(pks(result)[:4], [3, 6, 9, 12])
def test_field_exact(self):
self.assertEqual(pks(self.queryset.filter(name__exact='8')), [])
self.assertEqual(pks(self.queryset.filter(name__exact='magazine 2')), [1])
def test_content_exact(self):
self.assertEqual(pks(self.queryset.filter(content__exact='huge')), [])
def test_content_and(self):
self.assertEqual(pks(self.queryset.filter(content='huge').filter(summary='medium')), [])
self.assertEqual(len(self.queryset.filter(content='huge this')), 12)
self.assertEqual(len(self.queryset.filter(content='huge this').filter(summary='huge')), 4)
def test_content_or(self):
self.assertEqual(len(self.queryset.filter(content='huge medium')), 8)
self.assertEqual(len(self.queryset.filter(content='huge medium small')), 12)
def test_field_and(self):
self.assertEqual(pks(self.queryset.filter(name='8').filter(name='4')), [])
def test_field_or(self):
self.assertEqual(pks(self.queryset.filter(name='8 4')), [2, 4])
def test_field_in(self):
self.assertEqual(set(pks(self.queryset.filter(name__in=['magazine 2', 'article 4']))),
set(pks(Document.objects.filter(name__in=['magazine 2', 'article 4']))))
self.assertEqual(pks(self.queryset.filter(number__in=[4])),
pks(Document.objects.filter(number__in=[4])))
self.assertEqual(pks(self.queryset.filter(number__in=[4, 8])),
pks(Document.objects.filter(number__in=[4, 8])))
def test_private_fields(self):
self.assertEqual(pks(self.queryset.filter(django_id=4)),
pks(Document.objects.filter(id__in=[4])))
self.assertEqual(pks(self.queryset.filter(django_id__in=[2, 4])),
pks(Document.objects.filter(id__in=[2, 4])))
self.assertEqual(set(pks(self.queryset.models(Document))),
set(pks(Document.objects.all())))
def test_field_startswith(self):
self.assertEqual(len(self.queryset.filter(name__startswith='magaz')), 4)
self.assertEqual(set(pks(self.queryset.filter(text__startswith='This is'))),
set(pks(Document.objects.filter(text__startswith='This is'))))
def test_auto_query(self):
self.assertEqual(set(pks(self.queryset.auto_query("huge OR medium"))),
set(pks(Document.objects.filter(Q(text__contains="huge") |
Q(text__contains="medium")))))
self.assertEqual(set(pks(self.queryset.auto_query("huge AND medium"))),
set(pks(Document.objects.filter(Q(text__contains="huge") &
Q(text__contains="medium")))))
self.assertEqual(set(pks(self.queryset.auto_query("text:huge text:-this"))),
set(pks(Document.objects.filter(Q(text__contains="huge") &
~Q(text__contains="this")))))
self.assertEqual(len(self.queryset.filter(name=AutoQuery("8 OR 4"))), 2)
self.assertEqual(len(self.queryset.filter(name=AutoQuery("8 AND 4"))), 0)
def test_value_range(self):
self.assertEqual(set(pks(self.queryset.filter(number__lt=3))),
set(pks(Document.objects.filter(number__lt=3))))
self.assertEqual(set(pks(self.queryset.filter(django_id__gte=6))),
set(pks(Document.objects.filter(id__gte=6))))
def test_date_range(self):
date = datetime.date(year=2010, month=2, day=1)
self.assertEqual(set(pks(self.queryset.filter(date__gte=date))),
set(pks(Document.objects.filter(date__gte=date))))
date = datetime.date(year=2010, month=3, day=1)
self.assertEqual(set(pks(self.queryset.filter(date__lte=date))),
set(pks(Document.objects.filter(date__lte=date))))
def test_order_by(self):
# private order
self.assertEqual(pks(self.queryset.order_by("-django_id")),
pks(Document.objects.order_by("-id")))
# value order
self.assertEqual(pks(self.queryset.order_by("number")),
pks(Document.objects.order_by("number")))
# text order
self.assertEqual(pks(self.queryset.order_by("summary")),
pks(Document.objects.order_by("summary")))
# date order
self.assertEqual(pks(self.queryset.order_by("-date")),
pks(Document.objects.order_by("-date")))
def test_non_ascii_search(self):
"""
Regression test for #119.
"""
self.assertEqual(pks(self.queryset.filter(content='corrup\xe7\xe3o')),
pks(Document.objects.filter(summary__contains='corrup\xe7\xe3o')))
def test_multi_values_exact_search(self):
"""
Regression test for #103
"""
self.assertEqual(len(self.queryset.filter(tags__exact='tag')), 12)
self.assertEqual(len(self.queryset.filter(tags__exact='tag-test')), 8)
self.assertEqual(len(self.queryset.filter(tags__exact='tag-test-test')), 4)

View file

@ -0,0 +1,436 @@
from __future__ import unicode_literals
import datetime
from django.conf import settings
from django.test import TestCase
from haystack import indexes
from haystack import connections, reset_search_queries
from haystack.models import SearchResult
from haystack.query import SearchQuerySet, SQ
from core.models import MockModel, AnotherMockModel, AFourthMockModel
from core.tests.mocks import MockSearchResult
from xapian_tests.tests.test_backend import HaystackBackendTestCase
class MockQueryIndex(indexes.SearchIndex):
text = indexes.CharField(document=True)
pub_date = indexes.DateTimeField()
title = indexes.CharField()
foo = indexes.CharField()
def get_model(self):
return MockModel
class XapianSearchQueryTestCase(HaystackBackendTestCase, TestCase):
def get_index(self):
return MockQueryIndex()
def setUp(self):
super(XapianSearchQueryTestCase, self).setUp()
self.sq = connections['default'].get_query()
def test_build_query_all(self):
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query(<alldocuments>)')
def test_build_query_single_word(self):
self.sq.add_filter(SQ(content='hello'))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query((Zhello OR hello))')
def test_build_query_single_word_not(self):
self.sq.add_filter(~SQ(content='hello'))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query((<alldocuments> AND_NOT (Zhello OR hello)))')
def test_build_query_single_word_field_exact(self):
self.sq.add_filter(SQ(foo='hello'))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query((ZXFOOhello OR XFOOhello))')
def test_build_query_single_word_field_exact_not(self):
self.sq.add_filter(~SQ(foo='hello'))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query((<alldocuments> AND_NOT (ZXFOOhello OR XFOOhello)))')
def test_build_query_boolean(self):
self.sq.add_filter(SQ(content=True))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query((Ztrue OR true))')
def test_build_query_date(self):
self.sq.add_filter(SQ(content=datetime.date(2009, 5, 8)))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query((Z2009-05-08 OR 2009-05-08))')
def test_build_query_date_not(self):
self.sq.add_filter(~SQ(content=datetime.date(2009, 5, 8)))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query((<alldocuments> AND_NOT (Z2009-05-08 OR 2009-05-08)))')
def test_build_query_datetime(self):
self.sq.add_filter(SQ(content=datetime.datetime(2009, 5, 8, 11, 28)))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query((Z2009-05-08 OR 2009-05-08 OR Z11:28:00 OR 11:28:00))')
def test_build_query_datetime_not(self):
self.sq.add_filter(~SQ(content=datetime.datetime(2009, 5, 8, 11, 28)))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query((<alldocuments> AND_NOT '
'(Z2009-05-08 OR 2009-05-08 OR Z11:28:00 OR 11:28:00)))')
def test_build_query_float(self):
self.sq.add_filter(SQ(content=25.52))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query((Z25.52 OR 25.52))')
def test_build_query_multiple_words_and(self):
self.sq.add_filter(SQ(content='hello'))
self.sq.add_filter(SQ(content='world'))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query(((Zhello OR hello) AND (Zworld OR world)))')
def test_build_query_multiple_words_not(self):
self.sq.add_filter(~SQ(content='hello'))
self.sq.add_filter(~SQ(content='world'))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query(('
'(<alldocuments> AND_NOT (Zhello OR hello)) AND '
'(<alldocuments> AND_NOT (Zworld OR world))))')
def test_build_query_multiple_words_or(self):
self.sq.add_filter(SQ(content='hello') | SQ(content='world'))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query((Zhello OR hello OR Zworld OR world))')
def test_build_query_multiple_words_or_not(self):
self.sq.add_filter(~SQ(content='hello') | ~SQ(content='world'))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query(('
'(<alldocuments> AND_NOT (Zhello OR hello)) OR '
'(<alldocuments> AND_NOT (Zworld OR world))))')
def test_build_query_multiple_words_mixed(self):
self.sq.add_filter(SQ(content='why') | SQ(content='hello'))
self.sq.add_filter(~SQ(content='world'))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query(('
'(Zwhi OR why OR Zhello OR hello) AND '
'(<alldocuments> AND_NOT (Zworld OR world))))')
def test_build_query_multiple_word_field_exact(self):
self.sq.add_filter(SQ(foo='hello'))
self.sq.add_filter(SQ(title='world'))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query(('
'(ZXFOOhello OR XFOOhello) AND '
'(ZXTITLEworld OR XTITLEworld)))')
def test_build_query_multiple_word_field_exact_not(self):
self.sq.add_filter(~SQ(foo='hello'))
self.sq.add_filter(~SQ(title='world'))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query(('
'(<alldocuments> AND_NOT (ZXFOOhello OR XFOOhello)) AND '
'(<alldocuments> AND_NOT (ZXTITLEworld OR XTITLEworld))))')
def test_build_query_or(self):
self.sq.add_filter(SQ(content='hello world'))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query((Zhello OR hello OR Zworld OR world))')
def test_build_query_not_or(self):
self.sq.add_filter(~SQ(content='hello world'))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query('
'(<alldocuments> AND_NOT (Zhello OR hello OR Zworld OR world)))')
def test_build_query_boost(self):
self.sq.add_filter(SQ(content='hello'))
self.sq.add_boost('world', 5)
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query(('
'(Zhello OR hello) AND_MAYBE '
'5 * (Zworld OR world)))')
def test_build_query_not_in_filter_single_words(self):
self.sq.add_filter(SQ(content='why'))
self.sq.add_filter(~SQ(title__in=["Dune", "Jaws"]))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query(((Zwhi OR why) AND '
'(<alldocuments> AND_NOT (XTITLE^dune$ OR '
'XTITLE^jaws$))))')
def test_build_query_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.assertEqual(str(self.sq.build_query()),
'Xapian::Query(((Zwhi OR why) AND '
'((XTITLEa PHRASE 3 XTITLEfamous PHRASE 3 XTITLEpaper) OR '
'(XTITLEan PHRASE 3 XTITLEinfamous PHRASE 3 XTITLEarticle))))')
def test_build_query_in_filter_multiple_words_with_punctuation(self):
self.sq.add_filter(SQ(title__in=["A Famous Paper", "An Infamous Article", "My Store Inc."]))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query(('
'(XTITLEa PHRASE 3 XTITLEfamous PHRASE 3 XTITLEpaper) OR '
'(XTITLEan PHRASE 3 XTITLEinfamous PHRASE 3 XTITLEarticle) OR '
'(XTITLEmy PHRASE 3 XTITLEstore PHRASE 3 XTITLEinc.)))')
def test_build_query_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.assertEqual(str(self.sq.build_query()),
'Xapian::Query(((Zwhi OR why) AND '
'(<alldocuments> AND_NOT ((XTITLEa PHRASE 3 '
'XTITLEfamous PHRASE 3 '
'XTITLEpaper) OR (XTITLEan PHRASE 3 '
'XTITLEinfamous PHRASE 3 XTITLEarticle)))))')
def test_build_query_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.assertEqual(str(self.sq.build_query()),
'Xapian::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_build_query_with_models(self):
self.sq.add_filter(SQ(content='hello'))
self.sq.add_model(MockModel)
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query(((Zhello OR hello) AND '
'0 * CONTENTTYPEcore.mockmodel))')
self.sq.add_model(AnotherMockModel)
self.assertTrue(str(self.sq.build_query()) in (
'Xapian::Query(((Zhello OR hello) AND '
'(0 * CONTENTTYPEcore.anothermockmodel OR '
'0 * CONTENTTYPEcore.mockmodel)))',
'Xapian::Query(((Zhello OR hello) AND '
'(0 * CONTENTTYPEcore.mockmodel OR '
'0 * CONTENTTYPEcore.anothermockmodel)))'))
def test_build_query_with_punctuation(self):
self.sq.add_filter(SQ(content='http://www.example.com'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::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.assertEqual(str(self.sq.build_query()),
'Xapian::Query(((Zwhi OR why) AND '
'(XTITLE^1$ OR XTITLE^2$ OR XTITLE^3$)))')
class MockSearchIndex(indexes.SearchIndex):
text = indexes.CharField(document=True, use_template=True)
name = indexes.CharField(model_attr='author', faceted=True)
pub_date = indexes.DateTimeField(model_attr='pub_date')
title = indexes.CharField()
def get_model(self):
return MockModel
class SearchQueryTestCase(HaystackBackendTestCase, TestCase):
"""
Tests expected behavior of
SearchQuery.
"""
fixtures = ['initial_data.json']
def get_index(self):
return MockSearchIndex()
def setUp(self):
super(SearchQueryTestCase, self).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_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_build_query_gt(self):
self.sq.add_filter(SQ(name__gt='m'))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query((<alldocuments> AND_NOT VALUE_RANGE 3 a m))')
def test_build_query_gte(self):
self.sq.add_filter(SQ(name__gte='m'))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query(VALUE_RANGE 3 m zzzzzzzzzzzzzzzzzzzzzzzzzzzz'
'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'
'zzzzzzzzzzzzzz)')
def test_build_query_lt(self):
self.sq.add_filter(SQ(name__lt='m'))
self.assertEqual(str(self.sq.build_query()),
'Xapian::Query((<alldocuments> AND_NOT '
'VALUE_RANGE 3 m zzzzzzzzzzzzzzzzzzzzzz'
'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'
'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz))')
def test_build_query_lte(self):
self.sq.add_filter(SQ(name__lte='m'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query(VALUE_RANGE 3 a m)')
def test_build_query_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.assertEqual(str(self.sq.build_query()),
'Xapian::Query(((Zwhi OR why) AND '
'VALUE_RANGE 5 00010101000000 20090210015900 AND '
'(<alldocuments> AND_NOT VALUE_RANGE 3 a david) AND '
'VALUE_RANGE 7 b zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'
'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 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.assertEqual(str(connections['default'].queries[0]['query_string']),
'Xapian::Query((ZXNAMEbar OR XNAMEbar))')
# 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.assertEqual(str(connections['default'].queries[0]['query_string']),
'Xapian::Query(('
'ZXNAMEbar OR '
'XNAMEbar))')
self.assertEqual(str(connections['default'].queries[1]['query_string']),
'Xapian::Query(('
'(ZXNAMEbar OR XNAMEbar) AND '
'(ZXTEXTmoof OR XTEXTmoof)))')
# Restore.
settings.DEBUG = old_debug
class LiveSearchQuerySetTestCase(HaystackBackendTestCase, TestCase):
"""
SearchQuerySet specific tests
"""
fixtures = ['initial_data.json']
def get_index(self):
return MockSearchIndex()
def setUp(self):
super(LiveSearchQuerySetTestCase, self).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 BoostMockSearchIndex(indexes.SearchIndex):
text = indexes.CharField(
document=True, use_template=True,
template_name='search/indexes/core/mockmodel_template.txt'
)
author = indexes.CharField(model_attr='author', weight=2.0)
editor = indexes.CharField(model_attr='editor')
pub_date = indexes.DateField(model_attr='pub_date')
def get_model(self):
return AFourthMockModel
class BoostFieldTestCase(HaystackBackendTestCase, TestCase):
"""
Tests boosted fields.
"""
def get_index(self):
return BoostMockSearchIndex()
def setUp(self):
super(BoostFieldTestCase, self).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'
])

View file

@ -1,169 +0,0 @@
from __future__ import unicode_literals
import datetime
import os
import shutil
from django.conf import settings
from django.test import TestCase
from haystack import connections
from haystack.query import SQ
from core.models import MockModel, AnotherMockModel
class XapianSearchQueryTestCase(TestCase):
def setUp(self):
super(XapianSearchQueryTestCase, self).setUp()
self.sq = connections['default'].get_query()
def tearDown(self):
if os.path.exists(settings.HAYSTACK_CONNECTIONS['default']['PATH']):
shutil.rmtree(settings.HAYSTACK_CONNECTIONS['default']['PATH'])
super(XapianSearchQueryTestCase, self).tearDown()
def test_build_query_all(self):
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query(<alldocuments>)')
def test_build_query_single_word(self):
self.sq.add_filter(SQ(content='hello'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query((Zhello OR hello))')
def test_build_query_single_word_not(self):
self.sq.add_filter(~SQ(content='hello'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query((<alldocuments> AND_NOT (Zhello OR hello)))')
def test_build_query_single_word_field_exact(self):
self.sq.add_filter(SQ(foo='hello'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query((ZXFOOhello OR XFOOhello))')
def test_build_query_single_word_field_exact_not(self):
self.sq.add_filter(~SQ(foo='hello'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query((<alldocuments> AND_NOT (ZXFOOhello OR XFOOhello)))')
def test_build_query_boolean(self):
self.sq.add_filter(SQ(content=True))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query((Ztrue OR true))')
def test_build_query_date(self):
self.sq.add_filter(SQ(content=datetime.date(2009, 5, 8)))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query((Z20090508000000 OR 20090508000000))')
def test_build_query_date_not(self):
self.sq.add_filter(~SQ(content=datetime.date(2009, 5, 8)))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query((<alldocuments> AND_NOT (Z20090508000000 OR 20090508000000)))')
def test_build_query_datetime(self):
self.sq.add_filter(SQ(content=datetime.datetime(2009, 5, 8, 11, 28)))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query((Z20090508112800 OR 20090508112800))')
def test_build_query_datetime_not(self):
self.sq.add_filter(~SQ(content=datetime.datetime(2009, 5, 8, 11, 28)))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query((<alldocuments> AND_NOT (Z20090508112800 OR 20090508112800)))')
def test_build_query_float(self):
self.sq.add_filter(SQ(content=25.52))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query((Z25.52 OR 25.52))')
def test_build_query_multiple_words_and(self):
self.sq.add_filter(SQ(content='hello'))
self.sq.add_filter(SQ(content='world'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query(((Zhello OR hello) AND (Zworld OR world)))')
def test_build_query_multiple_words_not(self):
self.sq.add_filter(~SQ(content='hello'))
self.sq.add_filter(~SQ(content='world'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query(((<alldocuments> AND_NOT (Zhello OR hello)) AND (<alldocuments> AND_NOT (Zworld OR world))))')
def test_build_query_multiple_words_or(self):
self.sq.add_filter(SQ(content='hello') | SQ(content='world'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query((Zhello OR hello OR Zworld OR world))')
def test_build_query_multiple_words_or_not(self):
self.sq.add_filter(~SQ(content='hello') | ~SQ(content='world'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query(((<alldocuments> AND_NOT (Zhello OR hello)) OR (<alldocuments> AND_NOT (Zworld OR world))))')
def test_build_query_multiple_words_mixed(self):
self.sq.add_filter(SQ(content='why') | SQ(content='hello'))
self.sq.add_filter(~SQ(content='world'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query(((Zwhi OR why OR Zhello OR hello) AND (<alldocuments> AND_NOT (Zworld OR world))))')
def test_build_query_multiple_word_field_exact(self):
self.sq.add_filter(SQ(foo='hello'))
self.sq.add_filter(SQ(bar='world'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query(((ZXFOOhello OR XFOOhello) AND (ZXBARworld OR XBARworld)))')
def test_build_query_multiple_word_field_exact_not(self):
self.sq.add_filter(~SQ(foo='hello'))
self.sq.add_filter(~SQ(bar='world'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query(((<alldocuments> AND_NOT (ZXFOOhello OR XFOOhello)) AND (<alldocuments> AND_NOT (ZXBARworld OR XBARworld))))')
def test_build_query_phrase(self):
self.sq.add_filter(SQ(content='hello world'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query((hello PHRASE 2 world))')
def test_build_query_phrase_not(self):
self.sq.add_filter(~SQ(content='hello world'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query((<alldocuments> AND_NOT (hello PHRASE 2 world)))')
def test_build_query_boost(self):
self.sq.add_filter(SQ(content='hello'))
self.sq.add_boost('world', 5)
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query(((Zhello OR hello) AND_MAYBE 5 * (Zworld OR world)))')
def test_build_query_in_filter_single_words(self):
self.sq.add_filter(SQ(content='why'))
self.sq.add_filter(SQ(title__in=["Dune", "Jaws"]))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query(((Zwhi OR why) AND (ZXTITLEdune OR XTITLEdune OR ZXTITLEjaw OR XTITLEjaws)))')
def test_build_query_not_in_filter_single_words(self):
self.sq.add_filter(SQ(content='why'))
self.sq.add_filter(~SQ(title__in=["Dune", "Jaws"]))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query(((Zwhi OR why) AND (<alldocuments> AND_NOT (ZXTITLEdune OR XTITLEdune OR ZXTITLEjaw OR XTITLEjaws))))')
def test_build_query_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.assertEqual(str(self.sq.build_query()), 'Xapian::Query(((Zwhi OR why) AND ((XTITLEa PHRASE 3 XTITLEfamous PHRASE 3 XTITLEpaper) OR (XTITLEan PHRASE 3 XTITLEinfamous PHRASE 3 XTITLEarticle))))')
def test_build_query_in_filter_multiple_words_with_punctuation(self):
self.sq.add_filter(SQ(title__in=["A Famous Paper", "An Infamous Article", "My Store Inc."]))
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query(((XTITLEa PHRASE 3 XTITLEfamous PHRASE 3 XTITLEpaper) OR (XTITLEan PHRASE 3 XTITLEinfamous PHRASE 3 XTITLEarticle) OR (XTITLEmy PHRASE 3 XTITLEstore PHRASE 3 XTITLEinc.)))')
def test_build_query_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.assertEqual(str(self.sq.build_query()), 'Xapian::Query(((Zwhi OR why) AND (<alldocuments> AND_NOT ((XTITLEa PHRASE 3 XTITLEfamous PHRASE 3 XTITLEpaper) OR (XTITLEan PHRASE 3 XTITLEinfamous PHRASE 3 XTITLEarticle)))))')
def test_build_query_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.assertEqual(str(self.sq.build_query()), 'Xapian::Query(((Zwhi OR why) AND (ZXPUB_DATE20090706015621 OR XPUB_DATE20090706015621)))')
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_build_query_with_models(self):
self.sq.add_filter(SQ(content='hello'))
self.sq.add_model(MockModel)
self.assertEqual(str(self.sq.build_query()), 'Xapian::Query(((Zhello OR hello) AND 0 * CONTENTTYPEcore.mockmodel))')
self.sq.add_model(AnotherMockModel)
self.assertTrue(str(self.sq.build_query()) in (
'Xapian::Query(((Zhello OR hello) AND (0 * CONTENTTYPEcore.anothermockmodel OR 0 * CONTENTTYPEcore.mockmodel)))',
'Xapian::Query(((Zhello OR hello) AND (0 * CONTENTTYPEcore.mockmodel OR 0 * CONTENTTYPEcore.anothermockmodel)))'))
def test_build_query_with_punctuation(self):
self.sq.add_filter(SQ(content='http://www.example.com'))
self.assertEqual(str(self.sq.build_query()), 'Xapian::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.assertEqual(str(self.sq.build_query()), 'Xapian::Query(((Zwhi OR why) AND (ZXTITLE1 OR XTITLE1 OR ZXTITLE2 OR XTITLE2 OR ZXTITLE3 OR XTITLE3)))')

File diff suppressed because it is too large Load diff