Merged 2.0.X branch

This commit is contained in:
David Sauve 2012-04-20 12:07:44 -07:00
commit dcdc9eb319
9 changed files with 439 additions and 457 deletions

View File

@ -25,4 +25,4 @@ Thanks to:
* Jacob Kaplan-Moss for pointing out that write permission shouldn't be required for searching.
* glassresistor for assistance troubleshooting an issue with boosting a phrase query & a patch to make weighting schemes overridable.
* James Addison for helping to debug an intermittent issue with `order_by` and `build_schema`.
* Michael Opitz for a patch that enables support for `inmemorydb`.
* Michael Opitz for a patch that enables support for `inmemorydb`.

View File

@ -13,7 +13,7 @@ Requirements
- Python 2.4 (May work with 2.3, but untested)
- Django 1.0.x
- Django-Haystack 1.1.X (If you wish to use django-haystack 1.0.X, please use xapian-haystack 1.0.X)
- Django-Haystack 2.0.X
- Xapian 1.0.13+ (May work with earlier versions, but untested)
Notes
@ -33,13 +33,19 @@ Installation
or
``pip install xapian-haystack``
or
``easy_install xapian-haystack``
#. Add ``HAYSTACK_XAPIAN_PATH`` to ``settings.py``
#. Set ``HAYSTACK_SEARCH_ENGINE`` to ``xapian``
#. Set to something similar to:
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.xapian_backend.XapianEngine',
'PATH': os.path.join(os.path.dirname(__file__), 'xapian_index')
},
}
Configuration
-------------
@ -96,10 +102,10 @@ xapian-haystack is maintained by `David Sauve <mailto:david.sauve@bag-of-holding
License
-------
xapian-haystack is Copyright (c) 2009, 2010, 2011 David Sauve, 2009, 2010 Trapeze. It is free software, and may be redistributed under the terms specified in the LICENSE file.
xapian-haystack is Copyright (c) 2009, 2010, 2011, 2012 David Sauve, 2009, 2010 Trapeze. It is free software, and may be redistributed under the terms specified in the LICENSE file.
Questions, Comments, Concerns:
------------------------------
Feel free to open an issue here: `github.com/notanumber/xapian-haystack/issues <http://github.com/notanumber/xapian-haystack/issues>`_
Alternatively, ask questions on the django-haystack `mailing list <http://groups.google.com/group/django-haystack/>`_ or `irc channel <irc://irc.freenode.net/haystack>`_.
Alternatively, ask questions on the django-haystack `mailing list <http://groups.google.com/group/django-haystack/>`_ or `irc channel <irc://irc.freenode.net/haystack>`_.

View File

@ -1,4 +1,4 @@
# Copyright (C) 2009, 2010, 2011 David Sauve
# Copyright (C) 2009, 2010, 2011, 2012 David Sauve
# Copyright (C) 2009, 2010 Trapeze
import os
@ -8,6 +8,10 @@ INSTALLED_APPS += [
'xapian_tests',
]
HAYSTACK_SEARCH_ENGINE = 'xapian'
HAYSTACK_XAPIAN_PATH = os.path.join('tmp', 'test_xapian_query')
HAYSTACK_INCLUDE_SPELLING = True
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.xapian_backend.XapianEngine',
'PATH': os.path.join('tmp', 'test_xapian_query'),
'INCLUDE_SPELLING': True,
}
}

View File

@ -1,2 +1,2 @@
# Copyright (C) 2009, 2010, 2011 David Sauve
# Copyright (C) 2009, 2010, 2011, 2012 David Sauve
# Copyright (C) 2009, 2010 Trapeze

View File

@ -1,2 +1,2 @@
# Copyright (C) 2009, 2010, 2011 David Sauve
# Copyright (C) 2009, 2010, 2011, 2012 David Sauve
# Copyright (C) 2009, 2010 Trapeze

View File

@ -1,4 +1,4 @@
# Copyright (C) 2009, 2010, 2011 David Sauve
# Copyright (C) 2009, 2010, 2011, 2012 David Sauve
# Copyright (C) 2009, 2010 Trapeze
import warnings

View File

