test_elasticsearch5_backend.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764
  1. # -*- coding: utf-8 -*-
  2. import datetime
  3. import json
  4. import mock
  5. from django.db.models import Q
  6. from django.test import TestCase
  7. from elasticsearch.serializer import JSONSerializer
  8. from wagtail.search.backends.elasticsearch5 import Elasticsearch5SearchBackend
  9. from wagtail.search.query import MATCH_ALL
  10. from wagtail.tests.search import models
  11. from .elasticsearch_common_tests import ElasticsearchCommonSearchBackendTests
  12. class TestElasticsearch5SearchBackend(ElasticsearchCommonSearchBackendTests, TestCase):
  13. backend_path = 'wagtail.search.backends.elasticsearch5'
  14. class TestElasticsearch5SearchQuery(TestCase):
  15. def assertDictEqual(self, a, b):
  16. default = JSONSerializer().default
  17. self.assertEqual(
  18. json.dumps(a, sort_keys=True, default=default), json.dumps(b, sort_keys=True, default=default)
  19. )
  20. query_compiler_class = Elasticsearch5SearchBackend.query_compiler_class
  21. def test_simple(self):
  22. # Create a query
  23. query_compiler = self.query_compiler_class(models.Book.objects.all(), "Hello")
  24. # Check it
  25. expected_result = {'bool': {
  26. 'filter': {'match': {'content_type': 'searchtests.Book'}},
  27. 'must': {'multi_match': {'query': 'Hello', 'fields': ['_all', '_partials']}}
  28. }}
  29. self.assertDictEqual(query_compiler.get_query(), expected_result)
  30. def test_match_all(self):
  31. # Create a query
  32. query_compiler = self.query_compiler_class(models.Book.objects.all(), MATCH_ALL)
  33. # Check it
  34. expected_result = {'bool': {
  35. 'filter': {'match': {'content_type': 'searchtests.Book'}},
  36. 'must': {'match_all': {}}
  37. }}
  38. self.assertDictEqual(query_compiler.get_query(), expected_result)
  39. def test_and_operator(self):
  40. # Create a query
  41. query_compiler = self.query_compiler_class(models.Book.objects.all(), "Hello", operator='and')
  42. # Check it
  43. expected_result = {'bool': {
  44. 'filter': {'match': {'content_type': 'searchtests.Book'}},
  45. 'must': {'multi_match': {'query': 'Hello', 'fields': ['_all', '_partials'], 'operator': 'and'}}
  46. }}
  47. self.assertDictEqual(query_compiler.get_query(), expected_result)
  48. def test_filter(self):
  49. # Create a query
  50. query_compiler = self.query_compiler_class(models.Book.objects.filter(title="Test"), "Hello")
  51. # Check it
  52. expected_result = {'bool': {'filter': [
  53. {'match': {'content_type': 'searchtests.Book'}},
  54. {'term': {'title_filter': 'Test'}}
  55. ], 'must': {'multi_match': {'query': 'Hello', 'fields': ['_all', '_partials']}}}}
  56. self.assertDictEqual(query_compiler.get_query(), expected_result)
  57. def test_and_filter(self):
  58. # Create a query
  59. query_compiler = self.query_compiler_class(models.Book.objects.filter(title="Test", publication_date=datetime.date(2017, 10, 18)), "Hello")
  60. # Check it
  61. expected_result = {'bool': {'filter': [
  62. {'match': {'content_type': 'searchtests.Book'}},
  63. {'bool': {'must': [{'term': {'publication_date_filter': '2017-10-18'}}, {'term': {'title_filter': 'Test'}}]}}
  64. ], 'must': {'multi_match': {'query': 'Hello', 'fields': ['_all', '_partials']}}}}
  65. # Make sure field filters are sorted (as they can be in any order which may cause false positives)
  66. query = query_compiler.get_query()
  67. field_filters = query['bool']['filter'][1]['bool']['must']
  68. field_filters[:] = sorted(field_filters, key=lambda f: list(f['term'].keys())[0])
  69. self.assertDictEqual(query, expected_result)
  70. def test_or_filter(self):
  71. # Create a query
  72. query_compiler = self.query_compiler_class(models.Book.objects.filter(Q(title="Test") | Q(publication_date=datetime.date(2017, 10, 18))), "Hello")
  73. # Make sure field filters are sorted (as they can be in any order which may cause false positives)
  74. query = query_compiler.get_query()
  75. field_filters = query['bool']['filter'][1]['bool']['should']
  76. field_filters[:] = sorted(field_filters, key=lambda f: list(f['term'].keys())[0])
  77. # Check it
  78. expected_result = {'bool': {'filter': [
  79. {'match': {'content_type': 'searchtests.Book'}},
  80. {'bool': {'should': [{'term': {'publication_date_filter': '2017-10-18'}}, {'term': {'title_filter': 'Test'}}]}}
  81. ], 'must': {'multi_match': {'query': 'Hello', 'fields': ['_all', '_partials']}}}}
  82. self.assertDictEqual(query, expected_result)
  83. def test_negated_filter(self):
  84. # Create a query
  85. query_compiler = self.query_compiler_class(models.Book.objects.exclude(publication_date=datetime.date(2017, 10, 18)), "Hello")
  86. # Check it
  87. expected_result = {'bool': {'filter': [
  88. {'match': {'content_type': 'searchtests.Book'}},
  89. {'bool': {'mustNot': {'term': {'publication_date_filter': '2017-10-18'}}}}
  90. ], 'must': {'multi_match': {'query': 'Hello', 'fields': ['_all', '_partials']}}}}
  91. self.assertDictEqual(query_compiler.get_query(), expected_result)
  92. def test_fields(self):
  93. # Create a query
  94. query_compiler = self.query_compiler_class(models.Book.objects.all(), "Hello", fields=['title'])
  95. # Check it
  96. expected_result = {'bool': {
  97. 'filter': {'match': {'content_type': 'searchtests.Book'}},
  98. 'must': {'match': {'title': {'query': 'Hello'}}}
  99. }}
  100. self.assertDictEqual(query_compiler.get_query(), expected_result)
  101. def test_fields_with_and_operator(self):
  102. # Create a query
  103. query_compiler = self.query_compiler_class(models.Book.objects.all(), "Hello", fields=['title'], operator='and')
  104. # Check it
  105. expected_result = {'bool': {
  106. 'filter': {'match': {'content_type': 'searchtests.Book'}},
  107. 'must': {'match': {'title': {'query': 'Hello', 'operator': 'and'}}}
  108. }}
  109. self.assertDictEqual(query_compiler.get_query(), expected_result)
  110. def test_multiple_fields(self):
  111. # Create a query
  112. query_compiler = self.query_compiler_class(models.Book.objects.all(), "Hello", fields=['title', 'content'])
  113. # Check it
  114. expected_result = {'bool': {
  115. 'filter': {'match': {'content_type': 'searchtests.Book'}},
  116. 'must': {'multi_match': {'fields': ['title', 'content'], 'query': 'Hello'}}
  117. }}
  118. self.assertDictEqual(query_compiler.get_query(), expected_result)
  119. def test_multiple_fields_with_and_operator(self):
  120. # Create a query
  121. query_compiler = self.query_compiler_class(
  122. models.Book.objects.all(), "Hello", fields=['title', 'content'], operator='and'
  123. )
  124. # Check it
  125. expected_result = {'bool': {
  126. 'filter': {'match': {'content_type': 'searchtests.Book'}},
  127. 'must': {'multi_match': {'fields': ['title', 'content'], 'query': 'Hello', 'operator': 'and'}}
  128. }}
  129. self.assertDictEqual(query_compiler.get_query(), expected_result)
  130. def test_exact_lookup(self):
  131. # Create a query
  132. query_compiler = self.query_compiler_class(models.Book.objects.filter(title__exact="Test"), "Hello")
  133. # Check it
  134. expected_result = {'bool': {'filter': [
  135. {'match': {'content_type': 'searchtests.Book'}},
  136. {'term': {'title_filter': 'Test'}}
  137. ], 'must': {'multi_match': {'query': 'Hello', 'fields': ['_all', '_partials']}}}}
  138. self.assertDictEqual(query_compiler.get_query(), expected_result)
  139. def test_none_lookup(self):
  140. # Create a query
  141. query_compiler = self.query_compiler_class(models.Book.objects.filter(title=None), "Hello")
  142. # Check it
  143. expected_result = {'bool': {'filter': [
  144. {'match': {'content_type': 'searchtests.Book'}},
  145. {'bool': {'mustNot': {'exists': {'field': 'title_filter'}}}}
  146. ], 'must': {'multi_match': {'query': 'Hello', 'fields': ['_all', '_partials']}}}}
  147. self.assertDictEqual(query_compiler.get_query(), expected_result)
  148. def test_isnull_true_lookup(self):
  149. # Create a query
  150. query_compiler = self.query_compiler_class(models.Book.objects.filter(title__isnull=True), "Hello")
  151. # Check it
  152. expected_result = {'bool': {'filter': [
  153. {'match': {'content_type': 'searchtests.Book'}},
  154. {'bool': {'mustNot': {'exists': {'field': 'title_filter'}}}}
  155. ], 'must': {'multi_match': {'query': 'Hello', 'fields': ['_all', '_partials']}}}}
  156. self.assertDictEqual(query_compiler.get_query(), expected_result)
  157. def test_isnull_false_lookup(self):
  158. # Create a query
  159. query_compiler = self.query_compiler_class(models.Book.objects.filter(title__isnull=False), "Hello")
  160. # Check it
  161. expected_result = {'bool': {'filter': [
  162. {'match': {'content_type': 'searchtests.Book'}},
  163. {'exists': {'field': 'title_filter'}}
  164. ], 'must': {'multi_match': {'query': 'Hello', 'fields': ['_all', '_partials']}}}}
  165. self.assertDictEqual(query_compiler.get_query(), expected_result)
  166. def test_startswith_lookup(self):
  167. # Create a query
  168. query_compiler = self.query_compiler_class(models.Book.objects.filter(title__startswith="Test"), "Hello")
  169. # Check it
  170. expected_result = {'bool': {'filter': [
  171. {'match': {'content_type': 'searchtests.Book'}},
  172. {'prefix': {'title_filter': 'Test'}}
  173. ], 'must': {'multi_match': {'query': 'Hello', 'fields': ['_all', '_partials']}}}}
  174. self.assertDictEqual(query_compiler.get_query(), expected_result)
  175. def test_gt_lookup(self):
  176. # This also tests conversion of python dates to strings
  177. # Create a query
  178. query_compiler = self.query_compiler_class(
  179. models.Book.objects.filter(publication_date__gt=datetime.datetime(2014, 4, 29)), "Hello"
  180. )
  181. # Check it
  182. expected_result = {'bool': {'filter': [
  183. {'match': {'content_type': 'searchtests.Book'}},
  184. {'range': {'publication_date_filter': {'gt': '2014-04-29'}}}
  185. ], 'must': {'multi_match': {'query': 'Hello', 'fields': ['_all', '_partials']}}}}
  186. self.assertDictEqual(query_compiler.get_query(), expected_result)
  187. def test_lt_lookup(self):
  188. # Create a query
  189. query_compiler = self.query_compiler_class(
  190. models.Book.objects.filter(publication_date__lt=datetime.datetime(2014, 4, 29)), "Hello"
  191. )
  192. # Check it
  193. expected_result = {'bool': {'filter': [
  194. {'match': {'content_type': 'searchtests.Book'}},
  195. {'range': {'publication_date_filter': {'lt': '2014-04-29'}}}
  196. ], 'must': {'multi_match': {'query': 'Hello', 'fields': ['_all', '_partials']}}}}
  197. self.assertDictEqual(query_compiler.get_query(), expected_result)
  198. def test_gte_lookup(self):
  199. # Create a query
  200. query_compiler = self.query_compiler_class(
  201. models.Book.objects.filter(publication_date__gte=datetime.datetime(2014, 4, 29)), "Hello"
  202. )
  203. # Check it
  204. expected_result = {'bool': {'filter': [
  205. {'match': {'content_type': 'searchtests.Book'}},
  206. {'range': {'publication_date_filter': {'gte': '2014-04-29'}}}
  207. ], 'must': {'multi_match': {'query': 'Hello', 'fields': ['_all', '_partials']}}}}
  208. self.assertDictEqual(query_compiler.get_query(), expected_result)
  209. def test_lte_lookup(self):
  210. # Create a query
  211. query_compiler = self.query_compiler_class(
  212. models.Book.objects.filter(publication_date__lte=datetime.datetime(2014, 4, 29)), "Hello"
  213. )
  214. # Check it
  215. expected_result = {'bool': {'filter': [
  216. {'match': {'content_type': 'searchtests.Book'}},
  217. {'range': {'publication_date_filter': {'lte': '2014-04-29'}}}
  218. ], 'must': {'multi_match': {'query': 'Hello', 'fields': ['_all', '_partials']}}}}
  219. self.assertDictEqual(query_compiler.get_query(), expected_result)
  220. def test_range_lookup(self):
  221. start_date = datetime.datetime(2014, 4, 29)
  222. end_date = datetime.datetime(2014, 8, 19)
  223. # Create a query
  224. query_compiler = self.query_compiler_class(
  225. models.Book.objects.filter(publication_date__range=(start_date, end_date)), "Hello"
  226. )
  227. # Check it
  228. expected_result = {'bool': {'filter': [
  229. {'match': {'content_type': 'searchtests.Book'}},
  230. {'range': {'publication_date_filter': {'gte': '2014-04-29', 'lte': '2014-08-19'}}}
  231. ], 'must': {'multi_match': {'query': 'Hello', 'fields': ['_all', '_partials']}}}}
  232. self.assertDictEqual(query_compiler.get_query(), expected_result)
  233. def test_custom_ordering(self):
  234. # Create a query
  235. query_compiler = self.query_compiler_class(
  236. models.Book.objects.order_by('publication_date'), "Hello", order_by_relevance=False
  237. )
  238. # Check it
  239. expected_result = [{'publication_date_filter': 'asc'}]
  240. self.assertDictEqual(query_compiler.get_sort(), expected_result)
  241. def test_custom_ordering_reversed(self):
  242. # Create a query
  243. query_compiler = self.query_compiler_class(
  244. models.Book.objects.order_by('-publication_date'), "Hello", order_by_relevance=False
  245. )
  246. # Check it
  247. expected_result = [{'publication_date_filter': 'desc'}]
  248. self.assertDictEqual(query_compiler.get_sort(), expected_result)
  249. def test_custom_ordering_multiple(self):
  250. # Create a query
  251. query_compiler = self.query_compiler_class(
  252. models.Book.objects.order_by('publication_date', 'number_of_pages'), "Hello", order_by_relevance=False
  253. )
  254. # Check it
  255. expected_result = [{'publication_date_filter': 'asc'}, {'number_of_pages_filter': 'asc'}]
  256. self.assertDictEqual(query_compiler.get_sort(), expected_result)
  257. class TestElasticsearch5SearchResults(TestCase):
  258. fixtures = ['search']
  259. def assertDictEqual(self, a, b):
  260. default = JSONSerializer().default
  261. self.assertEqual(
  262. json.dumps(a, sort_keys=True, default=default), json.dumps
  263. )
  264. def get_results(self):
  265. backend = Elasticsearch5SearchBackend({})
  266. query_compiler = mock.MagicMock()
  267. query_compiler.queryset = models.Book.objects.all()
  268. query_compiler.get_query.return_value = 'QUERY'
  269. query_compiler.get_sort.return_value = None
  270. return backend.results_class(backend, query_compiler)
  271. def construct_search_response(self, results):
  272. return {
  273. '_shards': {'failed': 0, 'successful': 5, 'total': 5},
  274. 'hits': {
  275. 'hits': [
  276. {
  277. '_id': 'searchtests_book:' + str(result),
  278. '_index': 'wagtail',
  279. '_score': 1,
  280. '_type': 'searchtests_book',
  281. 'fields': {
  282. 'pk': [str(result)],
  283. }
  284. }
  285. for result in results
  286. ],
  287. 'max_score': 1,
  288. 'total': len(results)
  289. },
  290. 'timed_out': False,
  291. 'took': 2
  292. }
  293. @mock.patch('elasticsearch.Elasticsearch.search')
  294. def test_basic_search(self, search):
  295. search.return_value = self.construct_search_response([])
  296. results = self.get_results()
  297. list(results) # Performs search
  298. search.assert_any_call(
  299. body={'query': 'QUERY'},
  300. _source=False,
  301. stored_fields='pk',
  302. index='wagtail__searchtests_book',
  303. scroll='2m',
  304. size=100
  305. )
  306. @mock.patch('elasticsearch.Elasticsearch.search')
  307. def test_get_single_item(self, search):
  308. # Need to return something to prevent index error
  309. search.return_value = self.construct_search_response([1])
  310. results = self.get_results()
  311. results[10] # Performs search
  312. search.assert_any_call(
  313. from_=10,
  314. body={'query': 'QUERY'},
  315. _source=False,
  316. stored_fields='pk',
  317. index='wagtail__searchtests_book',
  318. size=1
  319. )
  320. @mock.patch('elasticsearch.Elasticsearch.search')
  321. def test_slice_results(self, search):
  322. search.return_value = self.construct_search_response([])
  323. results = self.get_results()[1:4]
  324. list(results) # Performs search
  325. search.assert_any_call(
  326. from_=1,
  327. body={'query': 'QUERY'},
  328. _source=False,
  329. stored_fields='pk',
  330. index='wagtail__searchtests_book',
  331. size=3
  332. )
  333. @mock.patch('elasticsearch.Elasticsearch.search')
  334. def test_slice_results_multiple_times(self, search):
  335. search.return_value = self.construct_search_response([])
  336. results = self.get_results()[10:][:10]
  337. list(results) # Performs search
  338. search.assert_any_call(
  339. from_=10,
  340. body={'query': 'QUERY'},
  341. _source=False,
  342. stored_fields='pk',
  343. index='wagtail__searchtests_book',
  344. size=10
  345. )
  346. @mock.patch('elasticsearch.Elasticsearch.search')
  347. def test_slice_results_and_get_item(self, search):
  348. # Need to return something to prevent index error
  349. search.return_value = self.construct_search_response([1])
  350. results = self.get_results()[10:]
  351. results[10] # Performs search
  352. search.assert_any_call(
  353. from_=20,
  354. body={'query': 'QUERY'},
  355. _source=False,
  356. stored_fields='pk',
  357. index='wagtail__searchtests_book',
  358. size=1
  359. )
  360. @mock.patch('elasticsearch.Elasticsearch.search')
  361. def test_result_returned(self, search):
  362. search.return_value = self.construct_search_response([1])
  363. results = self.get_results()
  364. self.assertEqual(results[0], models.Book.objects.get(id=1))
  365. @mock.patch('elasticsearch.Elasticsearch.search')
  366. def test_len_1(self, search):
  367. search.return_value = self.construct_search_response([1])
  368. results = self.get_results()
  369. self.assertEqual(len(results), 1)
  370. @mock.patch('elasticsearch.Elasticsearch.search')
  371. def test_len_2(self, search):
  372. search.return_value = self.construct_search_response([1, 2])
  373. results = self.get_results()
  374. self.assertEqual(len(results), 2)
  375. @mock.patch('elasticsearch.Elasticsearch.search')
  376. def test_duplicate_results(self, search): # Duplicates will not be removed
  377. search.return_value = self.construct_search_response([1, 1])
  378. results = list(self.get_results()) # Must cast to list so we only create one query
  379. self.assertEqual(len(results), 2)
  380. self.assertEqual(results[0], models.Book.objects.get(id=1))
  381. self.assertEqual(results[1], models.Book.objects.get(id=1))
  382. @mock.patch('elasticsearch.Elasticsearch.search')
  383. def test_result_order(self, search):
  384. search.return_value = self.construct_search_response(
  385. [1, 2, 3]
  386. )
  387. results = list(self.get_results()) # Must cast to list so we only create one query
  388. self.assertEqual(results[0], models.Book.objects.get(id=1))
  389. self.assertEqual(results[1], models.Book.objects.get(id=2))
  390. self.assertEqual(results[2], models.Book.objects.get(id=3))
  391. @mock.patch('elasticsearch.Elasticsearch.search')
  392. def test_result_order_2(self, search):
  393. search.return_value = self.construct_search_response(
  394. [3, 2, 1]
  395. )
  396. results = list(self.get_results()) # Must cast to list so we only create one query
  397. self.assertEqual(results[0], models.Book.objects.get(id=3))
  398. self.assertEqual(results[1], models.Book.objects.get(id=2))
  399. self.assertEqual(results[2], models.Book.objects.get(id=1))
  400. class TestElasticsearch5Mapping(TestCase):
  401. fixtures = ['search']
  402. def assertDictEqual(self, a, b):
  403. default = JSONSerializer().default
  404. self.assertEqual(
  405. json.dumps(a, sort_keys=True, default=default), json.dumps(b, sort_keys=True, default=default)
  406. )
  407. def setUp(self):
  408. # Create ES mapping
  409. self.es_mapping = Elasticsearch5SearchBackend.mapping_class(models.Book)
  410. # Create ES document
  411. self.obj = models.Book.objects.get(id=4)
  412. def test_get_document_type(self):
  413. self.assertEqual(self.es_mapping.get_document_type(), 'searchtests_book')
  414. def test_get_mapping(self):
  415. # Build mapping
  416. mapping = self.es_mapping.get_mapping()
  417. # Check
  418. expected_result = {
  419. 'searchtests_book': {
  420. 'properties': {
  421. 'pk': {'type': 'keyword', 'store': True, 'include_in_all': False},
  422. 'content_type': {'type': 'keyword', 'include_in_all': False},
  423. '_partials': {'analyzer': 'edgengram_analyzer', 'search_analyzer': 'standard', 'include_in_all': False, 'type': 'text'},
  424. 'title': {'type': 'text', 'boost': 2.0, 'include_in_all': True, 'analyzer': 'edgengram_analyzer', 'search_analyzer': 'standard'},
  425. 'title_filter': {'type': 'keyword', 'include_in_all': False},
  426. 'authors': {
  427. 'type': 'nested',
  428. 'properties': {
  429. 'name': {'type': 'text', 'include_in_all': True},
  430. 'date_of_birth_filter': {'type': 'date', 'include_in_all': False},
  431. },
  432. },
  433. 'publication_date_filter': {'type': 'date', 'include_in_all': False},
  434. 'number_of_pages_filter': {'type': 'integer', 'include_in_all': False},
  435. 'tags': {
  436. 'type': 'nested',
  437. 'properties': {
  438. 'name': {'type': 'text', 'include_in_all': True},
  439. 'slug_filter': {'type': 'keyword', 'include_in_all': False},
  440. },
  441. }
  442. }
  443. }
  444. }
  445. self.assertDictEqual(mapping, expected_result)
  446. def test_get_document_id(self):
  447. self.assertEqual(self.es_mapping.get_document_id(self.obj), 'searchtests_book:' + str(self.obj.pk))
  448. def test_get_document(self):
  449. # Get document
  450. document = self.es_mapping.get_document(self.obj)
  451. # Sort partials
  452. if '_partials' in document:
  453. document['_partials'].sort()
  454. # Check
  455. expected_result = {
  456. 'pk': '4',
  457. 'content_type': ["searchtests.Book"],
  458. '_partials': ['The Fellowship of the Ring'],
  459. 'title': 'The Fellowship of the Ring',
  460. 'title_filter': 'The Fellowship of the Ring',
  461. 'authors': [
  462. {
  463. 'name': 'J. R. R. Tolkien',
  464. 'date_of_birth_filter': datetime.date(1892, 1, 3)
  465. }
  466. ],
  467. 'publication_date_filter': datetime.date(1954, 7, 29),
  468. 'number_of_pages_filter': 423,
  469. 'tags': []
  470. }
  471. self.assertDictEqual(document, expected_result)
  472. class TestElasticsearch5MappingInheritance(TestCase):
  473. fixtures = ['search']
  474. def assertDictEqual(self, a, b):
  475. default = JSONSerializer().default
  476. self.assertEqual(
  477. json.dumps(a, sort_keys=True, default=default), json.dumps(b, sort_keys=True, default=default)
  478. )
  479. def setUp(self):
  480. # Create ES mapping
  481. self.es_mapping = Elasticsearch5SearchBackend.mapping_class(models.Novel)
  482. self.obj = models.Novel.objects.get(id=4)
  483. def test_get_document_type(self):
  484. self.assertEqual(self.es_mapping.get_document_type(), 'searchtests_book_searchtests_novel')
  485. def test_get_mapping(self):
  486. # Build mapping
  487. mapping = self.es_mapping.get_mapping()
  488. # Check
  489. expected_result = {
  490. 'searchtests_book_searchtests_novel': {
  491. 'properties': {
  492. # New
  493. 'searchtests_novel__setting': {'type': 'text', 'include_in_all': True, 'analyzer': 'edgengram_analyzer', 'search_analyzer': 'standard'},
  494. 'searchtests_novel__protagonist': {
  495. 'type': 'nested',
  496. 'properties': {
  497. 'name': {'type': 'text', 'boost': 0.5, 'include_in_all': True}
  498. }
  499. },
  500. 'searchtests_novel__characters': {
  501. 'type': 'nested',
  502. 'properties': {
  503. 'name': {'type': 'text', 'boost': 0.25, 'include_in_all': True}
  504. }
  505. },
  506. # Inherited
  507. 'pk': {'type': 'keyword', 'store': True, 'include_in_all': False},
  508. 'content_type': {'type': 'keyword', 'include_in_all': False},
  509. '_partials': {'analyzer': 'edgengram_analyzer', 'search_analyzer': 'standard', 'include_in_all': False, 'type': 'text'},
  510. 'title': {'type': 'text', 'boost': 2.0, 'include_in_all': True, 'analyzer': 'edgengram_analyzer', 'search_analyzer': 'standard'},
  511. 'title_filter': {'type': 'keyword', 'include_in_all': False},
  512. 'authors': {
  513. 'type': 'nested',
  514. 'properties': {
  515. 'name': {'type': 'text', 'include_in_all': True},
  516. 'date_of_birth_filter': {'type': 'date', 'include_in_all': False},
  517. },
  518. },
  519. 'publication_date_filter': {'type': 'date', 'include_in_all': False},
  520. 'number_of_pages_filter': {'type': 'integer', 'include_in_all': False},
  521. 'tags': {
  522. 'type': 'nested',
  523. 'properties': {
  524. 'name': {'type': 'text', 'include_in_all': True},
  525. 'slug_filter': {'type': 'keyword', 'include_in_all': False},
  526. },
  527. }
  528. }
  529. }
  530. }
  531. self.assertDictEqual(mapping, expected_result)
  532. def test_get_document_id(self):
  533. # This must be tests_searchtest instead of 'tests_searchtest_tests_searchtestchild'
  534. # as it uses the contents base content type name.
  535. # This prevents the same object being accidentally indexed twice.
  536. self.assertEqual(self.es_mapping.get_document_id(self.obj), 'searchtests_book:' + str(self.obj.pk))
  537. def test_get_document(self):
  538. # Build document
  539. document = self.es_mapping.get_document(self.obj)
  540. # Sort partials
  541. if '_partials' in document:
  542. document['_partials'].sort()
  543. # Sort characters
  544. if 'searchtests_novel__characters' in document:
  545. document['searchtests_novel__characters'].sort(key=lambda c: c['name'])
  546. # Check
  547. expected_result = {
  548. # New
  549. 'searchtests_novel__setting': "Middle Earth",
  550. 'searchtests_novel__protagonist': {
  551. 'name': "Frodo Baggins"
  552. },
  553. 'searchtests_novel__characters': [
  554. {
  555. 'name': "Bilbo Baggins"
  556. },
  557. {
  558. 'name': "Frodo Baggins"
  559. },
  560. {
  561. 'name': "Gandalf"
  562. }
  563. ],
  564. # Changed
  565. 'content_type': ["searchtests.Novel", "searchtests.Book"],
  566. '_partials': ['Middle Earth', 'The Fellowship of the Ring'],
  567. # Inherited
  568. 'pk': '4',
  569. 'title': 'The Fellowship of the Ring',
  570. 'title_filter': 'The Fellowship of the Ring',
  571. 'authors': [
  572. {
  573. 'name': 'J. R. R. Tolkien',
  574. 'date_of_birth_filter': datetime.date(1892, 1, 3)
  575. }
  576. ],
  577. 'publication_date_filter': datetime.date(1954, 7, 29),
  578. 'number_of_pages_filter': 423,
  579. 'tags': []
  580. }
  581. self.assertDictEqual(document, expected_result)
  582. class TestBackendConfiguration(TestCase):
  583. def test_default_settings(self):
  584. backend = Elasticsearch5SearchBackend(params={})
  585. self.assertEqual(len(backend.hosts), 1)
  586. self.assertEqual(backend.hosts[0]['host'], 'localhost')
  587. self.assertEqual(backend.hosts[0]['port'], 9200)
  588. self.assertEqual(backend.hosts[0]['use_ssl'], False)
  589. def test_hosts(self):
  590. # This tests that HOSTS goes to es_hosts
  591. backend = Elasticsearch5SearchBackend(params={
  592. 'HOSTS': [
  593. {
  594. 'host': '127.0.0.1',
  595. 'port': 9300,
  596. 'use_ssl': True,
  597. 'verify_certs': True,
  598. }
  599. ]
  600. })
  601. self.assertEqual(len(backend.hosts), 1)
  602. self.assertEqual(backend.hosts[0]['host'], '127.0.0.1')
  603. self.assertEqual(backend.hosts[0]['port'], 9300)
  604. self.assertEqual(backend.hosts[0]['use_ssl'], True)
  605. def test_urls(self):
  606. # This test backwards compatibility with old URLS setting
  607. backend = Elasticsearch5SearchBackend(params={
  608. 'URLS': [
  609. 'http://localhost:12345',
  610. 'https://127.0.0.1:54321',
  611. 'http://username:password@elasticsearch.mysite.com',
  612. 'https://elasticsearch.mysite.com/hello',
  613. ],
  614. })
  615. self.assertEqual(len(backend.hosts), 4)
  616. self.assertEqual(backend.hosts[0]['host'], 'localhost')
  617. self.assertEqual(backend.hosts[0]['port'], 12345)
  618. self.assertEqual(backend.hosts[0]['use_ssl'], False)
  619. self.assertEqual(backend.hosts[1]['host'], '127.0.0.1')
  620. self.assertEqual(backend.hosts[1]['port'], 54321)
  621. self.assertEqual(backend.hosts[1]['use_ssl'], True)
  622. self.assertEqual(backend.hosts[2]['host'], 'elasticsearch.mysite.com')
  623. self.assertEqual(backend.hosts[2]['port'], 80)
  624. self.assertEqual(backend.hosts[2]['use_ssl'], False)
  625. self.assertEqual(backend.hosts[2]['http_auth'], ('username', 'password'))
  626. self.assertEqual(backend.hosts[3]['host'], 'elasticsearch.mysite.com')
  627. self.assertEqual(backend.hosts[3]['port'], 443)
  628. self.assertEqual(backend.hosts[3]['use_ssl'], True)
  629. self.assertEqual(backend.hosts[3]['url_prefix'], '/hello')