class SearchQuerySetTestCase(TestCase): fixtures = ["base_data.json", "bulk_data.json"] def setUp(self): super(SearchQuerySetTestCase, self).setUp() # Stow. self.old_unified_index = connections["default"]._index self.ui = UnifiedIndex() self.bmmsi = BasicMockModelSearchIndex() self.cpkmmsi = CharPKMockModelSearchIndex() self.ui.build(indexes=[self.bmmsi, self.cpkmmsi]) connections["default"]._index = self.ui # Update the "index". backend = connections["default"].get_backend() backend.clear() backend.update(self.bmmsi, MockModel.objects.all()) self.msqs = SearchQuerySet() # Stow. reset_search_queries() def tearDown(self): # Restore. connections["default"]._index = self.old_unified_index super(SearchQuerySetTestCase, self).tearDown() def test_len(self): self.assertEqual(len(self.msqs), 23) def test_repr(self): reset_search_queries() self.assertEqual(len(connections["default"].queries), 0) self.assertRegexpMatches( repr(self.msqs), r"^<SearchQuerySet: query=<test_haystack.mocks.MockSearchQuery object" r" at 0x[0-9A-Fa-f]+>, using=None>$", ) def test_iter(self): reset_search_queries() self.assertEqual(len(connections["default"].queries), 0) msqs = self.msqs.all() results = [int(res.pk) for res in iter(msqs)] self.assertEqual(results, [res.pk for res in MOCK_SEARCH_RESULTS[:23]]) self.assertEqual(len(connections["default"].queries), 3) def test_slice(self): reset_search_queries() self.assertEqual(len(connections["default"].queries), 0) results = self.msqs.all() self.assertEqual([int(res.pk) for res in results[1:11]], [res.pk for res in MOCK_SEARCH_RESULTS[1:11]]) self.assertEqual(len(connections["default"].queries), 1) reset_search_queries() self.assertEqual(len(connections["default"].queries), 0) results = self.msqs.all() self.assertEqual(int(results[22].pk), MOCK_SEARCH_RESULTS[22].pk) self.assertEqual(len(connections["default"].queries), 1) def test_manual_iter(self): results = self.msqs.all() reset_search_queries() self.assertEqual(len(connections["default"].queries), 0) check = [result.pk for result in results._manual_iter()] self.assertEqual( check, [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", ], ) self.assertEqual(len(connections["default"].queries), 3) reset_search_queries() self.assertEqual(len(connections["default"].queries), 0) # Test to ensure we properly fill the cache, even if we get fewer # results back (not a handled model) than the hit count indicates. # This will hang indefinitely if broken. old_ui = self.ui self.ui.build(indexes=[self.cpkmmsi]) connections["default"]._index = self.ui self.cpkmmsi.update() results = self.msqs.all() loaded = [result.pk for result in results._manual_iter()] self.assertEqual(loaded, ["sometext", "1234"]) self.assertEqual(len(connections["default"].queries), 1) connections["default"]._index = old_ui def test_cache_is_full(self): reset_search_queries() self.assertEqual(len(connections["default"].queries), 0) self.assertEqual(self.msqs._cache_is_full(), False) results = self.msqs.all() fire_the_iterator_and_fill_cache = list(results) self.assertEqual(23, len(fire_the_iterator_and_fill_cache)) self.assertEqual(results._cache_is_full(), True) self.assertEqual(len(connections["default"].queries), 4) def test_all(self): sqs = self.msqs.all() self.assertTrue(isinstance(sqs, SearchQuerySet)) def test_filter(self): sqs = self.msqs.filter(content="foo") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_exclude(self): sqs = self.msqs.exclude(content="foo") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_order_by(self): sqs = self.msqs.order_by("foo") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertTrue("foo" in sqs.query.order_by) def test_models(self): # Stow. old_unified_index = connections["default"]._index ui = UnifiedIndex() bmmsi = BasicMockModelSearchIndex() bammsi = BasicAnotherMockModelSearchIndex() ui.build(indexes=[bmmsi, bammsi]) connections["default"]._index = ui msqs = SearchQuerySet() sqs = msqs.all() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 0) sqs = msqs.models(MockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) sqs = msqs.models(MockModel, AnotherMockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 2) # This will produce a warning. ui.build(indexes=[bmmsi]) sqs = msqs.models(AnotherMockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) def test_result_class(self): sqs = self.msqs.all() self.assertTrue(issubclass(sqs.query.result_class, SearchResult)) # Custom class. class IttyBittyResult(object): pass sqs = self.msqs.result_class(IttyBittyResult) self.assertTrue(issubclass(sqs.query.result_class, IttyBittyResult)) # Reset to default. sqs = self.msqs.result_class(None) self.assertTrue(issubclass(sqs.query.result_class, SearchResult)) def test_boost(self): sqs = self.msqs.boost("foo", 10) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.boost.keys()), 1) def test_highlight(self): sqs = self.msqs.highlight() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.query.highlight, True) def test_spelling_override(self): sqs = self.msqs.filter(content="not the spellchecking query") self.assertEqual(sqs.query.spelling_query, None) sqs = self.msqs.set_spelling_query("override") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.query.spelling_query, "override") def test_spelling_suggestions(self): # Test the case where spelling support is disabled. sqs = self.msqs.filter(content="Indx") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.spelling_suggestion(), None) self.assertEqual(sqs.spelling_suggestion("indexy"), None) def test_raw_search(self): self.assertEqual(len(self.msqs.raw_search("foo")), 23) self.assertEqual(len(self.msqs.raw_search("(content__exact:hello AND content__exact:world)")), 23) def test_load_all(self): # Models with character primary keys. sqs = SearchQuerySet() sqs.query.backend = CharPKMockSearchBackend("charpk") results = sqs.load_all().all() self.assertEqual(len(results._result_cache), 0) results._fill_cache(0, 2) self.assertEqual(len([result for result in results._result_cache if result is not None]), 2) # If nothing is handled, you get nothing. old_ui = connections["default"]._index ui = UnifiedIndex() ui.build(indexes=[]) connections["default"]._index = ui sqs = self.msqs.load_all() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs), 0) connections["default"]._index = old_ui # For full tests, see the solr_backend. def test_load_all_read_queryset(self): # Stow. old_ui = connections["default"]._index ui = UnifiedIndex() gafmmsi = GhettoAFifthMockModelSearchIndex() ui.build(indexes=[gafmmsi]) connections["default"]._index = ui gafmmsi.update() sqs = SearchQuerySet() results = sqs.load_all().all() results.query.backend = ReadQuerySetMockSearchBackend("default") results._fill_cache(0, 2) # The deleted result isn't returned self.assertEqual(len([result for result in results._result_cache if result is not None]), 1) # Register a SearchIndex with a read_queryset that returns deleted items rqstsi = TextReadQuerySetTestSearchIndex() ui.build(indexes=[rqstsi]) rqstsi.update() sqs = SearchQuerySet() results = sqs.load_all().all() results.query.backend = ReadQuerySetMockSearchBackend("default") results._fill_cache(0, 2) # Both the deleted and not deleted items are returned self.assertEqual(len([result for result in results._result_cache if result is not None]), 2) # Restore. connections["default"]._index = old_ui def test_auto_query(self): sqs = self.msqs.auto_query("test search -stuff") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), "<SQ: AND content__content=test search -stuff>") sqs = self.msqs.auto_query('test "my thing" search -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND content__content=test "my thing" search -stuff>') sqs = self.msqs.auto_query("test \"my thing\" search 'moar quotes' -stuff") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), "<SQ: AND content__content=test \"my thing\" search 'moar quotes' -stuff>" ) sqs = self.msqs.auto_query('test "my thing" search \'moar quotes\' "foo -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), '<SQ: AND content__content=test "my thing" search \'moar quotes\' "foo -stuff>', ) sqs = self.msqs.auto_query("test - stuff") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), "<SQ: AND content__content=test - stuff>") # Ensure bits in exact matches get escaped properly as well. sqs = self.msqs.auto_query('"pants:rule"') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND content__content="pants:rule">') # Now with a different fieldname sqs = self.msqs.auto_query("test search -stuff", fieldname="title") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), "<SQ: AND title__content=test search -stuff>") sqs = self.msqs.auto_query('test "my thing" search -stuff', fieldname="title") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND title__content=test "my thing" search -stuff>') def test_count(self): self.assertEqual(self.msqs.count(), 23) def test_facet_counts(self): self.assertEqual(self.msqs.facet_counts(), {}) def test_best_match(self): self.assertTrue(isinstance(self.msqs.best_match(), SearchResult)) def test_latest(self): self.assertTrue(isinstance(self.msqs.latest("pub_date"), SearchResult)) def test_more_like_this(self): mock = MockModel() mock.id = 1 self.assertEqual(len(self.msqs.more_like_this(mock)), 23) def test_facets(self): sqs = self.msqs.facet("foo") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.facets), 1) sqs2 = self.msqs.facet("foo").facet("bar") self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.facets), 2) def test_date_facets(self): try: sqs = self.msqs.date_facet( "foo", start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by="smarblaph" ) self.fail() except FacetingError as e: self.assertEqual( str(e), "The gap_by ('smarblaph') must be one of the following: year, month, day, hour, minute, second." ) sqs = self.msqs.date_facet( "foo", start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by="month" ) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.date_facets), 1) sqs2 = self.msqs.date_facet( "foo", start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by="month" ).date_facet("bar", start_date=datetime.date(2007, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by="year") self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.date_facets), 2) def test_query_facets(self): sqs = self.msqs.query_facet("foo", "[bar TO *]") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_facets), 1) sqs2 = self.msqs.query_facet("foo", "[bar TO *]").query_facet("bar", "[100 TO 499]") self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.query_facets), 2) # Test multiple query facets on a single field sqs3 = ( self.msqs.query_facet("foo", "[bar TO *]") .query_facet("bar", "[100 TO 499]") .query_facet("foo", "[1000 TO 1499]") ) self.assertTrue(isinstance(sqs3, SearchQuerySet)) self.assertEqual(len(sqs3.query.query_facets), 3) def test_stats(self): sqs = self.msqs.stats_facet("foo", "bar") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.stats), 1) sqs2 = self.msqs.stats_facet("foo", "bar").stats_facet("foo", "baz") self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.stats), 1) sqs3 = self.msqs.stats_facet("foo", "bar").stats_facet("moof", "baz") self.assertTrue(isinstance(sqs3, SearchQuerySet)) self.assertEqual(len(sqs3.query.stats), 2) def test_narrow(self): sqs = self.msqs.narrow("foo:moof") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.narrow_queries), 1) def test_clone(self): results = self.msqs.filter(foo="bar", foo__lt="10") clone = results._clone() self.assertTrue(isinstance(clone, SearchQuerySet)) self.assertEqual(str(clone.query), str(results.query)) self.assertEqual(clone._result_cache, []) self.assertEqual(clone._result_count, None) self.assertEqual(clone._cache_full, False) self.assertEqual(clone._using, results._using) def test_using(self): sqs = SearchQuerySet(using="default") self.assertNotEqual(sqs.query, None) self.assertEqual(sqs.query._using, "default") def test_chaining(self): sqs = self.msqs.filter(content="foo") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) # A second instance should inherit none of the changes from above. sqs = self.msqs.filter(content="bar") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_none(self): sqs = self.msqs.none() self.assertTrue(isinstance(sqs, EmptySearchQuerySet)) self.assertEqual(len(sqs), 0) def test___and__(self): sqs1 = self.msqs.filter(content="foo") sqs2 = self.msqs.filter(content="bar") sqs = sqs1 & sqs2 self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 2) def test___or__(self): sqs1 = self.msqs.filter(content="foo") sqs2 = self.msqs.filter(content="bar") sqs = sqs1 | sqs2 self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 2) def test_and_or(self): """ Combining AND queries with OR should give AND(OR(a, b), OR(c, d)) """ sqs1 = self.msqs.filter(content="foo").filter(content="oof") sqs2 = self.msqs.filter(content="bar").filter(content="rab") sqs = sqs1 | sqs2 self.assertEqual(sqs.query.query_filter.connector, "OR") self.assertEqual(repr(sqs.query.query_filter.children[0]), repr(sqs1.query.query_filter)) self.assertEqual(repr(sqs.query.query_filter.children[1]), repr(sqs2.query.query_filter)) def test_or_and(self): """ Combining OR queries with AND should give OR(AND(a, b), AND(c, d)) """ sqs1 = self.msqs.filter(content="foo").filter_or(content="oof") sqs2 = self.msqs.filter(content="bar").filter_or(content="rab") sqs = sqs1 & sqs2 self.assertEqual(sqs.query.query_filter.connector, "AND") self.assertEqual(repr(sqs.query.query_filter.children[0]), repr(sqs1.query.query_filter)) self.assertEqual(repr(sqs.query.query_filter.children[1]), repr(sqs2.query.query_filter))
class SearchQuerySetTestCase(TestCase): def setUp(self): super(SearchQuerySetTestCase, self).setUp() self.bsqs = SearchQuerySet(query=DummySearchQuery(backend=DummySearchBackend())) self.msqs = SearchQuerySet(query=MockSearchQuery(backend=MockSearchBackend())) self.mmsqs = SearchQuerySet(query=MockSearchQuery(backend=MixedMockSearchBackend())) # Stow. self.old_debug = settings.DEBUG settings.DEBUG = True self.old_site = haystack.site test_site = SearchSite() test_site.register(MockModel) test_site.register(CharPKMockModel) haystack.site = test_site backends.reset_search_queries() def tearDown(self): # Restore. haystack.site = self.old_site settings.DEBUG = self.old_debug super(SearchQuerySetTestCase, self).tearDown() def test_len(self): # Dummy always returns 0. self.assertEqual(len(self.bsqs), 0) self.assertEqual(len(self.msqs), 100) def test_repr(self): self.assertEqual(repr(self.bsqs), '[]') backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) self.assertEqual(repr(self.msqs), "[<SearchResult: core.MockModel (pk=0)>, <SearchResult: core.MockModel (pk=1)>, <SearchResult: core.MockModel (pk=2)>, <SearchResult: core.MockModel (pk=3)>, <SearchResult: core.MockModel (pk=4)>, <SearchResult: core.MockModel (pk=5)>, <SearchResult: core.MockModel (pk=6)>, <SearchResult: core.MockModel (pk=7)>, <SearchResult: core.MockModel (pk=8)>, <SearchResult: core.MockModel (pk=9)>, <SearchResult: core.MockModel (pk=10)>, <SearchResult: core.MockModel (pk=11)>, <SearchResult: core.MockModel (pk=12)>, <SearchResult: core.MockModel (pk=13)>, <SearchResult: core.MockModel (pk=14)>, <SearchResult: core.MockModel (pk=15)>, <SearchResult: core.MockModel (pk=16)>, <SearchResult: core.MockModel (pk=17)>, <SearchResult: core.MockModel (pk=18)>, '...(remaining elements truncated)...']") self.assertEqual(len(backends.queries), 1) def test_iter(self): # Dummy always returns []. self.assertEqual([result for result in self.bsqs.all()], []) backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) msqs = self.msqs.all() results = [result for result in msqs] self.assertEqual(results, MOCK_SEARCH_RESULTS) self.assertEqual(len(backends.queries), 10) def test_slice(self): backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) results = self.msqs.all() self.assertEqual(results[1:11], MOCK_SEARCH_RESULTS[1:11]) self.assertEqual(len(backends.queries), 1) backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) results = self.msqs.all() self.assertEqual(results[50], MOCK_SEARCH_RESULTS[50]) self.assertEqual(len(backends.queries), 1) def test_manual_iter(self): results = self.msqs.all() backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) for offset, result in enumerate(results._manual_iter()): self.assertEqual(result, MOCK_SEARCH_RESULTS[offset]) self.assertEqual(len(backends.queries), 10) backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) # Test to ensure we properly fill the cache, even if we get fewer # results back (not in the SearchSite) than the hit count indicates. # This will hang indefinitely if broken. results = self.mmsqs.all() loaded = [result.pk for result in results._manual_iter()] self.assertEqual(loaded, [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 15, 16, 17, 18, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]) self.assertEqual(len(backends.queries), 8) def test_fill_cache(self): backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) results = self.msqs.all() self.assertEqual(len(results._result_cache), 0) self.assertEqual(len(backends.queries), 0) results._fill_cache(0, 10) self.assertEqual(len([result for result in results._result_cache if result is not None]), 10) self.assertEqual(len(backends.queries), 1) results._fill_cache(10, 20) self.assertEqual(len([result for result in results._result_cache if result is not None]), 20) self.assertEqual(len(backends.queries), 2) backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) # Test to ensure we properly fill the cache, even if we get fewer # results back (not in the SearchSite) than the hit count indicates. results = self.mmsqs.all() self.assertEqual(len([result for result in results._result_cache if result is not None]), 0) self.assertEqual([result.pk for result in results._result_cache if result is not None], []) self.assertEqual(len(backends.queries), 0) results._fill_cache(0, 10) self.assertEqual(len([result for result in results._result_cache if result is not None]), 9) self.assertEqual([result.pk for result in results._result_cache if result is not None], [0, 1, 2, 3, 4, 5, 6, 7, 8]) self.assertEqual(len(backends.queries), 2) results._fill_cache(10, 20) self.assertEqual(len([result for result in results._result_cache if result is not None]), 17) self.assertEqual([result.pk for result in results._result_cache if result is not None], [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 15, 16, 17, 18, 19]) self.assertEqual(len(backends.queries), 4) results._fill_cache(20, 30) self.assertEqual(len([result for result in results._result_cache if result is not None]), 27) self.assertEqual([result.pk for result in results._result_cache if result is not None], [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]) self.assertEqual(len(backends.queries), 6) def test_cache_is_full(self): # Dummy always has a count of 0 and an empty _result_cache, hence True. self.assertEqual(self.bsqs._cache_is_full(), False) results = self.bsqs.all() fire_the_iterator_and_fill_cache = [result for result in results] self.assertEqual(results._cache_is_full(), True) backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) self.assertEqual(self.msqs._cache_is_full(), False) results = self.msqs.all() fire_the_iterator_and_fill_cache = [result for result in results] self.assertEqual(results._cache_is_full(), True) self.assertEqual(len(backends.queries), 10) def test_all(self): sqs = self.bsqs.all() self.assert_(isinstance(sqs, SearchQuerySet)) def test_filter(self): sqs = self.bsqs.filter(content='foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_exclude(self): sqs = self.bsqs.exclude(content='foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_order_by(self): sqs = self.bsqs.order_by('foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assert_('foo' in sqs.query.order_by) def test_models(self): mock_index_site = SearchSite() mock_index_site.register(MockModel) mock_index_site.register(AnotherMockModel) bsqs = SearchQuerySet(site=mock_index_site) sqs = bsqs.all() self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 0) sqs = bsqs.models(MockModel) self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) sqs = bsqs.models(MockModel, AnotherMockModel) self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 2) # This will produce a warning. mock_index_site.unregister(AnotherMockModel) sqs = bsqs.models(AnotherMockModel) self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) def test_result_class(self): sqs = self.bsqs.all() self.assertTrue(issubclass(sqs.query.result_class, SearchResult)) # Custom class. class IttyBittyResult(object): pass sqs = self.bsqs.result_class(IttyBittyResult) self.assertTrue(issubclass(sqs.query.result_class, IttyBittyResult)) # Reset to default. sqs = self.bsqs.result_class(None) self.assertTrue(issubclass(sqs.query.result_class, SearchResult)) def test_boost(self): sqs = self.bsqs.boost('foo', 10) self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.boost.keys()), 1) def test_highlight(self): sqs = self.bsqs.highlight() self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.query.highlight, True) def test_spelling(self): # Test the case where spelling support is disabled. sqs = self.bsqs.filter(content='Indx') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.spelling_suggestion(), None) self.assertEqual(sqs.spelling_suggestion('indexy'), None) def test_raw_search(self): self.assertEqual(len(self.bsqs.raw_search('foo')), 0) self.assertEqual(len(self.bsqs.raw_search('(content__exact hello AND content__exact world)')), 1) def test_load_all(self): # Models with character primary keys sqs = SearchQuerySet(query=MockSearchQuery(backend=CharPKMockSearchBackend())) results = sqs.load_all().all() self.assertEqual(len(results._result_cache), 0) results._fill_cache(0, 2) self.assertEqual(len([result for result in results._result_cache if result is not None]), 2) # If nothing is registered, you get nothing. haystack.site.unregister(MockModel) haystack.site.unregister(CharPKMockModel) sqs = self.msqs.load_all() self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs), 0) # For full tests, see the solr_backend. def test_auto_query(self): sqs = self.bsqs.auto_query('test search -stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND (content__exact=test AND content__exact=search AND NOT (content__exact=stuff))>') sqs = self.bsqs.auto_query('test "my thing" search -stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND (content__exact=my thing AND content__exact=test AND content__exact=search AND NOT (content__exact=stuff))>') sqs = self.bsqs.auto_query('test "my thing" search \'moar quotes\' -stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), "<SQ: AND (content__exact=my thing AND content__exact=test AND content__exact=search AND content__exact='moar AND content__exact=quotes' AND NOT (content__exact=stuff))>") sqs = self.bsqs.auto_query('test "my thing" search \'moar quotes\' "foo -stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND (content__exact=my thing AND content__exact=test AND content__exact=search AND content__exact=\'moar AND content__exact=quotes\' AND content__exact="foo AND NOT (content__exact=stuff))>') sqs = self.bsqs.auto_query('test - stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND (content__exact=test AND content__exact=- AND content__exact=stuff)>') # Ensure bits in exact matches get escaped properly as well. sqs = self.bsqs.auto_query('"pants:rule"') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND content__exact=pants:rule>') def test_count(self): self.assertEqual(self.bsqs.count(), 0) def test_facet_counts(self): self.assertEqual(self.bsqs.facet_counts(), {}) def test_best_match(self): self.assert_(isinstance(self.msqs.best_match(), SearchResult)) def test_latest(self): self.assert_(isinstance(self.msqs.latest('pub_date'), SearchResult)) def test_more_like_this(self): mock = MockModel() mock.id = 1 self.assertEqual(len(self.msqs.more_like_this(mock)), 100) def test_facets(self): sqs = self.bsqs.facet('foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.facets), 1) sqs2 = self.bsqs.facet('foo').facet('bar') self.assert_(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.facets), 2) def test_date_facets(self): try: sqs = self.bsqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='smarblaph') self.fail() except FacetingError, e: self.assertEqual(str(e), "The gap_by ('smarblaph') must be one of the following: year, month, day, hour, minute, second.") sqs = self.bsqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='month') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.date_facets), 1) sqs2 = self.bsqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='month').date_facet('bar', start_date=datetime.date(2007, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='year') self.assert_(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.date_facets), 2)
class SearchQuerySetTestCase(TestCase): fixtures = ['bulk_data.json'] def setUp(self): super(SearchQuerySetTestCase, self).setUp() # Stow. self.old_unified_index = connections['default']._index self.ui = UnifiedIndex() self.bmmsi = BasicMockModelSearchIndex() self.cpkmmsi = CharPKMockModelSearchIndex() self.ui.build(indexes=[self.bmmsi, self.cpkmmsi]) connections['default']._index = self.ui # Update the "index". backend = connections['default'].get_backend() backend.clear() backend.update(self.bmmsi, MockModel.objects.all()) self.msqs = SearchQuerySet() # Stow. self.old_debug = settings.DEBUG settings.DEBUG = True reset_search_queries() def tearDown(self): # Restore. connections['default']._index = self.old_unified_index settings.DEBUG = self.old_debug super(SearchQuerySetTestCase, self).tearDown() def test_len(self): self.assertEqual(len(self.msqs), 23) def test_repr(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) self.assertEqual(repr(self.msqs), "[<SearchResult: core.mockmodel (pk=u'1')>, <SearchResult: core.mockmodel (pk=u'2')>, <SearchResult: core.mockmodel (pk=u'3')>, <SearchResult: core.mockmodel (pk=u'4')>, <SearchResult: core.mockmodel (pk=u'5')>, <SearchResult: core.mockmodel (pk=u'6')>, <SearchResult: core.mockmodel (pk=u'7')>, <SearchResult: core.mockmodel (pk=u'8')>, <SearchResult: core.mockmodel (pk=u'9')>, <SearchResult: core.mockmodel (pk=u'10')>, <SearchResult: core.mockmodel (pk=u'11')>, <SearchResult: core.mockmodel (pk=u'12')>, <SearchResult: core.mockmodel (pk=u'13')>, <SearchResult: core.mockmodel (pk=u'14')>, <SearchResult: core.mockmodel (pk=u'15')>, <SearchResult: core.mockmodel (pk=u'16')>, <SearchResult: core.mockmodel (pk=u'17')>, <SearchResult: core.mockmodel (pk=u'18')>, <SearchResult: core.mockmodel (pk=u'19')>, '...(remaining elements truncated)...']") self.assertEqual(len(connections['default'].queries), 1) def test_iter(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) msqs = self.msqs.all() results = [int(res.pk) for res in msqs] self.assertEqual(results, [res.pk for res in MOCK_SEARCH_RESULTS[:23]]) self.assertEqual(len(connections['default'].queries), 3) def test_slice(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) results = self.msqs.all() self.assertEqual([int(res.pk) for res in results[1:11]], [res.pk for res in MOCK_SEARCH_RESULTS[1:11]]) self.assertEqual(len(connections['default'].queries), 1) reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) results = self.msqs.all() self.assertEqual(int(results[22].pk), MOCK_SEARCH_RESULTS[22].pk) self.assertEqual(len(connections['default'].queries), 1) def test_manual_iter(self): results = self.msqs.all() reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) check = [result.pk for result in results._manual_iter()] self.assertEqual(check, [u'1', u'2', u'3', u'4', u'5', u'6', u'7', u'8', u'9', u'10', u'11', u'12', u'13', u'14', u'15', u'16', u'17', u'18', u'19', u'20', u'21', u'22', u'23']) self.assertEqual(len(connections['default'].queries), 3) reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) # Test to ensure we properly fill the cache, even if we get fewer # results back (not a handled model) than the hit count indicates. # This will hang indefinitely if broken. old_ui = self.ui self.ui.build(indexes=[self.cpkmmsi]) connections['default']._index = self.ui self.cpkmmsi.update() results = self.msqs.all() loaded = [result.pk for result in results._manual_iter()] self.assertEqual(loaded, [u'sometext', u'1234']) self.assertEqual(len(connections['default'].queries), 1) connections['default']._index = old_ui def test_fill_cache(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) results = self.msqs.all() self.assertEqual(len(results._result_cache), 0) self.assertEqual(len(connections['default'].queries), 0) results._fill_cache(0, 10) self.assertEqual(len([result for result in results._result_cache if result is not None]), 10) self.assertEqual(len(connections['default'].queries), 1) results._fill_cache(10, 20) self.assertEqual(len([result for result in results._result_cache if result is not None]), 20) self.assertEqual(len(connections['default'].queries), 2) reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) # Test to ensure we properly fill the cache, even if we get fewer # results back (not a handled model) than the hit count indicates. sqs = SearchQuerySet().all() sqs.query.backend = MixedMockSearchBackend('default') results = sqs self.assertEqual(len([result for result in results._result_cache if result is not None]), 0) self.assertEqual([int(result.pk) for result in results._result_cache if result is not None], []) self.assertEqual(len(connections['default'].queries), 0) results._fill_cache(0, 10) self.assertEqual(len([result for result in results._result_cache if result is not None]), 9) self.assertEqual([int(result.pk) for result in results._result_cache if result is not None], [1, 2, 3, 4, 5, 6, 7, 8, 10]) self.assertEqual(len(connections['default'].queries), 2) results._fill_cache(10, 20) self.assertEqual(len([result for result in results._result_cache if result is not None]), 17) self.assertEqual([int(result.pk) for result in results._result_cache if result is not None], [1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 15, 16, 17, 18, 19, 20]) self.assertEqual(len(connections['default'].queries), 4) results._fill_cache(20, 30) self.assertEqual(len([result for result in results._result_cache if result is not None]), 20) self.assertEqual([int(result.pk) for result in results._result_cache if result is not None], [1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 15, 16, 17, 18, 19, 20, 21, 22, 23]) self.assertEqual(len(connections['default'].queries), 6) def test_cache_is_full(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) self.assertEqual(self.msqs._cache_is_full(), False) results = self.msqs.all() fire_the_iterator_and_fill_cache = [result for result in results] self.assertEqual(results._cache_is_full(), True) self.assertEqual(len(connections['default'].queries), 3) def test_all(self): sqs = self.msqs.all() self.assertTrue(isinstance(sqs, SearchQuerySet)) def test_filter(self): sqs = self.msqs.filter(content='foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_exclude(self): sqs = self.msqs.exclude(content='foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_order_by(self): sqs = self.msqs.order_by('foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertTrue('foo' in sqs.query.order_by) def test_models(self): # Stow. old_unified_index = connections['default']._index ui = UnifiedIndex() bmmsi = BasicMockModelSearchIndex() bammsi = BasicAnotherMockModelSearchIndex() ui.build(indexes=[bmmsi, bammsi]) connections['default']._index = ui msqs = SearchQuerySet() sqs = msqs.all() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 0) sqs = msqs.models(MockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) sqs = msqs.models(MockModel, AnotherMockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 2) # This will produce a warning. ui.build(indexes=[bmmsi]) sqs = msqs.models(AnotherMockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) def test_result_class(self): sqs = self.msqs.all() self.assertTrue(issubclass(sqs.query.result_class, SearchResult)) # Custom class. class IttyBittyResult(object): pass sqs = self.msqs.result_class(IttyBittyResult) self.assertTrue(issubclass(sqs.query.result_class, IttyBittyResult)) # Reset to default. sqs = self.msqs.result_class(None) self.assertTrue(issubclass(sqs.query.result_class, SearchResult)) def test_boost(self): sqs = self.msqs.boost('foo', 10) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.boost.keys()), 1) def test_highlight(self): sqs = self.msqs.highlight() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.query.highlight, True) def test_spelling(self): # Test the case where spelling support is disabled. sqs = self.msqs.filter(content='Indx') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.spelling_suggestion(), None) self.assertEqual(sqs.spelling_suggestion('indexy'), None) def test_raw_search(self): self.assertEqual(len(self.msqs.raw_search('foo')), 23) self.assertEqual(len(self.msqs.raw_search('(content__exact:hello AND content__exact:world)')), 23) def test_load_all(self): # Models with character primary keys. sqs = SearchQuerySet() sqs.query.backend = CharPKMockSearchBackend('charpk') results = sqs.load_all().all() self.assertEqual(len(results._result_cache), 0) results._fill_cache(0, 2) self.assertEqual(len([result for result in results._result_cache if result is not None]), 2) # If nothing is handled, you get nothing. old_ui = connections['default']._index ui = UnifiedIndex() ui.build(indexes=[]) connections['default']._index = ui sqs = self.msqs.load_all() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs), 0) connections['default']._index = old_ui # For full tests, see the solr_backend. def test_load_all_read_queryset(self): # Stow. old_ui = connections['default']._index ui = UnifiedIndex() gafmmsi = GhettoAFifthMockModelSearchIndex() ui.build(indexes=[gafmmsi]) connections['default']._index = ui gafmmsi.update() sqs = SearchQuerySet() results = sqs.load_all().all() results.query.backend = ReadQuerySetMockSearchBackend('default') results._fill_cache(0, 2) # The deleted result isn't returned self.assertEqual(len([result for result in results._result_cache if result is not None]), 1) # Register a SearchIndex with a read_queryset that returns deleted items rqstsi = TextReadQuerySetTestSearchIndex() ui.build(indexes=[rqstsi]) rqstsi.update() sqs = SearchQuerySet() results = sqs.load_all().all() results.query.backend = ReadQuerySetMockSearchBackend('default') results._fill_cache(0, 2) # Both the deleted and not deleted items are returned self.assertEqual(len([result for result in results._result_cache if result is not None]), 2) # Restore. connections['default']._index = old_ui def test_auto_query(self): sqs = self.msqs.auto_query('test search -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND content__contains=test search -stuff>') sqs = self.msqs.auto_query('test "my thing" search -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND content__contains=test "my thing" search -stuff>') sqs = self.msqs.auto_query('test "my thing" search \'moar quotes\' -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND content__contains=test "my thing" search \'moar quotes\' -stuff>') sqs = self.msqs.auto_query('test "my thing" search \'moar quotes\' "foo -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND content__contains=test "my thing" search \'moar quotes\' "foo -stuff>') sqs = self.msqs.auto_query('test - stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), "<SQ: AND content__contains=test - stuff>") # Ensure bits in exact matches get escaped properly as well. sqs = self.msqs.auto_query('"pants:rule"') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND content__contains="pants:rule">') # Now with a different fieldname sqs = self.msqs.auto_query('test search -stuff', fieldname='title') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), "<SQ: AND title__contains=test search -stuff>") sqs = self.msqs.auto_query('test "my thing" search -stuff', fieldname='title') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND title__contains=test "my thing" search -stuff>') def test_count(self): self.assertEqual(self.msqs.count(), 23) def test_facet_counts(self): self.assertEqual(self.msqs.facet_counts(), {}) def test_best_match(self): self.assertTrue(isinstance(self.msqs.best_match(), SearchResult)) def test_latest(self): self.assertTrue(isinstance(self.msqs.latest('pub_date'), SearchResult)) def test_more_like_this(self): mock = MockModel() mock.id = 1 self.assertEqual(len(self.msqs.more_like_this(mock)), 23) def test_facets(self): sqs = self.msqs.facet('foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.facets), 1) sqs2 = self.msqs.facet('foo').facet('bar') self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.facets), 2) def test_date_facets(self): try: sqs = self.msqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='smarblaph') self.fail() except FacetingError, e: self.assertEqual(str(e), "The gap_by ('smarblaph') must be one of the following: year, month, day, hour, minute, second.") sqs = self.msqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='month') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.date_facets), 1) sqs2 = self.msqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='month').date_facet('bar', start_date=datetime.date(2007, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='year') self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.date_facets), 2)
class SearchQuerySetTestCase(TestCase): def setUp(self): super(SearchQuerySetTestCase, self).setUp() self.bsqs = SearchQuerySet(query=DummySearchQuery(backend=DummySearchBackend())) self.msqs = SearchQuerySet(query=MockSearchQuery(backend=MockSearchBackend())) self.mmsqs = SearchQuerySet(query=MockSearchQuery(backend=MixedMockSearchBackend())) # Stow. self.old_site = haystack.site test_site = SearchSite() test_site.register(MockModel) haystack.site = test_site def tearDown(self): # Restore. haystack.site = self.old_site super(SearchQuerySetTestCase, self).tearDown() def test_len(self): # Dummy always returns 0. self.assertEqual(len(self.bsqs), 0) self.assertEqual(len(self.msqs), 100) def test_iter(self): # Dummy always returns []. self.assertEqual([result for result in self.bsqs.all()], []) msqs = self.msqs.all() results = [result for result in msqs] self.assertEqual(results, MOCK_SEARCH_RESULTS) def test_slice(self): self.assertEqual(self.msqs.all()[1:11], MOCK_SEARCH_RESULTS[1:11]) self.assertEqual(self.msqs.all()[50], MOCK_SEARCH_RESULTS[50]) def test_manual_iter(self): results = self.msqs.all() for offset, result in enumerate(results._manual_iter()): self.assertEqual(result, MOCK_SEARCH_RESULTS[offset]) # Test to ensure we properly fill the cache, even if we get fewer # results back (not in the SearchSite) than the hit count indicates. # This will hang indefinitely if broken. results = self.mmsqs.all() self.assertEqual([result.pk for result in results._manual_iter()], [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]) def test_fill_cache(self): results = self.msqs.all() self.assertEqual(len(results._result_cache), 0) results._fill_cache() self.assertEqual(len(results._result_cache), 10) results._fill_cache() self.assertEqual(len(results._result_cache), 20) # Test to ensure we properly fill the cache, even if we get fewer # results back (not in the SearchSite) than the hit count indicates. results = self.mmsqs.all() self.assertEqual(len(results._result_cache), 0) self.assertEqual([result.pk for result in results._result_cache], []) results._fill_cache() self.assertEqual(len(results._result_cache), 10) self.assertEqual([result.pk for result in results._result_cache], [0, 1, 2, 3, 4, 5, 6, 7, 8, 10]) results._fill_cache() self.assertEqual(len(results._result_cache), 20) self.assertEqual([result.pk for result in results._result_cache], [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 15, 17, 18, 19, 20, 21, 22]) results._fill_cache() self.assertEqual(len(results._result_cache), 27) self.assertEqual([result.pk for result in results._result_cache], [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]) def test_cache_is_full(self): # Dummy always has a count of 0 and an empty _result_cache, hence True. self.assertEqual(self.bsqs._cache_is_full(), True) self.assertEqual(self.msqs._cache_is_full(), False) results = self.msqs.all() fire_the_iterator_and_fill_cache = [result for result in results] self.assertEqual(results._cache_is_full(), True) def test_all(self): sqs = self.bsqs.all() self.assert_(isinstance(sqs, SearchQuerySet)) def test_filter(self): sqs = self.bsqs.filter(content='foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filters), 1) def test_exclude(self): sqs = self.bsqs.exclude(content='foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filters), 1) def test_order_by(self): sqs = self.bsqs.order_by('foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assert_('foo' in sqs.query.order_by) def test_models(self): mock_index_site = SearchSite() mock_index_site.register(MockModel) mock_index_site.register(AnotherMockModel) bsqs = SearchQuerySet(site=mock_index_site) sqs = bsqs.all() self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 0) sqs = bsqs.models(MockModel) self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) sqs = bsqs.models(MockModel, AnotherMockModel) self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 2) def test_boost(self): sqs = self.bsqs.boost('foo', 10) self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.boost.keys()), 1) def test_highlight(self): sqs = self.bsqs.highlight() self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.query.highlight, True) def test_spelling(self): # Test the case where spelling support is disabled. sqs = self.bsqs.filter(content='Indx') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.spelling_suggestion(), None) def test_raw_search(self): self.assertEqual(len(self.bsqs.raw_search('foo')), 0) self.assertEqual(len(self.bsqs.raw_search('content__exact hello AND content__exact world')), 1) def test_load_all(self): # If nothing is registered, you get nothing. haystack.site.unregister(MockModel) sqs = self.msqs.load_all() self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs), 0) # For full tests, see the solr_backend. def test_load_all_queryset(self): sqs = self.msqs.load_all() self.assertEqual(len(sqs._load_all_querysets), 0) sqs = sqs.load_all_queryset(MockModel, MockModel.objects.filter(id__gt=1)) self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs._load_all_querysets), 1) # For full tests, see the solr_backend. def test_auto_query(self): sqs = self.bsqs.auto_query('test search -stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual([repr(the_filter) for the_filter in sqs.query.query_filters], ['<QueryFilter: AND content__exact=test>', '<QueryFilter: AND content__exact=search>', '<QueryFilter: NOT content__exact=stuff>']) sqs = self.bsqs.auto_query('test "my thing" search -stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual([repr(the_filter) for the_filter in sqs.query.query_filters], ['<QueryFilter: AND content__exact=my thing>', '<QueryFilter: AND content__exact=test>', '<QueryFilter: AND content__exact=search>', '<QueryFilter: NOT content__exact=stuff>']) sqs = self.bsqs.auto_query('test "my thing" search \'moar quotes\' -stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual([repr(the_filter) for the_filter in sqs.query.query_filters], ['<QueryFilter: AND content__exact=my thing>', '<QueryFilter: AND content__exact=moar quotes>', '<QueryFilter: AND content__exact=test>', '<QueryFilter: AND content__exact=search>', '<QueryFilter: NOT content__exact=stuff>']) sqs = self.bsqs.auto_query('test "my thing" search \'moar quotes\' "foo -stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual([repr(the_filter) for the_filter in sqs.query.query_filters], ['<QueryFilter: AND content__exact=my thing>', '<QueryFilter: AND content__exact=moar quotes>', '<QueryFilter: AND content__exact=test>', '<QueryFilter: AND content__exact=search>', '<QueryFilter: AND content__exact="foo>', '<QueryFilter: NOT content__exact=stuff>']) sqs = self.bsqs.auto_query('test - stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual([repr(the_filter) for the_filter in sqs.query.query_filters], ['<QueryFilter: AND content__exact=test>', '<QueryFilter: AND content__exact=->', '<QueryFilter: AND content__exact=stuff>']) def test_count(self): self.assertEqual(self.bsqs.count(), 0) def test_facet_counts(self): self.assertEqual(self.bsqs.facet_counts(), {}) def test_best_match(self): self.assert_(isinstance(self.msqs.best_match(), SearchResult)) def test_latest(self): self.assert_(isinstance(self.msqs.latest('pub_date'), SearchResult)) def test_more_like_this(self): mock = MockModel() mock.id = 1 self.assertEqual(len(self.msqs.more_like_this(mock)), 100) def test_facets(self): sqs = self.bsqs.facet('foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.facets), 1) sqs2 = self.bsqs.facet('foo').facet('bar') self.assert_(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.facets), 2) def test_date_facets(self): sqs = self.bsqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='month') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.date_facets), 1) sqs2 = self.bsqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='month').date_facet('bar', start_date=datetime.date(2007, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='year') self.assert_(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.date_facets), 2) def test_query_facets(self): sqs = self.bsqs.query_facet('foo', '[bar TO *]') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_facets), 1) sqs2 = self.bsqs.query_facet('foo', '[bar TO *]').query_facet('bar', '[100 TO 499]') self.assert_(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.query_facets), 2) def test_narrow(self): sqs = self.bsqs.narrow('foo:moof') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.narrow_queries), 1) def test_clone(self): results = self.msqs.filter(foo='bar', foo__lt='10') clone = results._clone() self.assert_(isinstance(clone, SearchQuerySet)) self.assertEqual(clone.site, results.site) self.assertEqual(str(clone.query), str(results.query)) self.assertEqual(clone._result_cache, []) self.assertEqual(clone._result_count, None) self.assertEqual(clone._cache_full, False) def test_chaining(self): sqs = self.bsqs.filter(content='foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filters), 1) # A second instance should inherit none of the changes from above. sqs = self.bsqs.filter(content='bar') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filters), 1) def test_none(self): sqs = self.bsqs.none() self.assert_(isinstance(sqs, EmptySearchQuerySet)) self.assertEqual(len(sqs), 0)
class SearchQuerySetTestCase(TestCase): fixtures = ['bulk_data.json'] def setUp(self): super(SearchQuerySetTestCase, self).setUp() # Stow. self.old_unified_index = connections['default']._index self.ui = UnifiedIndex() self.bmmsi = BasicMockModelSearchIndex() self.cpkmmsi = CharPKMockModelSearchIndex() self.ui.build(indexes=[self.bmmsi, self.cpkmmsi]) connections['default']._index = self.ui # Update the "index". backend = connections['default'].get_backend() backend.clear() backend.update(self.bmmsi, MockModel.objects.all()) self.msqs = SearchQuerySet() # Stow. reset_search_queries() def tearDown(self): # Restore. connections['default']._index = self.old_unified_index super(SearchQuerySetTestCase, self).tearDown() def test_len(self): self.assertEqual(len(self.msqs), 23) def test_repr(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) self.assertRegexpMatches( repr(self.msqs), r'^<SearchQuerySet: query=<test_haystack.mocks.MockSearchQuery object' r' at 0x[0-9A-Fa-f]+>, using=None>$') def test_iter(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) msqs = self.msqs.all() results = [int(res.pk) for res in msqs] self.assertEqual(results, [res.pk for res in MOCK_SEARCH_RESULTS[:23]]) self.assertEqual(len(connections['default'].queries), 3) def test_slice(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) results = self.msqs.all() self.assertEqual([int(res.pk) for res in results[1:11]], [res.pk for res in MOCK_SEARCH_RESULTS[1:11]]) self.assertEqual(len(connections['default'].queries), 1) reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) results = self.msqs.all() self.assertEqual(int(results[22].pk), MOCK_SEARCH_RESULTS[22].pk) self.assertEqual(len(connections['default'].queries), 1) def test_manual_iter(self): results = self.msqs.all() reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) check = [result.pk for result in results._manual_iter()] self.assertEqual(check, [ u'1', u'2', u'3', u'4', u'5', u'6', u'7', u'8', u'9', u'10', u'11', u'12', u'13', u'14', u'15', u'16', u'17', u'18', u'19', u'20', u'21', u'22', u'23' ]) self.assertEqual(len(connections['default'].queries), 3) reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) # Test to ensure we properly fill the cache, even if we get fewer # results back (not a handled model) than the hit count indicates. # This will hang indefinitely if broken. old_ui = self.ui self.ui.build(indexes=[self.cpkmmsi]) connections['default']._index = self.ui self.cpkmmsi.update() results = self.msqs.all() loaded = [result.pk for result in results._manual_iter()] self.assertEqual(loaded, [u'sometext', u'1234']) self.assertEqual(len(connections['default'].queries), 1) connections['default']._index = old_ui def test_fill_cache(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) results = self.msqs.all() self.assertEqual(len(results._result_cache), 0) self.assertEqual(len(connections['default'].queries), 0) results._fill_cache(0, 10) self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 10) self.assertEqual(len(connections['default'].queries), 1) results._fill_cache(10, 20) self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 20) self.assertEqual(len(connections['default'].queries), 2) reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) # Test to ensure we properly fill the cache, even if we get fewer # results back (not a handled model) than the hit count indicates. sqs = SearchQuerySet().all() sqs.query.backend = MixedMockSearchBackend('default') results = sqs self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 0) self.assertEqual([ int(result.pk) for result in results._result_cache if result is not None ], []) self.assertEqual(len(connections['default'].queries), 0) results._fill_cache(0, 10) self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 9) self.assertEqual([ int(result.pk) for result in results._result_cache if result is not None ], [1, 2, 3, 4, 5, 6, 7, 8, 10]) self.assertEqual(len(connections['default'].queries), 2) results._fill_cache(10, 20) self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 17) self.assertEqual([ int(result.pk) for result in results._result_cache if result is not None ], [1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 15, 16, 17, 18, 19, 20]) self.assertEqual(len(connections['default'].queries), 4) results._fill_cache(20, 30) self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 20) self.assertEqual([ int(result.pk) for result in results._result_cache if result is not None ], [ 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 15, 16, 17, 18, 19, 20, 21, 22, 23 ]) self.assertEqual(len(connections['default'].queries), 6) def test_cache_is_full(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) self.assertEqual(self.msqs._cache_is_full(), False) results = self.msqs.all() fire_the_iterator_and_fill_cache = [result for result in results] self.assertEqual(results._cache_is_full(), True) self.assertEqual(len(connections['default'].queries), 3) def test_all(self): sqs = self.msqs.all() self.assertTrue(isinstance(sqs, SearchQuerySet)) def test_filter(self): sqs = self.msqs.filter(content='foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_exclude(self): sqs = self.msqs.exclude(content='foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_order_by(self): sqs = self.msqs.order_by('foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertTrue('foo' in sqs.query.order_by) def test_models(self): # Stow. old_unified_index = connections['default']._index ui = UnifiedIndex() bmmsi = BasicMockModelSearchIndex() bammsi = BasicAnotherMockModelSearchIndex() ui.build(indexes=[bmmsi, bammsi]) connections['default']._index = ui msqs = SearchQuerySet() sqs = msqs.all() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 0) sqs = msqs.models(MockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) sqs = msqs.models(MockModel, AnotherMockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 2) # This will produce a warning. ui.build(indexes=[bmmsi]) sqs = msqs.models(AnotherMockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) def test_result_class(self): sqs = self.msqs.all() self.assertTrue(issubclass(sqs.query.result_class, SearchResult)) # Custom class. class IttyBittyResult(object): pass sqs = self.msqs.result_class(IttyBittyResult) self.assertTrue(issubclass(sqs.query.result_class, IttyBittyResult)) # Reset to default. sqs = self.msqs.result_class(None) self.assertTrue(issubclass(sqs.query.result_class, SearchResult)) def test_boost(self): sqs = self.msqs.boost('foo', 10) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.boost.keys()), 1) def test_highlight(self): sqs = self.msqs.highlight() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.query.highlight, True) def test_spelling(self): # Test the case where spelling support is disabled. sqs = self.msqs.filter(content='Indx') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.spelling_suggestion(), None) self.assertEqual(sqs.spelling_suggestion('indexy'), None) def test_raw_search(self): self.assertEqual(len(self.msqs.raw_search('foo')), 23) self.assertEqual( len( self.msqs.raw_search( '(content__exact:hello AND content__exact:world)')), 23) def test_load_all(self): # Models with character primary keys. sqs = SearchQuerySet() sqs.query.backend = CharPKMockSearchBackend('charpk') results = sqs.load_all().all() self.assertEqual(len(results._result_cache), 0) results._fill_cache(0, 2) self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 2) # If nothing is handled, you get nothing. old_ui = connections['default']._index ui = UnifiedIndex() ui.build(indexes=[]) connections['default']._index = ui sqs = self.msqs.load_all() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs), 0) connections['default']._index = old_ui # For full tests, see the solr_backend. def test_load_all_read_queryset(self): # Stow. old_ui = connections['default']._index ui = UnifiedIndex() gafmmsi = GhettoAFifthMockModelSearchIndex() ui.build(indexes=[gafmmsi]) connections['default']._index = ui gafmmsi.update() sqs = SearchQuerySet() results = sqs.load_all().all() results.query.backend = ReadQuerySetMockSearchBackend('default') results._fill_cache(0, 2) # The deleted result isn't returned self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 1) # Register a SearchIndex with a read_queryset that returns deleted items rqstsi = TextReadQuerySetTestSearchIndex() ui.build(indexes=[rqstsi]) rqstsi.update() sqs = SearchQuerySet() results = sqs.load_all().all() results.query.backend = ReadQuerySetMockSearchBackend('default') results._fill_cache(0, 2) # Both the deleted and not deleted items are returned self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 2) # Restore. connections['default']._index = old_ui def test_auto_query(self): sqs = self.msqs.auto_query('test search -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND content__contains=test search -stuff>') sqs = self.msqs.auto_query('test "my thing" search -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), '<SQ: AND content__contains=test "my thing" search -stuff>') sqs = self.msqs.auto_query( 'test "my thing" search \'moar quotes\' -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), '<SQ: AND content__contains=test "my thing" search \'moar quotes\' -stuff>' ) sqs = self.msqs.auto_query( 'test "my thing" search \'moar quotes\' "foo -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), '<SQ: AND content__contains=test "my thing" search \'moar quotes\' "foo -stuff>' ) sqs = self.msqs.auto_query('test - stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), "<SQ: AND content__contains=test - stuff>") # Ensure bits in exact matches get escaped properly as well. sqs = self.msqs.auto_query('"pants:rule"') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND content__contains="pants:rule">') # Now with a different fieldname sqs = self.msqs.auto_query('test search -stuff', fieldname='title') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), "<SQ: AND title__contains=test search -stuff>") sqs = self.msqs.auto_query('test "my thing" search -stuff', fieldname='title') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), '<SQ: AND title__contains=test "my thing" search -stuff>') def test_count(self): self.assertEqual(self.msqs.count(), 23) def test_facet_counts(self): self.assertEqual(self.msqs.facet_counts(), {}) def test_best_match(self): self.assertTrue(isinstance(self.msqs.best_match(), SearchResult)) def test_latest(self): self.assertTrue(isinstance(self.msqs.latest('pub_date'), SearchResult)) def test_more_like_this(self): mock = MockModel() mock.id = 1 self.assertEqual(len(self.msqs.more_like_this(mock)), 23) def test_facets(self): sqs = self.msqs.facet('foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.facets), 1) sqs2 = self.msqs.facet('foo').facet('bar') self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.facets), 2) def test_date_facets(self): try: sqs = self.msqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='smarblaph') self.fail() except FacetingError as e: self.assertEqual( str(e), "The gap_by ('smarblaph') must be one of the following: year, month, day, hour, minute, second." ) sqs = self.msqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='month') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.date_facets), 1) sqs2 = self.msqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='month').date_facet( 'bar', start_date=datetime.date(2007, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='year') self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.date_facets), 2) def test_query_facets(self): sqs = self.msqs.query_facet('foo', '[bar TO *]') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_facets), 1) sqs2 = self.msqs.query_facet('foo', '[bar TO *]').query_facet( 'bar', '[100 TO 499]') self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.query_facets), 2) # Test multiple query facets on a single field sqs3 = self.msqs.query_facet('foo', '[bar TO *]').query_facet( 'bar', '[100 TO 499]').query_facet('foo', '[1000 TO 1499]') self.assertTrue(isinstance(sqs3, SearchQuerySet)) self.assertEqual(len(sqs3.query.query_facets), 3) def test_stats(self): sqs = self.msqs.stats_facet('foo', 'bar') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.stats), 1) sqs2 = self.msqs.stats_facet('foo', 'bar').stats_facet('foo', 'baz') self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.stats), 1) sqs3 = self.msqs.stats_facet('foo', 'bar').stats_facet('moof', 'baz') self.assertTrue(isinstance(sqs3, SearchQuerySet)) self.assertEqual(len(sqs3.query.stats), 2) def test_narrow(self): sqs = self.msqs.narrow('foo:moof') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.narrow_queries), 1) def test_clone(self): results = self.msqs.filter(foo='bar', foo__lt='10') clone = results._clone() self.assertTrue(isinstance(clone, SearchQuerySet)) self.assertEqual(str(clone.query), str(results.query)) self.assertEqual(clone._result_cache, []) self.assertEqual(clone._result_count, None) self.assertEqual(clone._cache_full, False) self.assertEqual(clone._using, results._using) def test_using(self): sqs = SearchQuerySet(using='default') self.assertNotEqual(sqs.query, None) self.assertEqual(sqs.query._using, 'default') def test_chaining(self): sqs = self.msqs.filter(content='foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) # A second instance should inherit none of the changes from above. sqs = self.msqs.filter(content='bar') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_none(self): sqs = self.msqs.none() self.assertTrue(isinstance(sqs, EmptySearchQuerySet)) self.assertEqual(len(sqs), 0) def test___and__(self): sqs1 = self.msqs.filter(content='foo') sqs2 = self.msqs.filter(content='bar') sqs = sqs1 & sqs2 self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 2) def test___or__(self): sqs1 = self.msqs.filter(content='foo') sqs2 = self.msqs.filter(content='bar') sqs = sqs1 | sqs2 self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 2) def test_and_or(self): """ Combining AND queries with OR should give AND(OR(a, b), OR(c, d)) """ sqs1 = self.msqs.filter(content='foo').filter(content='oof') sqs2 = self.msqs.filter(content='bar').filter(content='rab') sqs = sqs1 | sqs2 self.assertEqual(sqs.query.query_filter.connector, 'OR') self.assertEqual(repr(sqs.query.query_filter.children[0]), repr(sqs1.query.query_filter)) self.assertEqual(repr(sqs.query.query_filter.children[1]), repr(sqs2.query.query_filter)) def test_or_and(self): """ Combining OR queries with AND should give OR(AND(a, b), AND(c, d)) """ sqs1 = self.msqs.filter(content='foo').filter_or(content='oof') sqs2 = self.msqs.filter(content='bar').filter_or(content='rab') sqs = sqs1 & sqs2 self.assertEqual(sqs.query.query_filter.connector, 'AND') self.assertEqual(repr(sqs.query.query_filter.children[0]), repr(sqs1.query.query_filter)) self.assertEqual(repr(sqs.query.query_filter.children[1]), repr(sqs2.query.query_filter))
class SearchQuerySetTestCase(TestCase): fixtures = ["base_data.json", "bulk_data.json"] def setUp(self): super(SearchQuerySetTestCase, self).setUp() # Stow. self.old_unified_index = connections["default"]._index self.ui = UnifiedIndex() self.bmmsi = BasicMockModelSearchIndex() self.cpkmmsi = CharPKMockModelSearchIndex() self.uuidmmsi = SimpleMockUUIDModelIndex() self.ui.build(indexes=[self.bmmsi, self.cpkmmsi, self.uuidmmsi]) connections["default"]._index = self.ui # Update the "index". backend = connections["default"].get_backend() backend.clear() backend.update(self.bmmsi, MockModel.objects.all()) self.msqs = SearchQuerySet() # Stow. reset_search_queries() def tearDown(self): # Restore. connections["default"]._index = self.old_unified_index super(SearchQuerySetTestCase, self).tearDown() def test_len(self): self.assertEqual(len(self.msqs), 23) def test_repr(self): reset_search_queries() self.assertEqual(len(connections["default"].queries), 0) self.assertRegexpMatches( repr(self.msqs), r"^<SearchQuerySet: query=<test_haystack.mocks.MockSearchQuery object" r" at 0x[0-9A-Fa-f]+>, using=None>$", ) def test_iter(self): reset_search_queries() self.assertEqual(len(connections["default"].queries), 0) msqs = self.msqs.all() results = [int(res.pk) for res in iter(msqs)] self.assertEqual(results, [res.pk for res in MOCK_SEARCH_RESULTS[:23]]) self.assertEqual(len(connections["default"].queries), 3) def test_slice(self): reset_search_queries() self.assertEqual(len(connections["default"].queries), 0) results = self.msqs.all() self.assertEqual( [int(res.pk) for res in results[1:11]], [res.pk for res in MOCK_SEARCH_RESULTS[1:11]], ) self.assertEqual(len(connections["default"].queries), 1) reset_search_queries() self.assertEqual(len(connections["default"].queries), 0) results = self.msqs.all() self.assertEqual(int(results[22].pk), MOCK_SEARCH_RESULTS[22].pk) self.assertEqual(len(connections["default"].queries), 1) def test_manual_iter(self): results = self.msqs.all() reset_search_queries() self.assertEqual(len(connections["default"].queries), 0) check = [result.pk for result in results._manual_iter()] self.assertEqual( check, [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", ], ) self.assertEqual(len(connections["default"].queries), 3) reset_search_queries() self.assertEqual(len(connections["default"].queries), 0) # Test to ensure we properly fill the cache, even if we get fewer # results back (not a handled model) than the hit count indicates. # This will hang indefinitely if broken. # CharPK testing old_ui = self.ui self.ui.build(indexes=[self.cpkmmsi]) connections["default"]._index = self.ui self.cpkmmsi.update() results = self.msqs.all() loaded = [result.pk for result in results._manual_iter()] self.assertEqual(loaded, ["sometext", "1234"]) self.assertEqual(len(connections["default"].queries), 1) # UUID testing self.ui.build(indexes=[self.uuidmmsi]) connections["default"]._index = self.ui self.uuidmmsi.update() results = self.msqs.all() loaded = [result.pk for result in results._manual_iter()] self.assertEqual( loaded, [ "53554c58-7051-4350-bcc9-dad75eb248a9", "77554c58-7051-4350-bcc9-dad75eb24888", ], ) connections["default"]._index = old_ui def test_cache_is_full(self): reset_search_queries() self.assertEqual(len(connections["default"].queries), 0) self.assertEqual(self.msqs._cache_is_full(), False) results = self.msqs.all() fire_the_iterator_and_fill_cache = list(results) self.assertEqual(23, len(fire_the_iterator_and_fill_cache)) self.assertEqual(results._cache_is_full(), True) self.assertEqual(len(connections["default"].queries), 4) def test_all(self): sqs = self.msqs.all() self.assertTrue(isinstance(sqs, SearchQuerySet)) def test_filter(self): sqs = self.msqs.filter(content="foo") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_exclude(self): sqs = self.msqs.exclude(content="foo") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_order_by(self): sqs = self.msqs.order_by("foo") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertTrue("foo" in sqs.query.order_by) def test_models(self): # Stow. old_unified_index = connections["default"]._index ui = UnifiedIndex() bmmsi = BasicMockModelSearchIndex() bammsi = BasicAnotherMockModelSearchIndex() ui.build(indexes=[bmmsi, bammsi]) connections["default"]._index = ui msqs = SearchQuerySet() sqs = msqs.all() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 0) sqs = msqs.models(MockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) sqs = msqs.models(MockModel, AnotherMockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 2) # This will produce a warning. ui.build(indexes=[bmmsi]) sqs = msqs.models(AnotherMockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) def test_result_class(self): sqs = self.msqs.all() self.assertTrue(issubclass(sqs.query.result_class, SearchResult)) # Custom class. class IttyBittyResult(object): pass sqs = self.msqs.result_class(IttyBittyResult) self.assertTrue(issubclass(sqs.query.result_class, IttyBittyResult)) # Reset to default. sqs = self.msqs.result_class(None) self.assertTrue(issubclass(sqs.query.result_class, SearchResult)) def test_boost(self): sqs = self.msqs.boost("foo", 10) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.boost.keys()), 1) def test_highlight(self): sqs = self.msqs.highlight() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.query.highlight, True) def test_spelling_override(self): sqs = self.msqs.filter(content="not the spellchecking query") self.assertEqual(sqs.query.spelling_query, None) sqs = self.msqs.set_spelling_query("override") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.query.spelling_query, "override") def test_spelling_suggestions(self): # Test the case where spelling support is disabled. sqs = self.msqs.filter(content="Indx") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.spelling_suggestion(), None) self.assertEqual(sqs.spelling_suggestion("indexy"), None) def test_raw_search(self): self.assertEqual(len(self.msqs.raw_search("foo")), 23) self.assertEqual( len( self.msqs.raw_search("(content__exact:hello AND content__exact:world)") ), 23, ) def test_load_all(self): # Models with character primary keys. sqs = SearchQuerySet() sqs.query.backend = CharPKMockSearchBackend("charpk") results = sqs.load_all().all() self.assertEqual(len(results._result_cache), 0) results._fill_cache(0, 2) self.assertEqual( len([result for result in results._result_cache if result is not None]), 2 ) # Models with uuid primary keys. sqs = SearchQuerySet() sqs.query.backend = UUIDMockSearchBackend("uuid") results = sqs.load_all().all() self.assertEqual(len(results._result_cache), 0) results._fill_cache(0, 2) self.assertEqual( len([result for result in results._result_cache if result is not None]), 2 ) # If nothing is handled, you get nothing. old_ui = connections["default"]._index ui = UnifiedIndex() ui.build(indexes=[]) connections["default"]._index = ui sqs = self.msqs.load_all() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs), 0) connections["default"]._index = old_ui # For full tests, see the solr_backend. def test_load_all_read_queryset(self): # Stow. old_ui = connections["default"]._index ui = UnifiedIndex() gafmmsi = GhettoAFifthMockModelSearchIndex() ui.build(indexes=[gafmmsi]) connections["default"]._index = ui gafmmsi.update() sqs = SearchQuerySet() results = sqs.load_all().all() results.query.backend = ReadQuerySetMockSearchBackend("default") results._fill_cache(0, 2) # The deleted result isn't returned self.assertEqual( len([result for result in results._result_cache if result is not None]), 1 ) # Register a SearchIndex with a read_queryset that returns deleted items rqstsi = TextReadQuerySetTestSearchIndex() ui.build(indexes=[rqstsi]) rqstsi.update() sqs = SearchQuerySet() results = sqs.load_all().all() results.query.backend = ReadQuerySetMockSearchBackend("default") results._fill_cache(0, 2) # Both the deleted and not deleted items are returned self.assertEqual( len([result for result in results._result_cache if result is not None]), 2 ) # Restore. connections["default"]._index = old_ui def test_auto_query(self): sqs = self.msqs.auto_query("test search -stuff") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), "<SQ: AND content__content=test search -stuff>", ) sqs = self.msqs.auto_query('test "my thing" search -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), '<SQ: AND content__content=test "my thing" search -stuff>', ) sqs = self.msqs.auto_query("test \"my thing\" search 'moar quotes' -stuff") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), "<SQ: AND content__content=test \"my thing\" search 'moar quotes' -stuff>", ) sqs = self.msqs.auto_query('test "my thing" search \'moar quotes\' "foo -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), '<SQ: AND content__content=test "my thing" search \'moar quotes\' "foo -stuff>', ) sqs = self.msqs.auto_query("test - stuff") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), "<SQ: AND content__content=test - stuff>" ) # Ensure bits in exact matches get escaped properly as well. sqs = self.msqs.auto_query('"pants:rule"') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), '<SQ: AND content__content="pants:rule">' ) # Now with a different fieldname sqs = self.msqs.auto_query("test search -stuff", fieldname="title") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), "<SQ: AND title__content=test search -stuff>" ) sqs = self.msqs.auto_query('test "my thing" search -stuff', fieldname="title") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), '<SQ: AND title__content=test "my thing" search -stuff>', ) def test_count(self): self.assertEqual(self.msqs.count(), 23) def test_facet_counts(self): self.assertEqual(self.msqs.facet_counts(), {}) def test_best_match(self): self.assertTrue(isinstance(self.msqs.best_match(), SearchResult)) def test_latest(self): self.assertTrue(isinstance(self.msqs.latest("pub_date"), SearchResult)) def test_more_like_this(self): mock = MockModel() mock.id = 1 self.assertEqual(len(self.msqs.more_like_this(mock)), 23) def test_facets(self): sqs = self.msqs.facet("foo") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.facets), 1) sqs2 = self.msqs.facet("foo").facet("bar") self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.facets), 2) def test_date_facets(self): try: sqs = self.msqs.date_facet( "foo", start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by="smarblaph", ) self.fail() except FacetingError as e: self.assertEqual( str(e), "The gap_by ('smarblaph') must be one of the following: year, month, day, hour, minute, second.", ) sqs = self.msqs.date_facet( "foo", start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by="month", ) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.date_facets), 1) sqs2 = self.msqs.date_facet( "foo", start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by="month", ).date_facet( "bar", start_date=datetime.date(2007, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by="year", ) self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.date_facets), 2) def test_query_facets(self): sqs = self.msqs.query_facet("foo", "[bar TO *]") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_facets), 1) sqs2 = self.msqs.query_facet("foo", "[bar TO *]").query_facet( "bar", "[100 TO 499]" ) self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.query_facets), 2) # Test multiple query facets on a single field sqs3 = ( self.msqs.query_facet("foo", "[bar TO *]") .query_facet("bar", "[100 TO 499]") .query_facet("foo", "[1000 TO 1499]") ) self.assertTrue(isinstance(sqs3, SearchQuerySet)) self.assertEqual(len(sqs3.query.query_facets), 3) def test_stats(self): sqs = self.msqs.stats_facet("foo", "bar") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.stats), 1) sqs2 = self.msqs.stats_facet("foo", "bar").stats_facet("foo", "baz") self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.stats), 1) sqs3 = self.msqs.stats_facet("foo", "bar").stats_facet("moof", "baz") self.assertTrue(isinstance(sqs3, SearchQuerySet)) self.assertEqual(len(sqs3.query.stats), 2) def test_narrow(self): sqs = self.msqs.narrow("foo:moof") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.narrow_queries), 1) def test_clone(self): results = self.msqs.filter(foo="bar", foo__lt="10") clone = results._clone() self.assertTrue(isinstance(clone, SearchQuerySet)) self.assertEqual(str(clone.query), str(results.query)) self.assertEqual(clone._result_cache, []) self.assertEqual(clone._result_count, None) self.assertEqual(clone._cache_full, False) self.assertEqual(clone._using, results._using) def test_using(self): sqs = SearchQuerySet(using="default") self.assertNotEqual(sqs.query, None) self.assertEqual(sqs.query._using, "default") def test_chaining(self): sqs = self.msqs.filter(content="foo") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) # A second instance should inherit none of the changes from above. sqs = self.msqs.filter(content="bar") self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_none(self): sqs = self.msqs.none() self.assertTrue(isinstance(sqs, EmptySearchQuerySet)) self.assertEqual(len(sqs), 0) def test___and__(self): sqs1 = self.msqs.filter(content="foo") sqs2 = self.msqs.filter(content="bar") sqs = sqs1 & sqs2 self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 2) def test___or__(self): sqs1 = self.msqs.filter(content="foo") sqs2 = self.msqs.filter(content="bar") sqs = sqs1 | sqs2 self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 2) def test_and_or(self): """ Combining AND queries with OR should give AND(OR(a, b), OR(c, d)) """ sqs1 = self.msqs.filter(content="foo").filter(content="oof") sqs2 = self.msqs.filter(content="bar").filter(content="rab") sqs = sqs1 | sqs2 self.assertEqual(sqs.query.query_filter.connector, "OR") self.assertEqual( repr(sqs.query.query_filter.children[0]), repr(sqs1.query.query_filter) ) self.assertEqual( repr(sqs.query.query_filter.children[1]), repr(sqs2.query.query_filter) ) def test_or_and(self): """ Combining OR queries with AND should give OR(AND(a, b), AND(c, d)) """ sqs1 = self.msqs.filter(content="foo").filter_or(content="oof") sqs2 = self.msqs.filter(content="bar").filter_or(content="rab") sqs = sqs1 & sqs2 self.assertEqual(sqs.query.query_filter.connector, "AND") self.assertEqual( repr(sqs.query.query_filter.children[0]), repr(sqs1.query.query_filter) ) self.assertEqual( repr(sqs.query.query_filter.children[1]), repr(sqs2.query.query_filter) )
class SearchQuerySetTestCase(TestCase): fixtures = ['bulk_data.json'] def setUp(self): super(SearchQuerySetTestCase, self).setUp() # Stow. self.old_unified_index = connections['default']._index self.ui = UnifiedIndex() self.bmmsi = BasicMockModelSearchIndex() self.cpkmmsi = CharPKMockModelSearchIndex() self.ui.build(indexes=[self.bmmsi, self.cpkmmsi]) connections['default']._index = self.ui # Update the "index". backend = connections['default'].get_backend() backend.clear() backend.update(self.bmmsi, MockModel.objects.all()) self.msqs = SearchQuerySet() # Stow. self.old_debug = settings.DEBUG settings.DEBUG = True reset_search_queries() def tearDown(self): # Restore. connections['default']._index = self.old_unified_index settings.DEBUG = self.old_debug super(SearchQuerySetTestCase, self).tearDown() def test_len(self): self.assertEqual(len(self.msqs), 23) def test_repr(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) self.assertEqual( repr(self.msqs), "[<SearchResult: core.mockmodel (pk=u'1')>, <SearchResult: core.mockmodel (pk=u'2')>, <SearchResult: core.mockmodel (pk=u'3')>, <SearchResult: core.mockmodel (pk=u'4')>, <SearchResult: core.mockmodel (pk=u'5')>, <SearchResult: core.mockmodel (pk=u'6')>, <SearchResult: core.mockmodel (pk=u'7')>, <SearchResult: core.mockmodel (pk=u'8')>, <SearchResult: core.mockmodel (pk=u'9')>, <SearchResult: core.mockmodel (pk=u'10')>, <SearchResult: core.mockmodel (pk=u'11')>, <SearchResult: core.mockmodel (pk=u'12')>, <SearchResult: core.mockmodel (pk=u'13')>, <SearchResult: core.mockmodel (pk=u'14')>, <SearchResult: core.mockmodel (pk=u'15')>, <SearchResult: core.mockmodel (pk=u'16')>, <SearchResult: core.mockmodel (pk=u'17')>, <SearchResult: core.mockmodel (pk=u'18')>, <SearchResult: core.mockmodel (pk=u'19')>, '...(remaining elements truncated)...']" ) self.assertEqual(len(connections['default'].queries), 1) def test_iter(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) msqs = self.msqs.all() results = [int(res.pk) for res in msqs] self.assertEqual(results, [res.pk for res in MOCK_SEARCH_RESULTS[:23]]) self.assertEqual(len(connections['default'].queries), 3) def test_slice(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) results = self.msqs.all() self.assertEqual([int(res.pk) for res in results[1:11]], [res.pk for res in MOCK_SEARCH_RESULTS[1:11]]) self.assertEqual(len(connections['default'].queries), 1) reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) results = self.msqs.all() self.assertEqual(int(results[22].pk), MOCK_SEARCH_RESULTS[22].pk) self.assertEqual(len(connections['default'].queries), 1) def test_manual_iter(self): results = self.msqs.all() reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) check = [result.pk for result in results._manual_iter()] self.assertEqual(check, [ u'1', u'2', u'3', u'4', u'5', u'6', u'7', u'8', u'9', u'10', u'11', u'12', u'13', u'14', u'15', u'16', u'17', u'18', u'19', u'20', u'21', u'22', u'23' ]) self.assertEqual(len(connections['default'].queries), 3) reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) # Test to ensure we properly fill the cache, even if we get fewer # results back (not a handled model) than the hit count indicates. # This will hang indefinitely if broken. old_ui = self.ui self.ui.build(indexes=[self.cpkmmsi]) connections['default']._index = self.ui self.cpkmmsi.update() results = self.msqs.all() loaded = [result.pk for result in results._manual_iter()] self.assertEqual(loaded, [u'sometext', u'1234']) self.assertEqual(len(connections['default'].queries), 1) connections['default']._index = old_ui def test_fill_cache(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) results = self.msqs.all() self.assertEqual(len(results._result_cache), 0) self.assertEqual(len(connections['default'].queries), 0) results._fill_cache(0, 10) self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 10) self.assertEqual(len(connections['default'].queries), 1) results._fill_cache(10, 20) self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 20) self.assertEqual(len(connections['default'].queries), 2) reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) # Test to ensure we properly fill the cache, even if we get fewer # results back (not a handled model) than the hit count indicates. sqs = SearchQuerySet().all() sqs.query.backend = MixedMockSearchBackend('default') results = sqs self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 0) self.assertEqual([ int(result.pk) for result in results._result_cache if result is not None ], []) self.assertEqual(len(connections['default'].queries), 0) results._fill_cache(0, 10) self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 9) self.assertEqual([ int(result.pk) for result in results._result_cache if result is not None ], [1, 2, 3, 4, 5, 6, 7, 8, 10]) self.assertEqual(len(connections['default'].queries), 2) results._fill_cache(10, 20) self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 17) self.assertEqual([ int(result.pk) for result in results._result_cache if result is not None ], [1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 15, 16, 17, 18, 19, 20]) self.assertEqual(len(connections['default'].queries), 4) results._fill_cache(20, 30) self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 20) self.assertEqual([ int(result.pk) for result in results._result_cache if result is not None ], [ 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 15, 16, 17, 18, 19, 20, 21, 22, 23 ]) self.assertEqual(len(connections['default'].queries), 6) def test_cache_is_full(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) self.assertEqual(self.msqs._cache_is_full(), False) results = self.msqs.all() fire_the_iterator_and_fill_cache = [result for result in results] self.assertEqual(results._cache_is_full(), True) self.assertEqual(len(connections['default'].queries), 3) def test_all(self): sqs = self.msqs.all() self.assertTrue(isinstance(sqs, SearchQuerySet)) def test_filter(self): sqs = self.msqs.filter(content='foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_exclude(self): sqs = self.msqs.exclude(content='foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_order_by(self): sqs = self.msqs.order_by('foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertTrue('foo' in sqs.query.order_by) def test_models(self): # Stow. old_unified_index = connections['default']._index ui = UnifiedIndex() bmmsi = BasicMockModelSearchIndex() bammsi = BasicAnotherMockModelSearchIndex() ui.build(indexes=[bmmsi, bammsi]) connections['default']._index = ui msqs = SearchQuerySet() sqs = msqs.all() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 0) sqs = msqs.models(MockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) sqs = msqs.models(MockModel, AnotherMockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 2) # This will produce a warning. ui.build(indexes=[bmmsi]) sqs = msqs.models(AnotherMockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) def test_result_class(self): sqs = self.msqs.all() self.assertTrue(issubclass(sqs.query.result_class, SearchResult)) # Custom class. class IttyBittyResult(object): pass sqs = self.msqs.result_class(IttyBittyResult) self.assertTrue(issubclass(sqs.query.result_class, IttyBittyResult)) # Reset to default. sqs = self.msqs.result_class(None) self.assertTrue(issubclass(sqs.query.result_class, SearchResult)) def test_boost(self): sqs = self.msqs.boost('foo', 10) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.boost.keys()), 1) def test_highlight(self): sqs = self.msqs.highlight() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.query.highlight, True) def test_spelling(self): # Test the case where spelling support is disabled. sqs = self.msqs.filter(content='Indx') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.spelling_suggestion(), None) self.assertEqual(sqs.spelling_suggestion('indexy'), None) def test_raw_search(self): self.assertEqual(len(self.msqs.raw_search('foo')), 23) self.assertEqual( len( self.msqs.raw_search( '(content__exact:hello AND content__exact:world)')), 23) def test_load_all(self): # Models with character primary keys. sqs = SearchQuerySet() sqs.query.backend = CharPKMockSearchBackend('charpk') results = sqs.load_all().all() self.assertEqual(len(results._result_cache), 0) results._fill_cache(0, 2) self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 2) # If nothing is handled, you get nothing. old_ui = connections['default']._index ui = UnifiedIndex() ui.build(indexes=[]) connections['default']._index = ui sqs = self.msqs.load_all() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs), 0) connections['default']._index = old_ui # For full tests, see the solr_backend. def test_load_all_read_queryset(self): # Stow. old_ui = connections['default']._index ui = UnifiedIndex() gafmmsi = GhettoAFifthMockModelSearchIndex() ui.build(indexes=[gafmmsi]) connections['default']._index = ui gafmmsi.update() sqs = SearchQuerySet() results = sqs.load_all().all() results.query.backend = ReadQuerySetMockSearchBackend('default') results._fill_cache(0, 2) # The deleted result isn't returned self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 1) # Register a SearchIndex with a read_queryset that returns deleted items rqstsi = TextReadQuerySetTestSearchIndex() ui.build(indexes=[rqstsi]) rqstsi.update() sqs = SearchQuerySet() results = sqs.load_all().all() results.query.backend = ReadQuerySetMockSearchBackend('default') results._fill_cache(0, 2) # Both the deleted and not deleted items are returned self.assertEqual( len([ result for result in results._result_cache if result is not None ]), 2) # Restore. connections['default']._index = old_ui def test_auto_query(self): sqs = self.msqs.auto_query('test search -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), '<SQ: AND (content__contains=test AND content__contains=search AND NOT (content__contains=stuff))>' ) sqs = self.msqs.auto_query('test "my thing" search -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), '<SQ: AND (content__exact=my thing AND content__contains=test AND content__contains=search AND NOT (content__contains=stuff))>' ) sqs = self.msqs.auto_query( 'test "my thing" search \'moar quotes\' -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), "<SQ: AND (content__exact=my thing AND content__contains=test AND content__contains=search AND content__contains='moar AND content__contains=quotes' AND NOT (content__contains=stuff))>" ) sqs = self.msqs.auto_query( 'test "my thing" search \'moar quotes\' "foo -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), '<SQ: AND (content__exact=my thing AND content__contains=test AND content__contains=search AND content__contains=\'moar AND content__contains=quotes\' AND content__contains="foo AND NOT (content__contains=stuff))>' ) sqs = self.msqs.auto_query('test - stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), '<SQ: AND (content__contains=test AND content__contains=- AND content__contains=stuff)>' ) # Ensure bits in exact matches get escaped properly as well. sqs = self.msqs.auto_query('"pants:rule"') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND content__exact=pants:rule>') # Now with a different fieldname sqs = self.msqs.auto_query('test search -stuff', fieldname='title') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), '<SQ: AND (title__contains=test AND title__contains=search AND NOT (title__contains=stuff))>' ) sqs = self.msqs.auto_query('test "my thing" search -stuff', fieldname='title') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual( repr(sqs.query.query_filter), '<SQ: AND (title__exact=my thing AND title__contains=test AND title__contains=search AND NOT (title__contains=stuff))>' ) def test_count(self): self.assertEqual(self.msqs.count(), 23) def test_facet_counts(self): self.assertEqual(self.msqs.facet_counts(), {}) def test_best_match(self): self.assertTrue(isinstance(self.msqs.best_match(), SearchResult)) def test_latest(self): self.assertTrue(isinstance(self.msqs.latest('pub_date'), SearchResult)) def test_more_like_this(self): mock = MockModel() mock.id = 1 self.assertEqual(len(self.msqs.more_like_this(mock)), 23) def test_facets(self): sqs = self.msqs.facet('foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.facets), 1) sqs2 = self.msqs.facet('foo').facet('bar') self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.facets), 2) def test_date_facets(self): try: sqs = self.msqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='smarblaph') self.fail() except FacetingError, e: self.assertEqual( str(e), "The gap_by ('smarblaph') must be one of the following: year, month, day, hour, minute, second." ) sqs = self.msqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='month') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.date_facets), 1) sqs2 = self.msqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='month').date_facet( 'bar', start_date=datetime.date(2007, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='year') self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.date_facets), 2)
def search(request, about=False, api=False): sqs = SearchQuerySet() params = {} # Full text search q = request.GET.get('q', None) if q: sqs = sqs.auto_query(q).highlight() params['q'] = q # prepare fields for faceting; dates are special-cased later. summary = None date_fields = {} for facet in DiaryEntryIndex.search_facet_display: field = DiaryEntryIndex.fields[facet] if isinstance(field, haystack.fields.DateTimeField): date_fields[facet] = field else: sqs = sqs.facet(facet) # XXX: Set field-specific range limit for total casualties sqs = sqs.raw_params(**{'f.total_casualties_exact.facet.limit': 200}) # Narrow query set by given facets for key,val in request.GET.iteritems(): if val: # Add an "exact" param and split by '__'. If the field already has # e.g. __gte, the __exact addendum is ignored, since we only look # at the first two parts. field_name, lookup = (key + "__exact").rsplit(r'__')[0:2] # "type" is a reserved name for Solr, so munge it to "type_" if field_name == "type": field_name = "type_" field = DiaryEntryIndex.fields.get(field_name, None) if field and field.faceted: # Dates are handled specially below if isinstance(field, haystack.fields.DateTimeField): continue elif isinstance(field, haystack.fields.IntegerField): try: clean_val = int(val) except ValueError: continue elif isinstance(field, haystack.fields.FloatField): try: clean_val = float(val) except ValueError: continue else: clean_val = sqs.query.clean(val) if lookup == 'exact': sqs = sqs.narrow(u'%s:"%s"' % (field.index_fieldname + "_exact", clean_val)) elif lookup == 'gte': sqs = sqs.narrow(u"%s:[%s TO *]" % (field.index_fieldname, clean_val)) elif lookup == 'lte': sqs = sqs.narrow(u"%s:[* TO %s]" % (field.index_fieldname, clean_val)) else: continue params[key] = val # Narrow query set by given date facets for key, field in date_fields.iteritems(): start_str = request.GET.get(key + '__gte', '') end_str = request.GET.get(key + '__lte', '') start = None end = None if start_str: try: start = datetime.datetime(*[int(a) for a in (start_str + '-1-1').split('-')[0:3]]) except ValueError: start = None if end_str: try: end = datetime.datetime(*[int(a) for a in ((end_str + '-1-1').split('-')[0:3])]) except ValueError: end = None if not start and not end: # Legacy (deprecated) date format -- here to preserve old URLs day = int(request.GET.get(key + '__day', 0)) month = int(request.GET.get(key + '__month', 0)) year = int(request.GET.get(key + '__year', 0)) if year: if month: if day: start = datetime.datetime(year, month, day) end = start + datetime.timedelta(days=1) else: start = datetime.datetime(year, month, 1) next_month = start + datetime.timedelta(days=31) end = datetime.datetime(next_month.year, next_month.month, 1) else: start = datetime.datetime(year, 1, 1) end = datetime.datetime(year + 1, 1, 1) if start: sqs = sqs.narrow("%s:[%s TO *]" % (key, start.isoformat() + 'Z')) params[key + '__gte'] = start.strftime("%Y-%m-%d") else: start = DiaryEntryIndex.min_date if end: sqs = sqs.narrow("%s:[* TO %s]" % (key, end.isoformat() + 'Z')) params[key + '__lte'] = end.strftime("%Y-%m-%d") else: end = DiaryEntryIndex.max_date span = max(1, (end - start).days) gap = max(1, int(span / 100)) # target 100 facets sqs = sqs.date_facet(key, start, end, 'day', gap) # sorting sort = request.GET.get('sort', '') # Legacy sorting if request.GET.get('sort_by', ''): sort_by = request.GET.get('sort_by', '') if sort_by == 'casualties': sort_by = 'total_casualties' sort_dir = request.GET.get('sort_dir', 'asc') direction_indicator = '-' if sort_dir == 'desc' else '' sort = direction_indicator + sort_by if sort.strip('-') not in DiaryEntryIndex.fields: sort = DiaryEntryIndex.offer_to_sort_by[0][1] sqs = sqs.order_by(sort) params['sort'] = sort # Pagination p = paginator.Paginator(sqs, 10) try: page = p.page(int(request.GET.get('p', 1))) except (ValueError, paginator.InvalidPage, paginator.EmptyPage): page = p.page(p.num_pages) # Results Summaries and highlighting entries = [] for entry in page.object_list: if entry.highlighted: excerpt = mark_safe(u"... %s ..." % entry.highlighted['text'][0]) else: excerpt = (entry.summary or '')[0:200] + "..." entries.append((entry, excerpt)) # Choices counts = sqs.facet_counts() choices = utils.OrderedDict() for key in DiaryEntryIndex.search_facet_display: field = DiaryEntryIndex.fields[key] choice = None if isinstance(field, haystack.fields.CharField): facets = sorted((k, k, c) for k, c in counts['fields'][key] if c > 0) if facets: choice = { 'choices': facets, 'type': 'text', 'value': params.get(key, ''), } elif isinstance(field, haystack.fields.IntegerField): # Integer choices facets = sorted([(int(k), c) for k,c in counts['fields'][key] if c > 0]) if facets: choice = { 'type': 'min_max', 'counts': [c for k,c in facets], 'vals': [k for k,c in facets], 'min_value': facets[0][0], 'max_value': facets[-1][0], 'chosen_min': params.get(key + '__gte', ''), 'chosen_max': params.get(key + '__lte', ''), } elif isinstance(field, haystack.fields.DateTimeField): facets = sorted(counts['dates'].get(key, {}).iteritems()) if facets: val_counts = [] vals = [] last_dt = None for d,c in facets: if c > 0: try: last_dt = _iso_to_datetime(d) val_counts.append(c) vals.append(last_dt.strftime('%Y-%m-%d')) except (TypeError, ValueError): pass if vals and last_dt: max_value = min( _iso_to_datetime(counts['dates'][key]['end']), DiaryEntryIndex.max_date, last_dt + datetime.timedelta( days=int(re.sub('[^\d]', '', counts['dates'][key]['gap'])) ) ) vals.append(max_value.strftime('%Y-%m-%d')) val_counts.append(0) choice = { 'type': 'date', 'counts': val_counts, 'vals': vals, 'min_value': vals[0], 'max_value': vals[-1], 'chosen_min': params.get(key + '__gte', ''), 'chosen_max': params.get(key + '__lte', ''), } if choice: choice['title'] = fix_constraint_name(key) choices[key] = choice search_url = reverse('afg.search') # Links to remove constraints constraints = {} exclude = set(('sort',)) for key in params.keys(): if key not in exclude: value = params.pop(key) constraints[key] = { 'value': value, 'removelink': "%s?%s" % (search_url, urllib.urlencode(params)), 'title': fix_constraint_name(key) } params[key] = value # Links to change sorting sort_links = [] current_sort = params.pop('sort') current_key = sort.strip('-') for display, new_key in DiaryEntryIndex.offer_to_sort_by: # Change directions only if it's the same sort key if current_key == new_key: direction = '' if current_sort[0] == '-' else '-' else: direction = '-' if current_sort[0] == '-' else '' params['sort'] = direction + new_key sort_links.append({ 'link': "%s?%s" % (search_url, urllib.urlencode(params)), 'title': display, 'desc': current_sort[0] == '-', 'current': current_key == new_key, }) params['sort'] = current_sort if api: remapped_choices = {} for choice, opts in choices.iteritems(): if opts['type'] in ('min_max', 'date'): remapped_choices[choice] = opts else: remapped_choices[choice] = { 'value': opts['value'], 'title': opts['title'], 'choices': [], } for disp, val, count in opts['choices']: if disp or val: remapped_choices[choice]['choices'].append({ 'value': val, 'display': disp, 'count': count, }) return utils.render_json(request, { 'pagination': { 'p': page.number, 'num_pages': page.paginator.num_pages, 'num_results': page.paginator.count, }, 'entries': [{ 'title': e.title, 'date': e.date.isoformat(), 'release': e.release, 'region': e.region, 'report_key': e.report_key, 'excerpt': x, 'total_casualties': e.total_casualties, } for (e,x) in entries], 'choices': remapped_choices, 'sort': params['sort'], 'params': params, }) return utils.render_request(request, "afg/search.html", {'page': page, 'about': about, 'entries': entries, 'params': request.GET, 'choices': choices, 'qstring': '%s?%s' % (search_url, urllib.urlencode(params)), 'constraints': constraints, 'sort_links': sort_links, })
class SearchQuerySetTestCase(TestCase): def setUp(self): super(SearchQuerySetTestCase, self).setUp() self.bsqs = SearchQuerySet(query=DummySearchQuery(backend=DummySearchBackend())) self.msqs = SearchQuerySet(query=MockSearchQuery(backend=MockSearchBackend())) self.mmsqs = SearchQuerySet(query=MockSearchQuery(backend=MixedMockSearchBackend())) # Stow. self.old_debug = settings.DEBUG settings.DEBUG = True self.old_site = haystack.site test_site = SearchSite() test_site.register(MockModel) test_site.register(CharPKMockModel) haystack.site = test_site backends.reset_search_queries() def tearDown(self): # Restore. haystack.site = self.old_site settings.DEBUG = self.old_debug super(SearchQuerySetTestCase, self).tearDown() def test_len(self): # Dummy always returns 0. self.assertEqual(len(self.bsqs), 0) self.assertEqual(len(self.msqs), 100) def test_repr(self): self.assertEqual(repr(self.bsqs), '[]') backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) self.assertEqual(repr(self.msqs), "[<SearchResult: core.MockModel (pk=0)>, <SearchResult: core.MockModel (pk=1)>, <SearchResult: core.MockModel (pk=2)>, <SearchResult: core.MockModel (pk=3)>, <SearchResult: core.MockModel (pk=4)>, <SearchResult: core.MockModel (pk=5)>, <SearchResult: core.MockModel (pk=6)>, <SearchResult: core.MockModel (pk=7)>, <SearchResult: core.MockModel (pk=8)>, <SearchResult: core.MockModel (pk=9)>, <SearchResult: core.MockModel (pk=10)>, <SearchResult: core.MockModel (pk=11)>, <SearchResult: core.MockModel (pk=12)>, <SearchResult: core.MockModel (pk=13)>, <SearchResult: core.MockModel (pk=14)>, <SearchResult: core.MockModel (pk=15)>, <SearchResult: core.MockModel (pk=16)>, <SearchResult: core.MockModel (pk=17)>, <SearchResult: core.MockModel (pk=18)>, '...(remaining elements truncated)...']") self.assertEqual(len(backends.queries), 1) def test_iter(self): # Dummy always returns []. self.assertEqual([result for result in self.bsqs.all()], []) backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) msqs = self.msqs.all() results = [result for result in msqs] self.assertEqual(results, MOCK_SEARCH_RESULTS) self.assertEqual(len(backends.queries), 10) def test_slice(self): backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) results = self.msqs.all() self.assertEqual(results[1:11], MOCK_SEARCH_RESULTS[1:11]) self.assertEqual(len(backends.queries), 1) backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) results = self.msqs.all() self.assertEqual(results[50], MOCK_SEARCH_RESULTS[50]) self.assertEqual(len(backends.queries), 1) def test_manual_iter(self): results = self.msqs.all() backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) for offset, result in enumerate(results._manual_iter()): self.assertEqual(result, MOCK_SEARCH_RESULTS[offset]) self.assertEqual(len(backends.queries), 10) backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) # Test to ensure we properly fill the cache, even if we get fewer # results back (not in the SearchSite) than the hit count indicates. # This will hang indefinitely if broken. results = self.mmsqs.all() loaded = [result.pk for result in results._manual_iter()] self.assertEqual(loaded, [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 15, 16, 17, 18, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]) self.assertEqual(len(backends.queries), 8) def test_fill_cache(self): backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) results = self.msqs.all() self.assertEqual(len(results._result_cache), 0) self.assertEqual(len(backends.queries), 0) results._fill_cache(0, 10) self.assertEqual(len([result for result in results._result_cache if result is not None]), 10) self.assertEqual(len(backends.queries), 1) results._fill_cache(10, 20) self.assertEqual(len([result for result in results._result_cache if result is not None]), 20) self.assertEqual(len(backends.queries), 2) backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) # Test to ensure we properly fill the cache, even if we get fewer # results back (not in the SearchSite) than the hit count indicates. results = self.mmsqs.all() self.assertEqual(len([result for result in results._result_cache if result is not None]), 0) self.assertEqual([result.pk for result in results._result_cache if result is not None], []) self.assertEqual(len(backends.queries), 0) results._fill_cache(0, 10) self.assertEqual(len([result for result in results._result_cache if result is not None]), 9) self.assertEqual([result.pk for result in results._result_cache if result is not None], [0, 1, 2, 3, 4, 5, 6, 7, 8]) self.assertEqual(len(backends.queries), 2) results._fill_cache(10, 20) self.assertEqual(len([result for result in results._result_cache if result is not None]), 17) self.assertEqual([result.pk for result in results._result_cache if result is not None], [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 15, 16, 17, 18, 19]) self.assertEqual(len(backends.queries), 4) results._fill_cache(20, 30) self.assertEqual(len([result for result in results._result_cache if result is not None]), 27) self.assertEqual([result.pk for result in results._result_cache if result is not None], [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]) self.assertEqual(len(backends.queries), 6) def test_cache_is_full(self): # Dummy always has a count of 0 and an empty _result_cache, hence True. self.assertEqual(self.bsqs._cache_is_full(), False) results = self.bsqs.all() fire_the_iterator_and_fill_cache = [result for result in results] self.assertEqual(results._cache_is_full(), True) backends.reset_search_queries() self.assertEqual(len(backends.queries), 0) self.assertEqual(self.msqs._cache_is_full(), False) results = self.msqs.all() fire_the_iterator_and_fill_cache = [result for result in results] self.assertEqual(results._cache_is_full(), True) self.assertEqual(len(backends.queries), 10) def test_all(self): sqs = self.bsqs.all() self.assert_(isinstance(sqs, SearchQuerySet)) def test_filter(self): sqs = self.bsqs.filter(content='foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_exclude(self): sqs = self.bsqs.exclude(content='foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_order_by(self): sqs = self.bsqs.order_by('foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assert_('foo' in sqs.query.order_by) def test_models(self): mock_index_site = SearchSite() mock_index_site.register(MockModel) mock_index_site.register(AnotherMockModel) bsqs = SearchQuerySet(site=mock_index_site) sqs = bsqs.all() self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 0) sqs = bsqs.models(MockModel) self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) sqs = bsqs.models(MockModel, AnotherMockModel) self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 2) # This will produce a warning. mock_index_site.unregister(AnotherMockModel) sqs = bsqs.models(AnotherMockModel) self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) def test_boost(self): sqs = self.bsqs.boost('foo', 10) self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.boost.keys()), 1) def test_highlight(self): sqs = self.bsqs.highlight() self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.query.highlight, True) def test_spelling(self): # Test the case where spelling support is disabled. sqs = self.bsqs.filter(content='Indx') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.spelling_suggestion(), None) self.assertEqual(sqs.spelling_suggestion('indexy'), None) def test_raw_search(self): self.assertEqual(len(self.bsqs.raw_search('foo')), 0) self.assertEqual(len(self.bsqs.raw_search('(content__exact hello AND content__exact world)')), 1) def test_load_all(self): # Models with character primary keys sqs = SearchQuerySet(query=MockSearchQuery(backend=CharPKMockSearchBackend())) results = sqs.load_all().all() self.assertEqual(len(results._result_cache), 0) results._fill_cache(0, 2) self.assertEqual(len([result for result in results._result_cache if result is not None]), 2) # If nothing is registered, you get nothing. haystack.site.unregister(MockModel) haystack.site.unregister(CharPKMockModel) sqs = self.msqs.load_all() self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs), 0) # For full tests, see the solr_backend. def test_auto_query(self): sqs = self.bsqs.auto_query('test search -stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND (content__exact=test AND content__exact=search AND NOT (content__exact=stuff))>') sqs = self.bsqs.auto_query('test "my thing" search -stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND (content__exact=my thing AND content__exact=test AND content__exact=search AND NOT (content__exact=stuff))>') sqs = self.bsqs.auto_query('test "my thing" search \'moar quotes\' -stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), "<SQ: AND (content__exact=my thing AND content__exact=test AND content__exact=search AND content__exact='moar AND content__exact=quotes' AND NOT (content__exact=stuff))>") sqs = self.bsqs.auto_query('test "my thing" search \'moar quotes\' "foo -stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND (content__exact=my thing AND content__exact=test AND content__exact=search AND content__exact=\'moar AND content__exact=quotes\' AND content__exact="foo AND NOT (content__exact=stuff))>') sqs = self.bsqs.auto_query('test - stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND (content__exact=test AND content__exact=- AND content__exact=stuff)>') # Ensure bits in exact matches get escaped properly as well. sqs = self.bsqs.auto_query('"pants:rule"') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND content__exact=pants:rule>') def test_count(self): self.assertEqual(self.bsqs.count(), 0) def test_facet_counts(self): self.assertEqual(self.bsqs.facet_counts(), {}) def test_best_match(self): self.assert_(isinstance(self.msqs.best_match(), SearchResult)) def test_latest(self): self.assert_(isinstance(self.msqs.latest('pub_date'), SearchResult)) def test_more_like_this(self): mock = MockModel() mock.id = 1 self.assertEqual(len(self.msqs.more_like_this(mock)), 100) def test_facets(self): sqs = self.bsqs.facet('foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.facets), 1) sqs2 = self.bsqs.facet('foo').facet('bar') self.assert_(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.facets), 2) def test_date_facets(self): try: sqs = self.bsqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='smarblaph') self.fail() except FacetingError, e: self.assertEqual(str(e), "The gap_by ('smarblaph') must be one of the following: year, month, day, hour, minute, second.") sqs = self.bsqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='month') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.date_facets), 1) sqs2 = self.bsqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='month').date_facet('bar', start_date=datetime.date(2007, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='year') self.assert_(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.date_facets), 2)
class SearchQuerySetTestCase(TestCase): fixtures = ['base_data.json', 'bulk_data.json'] def setUp(self): super(SearchQuerySetTestCase, self).setUp() # Stow. self.old_unified_index = connections['default']._index self.ui = UnifiedIndex() self.bmmsi = BasicMockModelSearchIndex() self.cpkmmsi = CharPKMockModelSearchIndex() self.uuidmmsi = SimpleMockUUIDModelIndex() self.ui.build(indexes=[self.bmmsi, self.cpkmmsi, self.uuidmmsi]) connections['default']._index = self.ui # Update the "index". backend = connections['default'].get_backend() backend.clear() backend.update(self.bmmsi, MockModel.objects.all()) self.msqs = SearchQuerySet() # Stow. reset_search_queries() def tearDown(self): # Restore. connections['default']._index = self.old_unified_index super(SearchQuerySetTestCase, self).tearDown() def test_len(self): self.assertEqual(len(self.msqs), 23) def test_repr(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) self.assertRegexpMatches(repr(self.msqs), r'^<SearchQuerySet: query=<test_haystack.mocks.MockSearchQuery object' r' at 0x[0-9A-Fa-f]+>, using=None>$') def test_iter(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) msqs = self.msqs.all() results = [int(res.pk) for res in iter(msqs)] self.assertEqual(results, [res.pk for res in MOCK_SEARCH_RESULTS[:23]]) self.assertEqual(len(connections['default'].queries), 3) def test_slice(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) results = self.msqs.all() self.assertEqual([int(res.pk) for res in results[1:11]], [res.pk for res in MOCK_SEARCH_RESULTS[1:11]]) self.assertEqual(len(connections['default'].queries), 1) reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) results = self.msqs.all() self.assertEqual(int(results[22].pk), MOCK_SEARCH_RESULTS[22].pk) self.assertEqual(len(connections['default'].queries), 1) def test_manual_iter(self): results = self.msqs.all() reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) check = [result.pk for result in results._manual_iter()] self.assertEqual(check, [u'1', u'2', u'3', u'4', u'5', u'6', u'7', u'8', u'9', u'10', u'11', u'12', u'13', u'14', u'15', u'16', u'17', u'18', u'19', u'20', u'21', u'22', u'23']) self.assertEqual(len(connections['default'].queries), 3) reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) # Test to ensure we properly fill the cache, even if we get fewer # results back (not a handled model) than the hit count indicates. # This will hang indefinitely if broken. # CharPK testing old_ui = self.ui self.ui.build(indexes=[self.cpkmmsi]) connections['default']._index = self.ui self.cpkmmsi.update() results = self.msqs.all() loaded = [result.pk for result in results._manual_iter()] self.assertEqual(loaded, [u'sometext', u'1234']) self.assertEqual(len(connections['default'].queries), 1) #UUID testing self.ui.build(indexes=[self.uuidmmsi]) connections['default']._index = self.ui self.uuidmmsi.update() results = self.msqs.all() loaded = [result.pk for result in results._manual_iter()] self.assertEqual(loaded, [u'53554c58-7051-4350-bcc9-dad75eb248a9', u'77554c58-7051-4350-bcc9-dad75eb24888']) connections['default']._index = old_ui def test_cache_is_full(self): reset_search_queries() self.assertEqual(len(connections['default'].queries), 0) self.assertEqual(self.msqs._cache_is_full(), False) results = self.msqs.all() fire_the_iterator_and_fill_cache = list(results) self.assertEqual(23, len(fire_the_iterator_and_fill_cache)) self.assertEqual(results._cache_is_full(), True) self.assertEqual(len(connections['default'].queries), 4) def test_all(self): sqs = self.msqs.all() self.assertTrue(isinstance(sqs, SearchQuerySet)) def test_filter(self): sqs = self.msqs.filter(content='foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_exclude(self): sqs = self.msqs.exclude(content='foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_order_by(self): sqs = self.msqs.order_by('foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertTrue('foo' in sqs.query.order_by) def test_models(self): # Stow. old_unified_index = connections['default']._index ui = UnifiedIndex() bmmsi = BasicMockModelSearchIndex() bammsi = BasicAnotherMockModelSearchIndex() ui.build(indexes=[bmmsi, bammsi]) connections['default']._index = ui msqs = SearchQuerySet() sqs = msqs.all() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 0) sqs = msqs.models(MockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) sqs = msqs.models(MockModel, AnotherMockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 2) # This will produce a warning. ui.build(indexes=[bmmsi]) sqs = msqs.models(AnotherMockModel) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) def test_result_class(self): sqs = self.msqs.all() self.assertTrue(issubclass(sqs.query.result_class, SearchResult)) # Custom class. class IttyBittyResult(object): pass sqs = self.msqs.result_class(IttyBittyResult) self.assertTrue(issubclass(sqs.query.result_class, IttyBittyResult)) # Reset to default. sqs = self.msqs.result_class(None) self.assertTrue(issubclass(sqs.query.result_class, SearchResult)) def test_boost(self): sqs = self.msqs.boost('foo', 10) self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.boost.keys()), 1) def test_highlight(self): sqs = self.msqs.highlight() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.query.highlight, True) def test_spelling_override(self): sqs = self.msqs.filter(content='not the spellchecking query') self.assertEqual(sqs.query.spelling_query, None) sqs = self.msqs.set_spelling_query('override') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.query.spelling_query, 'override') def test_spelling_suggestions(self): # Test the case where spelling support is disabled. sqs = self.msqs.filter(content='Indx') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs.spelling_suggestion(), None) self.assertEqual(sqs.spelling_suggestion('indexy'), None) def test_raw_search(self): self.assertEqual(len(self.msqs.raw_search('foo')), 23) self.assertEqual(len(self.msqs.raw_search('(content__exact:hello AND content__exact:world)')), 23) def test_load_all(self): # Models with character primary keys. sqs = SearchQuerySet() sqs.query.backend = CharPKMockSearchBackend('charpk') results = sqs.load_all().all() self.assertEqual(len(results._result_cache), 0) results._fill_cache(0, 2) self.assertEqual(len([result for result in results._result_cache if result is not None]), 2) # Models with uuid primary keys. sqs = SearchQuerySet() sqs.query.backend = UUIDMockSearchBackend('uuid') results = sqs.load_all().all() self.assertEqual(len(results._result_cache), 0) results._fill_cache(0, 2) self.assertEqual(len([result for result in results._result_cache if result is not None]), 2) # If nothing is handled, you get nothing. old_ui = connections['default']._index ui = UnifiedIndex() ui.build(indexes=[]) connections['default']._index = ui sqs = self.msqs.load_all() self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs), 0) connections['default']._index = old_ui # For full tests, see the solr_backend. def test_load_all_read_queryset(self): # Stow. old_ui = connections['default']._index ui = UnifiedIndex() gafmmsi = GhettoAFifthMockModelSearchIndex() ui.build(indexes=[gafmmsi]) connections['default']._index = ui gafmmsi.update() sqs = SearchQuerySet() results = sqs.load_all().all() results.query.backend = ReadQuerySetMockSearchBackend('default') results._fill_cache(0, 2) # The deleted result isn't returned self.assertEqual(len([result for result in results._result_cache if result is not None]), 1) # Register a SearchIndex with a read_queryset that returns deleted items rqstsi = TextReadQuerySetTestSearchIndex() ui.build(indexes=[rqstsi]) rqstsi.update() sqs = SearchQuerySet() results = sqs.load_all().all() results.query.backend = ReadQuerySetMockSearchBackend('default') results._fill_cache(0, 2) # Both the deleted and not deleted items are returned self.assertEqual(len([result for result in results._result_cache if result is not None]), 2) # Restore. connections['default']._index = old_ui def test_auto_query(self): sqs = self.msqs.auto_query('test search -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND content__content=test search -stuff>') sqs = self.msqs.auto_query('test "my thing" search -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND content__content=test "my thing" search -stuff>') sqs = self.msqs.auto_query('test "my thing" search \'moar quotes\' -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND content__content=test "my thing" search \'moar quotes\' -stuff>') sqs = self.msqs.auto_query('test "my thing" search \'moar quotes\' "foo -stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND content__content=test "my thing" search \'moar quotes\' "foo -stuff>') sqs = self.msqs.auto_query('test - stuff') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), "<SQ: AND content__content=test - stuff>") # Ensure bits in exact matches get escaped properly as well. sqs = self.msqs.auto_query('"pants:rule"') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND content__content="pants:rule">') # Now with a different fieldname sqs = self.msqs.auto_query('test search -stuff', fieldname='title') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), "<SQ: AND title__content=test search -stuff>") sqs = self.msqs.auto_query('test "my thing" search -stuff', fieldname='title') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(repr(sqs.query.query_filter), '<SQ: AND title__content=test "my thing" search -stuff>') def test_count(self): self.assertEqual(self.msqs.count(), 23) def test_facet_counts(self): self.assertEqual(self.msqs.facet_counts(), {}) def test_best_match(self): self.assertTrue(isinstance(self.msqs.best_match(), SearchResult)) def test_latest(self): self.assertTrue(isinstance(self.msqs.latest('pub_date'), SearchResult)) def test_more_like_this(self): mock = MockModel() mock.id = 1 self.assertEqual(len(self.msqs.more_like_this(mock)), 23) def test_facets(self): sqs = self.msqs.facet('foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.facets), 1) sqs2 = self.msqs.facet('foo').facet('bar') self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.facets), 2) def test_date_facets(self): try: sqs = self.msqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='smarblaph') self.fail() except FacetingError as e: self.assertEqual(str(e), "The gap_by ('smarblaph') must be one of the following: year, month, day, hour, minute, second.") sqs = self.msqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='month') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.date_facets), 1) sqs2 = self.msqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='month').date_facet('bar', start_date=datetime.date(2007, 2, 25), end_date=datetime.date(2009, 2, 25), gap_by='year') self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.date_facets), 2) def test_query_facets(self): sqs = self.msqs.query_facet('foo', '[bar TO *]') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_facets), 1) sqs2 = self.msqs.query_facet('foo', '[bar TO *]').query_facet('bar', '[100 TO 499]') self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.query_facets), 2) # Test multiple query facets on a single field sqs3 = self.msqs.query_facet('foo', '[bar TO *]').query_facet('bar', '[100 TO 499]').query_facet('foo', '[1000 TO 1499]') self.assertTrue(isinstance(sqs3, SearchQuerySet)) self.assertEqual(len(sqs3.query.query_facets), 3) def test_stats(self): sqs = self.msqs.stats_facet('foo', 'bar') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.stats), 1) sqs2 = self.msqs.stats_facet('foo', 'bar').stats_facet('foo', 'baz') self.assertTrue(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.stats), 1) sqs3 = self.msqs.stats_facet('foo', 'bar').stats_facet('moof', 'baz') self.assertTrue(isinstance(sqs3, SearchQuerySet)) self.assertEqual(len(sqs3.query.stats), 2) def test_narrow(self): sqs = self.msqs.narrow('foo:moof') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.narrow_queries), 1) def test_clone(self): results = self.msqs.filter(foo='bar', foo__lt='10') clone = results._clone() self.assertTrue(isinstance(clone, SearchQuerySet)) self.assertEqual(str(clone.query), str(results.query)) self.assertEqual(clone._result_cache, []) self.assertEqual(clone._result_count, None) self.assertEqual(clone._cache_full, False) self.assertEqual(clone._using, results._using) def test_using(self): sqs = SearchQuerySet(using='default') self.assertNotEqual(sqs.query, None) self.assertEqual(sqs.query._using, 'default') def test_chaining(self): sqs = self.msqs.filter(content='foo') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) # A second instance should inherit none of the changes from above. sqs = self.msqs.filter(content='bar') self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 1) def test_none(self): sqs = self.msqs.none() self.assertTrue(isinstance(sqs, EmptySearchQuerySet)) self.assertEqual(len(sqs), 0) def test___and__(self): sqs1 = self.msqs.filter(content='foo') sqs2 = self.msqs.filter(content='bar') sqs = sqs1 & sqs2 self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 2) def test___or__(self): sqs1 = self.msqs.filter(content='foo') sqs2 = self.msqs.filter(content='bar') sqs = sqs1 | sqs2 self.assertTrue(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filter), 2) def test_and_or(self): """ Combining AND queries with OR should give AND(OR(a, b), OR(c, d)) """ sqs1 = self.msqs.filter(content='foo').filter(content='oof') sqs2 = self.msqs.filter(content='bar').filter(content='rab') sqs = sqs1 | sqs2 self.assertEqual(sqs.query.query_filter.connector, 'OR') self.assertEqual(repr(sqs.query.query_filter.children[0]), repr(sqs1.query.query_filter)) self.assertEqual(repr(sqs.query.query_filter.children[1]), repr(sqs2.query.query_filter)) def test_or_and(self): """ Combining OR queries with AND should give OR(AND(a, b), AND(c, d)) """ sqs1 = self.msqs.filter(content='foo').filter_or(content='oof') sqs2 = self.msqs.filter(content='bar').filter_or(content='rab') sqs = sqs1 & sqs2 self.assertEqual(sqs.query.query_filter.connector, 'AND') self.assertEqual(repr(sqs.query.query_filter.children[0]), repr(sqs1.query.query_filter)) self.assertEqual(repr(sqs.query.query_filter.children[1]), repr(sqs2.query.query_filter))
def _parse_facets(qd): """ Given a request, returns a quadruple of: * A SearchQuerySet constrained by the request.GET vars. * a dict of constraints that have been applied * a list of potential choices for new constraints * a querystring that produces these constraints (without pagination or sorting) """ sqs = SearchQuerySet() constraints = [] # Process constraints, and execute facets for name, field in settings.ALL_FIELDS.iteritems(): if field['document']: terms = qd.get(field['name'], qd.get('document_', None)) if terms: sqs = sqs.auto_query(terms).highlight() constraints.append({ 'value': terms, 'key': name, 'type': 'document', 'display': field['display_name'], }) elif field['faceted']: sqs = sqs.raw_params(**{ 'f.%s_exact.facet.limit' % field['name']: field['facet_limit'] }) if field['type'] == 'date': sqs, start, end = _narrow_range(sqs, field, qd) if start: constraints.append({ 'value': start.strftime("%Y-%m-%d"), 'key': name + '__gte', 'type': 'date', 'display': "%s - more than" % field['display_name'], }) if end: constraints.append({ 'value': end.strftime("%Y-%m-%d"), 'key': name + '__lte', 'type': 'date', 'display': "%s - less than" % field['display_name'], }) if start or end or name in settings.FACET_DISPLAY: if not end: end = utils.d_to_dt(getattr(SearchQuerySet().order_by("-" + name)[0], name)) if not start: start = utils.d_to_dt(getattr(SearchQuerySet().order_by(name)[0], name)) span = max(1, (end - start).days) gap = max(1, int(span / 100)) sqs = sqs.date_facet(name, start, end, 'day', gap) elif field['type'] in ('int', 'float', 'latitude', 'longitude'): sqs, start, end = _narrow_range(sqs, field, qd) if start: constraints.append({ 'value': start, 'key': name + '__gte', 'type': 'min_max', 'display': "%s - more than" % field['display_name'], }) if end: constraints.append({ 'value': end, 'key': name + '__lte', 'type': 'min_max', 'display': "%s - less than" % field['display_name'], }) if start or end or name in settings.FACET_DISPLAY: sqs = sqs.facet(name) # if not end: # end = getattr(SearchQuerySet().order_by("-" + name)[0], name) # if not start: # start = getattr(SearchQuerySet().order_by(name)[0], name) # span = max(1, (end - start)) # gap = max(1, int(span / 100)) # exact = "%s_exact" % name # sqs = sqs.raw_params(**{ # "facet.range": exact, # "f.%s.facet.range.start" % exact: start, # "f.%s.facet.range.end" % exact: end, # "f.%s.facet.range.gap" % exact: gap, # }) else: val = qd.get(name, None) if val is not None: constraints.append({ 'value': val, 'key': name, 'type': 'text', 'display': field['display_name'], }) sqs = sqs.narrow('%s:"%s"' % (name + "_exact", val)) if val is not None or name in settings.FACET_DISPLAY: sqs = sqs.facet(name) # Constraint removal links queryargs = dict((p['key'], p['value']) for p in constraints) querystr = urllib.urlencode(queryargs) for param in constraints: # remove key value = queryargs.pop(param['key']) remainder = urllib.urlencode(queryargs) param.update({'remove_link': remainder}) # restore key queryargs[param['key']] = value # Choices counts = sqs.facet_counts() choices = [] for name in settings.FACET_DISPLAY: field = settings.ALL_FIELDS[name] choice = {'field': field} if field['document']: choice.update({ 'type': 'document', 'value': queryargs.get(name, ''), }) elif field['type'] in ('text', 'char'): facets = sorted((k, c) for k, c in counts['fields'][name] if c > 0) if facets: choice.update({ 'choices': facets, 'type': 'text', 'value': queryargs.get(name, '') }) elif field['type'] in ('int', 'float', 'latitude', 'longitude'): facets = sorted([(int(k), c) for k,c in counts['fields'][name] if c > 0]) if facets: choice.update({ 'type': 'min_max', 'counts': [c for k,c in facets], 'vals': [k for k,c in facets], 'min_value': facets[0][0], 'max_value': facets[-1][0], 'chosen_min': queryargs.get(name + '__gte', ''), 'chosen_max': queryargs.get(name + '__lte', ''), }) elif field['type'] == 'date': facets = sorted(counts['dates'].get(name, {}).iteritems()) if facets: val_counts = [] vals = [] last_dt = None for d,c in facets: if c > 0: try: last_dt = utils.iso_to_datetime(d) val_counts.append(c) vals.append(last_dt.strftime('%Y-%m-%d')) except (TypeError, ValueError): pass if vals and last_dt: max_value = min( utils.iso_to_datetime(counts['dates'][name]['end']), utils.d_to_dt( getattr(SearchQuerySet().order_by('-' + name)[0], name) ) + datetime.timedelta(days=1), last_dt + datetime.timedelta( days=int(re.sub('[^\d]', '', counts['dates'][name]['gap'])) ), ) vals.append(max_value.strftime('%Y-%m-%d')) val_counts.append(0) choice.update({ 'type': 'date', 'counts': val_counts, 'vals': vals, 'min_value': vals[0], 'max_value': vals[-1], 'chosen_min': queryargs.get(name + '__gte', ''), 'chosen_max': queryargs.get(name + '__lte', ''), }) choices.append(choice) return sqs, choices, constraints, querystr
def races_formatted_search(sport='', viewport=[], start_date='', end_date='', distances=[], search_expr='', format='JSON'): sqs = SearchQuerySet() sqs = sqs.filter(validated="true") print(sport) if sport: sqs = sqs.filter(sport__exact=sport) print("sport filtered") # dates required for facet generation # TODO : fix if start_date: sqs = sqs.filter(date__gte=start_date) else: start_date = '2010-01-01' if end_date: sqs = sqs.filter(date__lte=end_date) else: end_date = '2020-01-01' if distances: sqs = sqs.filter(distance_cat__in=distances) if search_expr: sqs = sqs.filter(content=search_expr) # search from map bounds if len(viewport) == 4: lat_lo = viewport[0] lng_lo = viewport[1] lat_hi = viewport[2] lng_hi = viewport[3] if lat_lo and lng_lo and lat_hi and lng_hi: downtown_bottom_left = Point(float(lng_lo), float(lat_lo)) downtown_top_right = Point(float(lng_hi), float(lat_hi)) sqs = sqs.within('location', downtown_bottom_left, downtown_top_right) sqs = sqs.order_by('date') sqs = sqs.facet('distance_cat').facet('administrative_area_level_1').facet('administrative_area_level_2') # facet for dates if defined sqs = sqs.date_facet(field='date', start_date=datetime.strptime(start_date, '%Y-%m-%d'), end_date=datetime.strptime(end_date, '%Y-%m-%d'), gap_by='month') facet = sqs.facet_counts() if len(facet) > 0: # convert datetime into serializable isoformat facet_dates = [] for f in facet['dates']['date']: facet_dates.append((f[0].isoformat(), f[1])) facet['dates']['date'] = facet_dates # build the JSON response races = [] result_html = [] # Uniqfy the event list (multiple races from an event) seen = {} rank = 1 prev_month = cur_month = 0 month_iterator = 0 for sr in sqs: cur_month = sr.date.strftime('%m%Y') event_id = sr.get_stored_fields()['event_id'] event_title = sr.get_stored_fields()['event_title'] location = sr.get_stored_fields()['location'] rendered = sr.get_stored_fields()['rendered'] if event_id in seen: continue seen[event_id] = 1 race_data = {'id': int(event_id), 'name': event_title, 'score': str(sr.score), # 'rankClass': "primary" if (rank <= 10) else "secondary", 'rankClass': "secondary", 'lat': str(location.get_coords()[1]), 'lng': str(location.get_coords()[0]), 'date': sr.date.strftime('%Y%m%d'), } races.append(race_data) if prev_month != cur_month: result_html.append(render_to_string('html_utils/result_month_spacer.html', {'last_date': sr.date, 'count': facet['dates']['date'][month_iterator][1] })) month_iterator += 1 result_html.append(rendered) prev_month = cur_month rank += 1 if not races: result_html = render_to_string('html_utils/search_no_result_alert.html', {'search_expr': search_expr}) data = {'count': sqs.count(), 'races': races, 'html': result_html, 'facet': facet, } if format.lower() == 'dict': return data return dumps(data)
class SearchQuerySetTestCase(TestCase): def setUp(self): super(SearchQuerySetTestCase, self).setUp() self.bsqs = SearchQuerySet(query=DummySearchQuery(backend=DummySearchBackend())) self.msqs = SearchQuerySet(query=MockSearchQuery(backend=MockSearchBackend())) def test_len(self): # Dummy always returns 0. self.assertEqual(len(self.bsqs), 0) self.assertEqual(len(self.msqs), 100) def test_iter(self): # Dummy always returns []. self.assertEqual([result for result in self.bsqs.all()], []) msqs = self.msqs.all() results = [result for result in msqs] self.assertEqual(results, MOCK_SEARCH_RESULTS) def test_slice(self): self.assertEqual(self.msqs.all()[1:11], MOCK_SEARCH_RESULTS[1:11]) self.assertEqual(self.msqs.all()[50], MOCK_SEARCH_RESULTS[50]) def test_manual_iter(self): results = self.msqs.all() for offset, result in enumerate(results._manual_iter()): self.assertEqual(result, MOCK_SEARCH_RESULTS[offset]) def test_fill_cache(self): results = self.msqs.all() self.assertEqual(len(results._result_cache), 0) results._fill_cache() self.assertEqual(len(results._result_cache), 20) def test_cache_is_full(self): # Dummy always has a count of 0 and an empty _result_cache, hence True. self.assertEqual(self.bsqs._cache_is_full(), True) self.assertEqual(self.msqs._cache_is_full(), False) results = self.msqs.all() fire_the_iterator_and_fill_cache = [result for result in results] self.assertEqual(results._cache_is_full(), True) def test_all(self): sqs = self.bsqs.all() self.assert_(isinstance(sqs, SearchQuerySet)) def test_filter(self): sqs = self.bsqs.filter(content='foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filters), 1) def test_exclude(self): sqs = self.bsqs.exclude(content='foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filters), 1) def test_order_by(self): sqs = self.bsqs.order_by('foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assert_('foo' in sqs.query.order_by) def test_models(self): mock_index_site = SearchSite() mock_index_site.register(MockModel) mock_index_site.register(AnotherMockModel) bsqs = SearchQuerySet(site=mock_index_site) sqs = bsqs.all() self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 0) sqs = bsqs.models(MockModel) self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 1) sqs = bsqs.models(MockModel, AnotherMockModel) self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.models), 2) def test_boost(self): sqs = self.bsqs.boost(foo=10) self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.boost.keys()), 1) def test_highlight(self): sqs = self.bsqs.highlight() self.assert_(isinstance(sqs, SearchQuerySet)) self.assert_(sqs.query.highlight, True) def test_raw_search(self): self.assertEqual(len(self.bsqs.raw_search('foo')), 0) self.assertEqual(len(self.bsqs.raw_search('content__exact hello AND content__exact world')), 1) def test_load_all(self): sqs = self.msqs.load_all() self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(sqs[0].object.foo, 'bar') def test_auto_query(self): sqs = self.bsqs.auto_query('test search -stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual([repr(the_filter) for the_filter in sqs.query.query_filters], ['<QueryFilter: AND content__exact=test>', '<QueryFilter: AND content__exact=search>', '<QueryFilter: NOT content__exact=stuff>']) sqs = self.bsqs.auto_query('test "my thing" search -stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual([repr(the_filter) for the_filter in sqs.query.query_filters], ['<QueryFilter: AND content__exact=my thing>', '<QueryFilter: AND content__exact=test>', '<QueryFilter: AND content__exact=search>', '<QueryFilter: NOT content__exact=stuff>']) sqs = self.bsqs.auto_query('test "my thing" search \'moar quotes\' -stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual([repr(the_filter) for the_filter in sqs.query.query_filters], ['<QueryFilter: AND content__exact=my thing>', '<QueryFilter: AND content__exact=moar quotes>', '<QueryFilter: AND content__exact=test>', '<QueryFilter: AND content__exact=search>', '<QueryFilter: NOT content__exact=stuff>']) sqs = self.bsqs.auto_query('test "my thing" search \'moar quotes\' "foo -stuff') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual([repr(the_filter) for the_filter in sqs.query.query_filters], ['<QueryFilter: AND content__exact=my thing>', '<QueryFilter: AND content__exact=moar quotes>', '<QueryFilter: AND content__exact=test>', '<QueryFilter: AND content__exact=search>', '<QueryFilter: AND content__exact="foo>', '<QueryFilter: NOT content__exact=stuff>']) def test_count(self): self.assertEqual(self.bsqs.count(), 0) def test_facet_counts(self): self.assertEqual(self.bsqs.facet_counts(), {}) def test_best_match(self): self.assert_(isinstance(self.msqs.best_match(), SearchResult)) def test_latest(self): self.assert_(isinstance(self.msqs.latest('pub_date'), SearchResult)) def test_more_like_this(self): mock = MockModel() mock.id = 1 self.assertEqual(len(self.msqs.more_like_this(mock)), 100) def test_facets(self): sqs = self.bsqs.facet('foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.facets), 1) sqs2 = self.bsqs.facet('foo').facet('bar') self.assert_(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.facets), 2) def test_date_facets(self): sqs = self.bsqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap='/MONTH') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.date_facets), 1) sqs2 = self.bsqs.date_facet('foo', start_date=datetime.date(2008, 2, 25), end_date=datetime.date(2009, 2, 25), gap='/MONTH').date_facet('bar', start_date=datetime.date(2007, 2, 25), end_date=datetime.date(2009, 2, 25), gap='/YEAR') self.assert_(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.date_facets), 2) def test_query_facets(self): sqs = self.bsqs.query_facet('foo', '[bar TO *]') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_facets), 1) sqs2 = self.bsqs.query_facet('foo', '[bar TO *]').query_facet('bar', '[100 TO 499]') self.assert_(isinstance(sqs2, SearchQuerySet)) self.assertEqual(len(sqs2.query.query_facets), 2) def test_narrow(self): sqs = self.bsqs.narrow('foo:moof') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.narrow_queries), 1) def test_clone(self): results = self.msqs.filter(foo='bar', foo__lt='10') clone = results._clone() self.assert_(isinstance(clone, SearchQuerySet)) self.assertEqual(clone.site, results.site) self.assertEqual(str(clone.query), str(results.query)) self.assertEqual(clone._result_cache, []) self.assertEqual(clone._result_count, None) self.assertEqual(clone._cache_full, False) def test_chaining(self): sqs = self.bsqs.filter(content='foo') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filters), 1) # A second instance should inherit none of the changes from above. sqs = self.bsqs.filter(content='bar') self.assert_(isinstance(sqs, SearchQuerySet)) self.assertEqual(len(sqs.query.query_filters), 1)
class Searcher(object): ''' A generic class for searching any indexed model. TODO: * find a different way to configure facet behaviour, perhaps directly in the index class??? e.g. meta? ''' def __init__(self, model=None, facets={}, sort_config={}): self.model = model self.queryset = None self.field_facets = facets.get('fields', {}) self.date_facets = facets.get('dates', {}) self.query_facets = facets.get('queries', {}) self.facets = FacetList() self.indexed_fields = connections['default'].get_unified_index().all_searchfields() self.sort_config = sort_config def search(self, filters=None, keywords=None, order_by='', **kwargs): ''' filters the field-value which all results must match keywords plain search string for matching text order_by the sort order of the results (a field name) ''' logger.debug("Searching with filters %s" % filters) self.filters = filters or {} self.cleaned_filters = self._clean_filters(self.filters) self.queryset = SearchQuerySet() self.use_default_order = not order_by self.order_by = self.clean_sort_order(order_by) self.keywords = keywords or self.filters.get(KEYWORD_PARAM, '') if self.keywords and not USE_DEFAULT_SORT_WITH_KEYWORD and self.use_default_order: self.order_by = '' self.queryset = self.queryset.models(self.model).filter(**kwargs) self._narrow_queryset(self.cleaned_filters) self._keyword_filtered() self._field_faceted() self._query_faceted() self._date_faceted() self._ordered() self.facets = self._facets() self.search_performed = True return self.queryset @property def default_sort_order(self): for conf in self.sort_config: if conf.get('default', False): return conf.get('field') if not conf.get('reverse', False) else '-%s'% conf.get('field') return None def clean_sort_order(self, sort_order): ''' Ensures the sort_order exists in the sort_config, otherwise returns the default_sort_order. ''' config = self.get_sort_order_config(sort_order) if config: return sort_order return self.default_sort_order def get_sort_order_config(self, sort_order): ''' Helper to get the config of a specific sort order ''' if not sort_order: return {} is_reverse = sort_order.startswith('-') field = sort_order[1:] if is_reverse else sort_order for conf in self.sort_config: if conf['field'] == field and conf.get('reverse', False) == is_reverse: return conf logger.warning('No sort_order config found for %s' % sort_order) return {} def url_param(self): ''' Construct the URL parameters from the last search performed. This is a combination of facet parameters, keywords, and sorting. This does not include the question mark, only the '&' joined key-values. ''' if not self.search_performed: raise SearcherError('No search has been performed') return self.facets.url_param() def _clean_filters(self, filters): ''' Helper to ensure only indexed fields are filtered ''' cleaned = {} for key, value in filters.items(): if key in self.indexed_fields and value: cleaned[key] = value return cleaned def _facets(self): ''' Fetch and parse facet counts. ''' facet_counts = self.queryset.facet_counts() facets = self._parse_field_facets(facet_counts.get('fields', {})) facets = facets + self._parse_query_facets(facet_counts.get('queries', {})) facets = facets + self._parse_date_facets(facet_counts.get('dates', {})) extra_params = {} if self.keywords: extra_params[KEYWORD_PARAM] = self.keywords if self.order_by and not self.use_default_order: extra_params[SORT_PARAM] = self.order_by facet_list = FacetList(extra_params=extra_params) for facet in facets: facet_list.append(facet) return facet_list @property def sort_options(self): ''' A list of sort option dicts ( {'url':'...', 'label': 'A name', 'selected':True} ) ''' opts = [] use_default = not self.order_by # Remove any existing sort order parameters that may have been # included as extra_params to the FacetList. This prevents multiple # sort orders from being included in the sort urls. query = parse_qs(self.facets.url_param()) query.pop(SORT_PARAM, None) query = dict((k, v[0]) for k, v in query.iteritems()) for conf in self.sort_config: reverse = conf.get('reverse', False) field = conf.get('field') if not reverse else '-%s'% conf.get('field') is_default = conf.get('default', False) if not is_default: sort_query = {SORT_PARAM: field} sort_query.update(query) url = '?%s' % urlencode(sort_query) else: url = '?%s' % urlencode(query) opts.append({ 'url': url, 'label': conf.get('label'), 'selected': (use_default and is_default) or (self.order_by == field), }) return opts def _ordered(self): if not self.order_by: return self.queryset = self.queryset.order_by(self.order_by) def _keyword_filtered(self): if self.keywords: self.queryset = self.queryset.filter(text=self.queryset.query.clean(self.keywords)) def _field_faceted(self): ''' See search_indexes.py for the defined faceted fields. ''' for field, config in self.field_facets.iteritems(): self.queryset = self.queryset.facet(field) def _query_faceted(self): for field, queries in self.query_facets.iteritems(): for query in queries: self.queryset = self.queryset.query_facet(field, query) def _date_faceted(self): for field, config in self.date_facets.iteritems(): self.queryset = self.queryset.date_facet(field, start_date=config['start_date'], end_date=config['end_date'], gap_by=config['gap_by']) def _parse_date_facets(self, facet_items): ''' Parse date faceted fields like: {'departure_dates': {'2010-04-24T18:17:03Z': 105, '2010-05-01T00:00:00Z': 323, '2010-06-01T00:00:00Z': 334, '2010-07-01T00:00:00Z': 468, '2010-08-01T00:00:00Z': 504, '2010-09-01T00:00:00Z': 515, '2010-10-01T00:00:00Z': 519, '2010-11-01T00:00:00Z': 478, '2010-12-01T00:00:00Z': 457, '2011-01-01T00:00:00Z': 370, '2011-02-01T00:00:00Z': 357, '2011-03-01T00:00:00Z': 370, '2011-04-01T00:00:00Z': 359, 'end': '2011-05-01T00:00:00Z', 'gap': '+1MONTH/MONTH'}} ''' facets = [] for field, date_counts in facet_items.iteritems(): facet = Facet(field=field, label=field.replace('_', ' ').title()) gap = date_counts['gap'] for date_string, count in date_counts.iteritems(): match = DATETIME_REGEX.search(date_string) if not match: continue data = match.groupdict() date = datetime_safe.date(int(data['year']), int(data['month']), int(data['day'])) item = FacetItem(date, count) if gap == '+1MONTH/MONTH': item.label=date.strftime("%B") item.value=date.strftime("%Y-%m") elif gap == '+1YEAR/YEAR': item.label=date.strftime("%Y") item.value=date.strftime("%Y-01") item.is_selected = self._is_selected_facet(field, check_parse_date(item.value)) item.facet = facet facet.items.append(item) facet.items.sort(key=lambda x: x.value) facets.append(facet) return facets def _parse_query_facets(self, facet_items): ''' Parses query facet counts like this: { 'min_price_USD_exact:[2001 TO *]': 306, 'duration_exact:[16 TO 25]': 149, 'min_price_USD_exact:[500 TO 1000]': 195, 'duration_exact:[41 TO *]': 35, 'duration_exact:[26 TO 40]': 52, 'min_price_USD_exact:[0 TO 500]': 88, 'duration_exact:[6 TO 10]': 329, 'min_price_USD_exact:[1001 TO 2000]': 292, 'duration_exact:[11 TO 15]': 256, 'duration_exact:[* TO 5]': 205 } Returns a list of Facet objects (one per parsed field name). ''' facets = {} for field_query, count in facet_items.iteritems(): field, sep, query = field_query.partition(':') field = field.replace('_exact', '') if field in facets: facet = facets[field] else: facet = QueryFacet(field=field, label=field.replace('_', ' ').title()) facets[field] = facet item = FacetItem(query, count, label=humanize_range(query)) item.is_selected = self._is_selected_facet(field, item.value) item.facet = facet facet.items.append(item) return [facet for field,facet in facets.iteritems()] def _parse_field_facets(self, facet_items): ''' Parses field facet counts like this: {'region': [('South America', 222), ('Asia', 134), ('Antarctica', 10), ('South Pacific', 2)], 'service_level': [('Standard', 351), ('Basic', 202), ('Comfort', 93), ('Superior', 25)],} ''' facets = [] for field, counts in facet_items.iteritems(): conf = self.field_facets[field] label = conf.get('label', field.replace('_', ' ').title()) facet = Facet(field=field, label=label) for count in counts: item = FacetItem(count[0], count[1]) item.is_selected = self._is_selected_facet(field, item.value) item.facet = facet facet.items.append(item) facets.append(facet) return facets def _is_selected_facet(self, field, facet_value): ''' Checks the existing narrow_queries to check if a given field:value exists, and is thus selected. Be careful not to modify facet_value as it would be reflected in the facet. ''' for narrow in self.queryset.query.narrow_queries: value = self._solr_escape_value(facet_value) if narrow.partition(':') == (field, ':', value) or \ narrow.partition(':') == ('%s_exact'%field, ':', value): return True return False def _narrow_queryset(self, filters): ''' Helper to narrow a queryset using a dict of key-value pairs ''' if not filters: return for field, value in filters.iteritems(): # Generally, django-haystack will use the correct _exact field # for filtering on facets, but for custom query facets it doesn't # so we just make sure that the _exact field is used. value = check_parse_date(value) value = self._solr_escape_value(value) field = '%s_exact' % field if self.indexed_fields[field].faceted else field self.queryset = self.queryset.narrow('%(field)s:%(value)s' % { 'field':field, 'value':value}) def _solr_escape_value(self, value): ''' Escape Solr special characters ''' # ranges shouldn't have spaces escaped if '[' in value: return value ESCAPE_CHARS_RE = re.compile(r'(?<!\\)(?P<char>[&|+\-!(){}[\]^ "~*?:])') escaped = ESCAPE_CHARS_RE.sub(r'\\\g<char>', value) return escaped