searching.rst 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. .. _wagtailsearch_searching:
  2. =========
  3. Searching
  4. =========
  5. .. _wagtailsearch_searching_pages:
  6. Searching QuerySets
  7. ===================
  8. Wagtail search is built on Django's `QuerySet API <https://docs.djangoproject.com/en/stable/ref/models/querysets/>`_. You should be able to search any Django QuerySet provided the model and the fields being filtered on have been added to the search index.
  9. Searching Pages
  10. ---------------
  11. Wagtail provides a shortcut for searching pages: the ``.search()`` ``QuerySet`` method. You can call this on any ``PageQuerySet``. For example:
  12. .. code-block:: python
  13. # Search future EventPages
  14. >>> from wagtail.core.models import EventPage
  15. >>> EventPage.objects.filter(date__gt=timezone.now()).search("Hello world!")
  16. All other methods of ``PageQuerySet`` can be used with ``search()``. For example:
  17. .. code-block:: python
  18. # Search all live EventPages that are under the events index
  19. >>> EventPage.objects.live().descendant_of(events_index).search("Event")
  20. [<EventPage: Event 1>, <EventPage: Event 2>]
  21. .. note::
  22. The ``search()`` method will convert your ``QuerySet`` into an instance of one of Wagtail's ``SearchResults`` classes (depending on backend). This means that you must perform filtering before calling ``search()``.
  23. .. note::
  24. Before the ``autocomplete()`` method was introduced, the search method also did partial matching. This behaviour is will be deprecated and you should
  25. either switch to the new ``autocomplete()`` method or pass ``partial_match=False`` into the search method to opt-in to the new behaviour. The
  26. partial matching in ``search()`` will be completely removed in a future release.
  27. Autocomplete searches
  28. ---------------------
  29. Wagtail provides a separate method which performs partial matching on specific autocomplete fields. This is useful for suggesting pages to the user in real-time as they type their query.
  30. .. code-block:: python
  31. >>> EventPage.objects.live().autocomplete("Eve")
  32. [<EventPage: Event 1>, <EventPage: Event 2>]
  33. .. tip::
  34. This method should only be used for real-time autocomplete and actual search requests should always use the ``search()`` method.
  35. .. _wagtailsearch_images_documents_custom_models:
  36. Searching Images, Documents and custom models
  37. ---------------------------------------------
  38. Wagtail's document and image models provide a ``search`` method on their QuerySets, just as pages do:
  39. .. code-block:: python
  40. >>> from wagtail.images.models import Image
  41. >>> Image.objects.filter(uploaded_by_user=user).search("Hello")
  42. [<Image: Hello>, <Image: Hello world!>]
  43. :ref:`Custom models <wagtailsearch_indexing_models>` can be searched by using the ``search`` method on the search backend directly:
  44. .. code-block:: python
  45. >>> from myapp.models import Book
  46. >>> from wagtail.search.backends import get_search_backend
  47. # Search books
  48. >>> s = get_search_backend()
  49. >>> s.search("Great", Book)
  50. [<Book: Great Expectations>, <Book: The Great Gatsby>]
  51. You can also pass a QuerySet into the ``search`` method which allows you to add filters to your search results:
  52. .. code-block:: python
  53. >>> from myapp.models import Book
  54. >>> from wagtail.search.backends import get_search_backend
  55. # Search books
  56. >>> s = get_search_backend()
  57. >>> s.search("Great", Book.objects.filter(published_date__year__lt=1900))
  58. [<Book: Great Expectations>]
  59. .. _wagtailsearch_specifying_fields:
  60. Specifying the fields to search
  61. -------------------------------
  62. By default, Wagtail will search all fields that have been indexed using ``index.SearchField``.
  63. This can be limited to a certain set of fields by using the ``fields`` keyword argument:
  64. .. code-block:: python
  65. # Search just the title field
  66. >>> EventPage.objects.search("Event", fields=["title"])
  67. [<EventPage: Event 1>, <EventPage: Event 2>]
  68. .. _wagtailsearch_faceted_search:
  69. Faceted search
  70. --------------
  71. Wagtail supports faceted search which is a kind of filtering based on a taxonomy
  72. field (such as category or page type).
  73. The ``.facet(field_name)`` method returns an ``OrderedDict``. The keys are
  74. the IDs of the related objects that have been referenced by the specified field, and the
  75. values are the number of references found for each ID. The results are ordered by number
  76. of references descending.
  77. For example, to find the most common page types in the search results:
  78. .. code-block:: python
  79. >>> Page.objects.search("Test").facet("content_type_id")
  80. # Note: The keys correspond to the ID of a ContentType object; the values are the
  81. # number of pages returned for that type
  82. OrderedDict([
  83. ('2', 4), # 4 pages have content_type_id == 2
  84. ('1', 2), # 2 pages have content_type_id == 1
  85. ])
  86. Changing search behaviour
  87. -------------------------
  88. Search operator
  89. ^^^^^^^^^^^^^^^
  90. The search operator specifies how search should behave when the user has typed in multiple search terms. There are two possible values:
  91. - "or" - The results must match at least one term (default for Elasticsearch)
  92. - "and" - The results must match all terms (default for database search)
  93. Both operators have benefits and drawbacks. The "or" operator will return many more results but will likely contain a lot of results that aren't relevant. The "and" operator only returns results that contain all search terms, but require the user to be more precise with their query.
  94. We recommend using the "or" operator when ordering by relevance and the "and" operator when ordering by anything else (note: the database backend doesn't currently support ordering by relevance).
  95. Here's an example of using the ``operator`` keyword argument:
  96. .. code-block:: python
  97. # The database contains a "Thing" model with the following items:
  98. # - Hello world
  99. # - Hello
  100. # - World
  101. # Search with the "or" operator
  102. >>> s = get_search_backend()
  103. >>> s.search("Hello world", Things, operator="or")
  104. # All records returned as they all contain either "hello" or "world"
  105. [<Thing: Hello World>, <Thing: Hello>, <Thing: World>]
  106. # Search with the "and" operator
  107. >>> s = get_search_backend()
  108. >>> s.search("Hello world", Things, operator="and")
  109. # Only "hello world" returned as that's the only item that contains both terms
  110. [<Thing: Hello world>]
  111. For page, image and document models, the ``operator`` keyword argument is also supported on the QuerySet's ``search`` method:
  112. .. code-block:: python
  113. >>> Page.objects.search("Hello world", operator="or")
  114. # All pages containing either "hello" or "world" are returned
  115. [<Page: Hello World>, <Page: Hello>, <Page: World>]
  116. Phrase searching
  117. ^^^^^^^^^^^^^^^^
  118. Phrase searching is used for finding whole sentence or phrase rather than individual terms.
  119. The terms must appear together and in the same order.
  120. For example:
  121. .. code-block:: python
  122. >>> from wagtail.search.query import Phrase
  123. >>> Page.objects.search(Phrase("Hello world"))
  124. [<Page: Hello World>]
  125. >>> Page.objects.search(Phrase("World hello"))
  126. [<Page: World Hello day>]
  127. If you are looking to implement phrase queries using the double-quote syntax, see :ref:`wagtailsearch_query_string_parsing`.
  128. .. _wagtailsearch_complex_queries:
  129. Complex search queries
  130. ^^^^^^^^^^^^^^^^^^^^^^
  131. Through the use of search query classes, Wagtail also supports building search queries as Python
  132. objects which can be wrapped by and combined with other search queries. The following classes are
  133. available:
  134. ``PlainText(query_string, operator=None, boost=1.0)``
  135. This class wraps a string of separate terms. This is the same as searching without query classes.
  136. It takes a query string, operator and boost.
  137. For example:
  138. .. code-block:: python
  139. >>> from wagtail.search.query import PlainText
  140. >>> Page.objects.search(PlainText("Hello world"))
  141. # Multiple plain text queries can be combined. This example will match both "hello world" and "Hello earth"
  142. >>> Page.objects.search(PlainText("Hello") & (PlainText("world") | PlainText("earth")))
  143. ``Phrase(query_string)``
  144. This class wraps a string containing a phrase. See previous section for how this works.
  145. For example:
  146. .. code-block:: python
  147. # This example will match both the phrases "hello world" and "Hello earth"
  148. >>> Page.objects.search(Phrase("Hello world") | Phrase("Hello earth"))
  149. ``Boost(query, boost)``
  150. This class boosts the score of another query.
  151. For example:
  152. .. code-block:: python
  153. >>> from wagtail.search.query import PlainText, Boost
  154. # This example will match both the phrases "hello world" and "Hello earth" but matches for "hello world" will be ranked higher
  155. >>> Page.objects.search(Boost(Phrase("Hello world"), 10.0) | Phrase("Hello earth"))
  156. Note that this isn't supported by the PostgreSQL or database search backends.
  157. .. _wagtailsearch_query_string_parsing:
  158. Query string parsing
  159. ^^^^^^^^^^^^^^^^^^^^
  160. The previous sections show how to construct a phrase search query manually, but a lot of search engines (Wagtail admin included, try it!)
  161. support writing phrase queries by wrapping the phrase with double-quotes. In addition to phrases, you might also want to allow users to
  162. add filters into the query using the colon syntax (``hello world published:yes``).
  163. These two features can be implemented using the ``parse_query_string`` utility function. This function takes a query string that a user
  164. typed and returns a query object and dictionary of filters:
  165. For example:
  166. .. code-block:: python
  167. >>> from wagtail.search.utils import parse_query_string
  168. >>> filters, query = parse_query_string('my query string "this is a phrase" this-is-a:filter', operator='and')
  169. >>> filters
  170. {
  171. 'this-is-a': 'filter',
  172. }
  173. >>> query
  174. And([
  175. PlainText("my query string", operator='and'),
  176. Phrase("this is a phrase"),
  177. ])
  178. Here's an example of how this function can be used in a search view:
  179. .. code-block:: python
  180. from wagtail.search.utils import parse_query_string
  181. def search(request):
  182. query_string = request.GET['query']
  183. # Parse query
  184. filters, query = parse_query_string(query_string, operator='and')
  185. # Published filter
  186. # An example filter that accepts either `published:yes` or `published:no` and filters the pages accordingly
  187. published_filter = filters.get('published')
  188. published_filter = published_filter and published_filter.lower()
  189. if published_filter in ['yes', 'true']:
  190. pages = pages.filter(live=True)
  191. elif published_filter in ['no', 'false']:
  192. pages = pages.filter(live=False)
  193. # Search
  194. pages = pages.search(query)
  195. return render(request, 'search_results.html', {'pages': pages})
  196. Custom ordering
  197. ^^^^^^^^^^^^^^^
  198. By default, search results are ordered by relevance, if the backend supports it. To preserve the QuerySet's existing ordering, the ``order_by_relevance`` keyword argument needs to be set to ``False`` on the ``search()`` method.
  199. For example:
  200. .. code-block:: python
  201. # Get a list of events ordered by date
  202. >>> EventPage.objects.order_by('date').search("Event", order_by_relevance=False)
  203. # Events ordered by date
  204. [<EventPage: Easter>, <EventPage: Halloween>, <EventPage: Christmas>]
  205. .. _wagtailsearch_annotating_results_with_score:
  206. Annotating results with score
  207. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  208. For each matched result, Elasticsearch calculates a "score", which is a number
  209. that represents how relevant the result is based on the user's query. The
  210. results are usually ordered based on the score.
  211. There are some cases where having access to the score is useful (such as
  212. programmatically combining two queries for different models). You can add the
  213. score to each result by calling the ``.annotate_score(field)`` method on the
  214. ``SearchQuerySet``.
  215. For example:
  216. .. code-block:: python
  217. >>> events = EventPage.objects.search("Event").annotate_score("_score")
  218. >>> for event in events:
  219. ... print(event.title, event._score)
  220. ...
  221. ("Easter", 2.5),
  222. ("Halloween", 1.7),
  223. ("Christmas", 1.5),
  224. Note that the score itself is arbitrary and it is only useful for comparison
  225. of results for the same query.
  226. .. _wagtailsearch_frontend_views:
  227. An example page search view
  228. ===========================
  229. Here's an example Django view that could be used to add a "search" page to your site:
  230. .. code-block:: python
  231. # views.py
  232. from django.shortcuts import render
  233. from wagtail.core.models import Page
  234. from wagtail.search.models import Query
  235. def search(request):
  236. # Search
  237. search_query = request.GET.get('query', None)
  238. if search_query:
  239. search_results = Page.objects.live().search(search_query)
  240. # Log the query so Wagtail can suggest promoted results
  241. Query.get(search_query).add_hit()
  242. else:
  243. search_results = Page.objects.none()
  244. # Render template
  245. return render(request, 'search_results.html', {
  246. 'search_query': search_query,
  247. 'search_results': search_results,
  248. })
  249. And here's a template to go with it:
  250. .. code-block:: html+django
  251. {% extends "base.html" %}
  252. {% load wagtailcore_tags %}
  253. {% block title %}Search{% endblock %}
  254. {% block content %}
  255. <form action="{% url 'search' %}" method="get">
  256. <input type="text" name="query" value="{{ search_query }}">
  257. <input type="submit" value="Search">
  258. </form>
  259. {% if search_results %}
  260. <ul>
  261. {% for result in search_results %}
  262. <li>
  263. <h4><a href="{% pageurl result %}">{{ result }}</a></h4>
  264. {% if result.search_description %}
  265. {{ result.search_description|safe }}
  266. {% endif %}
  267. </li>
  268. {% endfor %}
  269. </ul>
  270. {% elif search_query %}
  271. No results found
  272. {% else %}
  273. Please type something into the search box
  274. {% endif %}
  275. {% endblock %}
  276. Promoted search results
  277. =======================
  278. "Promoted search results" allow editors to explicitly link relevant content to search terms, so results pages can contain curated content in addition to results from the search engine.
  279. This functionality is provided by the :mod:`~wagtail.contrib.search_promotions` contrib module.