@ -1,9 +1,8 @@
# Copyright (C) 2009, 2010, 2011 David Sauve
# Copyright (C) 2009, 2010, 2011, 2012 David Sauve
# Copyright (C) 2009, 2010 Trapeze
# Based on original code by Daniel Lindsley as part of the Haystack test suite.
import cPickle as pickle
import datetime
import os
import shutil
@ -14,12 +13,12 @@ from django.conf import settings
from django.db import models
from django.test import TestCase
from haystack import indexes, sites, backends
from haystack.backends.xapian_backend import SearchBackend, SearchQuery, _marshal_value
from haystack.exceptions import HaystackError
from haystack import connections, reset_search_queries
from haystack import indexes
from haystack.backends.xapian_backend import _marshal_value
from haystack.models import SearchResult
from haystack.query import SearchQuerySet, SQ
from haystack.sites import SearchSite
from haystack.utils.loading import UnifiedIndex
from core.models import MockTag, MockModel, AnotherMockModel, AFourthMockModel
from core.tests.mocks import MockSearchResult
@ -35,16 +34,16 @@ class XapianMockModel(models.Model):
pub_date = models.DateTimeField(default=datetime.datetime.now)
exp_date = models.DateTimeField(default=datetime.datetime.now)
tag = models.ForeignKey(MockTag)
value = models.IntegerField(default=0)
flag = models.BooleanField(default=True)
slug = models.SlugField()
popularity = models.FloatField(default=0.0)
url = models.URLField()
def __unicode__(self):
return self.author
def hello(self):
return 'World!'
@ -64,13 +63,16 @@ class XapianMockSearchIndex(indexes.SearchIndex):
month = indexes.CharField(indexed=False)
url = indexes.CharField(model_attr='url')
empty = indexes.CharField()
# Various MultiValueFields
sites = indexes.MultiValueField()
tags = indexes.MultiValueField()
keys = indexes.MultiValueField()
titles = indexes.MultiValueField()
def get_model(self):
return XapianMockModel
def prepare_sites(self, obj):
return ['%d' % (i * obj.id) for i in xrange(1, 4)]
@ -92,7 +94,6 @@ class XapianMockSearchIndex(indexes.SearchIndex):
return ['object two title one', 'object two title two']
else:
return ['object three title one', 'object three title two']
pub_date = indexes.DateField(model_attr='pub_date')
def prepare_month(self, obj):
return '%02d' % obj.pub_date.month
@ -110,18 +111,23 @@ class XapianBoostMockSearchIndex(indexes.SearchIndex):
editor = indexes.CharField(model_attr='editor')
pub_date = indexes.DateField(model_attr='pub_date')
def get_model(self):
return AFourthMockModel
class XapianSearchBackendTestCase(TestCase):
def setUp(self):
super(XapianSearchBackendTestCase, self).setUp()
self.site = SearchSite()
self.backend = SearchBackend(site=self.site)
self.index = XapianMockSearchIndex(XapianMockModel, backend=self.backend)
self.site.register(XapianMockModel, XapianMockSearchIndex)
self.old_ui = connections['default'].get_unified_index()
self.ui = UnifiedIndex()
self.index = XapianMockSearchIndex()
self.ui.build(indexes=[self.index])
self.backend = connections['default'].get_backend()
connections['default']._index = self.ui
self.sample_objs = []
for i in xrange(1, 4):
mock = XapianMockModel()
mock.id = i
@ -133,69 +139,70 @@ class XapianSearchBackendTestCase(TestCase):
mock.slug = 'http://example.com/%d/' % i
mock.url = 'http://example.com/%d/' % i
self.sample_objs.append(mock)
self.sample_objs[0].popularity = 834.0
self.sample_objs[1].popularity = 35.5
self.sample_objs[2].popularity = 972.0
def tearDown(self):
if os.path.exists(settings.HAYSTACK_XAPIAN_PATH):
shutil.rmtree(settings.HAYSTACK_XAPIAN_PATH)
if os.path.exists(settings.HAYSTACK_CONNECTIONS['default']['PATH']):
shutil.rmtree(settings.HAYSTACK_CONNECTIONS['default']['PATH'])
connections['default']._index = self.old_ui
super(XapianSearchBackendTestCase, self).tearDown()
def test_update(self):
self.backend.update(self.index, self.sample_objs)
self.assertEqual(self.backend.document_count(), 3)
self.assertEqual([result.pk for result in self.backend.search(xapian.Query(''))['results']], [1, 2, 3])
def test_duplicate_update(self):
self.backend.update(self.index, self.sample_objs)
self.backend.update(self.index, self.sample_objs) # Duplicates should be updated, not appended -- http://github.com/notanumber/xapian-haystack/issues/#issue/6
self.backend.update(self.index, self.sample_objs) # Duplicates should be updated, not appended -- http://github.com/notanumber/xapian-haystack/issues/#issue/6
self.assertEqual(self.backend.document_count(), 3)
def test_remove(self):
self.backend.update(self.index, self.sample_objs)
self.assertEqual(self.backend.document_count(), 3)
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])
def test_clear(self):
self.backend.update(self.index, self.sample_objs)
self.assertEqual(self.backend.document_count(), 3)
self.backend.clear()
self.assertEqual(self.backend.document_count(), 0)
self.backend.update(self.index, self.sample_objs)
self.assertEqual(self.backend.document_count(), 3)
self.backend.clear([AnotherMockModel])
self.assertEqual(self.backend.document_count(), 3)
self.backend.clear([XapianMockModel])
self.assertEqual(self.backend.document_count(), 0)
self.backend.update(self.index, self.sample_objs)
self.assertEqual(self.backend.document_count(), 3)
self.backend.clear([AnotherMockModel, XapianMockModel])
self.assertEqual(self.backend.document_count(), 0)
def test_search(self):
self.backend.update(self.index, self.sample_objs)
self.assertEqual(self.backend.document_count(), 3)
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])
# Ensure that swapping the ``result_class`` works.
self.assertTrue(isinstance(self.backend.search(xapian.Query('indexed'), result_class=MockSearchResult)['results'][0], MockSearchResult))
@ -209,33 +216,33 @@ class XapianSearchBackendTestCase(TestCase):
def test_search_by_mvf(self):
self.backend.update(self.index, self.sample_objs)
self.assertEqual(self.backend.document_count(), 3)
self.assertEqual(self.backend.search(xapian.Query('ab'))['hits'], 1)
self.assertEqual(self.backend.search(xapian.Query('b'))['hits'], 1)
self.assertEqual(self.backend.search(xapian.Query('to'))['hits'], 1)
self.assertEqual(self.backend.search(xapian.Query('one'))['hits'], 3)
def test_field_facets(self):
self.backend.update(self.index, self.sample_objs)
self.assertEqual(self.backend.document_count(), 3)
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)])
results = self.backend.search(xapian.Query('indexed'), facets=['flag'])
self.assertEqual(results['hits'], 3)
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)])
def test_date_facets(self):
self.backend.update(self.index, self.sample_objs)
self.assertEqual(self.backend.document_count(), 3)
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'}})
self.assertEqual(results['hits'], 3)
@ -246,7 +253,7 @@ class XapianSearchBackendTestCase(TestCase):
('2008-11-26T00:00:00', 0),
('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}})
self.assertEqual(results['hits'], 3)
self.assertEqual(results['facets']['dates']['pub_date'], [
@ -254,137 +261,107 @@ class XapianSearchBackendTestCase(TestCase):
('2009-02-16T00:00:00', 3),
('2009-02-01T00:00:00', 0)
])
def test_query_facets(self):
self.backend.update(self.index, self.sample_objs)
self.assertEqual(self.backend.document_count(), 3)
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.backend.update(self.index, self.sample_objs)
self.assertEqual(self.backend.document_count(), 3)
self.assertEqual(self.backend.search(xapian.Query(), narrow_queries=set(['name:david1'])), {'hits': 0, 'results': []})
results = self.backend.search(xapian.Query('indexed'), narrow_queries=set(['name:david1']))
self.assertEqual(results['hits'], 1)
def test_highlight(self):
self.backend.update(self.index, self.sample_objs)
self.assertEqual(self.backend.document_count(), 3)
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'])
def test_spelling_suggestion(self):
self.backend.update(self.index, self.sample_objs)
self.assertEqual(self.backend.document_count(), 3)
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('indxed'))['hits'], 0)
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('XNAMEdavid'))['hits'], 0)
self.assertEqual(self.backend.search(xapian.Query('XNAMEdavid'))['spelling_suggestion'], 'david1')
def test_more_like_this(self):
self.backend.update(self.index, self.sample_objs)
self.assertEqual(self.backend.document_count(), 3)
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])
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])
# 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))
def test_use_correct_site(self):
test_site = SearchSite()
test_site.register(XapianMockModel, XapianMockSearchIndex)
self.backend.update(self.index, self.sample_objs)
# Make sure that ``_process_results`` uses the right ``site``.
self.assertEqual(self.backend.search(xapian.Query('indexed'))['hits'], 3)
self.assertEqual([result.pk for result in self.backend.search(xapian.Query('indexed'))['results']], [1, 2, 3])
self.site.unregister(XapianMockModel)
self.assertEqual(len(self.site.get_indexed_models()), 0)
self.backend.site = test_site
self.assertTrue(len(self.backend.site.get_indexed_models()) > 0)
# Should still be there, despite the main ``site`` not having that model
# registered any longer.
self.assertEqual(self.backend.search(xapian.Query('indexed'))['hits'], 3)
self.assertEqual([result.pk for result in self.backend.search(xapian.Query('indexed'))['results']], [1, 2, 3])
# Unregister it on the backend & make sure it takes effect.
self.backend.site.unregister(XapianMockModel)
self.assertEqual(len(self.backend.site.get_indexed_models()), 0)
self.assertEqual(self.backend.search(xapian.Query('indexed'))['hits'], 0)
# Nuke it & fallback on the main ``site``.
self.backend.site = haystack.site
self.assertEqual(self.backend.search(xapian.Query('indexed'))['hits'], 0)
self.site.register(XapianMockModel, XapianMockSearchIndex)
self.assertEqual(self.backend.search(xapian.Query('indexed'))['hits'], 3)
def test_order_by(self):
self.backend.update(self.index, self.sample_objs)
self.assertEqual(self.backend.document_count(), 3)
results = self.backend.search(xapian.Query(''), sort_by=['pub_date'])
self.assertEqual([result.pk for result in 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])
results = self.backend.search(xapian.Query(''), sort_by=['exp_date'])
self.assertEqual([result.pk for result in 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])
results = self.backend.search(xapian.Query(''), sort_by=['id'])
self.assertEqual([result.pk for result in 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])
results = self.backend.search(xapian.Query(''), sort_by=['value'])
self.assertEqual([result.pk for result in 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])
results = self.backend.search(xapian.Query(''), sort_by=['popularity'])
self.assertEqual([result.pk for result in 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])
results = self.backend.search(xapian.Query(''), sort_by=['flag', 'id'])
self.assertEqual([result.pk for result in 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])
def test_verify_type(self):
self.backend.update(self.index, self.sample_objs)
self.assertEqual(self.backend.document_count(), 3)
@ -404,9 +381,9 @@ class XapianSearchBackendTestCase(TestCase):
self.assertEqual(_marshal_value(datetime.datetime(2009, 5, 9, 0, 0)), u'20090509000000')
self.assertEqual(_marshal_value(datetime.datetime(1899, 5, 18, 0, 0)), u'18990518000000')
self.assertEqual(_marshal_value(datetime.datetime(2009, 5, 18, 1, 16, 30, 250)), u'20090518011630000250')
def test_build_schema(self):
(content_field_name, fields) = self.backend.build_schema(self.site.all_searchfields())
(content_field_name, fields) = self.backend.build_schema(connections['default'].get_unified_index().all_searchfields())
self.assertEqual(content_field_name, 'text')
self.assertEqual(len(fields), 15)
self.assertEqual(fields, [
@ -426,7 +403,7 @@ class XapianSearchBackendTestCase(TestCase):
{'column': 13, 'type': 'text', 'field_name': 'url', 'multi_valued': 'false'},
{'column': 14, 'type': 'long', 'field_name': 'value', 'multi_valued': 'false'}
])
def test_parse_query(self):
self.backend.update(self.index, self.sample_objs)
self.assertEqual(str(self.backend.parse_query('indexed')), 'Xapian::Query(Zindex:(pos=1))')
@ -451,49 +428,58 @@ class LiveXapianMockSearchIndex(indexes.SearchIndex):
created = indexes.DateField()
title = indexes.CharField()
def get_model(self):
return MockModel
class LiveXapianSearchQueryTestCase(TestCase):
"""
SearchQuery specific tests
"""
fixtures = ['initial_data.json']
def setUp(self):
super(LiveXapianSearchQueryTestCase, self).setUp()
site = SearchSite()
backend = SearchBackend(site=site)
index = LiveXapianMockSearchIndex(MockModel, backend=backend)
site.register(MockModel, LiveXapianMockSearchIndex)
self.old_ui = connections['default'].get_unified_index()
ui = UnifiedIndex()
index = LiveXapianMockSearchIndex()
ui.build(indexes=[index])
backend = connections['default'].get_backend()
connections['default']._index = ui
backend.update(index, MockModel.objects.all())
self.sq = SearchQuery(backend=backend)
self.sq = connections['default'].get_query()
def tearDown(self):
connections['default']._index = self.old_ui
super(LiveXapianSearchQueryTestCase, self).tearDown()
def test_get_spelling(self):
self.sq.add_filter(SQ(content='indxd'))
self.assertEqual(self.sq.get_spelling_suggestion(), u'indexed')
self.assertEqual(self.sq.get_spelling_suggestion('indxd'), u'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()), u'Xapian::Query((<alldocuments> AND_NOT VALUE_RANGE 2 a m))')
def test_build_query_gte(self):
self.sq.add_filter(SQ(name__gte='m'))
self.assertEqual(str(self.sq.build_query()), u'Xapian::Query(VALUE_RANGE 2 m zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz)')
def test_build_query_lt(self):
self.sq.add_filter(SQ(name__lt='m'))
self.assertEqual(str(self.sq.build_query()), u'Xapian::Query((<alldocuments> AND_NOT VALUE_RANGE 2 m zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz))')
def test_build_query_lte(self):
self.sq.add_filter(SQ(name__lte='m'))
self.assertEqual(str(self.sq.build_query()), u'Xapian::Query(VALUE_RANGE 2 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)))
@ -502,35 +488,35 @@ class LiveXapianSearchQueryTestCase(TestCase):
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()), u'Xapian::Query(((Zwhi OR why) AND VALUE_RANGE 3 00010101000000 20090210015900 AND (<alldocuments> AND_NOT VALUE_RANGE 2 a david) AND (<alldocuments> AND_NOT VALUE_RANGE 1 20090212121300 99990101000000) AND VALUE_RANGE 5 b zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz AND (Q1 OR Q2 OR Q3)))')
def test_log_query(self):
backends.reset_search_queries()
self.assertEqual(len(backends.queries), 0)
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(backends.queries), 0)
self.assertEqual(len(connections['default'].queries), 0)
settings.DEBUG = True
# Redefine it to clear out the cached results.
self.sq = SearchQuery(backend=SearchBackend())
self.sq = connections['default'].get_query()
self.sq.add_filter(SQ(name='bar'))
len(self.sq.get_results())
self.assertEqual(len(backends.queries), 1)
self.assertEqual(str(backends.queries[0]['query_string']), u'Xapian::Query((ZXNAMEbar OR XNAMEbar))')
self.assertEqual(len(connections['default'].queries), 1)
self.assertEqual(str(connections['default'].queries[0]['query_string']), u'Xapian::Query((ZXNAMEbar OR XNAMEbar))')
# And again, for good measure.
self.sq = SearchQuery(backend=SearchBackend())
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(backends.queries), 2)
self.assertEqual(str(backends.queries[0]['query_string']), u'Xapian::Query((ZXNAMEbar OR XNAMEbar))')
self.assertEqual(str(backends.queries[1]['query_string']), u'Xapian::Query(((ZXNAMEbar OR XNAMEbar) AND (ZXTEXTmoof OR XTEXTmoof)))')
self.assertEqual(len(connections['default'].queries), 2)
self.assertEqual(str(connections['default'].queries[0]['query_string']), u'Xapian::Query((ZXNAMEbar OR XNAMEbar))')
self.assertEqual(str(connections['default'].queries[1]['query_string']), u'Xapian::Query(((ZXNAMEbar OR XNAMEbar) AND (ZXTEXTmoof OR XTEXTmoof)))')
# Restore.
settings.DEBUG = old_debug
@ -540,28 +526,34 @@ class LiveXapianSearchQuerySetTestCase(TestCase):
SearchQuerySet specific tests
"""
fixtures = ['initial_data.json']
def setUp(self):
super(LiveXapianSearchQuerySetTestCase, self).setUp()
site = SearchSite()
backend = SearchBackend(site=site)
index = LiveXapianMockSearchIndex(MockModel, backend=backend)
site.register(MockModel, LiveXapianMockSearchIndex)
backend.update(index, MockModel.objects.all())
self.sq = SearchQuery(backend=backend)
self.sqs = SearchQuerySet(query=self.sq)
self.old_ui = connections['default'].get_unified_index()
self.ui = UnifiedIndex()
self.index = LiveXapianMockSearchIndex()
self.ui.build(indexes=[self.index])
self.backend = connections['default'].get_backend()
connections['default']._index = self.ui
self.backend.update(self.index, MockModel.objects.all())
self.sq = connections['default'].get_query()
self.sqs = SearchQuerySet()
def tearDown(self):
connections['default']._index = self.old_ui
super(LiveXapianSearchQuerySetTestCase, self).tearDown()
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))
@ -571,15 +563,13 @@ class XapianBoostBackendTestCase(TestCase):
def setUp(self):
super(XapianBoostBackendTestCase, self).setUp()
self.site = SearchSite()
self.sb = SearchBackend(site=self.site)
self.smmi = XapianBoostMockSearchIndex(AFourthMockModel, backend=self.sb)
self.site.register(AFourthMockModel, XapianBoostMockSearchIndex)
# Stow.
import haystack
self.old_site = haystack.site
haystack.site = self.site
self.old_ui = connections['default'].get_unified_index()
self.ui = UnifiedIndex()
self.index = XapianBoostMockSearchIndex()
self.ui.build(indexes=[self.index])
self.sb = connections['default'].get_backend()
connections['default']._index = self.ui
self.sample_objs = []
@ -596,15 +586,14 @@ class XapianBoostBackendTestCase(TestCase):
self.sample_objs.append(mock)
def tearDown(self):
import haystack
haystack.site = self.old_site
connections['default']._index = self.old_ui
super(XapianBoostBackendTestCase, self).tearDown()
def test_boost(self):
self.sb.update(self.smmi, self.sample_objs)
self.sb.update(self.index, self.sample_objs)
sqs = SearchQuerySet()
self.assertEqual(len(sqs.all()), 4)
results = sqs.filter(SQ(author='daniel') | SQ(editor='daniel'))

View File

@ -1,4 +1,4 @@
# Copyright (C) 2009, 2010, 2011 David Sauve
# Copyright (C) 2009, 2010, 2011, 2012 David Sauve
# Copyright (C) 2009, 2010 Trapeze
import datetime
@ -8,7 +8,7 @@ import shutil
from django.conf import settings
from django.test import TestCase
from haystack.backends.xapian_backend import SearchBackend, SearchQuery
from haystack import connections
from haystack.query import SQ
from core.models import MockModel, AnotherMockModel
@ -17,41 +17,41 @@ from core.models import MockModel, AnotherMockModel
class XapianSearchQueryTestCase(TestCase):
def setUp(self):
super(XapianSearchQueryTestCase, self).setUp()
self.sq = SearchQuery(backend=SearchBackend())
self.sq = connections['default'].get_query()
def tearDown(self):
if os.path.exists(settings.HAYSTACK_XAPIAN_PATH):
shutil.rmtree(settings.HAYSTACK_XAPIAN_PATH)
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()), u'Xapian::Query(<alldocuments>)')
def test_build_query_single_word(self):
self.sq.add_filter(SQ(content='hello'))
self.assertEqual(str(self.sq.build_query()), u'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()), u'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()), u'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()), u'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()), u'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()), u'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()), u'Xapian::Query((<alldocuments> AND_NOT (Z20090508000000 OR 20090508000000)))')
@ -59,7 +59,7 @@ class XapianSearchQueryTestCase(TestCase):
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()), u'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()), u'Xapian::Query((<alldocuments> AND_NOT (Z20090508112800 OR 20090508112800)))')
@ -67,68 +67,68 @@ class XapianSearchQueryTestCase(TestCase):
def test_build_query_float(self):
self.sq.add_filter(SQ(content=25.52))
self.assertEqual(str(self.sq.build_query()), u'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()), u'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()), u'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()), u'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()), u'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()), u'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()), u'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()), u'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()), u'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()), u'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()), u'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()), u'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()), u'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()), u'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()), u'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.)))')
@ -137,30 +137,30 @@ class XapianSearchQueryTestCase(TestCase):
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()), u'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()), u'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()), u'Xapian::Query(((Zhello OR hello) AND 0 * XCONTENTTYPEcore.mockmodel))')
self.sq.add_model(AnotherMockModel)
self.assertTrue(str(self.sq.build_query()) in u'Xapian::Query(((Zhello OR hello) AND (0 * XCONTENTTYPEcore.anothermockmodel OR 0 * XCONTENTTYPEcore.mockmodel)))' or u'Xapian::Query(((Zhello OR hello) AND (0 * XCONTENTTYPEcore.mockmodel OR 0 * XCONTENTTYPEcore.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()), u'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)))

File diff suppressed because it is too large Load Diff