├── .editorconfig ├── .gitchangelog.rc ├── .github ├── issue_template.md ├── pull_request_template.md └── workflows │ ├── black+isort.yml │ ├── codeql-analysis.yml │ ├── docs.yml │ ├── flake8.yml │ └── test.yml ├── .gitignore ├── AUTHORS ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── _build │ └── .gitignore ├── _static │ └── .gitignore ├── _templates │ └── .gitignore ├── admin.rst ├── architecture_overview.rst ├── autocomplete.rst ├── backend_support.rst ├── best_practices.rst ├── boost.rst ├── changelog.rst ├── conf.py ├── contributing.rst ├── creating_new_backends.rst ├── debugging.rst ├── faceting.rst ├── faq.rst ├── glossary.rst ├── haystack_theme │ ├── layout.html │ ├── static │ │ └── documentation.css │ └── theme.conf ├── highlighting.rst ├── index.rst ├── inputtypes.rst ├── installing_search_engines.rst ├── management_commands.rst ├── migration_from_1_to_2.rst ├── multiple_index.rst ├── other_apps.rst ├── python3.rst ├── rich_content_extraction.rst ├── running_tests.rst ├── searchbackend_api.rst ├── searchfield_api.rst ├── searchindex_api.rst ├── searchquery_api.rst ├── searchqueryset_api.rst ├── searchresult_api.rst ├── settings.rst ├── signal_processors.rst ├── spatial.rst ├── templatetags.rst ├── toc.rst ├── tutorial.rst ├── utils.rst ├── views_and_forms.rst └── who_uses.rst ├── example_project ├── __init__.py ├── bare_bones_app │ ├── __init__.py │ ├── models.py │ └── search_indexes.py ├── regular_app │ ├── __init__.py │ ├── models.py │ └── search_indexes.py ├── settings.py └── templates │ └── search │ └── indexes │ ├── bare_bones_app │ └── cat_text.txt │ └── regular_app │ └── dog_text.txt ├── haystack ├── __init__.py ├── admin.py ├── apps.py ├── backends │ ├── __init__.py │ ├── elasticsearch2_backend.py │ ├── elasticsearch5_backend.py │ ├── elasticsearch_backend.py │ ├── simple_backend.py │ ├── solr_backend.py │ └── whoosh_backend.py ├── constants.py ├── exceptions.py ├── fields.py ├── forms.py ├── generic_views.py ├── indexes.py ├── inputs.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── build_solr_schema.py │ │ ├── clear_index.py │ │ ├── haystack_info.py │ │ ├── rebuild_index.py │ │ └── update_index.py ├── manager.py ├── models.py ├── panels.py ├── query.py ├── routers.py ├── signals.py ├── templates │ ├── panels │ │ └── haystack.html │ └── search_configuration │ │ ├── schema.xml │ │ └── solrconfig.xml ├── templatetags │ ├── __init__.py │ ├── highlight.py │ └── more_like_this.py ├── urls.py ├── utils │ ├── __init__.py │ ├── app_loading.py │ ├── geo.py │ ├── highlighting.py │ ├── loading.py │ └── log.py └── views.py ├── pyproject.toml ├── setup.cfg ├── setup.py ├── test_haystack ├── __init__.py ├── core │ ├── __init__.py │ ├── admin.py │ ├── custom_identifier.py │ ├── fixtures │ │ ├── base_data.json │ │ └── bulk_data.json │ ├── models.py │ ├── templates │ │ ├── 404.html │ │ ├── base.html │ │ ├── search │ │ │ ├── indexes │ │ │ │ ├── bar.txt │ │ │ │ ├── core │ │ │ │ │ ├── mockmodel_content.txt │ │ │ │ │ ├── mockmodel_extra.txt │ │ │ │ │ ├── mockmodel_template.txt │ │ │ │ │ └── mockmodel_text.txt │ │ │ │ └── foo.txt │ │ │ └── search.html │ │ └── test_suggestion.html │ └── urls.py ├── discovery │ ├── __init__.py │ ├── models.py │ ├── search_indexes.py │ └── templates │ │ └── search │ │ └── indexes │ │ └── bar_text.txt ├── elasticsearch2_tests │ ├── __init__.py │ ├── test_backend.py │ ├── test_inputs.py │ └── test_query.py ├── elasticsearch5_tests │ ├── __init__.py │ ├── test_backend.py │ ├── test_inputs.py │ └── test_query.py ├── elasticsearch_tests │ ├── __init__.py │ ├── test_elasticsearch_backend.py │ ├── test_elasticsearch_query.py │ └── test_inputs.py ├── mocks.py ├── multipleindex │ ├── __init__.py │ ├── models.py │ ├── routers.py │ ├── search_indexes.py │ └── tests.py ├── results_per_page_urls.py ├── run_tests.py ├── settings.py ├── simple_tests │ ├── __init__.py │ ├── search_indexes.py │ ├── test_simple_backend.py │ └── test_simple_query.py ├── solr_tests │ ├── __init__.py │ ├── content_extraction │ │ └── test.pdf │ ├── server │ │ ├── .gitignore │ │ ├── confdir │ │ │ ├── schema.xml │ │ │ └── solrconfig.xml │ │ ├── get-solr-download-url.py │ │ ├── start-solr-test-server.sh │ │ └── wait-for-solr │ ├── test_admin.py │ ├── test_inputs.py │ ├── test_solr_backend.py │ ├── test_solr_management_commands.py │ ├── test_solr_query.py │ └── test_templatetags.py ├── spatial │ ├── __init__.py │ ├── fixtures │ │ └── sample_spatial_data.json │ ├── models.py │ ├── search_indexes.py │ └── test_spatial.py ├── test_altered_internal_names.py ├── test_app_loading.py ├── test_app_using_appconfig │ ├── __init__.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── search_indexes.py │ └── tests.py ├── test_app_with_hierarchy │ ├── __init__.py │ └── contrib │ │ ├── __init__.py │ │ └── django │ │ ├── __init__.py │ │ └── hierarchal_app_django │ │ ├── __init__.py │ │ └── models.py ├── test_app_without_models │ ├── __init__.py │ ├── urls.py │ └── views.py ├── test_backends.py ├── test_discovery.py ├── test_fields.py ├── test_forms.py ├── test_generic_views.py ├── test_indexes.py ├── test_inputs.py ├── test_loading.py ├── test_management_commands.py ├── test_managers.py ├── test_models.py ├── test_query.py ├── test_templatetags.py ├── test_utils.py ├── test_views.py ├── utils.py └── whoosh_tests │ ├── __init__.py │ ├── test_forms.py │ ├── test_inputs.py │ ├── test_whoosh_backend.py │ ├── test_whoosh_query.py │ └── testcases.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # See http://editorconfig.org for format details and 2 | # http://editorconfig.org/#download for editor / IDE integration 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | end_of_line = lf 12 | charset = utf-8 13 | 14 | # Makefiles always use tabs for indentation 15 | [Makefile] 16 | indent_style = tab 17 | 18 | # We don't want to apply our defaults to third-party code or minified bundles: 19 | [**/{external,vendor}/**,**.min.{js,css}] 20 | indent_style = ignore 21 | indent_size = ignore 22 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | * [ ] Tested with the latest Haystack release 2 | * [ ] Tested with the current Haystack master branch 3 | 4 | ## Expected behaviour 5 | 6 | ## Actual behaviour 7 | 8 | ## Steps to reproduce the behaviour 9 | 10 | 1. 11 | 12 | ## Configuration 13 | 14 | * Operating system version: 15 | * Search engine version: 16 | * Python version: 17 | * Django version: 18 | * Haystack version: -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Hey, thanks for contributing to Haystack. Please review [the contributor guidelines](https://django-haystack.readthedocs.io/en/latest/contributing.html) and confirm that [the tests pass](https://django-haystack.readthedocs.io/en/latest/running_tests.html) with at least one search engine. 2 | 3 | # Once your pull request has been submitted, the full test suite will be executed on https://github.com/django-haystack/django-haystack/actions/workflows/test.yml. Pull requests with passing tests are far more likely to be reviewed and merged. -------------------------------------------------------------------------------- /.github/workflows/black+isort.yml: -------------------------------------------------------------------------------- 1 | name: black+isort 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | check: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Set up Python 11 | uses: actions/setup-python@v2 12 | with: 13 | python-version: 3.9 14 | - name: Install tools 15 | run: pip install black isort 16 | - name: Run black+isort 17 | run: | 18 | black --check --diff . 19 | isort --check . 20 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master, ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 6 * * 5' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | 21 | # Initializes the CodeQL tools for scanning. 22 | - name: Initialize CodeQL 23 | uses: github/codeql-action/init@v1 24 | with: 25 | languages: python 26 | 27 | - name: Perform CodeQL Analysis 28 | uses: github/codeql-action/analyze@v1 29 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build docs 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Set up Python 11 | uses: actions/setup-python@v2 12 | with: 13 | python-version: 3.9 14 | - name: Install dependencies 15 | run: pip install sphinx 16 | - name: Build docs 17 | run: cd docs && make html 18 | -------------------------------------------------------------------------------- /.github/workflows/flake8.yml: -------------------------------------------------------------------------------- 1 | name: flake8 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | check: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Set up Python 11 | uses: actions/setup-python@v2 12 | with: 13 | python-version: 3.9 14 | - name: Install tools 15 | run: pip install flake8 flake8-assertive flake8-bugbear flake8-builtins flake8-comprehensions flake8-logging-format 16 | - name: Run flake8 17 | run: flake8 example_project haystack 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | test: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | django-version: [2.2, 3.1, 3.2] 12 | python-version: [3.6, 3.7, 3.8, 3.9] 13 | elastic-version: [1.7, 2.4, 5.5] 14 | include: 15 | - django-version: 2.2 16 | python-version: 3.5 17 | elastic-version: 1.7 18 | - django-version: 2.2 19 | python-version: 3.5 20 | elastic-version: 2.4 21 | - django-version: 2.2 22 | python-version: 3.5 23 | elastic-version: 5.5 24 | services: 25 | elastic: 26 | image: elasticsearch:${{ matrix.elastic-version }} 27 | ports: 28 | - 9200:9200 29 | solr: 30 | image: solr:6 31 | ports: 32 | - 9001:9001 33 | steps: 34 | - uses: actions/checkout@v2 35 | - name: Set up Python ${{ matrix.python-version }} 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: ${{ matrix.python-version }} 39 | - name: Install system dependencies 40 | run: sudo apt install --no-install-recommends -y gdal-bin 41 | - name: Install dependencies 42 | run: | 43 | python -m pip install --upgrade pip setuptools wheel 44 | pip install coverage requests 45 | pip install django==${{ matrix.django-version }} elasticsearch==${{ matrix.elastic-version }} 46 | python setup.py clean build install 47 | - name: Run test 48 | run: coverage run setup.py test 49 | 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .settings 2 | *.pyc 3 | .DS_Store 4 | _build 5 | .*.sw[po] 6 | *.egg-info 7 | dist 8 | build 9 | MANIFEST 10 | .tox 11 | env 12 | env3 13 | *.egg 14 | .eggs 15 | .coverage 16 | .idea 17 | 18 | # Build artifacts from test setup 19 | *.tgz 20 | test_haystack/solr_tests/server/solr4/ 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Haystack is open-source and, as such, grows (or shrinks) & improves in part 4 | due to the community. Below are some guidelines on how to help with the project. 5 | 6 | ## Philosophy 7 | 8 | - Haystack is BSD-licensed. All contributed code must be either 9 | - the original work of the author, contributed under the BSD, or... 10 | - work taken from another project released under a BSD-compatible license. 11 | - GPL'd (or similar) works are not eligible for inclusion. 12 | - Haystack's git master branch should always be stable, production-ready & 13 | passing all tests. 14 | - Major releases (1.x.x) are commitments to backward-compatibility of the public APIs. 15 | Any documented API should ideally not change between major releases. 16 | The exclusion to this rule is in the event of either a security issue 17 | or to accommodate changes in Django itself. 18 | - Minor releases (x.3.x) are for the addition of substantial features or major 19 | bugfixes. 20 | - Patch releases (x.x.4) are for minor features or bugfixes. 21 | 22 | ## Guidelines For Reporting An Issue/Feature 23 | 24 | So you've found a bug or have a great idea for a feature. Here's the steps you 25 | should take to help get it added/fixed in Haystack: 26 | 27 | - First, check to see if there's an existing issue/pull request for the 28 | bug/feature. All issues are at https://github.com/toastdriven/django-haystack/issues 29 | and pull reqs are at https://github.com/toastdriven/django-haystack/pulls. 30 | - If there isn't one there, please file an issue. The ideal report includes: 31 | - A description of the problem/suggestion. 32 | - How to recreate the bug. 33 | - If relevant, including the versions of your: 34 | - Python interpreter 35 | - Django 36 | - Haystack 37 | - Search engine used (as well as bindings) 38 | - Optionally of the other dependencies involved 39 | - Ideally, creating a pull request with a (failing) test case demonstrating 40 | what's wrong. This makes it easy for us to reproduce & fix the problem. 41 | 42 | Github has a great guide for writing an effective pull request: 43 | https://github.com/blog/1943-how-to-write-the-perfect-pull-request 44 | 45 | Instructions for running the tests are at 46 | https://django-haystack.readthedocs.io/en/latest/running_tests.html 47 | 48 | You might also hop into the IRC channel (`#haystack` on `irc.freenode.net`) 49 | & raise your question there, as there may be someone who can help you with a 50 | work-around. 51 | 52 | ## Guidelines For Contributing Code 53 | 54 | If you're ready to take the plunge & contribute back some code/docs, the 55 | process should look like: 56 | 57 | - Fork the project on GitHub into your own account. 58 | - Clone your copy of Haystack. 59 | - Make a new branch in git & commit your changes there. 60 | - Push your new branch up to GitHub. 61 | - Again, ensure there isn't already an issue or pull request out there on it. 62 | If there is & you feel you have a better fix, please take note of the issue 63 | number & mention it in your pull request. 64 | - Create a new pull request (based on your branch), including what the 65 | problem/feature is, versions of your software & referencing any related 66 | issues/pull requests. 67 | 68 | In order to be merged into Haystack, contributions must have the following: 69 | 70 | - A solid patch that: 71 | - is clear. 72 | - works across all supported versions of Python/Django. 73 | - follows the existing style of the code base formatted with 74 | [`isort`](https://pypi.org/project/isort/) and 75 | [`Black`](https://pypi.org/project/black/) using the provided 76 | configuration in the repo 77 | - comments included as needed to explain why the code functions as it does 78 | - A test case that demonstrates the previous flaw that now passes 79 | with the included patch. 80 | - If it adds/changes a public API, it must also include documentation 81 | for those changes. 82 | - Must be appropriately licensed (see [Philosophy](#philosophy)). 83 | - Adds yourself to the AUTHORS file. 84 | 85 | If your contribution lacks any of these things, they will have to be added 86 | by a core contributor before being merged into Haystack proper, which may take 87 | substantial time for the all-volunteer team to get to. 88 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2013, Daniel Lindsley. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Haystack nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | --- 30 | 31 | Prior to April 17, 2009, this software was released under the MIT license. 32 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include docs * 2 | recursive-include haystack/templates *.xml *.html 3 | include AUTHORS 4 | include LICENSE 5 | include README.rst 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://github.com/django-haystack/django-haystack/actions/workflows/test.yml/badge.svg 2 | :target: https://github.com/django-haystack/django-haystack/actions/workflows/test.yml 3 | .. image:: https://img.shields.io/pypi/v/django-haystack.svg 4 | :target: https://pypi.python.org/pypi/django-haystack/ 5 | .. image:: https://img.shields.io/pypi/pyversions/django-haystack.svg 6 | :target: https://pypi.python.org/pypi/django-haystack/ 7 | .. image:: https://img.shields.io/pypi/dm/django-haystack.svg 8 | :target: https://pypi.python.org/pypi/django-haystack/ 9 | .. image:: https://readthedocs.org/projects/django-haystack/badge/ 10 | :target: https://django-haystack.readthedocs.io/ 11 | .. image:: https://img.shields.io/badge/code%20style-black-000.svg 12 | :target: https://github.com/psf/black 13 | .. image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336 14 | :target: https://pycqa.github.io/isort/ 15 | 16 | ======== 17 | Haystack 18 | ======== 19 | 20 | :author: Daniel Lindsley 21 | :date: 2013/07/28 22 | 23 | Haystack provides modular search for Django. It features a unified, familiar 24 | API that allows you to plug in different search backends (such as Solr_, 25 | Elasticsearch_, Whoosh_, Xapian_, etc.) without having to modify your code. 26 | 27 | .. _Solr: http://lucene.apache.org/solr/ 28 | .. _Elasticsearch: https://www.elastic.co/products/elasticsearch 29 | .. _Whoosh: https://github.com/mchaput/whoosh/ 30 | .. _Xapian: http://xapian.org/ 31 | 32 | Haystack is BSD licensed, plays nicely with third-party app without needing to 33 | modify the source and supports advanced features like faceting, More Like This, 34 | highlighting, spatial search and spelling suggestions. 35 | 36 | You can find more information at http://haystacksearch.org/. 37 | 38 | 39 | Getting Help 40 | ============ 41 | 42 | There is a mailing list (http://groups.google.com/group/django-haystack/) 43 | available for general discussion and an IRC channel (#haystack on 44 | irc.freenode.net). 45 | 46 | 47 | Documentation 48 | ============= 49 | 50 | * Development version: http://docs.haystacksearch.org/ 51 | * v2.8.X: https://django-haystack.readthedocs.io/en/v2.8.1/ 52 | * v2.7.X: https://django-haystack.readthedocs.io/en/v2.7.0/ 53 | * v2.6.X: https://django-haystack.readthedocs.io/en/v2.6.0/ 54 | 55 | See the `changelog `_ 56 | 57 | Requirements 58 | ============ 59 | 60 | Haystack has a relatively easily-met set of requirements. 61 | 62 | * Python 3.5+ 63 | * A supported version of Django: https://www.djangoproject.com/download/#supported-versions 64 | 65 | Additionally, each backend has its own requirements. You should refer to 66 | https://django-haystack.readthedocs.io/en/latest/installing_search_engines.html for more 67 | details. 68 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | 9 | # Internal variables. 10 | PAPEROPT_a4 = -D latex_paper_size=a4 11 | PAPEROPT_letter = -D latex_paper_size=letter 12 | ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 13 | 14 | .PHONY: help clean html web pickle htmlhelp latex changes linkcheck 15 | 16 | help: 17 | @echo "Please use \`make ' where is one of" 18 | @echo " html to make standalone HTML files" 19 | @echo " pickle to make pickle files" 20 | @echo " json to make JSON files" 21 | @echo " htmlhelp to make HTML files and a HTML help project" 22 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 23 | @echo " changes to make an overview over all changed/added/deprecated items" 24 | @echo " linkcheck to check all external links for integrity" 25 | 26 | clean: 27 | -rm -rf _build/* 28 | 29 | html: 30 | mkdir -p _build/html _build/doctrees 31 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html 32 | @echo 33 | @echo "Build finished. The HTML pages are in _build/html." 34 | 35 | pickle: 36 | mkdir -p _build/pickle _build/doctrees 37 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle 38 | @echo 39 | @echo "Build finished; now you can process the pickle files." 40 | 41 | web: pickle 42 | 43 | json: 44 | mkdir -p _build/json _build/doctrees 45 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) _build/json 46 | @echo 47 | @echo "Build finished; now you can process the JSON files." 48 | 49 | htmlhelp: 50 | mkdir -p _build/htmlhelp _build/doctrees 51 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp 52 | @echo 53 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 54 | ".hhp project file in _build/htmlhelp." 55 | 56 | latex: 57 | mkdir -p _build/latex _build/doctrees 58 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex 59 | @echo 60 | @echo "Build finished; the LaTeX files are in _build/latex." 61 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 62 | "run these through (pdf)latex." 63 | 64 | changes: 65 | mkdir -p _build/changes _build/doctrees 66 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes 67 | @echo 68 | @echo "The overview file is in _build/changes." 69 | 70 | linkcheck: 71 | mkdir -p _build/linkcheck _build/doctrees 72 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck 73 | @echo 74 | @echo "Link check complete; look for any errors in the above output " \ 75 | "or in _build/linkcheck/output.txt." 76 | 77 | epub: 78 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) _build/epub 79 | @echo 80 | @echo "Build finished. The epub file is in _build/epub." 81 | -------------------------------------------------------------------------------- /docs/_build/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/docs/_build/.gitignore -------------------------------------------------------------------------------- /docs/_static/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/docs/_static/.gitignore -------------------------------------------------------------------------------- /docs/_templates/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/docs/_templates/.gitignore -------------------------------------------------------------------------------- /docs/admin.rst: -------------------------------------------------------------------------------- 1 | .. _ref-admin: 2 | 3 | =================== 4 | Django Admin Search 5 | =================== 6 | 7 | Haystack comes with a base class to support searching via Haystack in the 8 | Django admin. To use Haystack to search, inherit from ``haystack.admin.SearchModelAdmin`` 9 | instead of ``django.contrib.admin.ModelAdmin``. 10 | 11 | For example:: 12 | 13 | from haystack.admin import SearchModelAdmin 14 | from .models import MockModel 15 | 16 | 17 | class MockModelAdmin(SearchModelAdmin): 18 | haystack_connection = 'solr' 19 | date_hierarchy = 'pub_date' 20 | list_display = ('author', 'pub_date') 21 | 22 | 23 | admin.site.register(MockModel, MockModelAdmin) 24 | 25 | You can also specify the Haystack connection used by the search with the 26 | ``haystack_connection`` property on the model admin class. If not specified, 27 | the default connection will be used. 28 | 29 | If you already have a base model admin class you use, there is also a mixin 30 | you can use instead:: 31 | 32 | from django.contrib import admin 33 | from haystack.admin import SearchModelAdminMixin 34 | from .models import MockModel 35 | 36 | 37 | class MyCustomModelAdmin(admin.ModelAdmin): 38 | pass 39 | 40 | 41 | class MockModelAdmin(SearchModelAdminMixin, MyCustomModelAdmin): 42 | haystack_connection = 'solr' 43 | date_hierarchy = 'pub_date' 44 | list_display = ('author', 'pub_date') 45 | 46 | 47 | admin.site.register(MockModel, MockModelAdmin) 48 | -------------------------------------------------------------------------------- /docs/architecture_overview.rst: -------------------------------------------------------------------------------- 1 | .. _ref-architecture-overview: 2 | 3 | ===================== 4 | Architecture Overview 5 | ===================== 6 | 7 | ``SearchQuerySet`` 8 | ------------------ 9 | 10 | One main implementation. 11 | 12 | * Standard API that loosely follows ``QuerySet`` 13 | * Handles most queries 14 | * Allows for custom "parsing"/building through API 15 | * Dispatches to ``SearchQuery`` for actual query 16 | * Handles automatically creating a query 17 | * Allows for raw queries to be passed straight to backend. 18 | 19 | 20 | ``SearchQuery`` 21 | --------------- 22 | 23 | Implemented per-backend. 24 | 25 | * Method for building the query out of the structured data. 26 | * Method for cleaning a string of reserved characters used by the backend. 27 | 28 | Main class provides: 29 | 30 | * Methods to add filters/models/order-by/boost/limits to the search. 31 | * Method to perform a raw search. 32 | * Method to get the number of hits. 33 | * Method to return the results provided by the backend (likely not a full list). 34 | 35 | 36 | ``SearchBackend`` 37 | ----------------- 38 | 39 | Implemented per-backend. 40 | 41 | * Connects to search engine 42 | * Method for saving new docs to index 43 | * Method for removing docs from index 44 | * Method for performing the actual query 45 | 46 | 47 | ``SearchSite`` 48 | -------------- 49 | 50 | One main implementation. 51 | 52 | * Standard API that loosely follows ``django.contrib.admin.sites.AdminSite`` 53 | * Handles registering/unregistering models to search on a per-site basis. 54 | * Provides a means of adding custom indexes to a model, like ``ModelAdmins``. 55 | 56 | 57 | ``SearchIndex`` 58 | --------------- 59 | 60 | Implemented per-model you wish to index. 61 | 62 | * Handles generating the document to be indexed. 63 | * Populates additional fields to accompany the document. 64 | * Provides a way to limit what types of objects get indexed. 65 | * Provides a way to index the document(s). 66 | * Provides a way to remove the document(s). 67 | -------------------------------------------------------------------------------- /docs/backend_support.rst: -------------------------------------------------------------------------------- 1 | .. _ref-backend-support: 2 | 3 | =============== 4 | Backend Support 5 | =============== 6 | 7 | 8 | Supported Backends 9 | ================== 10 | 11 | * Solr_ 12 | * ElasticSearch_ 13 | * Whoosh_ 14 | * Xapian_ 15 | 16 | .. _Solr: http://lucene.apache.org/solr/ 17 | .. _ElasticSearch: http://elasticsearch.org/ 18 | .. _Whoosh: https://github.com/mchaput/whoosh/ 19 | .. _Xapian: http://xapian.org/ 20 | 21 | 22 | Backend Capabilities 23 | ==================== 24 | 25 | Solr 26 | ---- 27 | 28 | **Complete & included with Haystack.** 29 | 30 | * Full SearchQuerySet support 31 | * Automatic query building 32 | * "More Like This" functionality 33 | * Term Boosting 34 | * Faceting 35 | * Stored (non-indexed) fields 36 | * Highlighting 37 | * Spatial search 38 | * Requires: pysolr (2.0.13+) & Solr 3.5+ 39 | 40 | ElasticSearch 41 | ------------- 42 | 43 | **Complete & included with Haystack.** 44 | 45 | * Full SearchQuerySet support 46 | * Automatic query building 47 | * "More Like This" functionality 48 | * Term Boosting 49 | * Faceting (up to 100 facets) 50 | * Stored (non-indexed) fields 51 | * Highlighting 52 | * Spatial search 53 | * Requires: `elasticsearch-py `_ 1.x, 2.x, or 5.X. 54 | 55 | Whoosh 56 | ------ 57 | 58 | **Complete & included with Haystack.** 59 | 60 | * Full SearchQuerySet support 61 | * Automatic query building 62 | * "More Like This" functionality 63 | * Term Boosting 64 | * Stored (non-indexed) fields 65 | * Highlighting 66 | * Requires: whoosh (2.0.0+) 67 | * Per-field analyzers 68 | 69 | Xapian 70 | ------ 71 | 72 | **Complete & available as a third-party download.** 73 | 74 | * Full SearchQuerySet support 75 | * Automatic query building 76 | * "More Like This" functionality 77 | * Term Boosting 78 | * Faceting 79 | * Stored (non-indexed) fields 80 | * Highlighting 81 | * Requires: Xapian 1.0.5+ & python-xapian 1.0.5+ 82 | * Backend can be downloaded here: `xapian-haystack `__ 83 | 84 | Backend Support Matrix 85 | ====================== 86 | 87 | +----------------+------------------------+---------------------+----------------+------------+----------+---------------+--------------+---------+ 88 | | Backend | SearchQuerySet Support | Auto Query Building | More Like This | Term Boost | Faceting | Stored Fields | Highlighting | Spatial | 89 | +================+========================+=====================+================+============+==========+===============+==============+=========+ 90 | | Solr | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | 91 | +----------------+------------------------+---------------------+----------------+------------+----------+---------------+--------------+---------+ 92 | | ElasticSearch | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | 93 | +----------------+------------------------+---------------------+----------------+------------+----------+---------------+--------------+---------+ 94 | | Whoosh | Yes | Yes | Yes | Yes | No | Yes | Yes | No | 95 | +----------------+------------------------+---------------------+----------------+------------+----------+---------------+--------------+---------+ 96 | | Xapian | Yes | Yes | Yes | Yes | Yes | Yes | Yes (plugin) | No | 97 | +----------------+------------------------+---------------------+----------------+------------+----------+---------------+--------------+---------+ 98 | 99 | 100 | Unsupported Backends & Alternatives 101 | =================================== 102 | 103 | If you have a search engine which you would like to see supported in Haystack, the current recommendation is 104 | to develop a plugin following the lead of `xapian-haystack `_ so 105 | that project can be developed and tested independently of the core Haystack release schedule. 106 | 107 | Sphinx 108 | ------ 109 | 110 | This backend has been requested multiple times over the years but does not yet have a volunteer maintainer. If 111 | you would like to work on it, please contact the Haystack maintainers so your project can be linked here and, 112 | if desired, added to the `django-haystack `_ organization on GitHub. 113 | 114 | In the meantime, Sphinx users should consider Jorge C. Leitão's 115 | `django-sphinxql `_ project. 116 | -------------------------------------------------------------------------------- /docs/boost.rst: -------------------------------------------------------------------------------- 1 | .. _ref-boost: 2 | 3 | ===== 4 | Boost 5 | ===== 6 | 7 | 8 | Scoring is a critical component of good search. Normal full-text searches 9 | automatically score a document based on how well it matches the query provided. 10 | However, sometimes you want certain documents to score better than they 11 | otherwise would. Boosting is a way to achieve this. There are three types of 12 | boost: 13 | 14 | * Term Boost 15 | * Document Boost 16 | * Field Boost 17 | 18 | .. note:: 19 | 20 | Document & Field boost support was added in Haystack 1.1. 21 | 22 | Despite all being types of boost, they take place at different times and have 23 | slightly different effects on scoring. 24 | 25 | Term boost happens at query time (when the search query is run) and is based 26 | around increasing the score if a certain word/phrase is seen. 27 | 28 | On the other hand, document & field boosts take place at indexing time (when 29 | the document is being added to the index). Document boost causes the relevance 30 | of the entire result to go up, where field boost causes only searches within 31 | that field to do better. 32 | 33 | .. warning:: 34 | 35 | Be warned that boost is very, very sensitive & can hurt overall search 36 | quality if over-zealously applied. Even very small adjustments can affect 37 | relevance in a big way. 38 | 39 | Term Boost 40 | ========== 41 | 42 | Term boosting is achieved by using ``SearchQuerySet.boost``. You provide it 43 | the term you want to boost on & a floating point value (based around ``1.0`` 44 | as 100% - no boost). 45 | 46 | Example:: 47 | 48 | # Slight increase in relevance for documents that include "banana". 49 | sqs = SearchQuerySet().boost('banana', 1.1) 50 | 51 | # Big decrease in relevance for documents that include "blueberry". 52 | sqs = SearchQuerySet().boost('blueberry', 0.8) 53 | 54 | See the :doc:`searchqueryset_api` docs for more details on using this method. 55 | 56 | 57 | Document Boost 58 | ============== 59 | 60 | Document boosting is done by adding a ``boost`` field to the prepared data 61 | ``SearchIndex`` creates. The best way to do this is to override 62 | ``SearchIndex.prepare``:: 63 | 64 | from haystack import indexes 65 | from notes.models import Note 66 | 67 | 68 | class NoteSearchIndex(indexes.SearchIndex, indexes.Indexable): 69 | # Your regular fields here then... 70 | 71 | def prepare(self, obj): 72 | data = super(NoteSearchIndex, self).prepare(obj) 73 | data['boost'] = 1.1 74 | return data 75 | 76 | 77 | Another approach might be to add a new field called ``boost``. However, this 78 | can skew your schema and is not encouraged. 79 | 80 | 81 | Field Boost 82 | =========== 83 | 84 | Field boosting is enabled by setting the ``boost`` kwarg on the desired field. 85 | An example of this might be increasing the significance of a ``title``:: 86 | 87 | from haystack import indexes 88 | from notes.models import Note 89 | 90 | 91 | class NoteSearchIndex(indexes.SearchIndex, indexes.Indexable): 92 | text = indexes.CharField(document=True, use_template=True) 93 | title = indexes.CharField(model_attr='title', boost=1.125) 94 | 95 | def get_model(self): 96 | return Note 97 | 98 | .. note:: 99 | 100 | Field boosting only has an effect when the SearchQuerySet filters on the 101 | field which has been boosted. If you are using a default search view or 102 | form you will need override the search method or other include the field 103 | in your search query. This example CustomSearchForm searches the automatic 104 | ``content`` field and the ``title`` field which has been boosted:: 105 | 106 | from haystack.forms import SearchForm 107 | 108 | class CustomSearchForm(SearchForm): 109 | 110 | def search(self): 111 | if not self.is_valid(): 112 | return self.no_query_found() 113 | 114 | if not self.cleaned_data.get('q'): 115 | return self.no_query_found() 116 | 117 | q = self.cleaned_data['q'] 118 | sqs = self.searchqueryset.filter(SQ(content=AutoQuery(q)) | SQ(title=AutoQuery(q))) 119 | 120 | if self.load_all: 121 | sqs = sqs.load_all() 122 | 123 | return sqs.highlight() 124 | -------------------------------------------------------------------------------- /docs/creating_new_backends.rst: -------------------------------------------------------------------------------- 1 | .. _ref-creating-new-backends: 2 | 3 | ===================== 4 | Creating New Backends 5 | ===================== 6 | 7 | The process should be fairly simple. 8 | 9 | #. Create new backend file. Name is important. 10 | #. Two classes inside. 11 | 12 | #. SearchBackend (inherit from haystack.backends.BaseSearchBackend) 13 | #. SearchQuery (inherit from haystack.backends.BaseSearchQuery) 14 | 15 | 16 | SearchBackend 17 | ============= 18 | 19 | Responsible for the actual connection and low-level details of interacting with 20 | the backend. 21 | 22 | * Connects to search engine 23 | * Method for saving new docs to index 24 | * Method for removing docs from index 25 | * Method for performing the actual query 26 | 27 | 28 | SearchQuery 29 | =========== 30 | 31 | Responsible for taking structured data about the query and converting it into a 32 | backend appropriate format. 33 | 34 | * Method for creating the backend specific query - ``build_query``. 35 | -------------------------------------------------------------------------------- /docs/glossary.rst: -------------------------------------------------------------------------------- 1 | .. _ref-glossary: 2 | 3 | ======== 4 | Glossary 5 | ======== 6 | 7 | Search is a domain full of its own jargon and definitions. As this may be an 8 | unfamiliar territory to many developers, what follows are some commonly used 9 | terms and what they mean. 10 | 11 | 12 | Engine 13 | An engine, for the purposes of Haystack, is a third-party search solution. 14 | It might be a full service (i.e. Solr_) or a library to build an 15 | engine with (i.e. Whoosh_) 16 | 17 | .. _Solr: http://lucene.apache.org/solr/ 18 | .. _Whoosh: https://github.com/mchaput/whoosh/ 19 | 20 | Index 21 | The datastore used by the engine is called an index. Its structure can vary 22 | wildly between engines but commonly they resemble a document store. This is 23 | the source of all information in Haystack. 24 | 25 | Document 26 | A document is essentially a record within the index. It usually contains at 27 | least one blob of text that serves as the primary content the engine searches 28 | and may have additional data hung off it. 29 | 30 | Corpus 31 | A term for a collection of documents. When talking about the documents stored 32 | by the engine (rather than the technical implementation of the storage), this 33 | term is commonly used. 34 | 35 | Field 36 | Within the index, each document may store extra data with the main content as 37 | a field. Also sometimes called an attribute, this usually represents metadata 38 | or extra content about the document. Haystack can use these fields for 39 | filtering and display. 40 | 41 | Term 42 | A term is generally a single word (or word-like) string of characters used 43 | in a search query. 44 | 45 | Stemming 46 | A means of determining if a word has any root words. This varies by language, 47 | but in English, this generally consists of removing plurals, an action form of 48 | the word, et cetera. For instance, in English, 'giraffes' would stem to 49 | 'giraffe'. Similarly, 'exclamation' would stem to 'exclaim'. This is useful 50 | for finding variants of the word that may appear in other documents. 51 | 52 | Boost 53 | Boost provides a means to take a term or phrase from a search query and alter 54 | the relevance of a result based on if that term is found in the result, a form 55 | of weighting. For instance, if you wanted to more heavily weight results that 56 | included the word 'zebra', you'd specify a boost for that term within the 57 | query. 58 | 59 | More Like This 60 | Incorporating techniques from information retrieval and artificial 61 | intelligence, More Like This is a technique for finding other documents within 62 | the index that closely resemble the document in question. This is useful for 63 | programmatically generating a list of similar content for a user to browse 64 | based on the current document they are viewing. 65 | 66 | Faceting 67 | Faceting is a way to provide insight to the user into the contents of your 68 | corpus. In its simplest form, it is a set of document counts returned with 69 | results when performing a query. These counts can be used as feedback for 70 | the user, allowing the user to choose interesting aspects of their search 71 | results and "drill down" into those results. 72 | 73 | An example might be providing a facet on an ``author`` field, providing back a 74 | list of authors and the number of documents in the index they wrote. This 75 | could be presented to the user with a link, allowing the user to click and 76 | narrow their original search to all results by that author. 77 | -------------------------------------------------------------------------------- /docs/haystack_theme/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "basic/layout.html" %} 2 | 3 | {%- block extrahead %} 4 | 5 | 6 | {% endblock %} 7 | 8 | {%- block header %} 9 | 22 | {% endblock %} -------------------------------------------------------------------------------- /docs/haystack_theme/static/documentation.css: -------------------------------------------------------------------------------- 1 | a, a:link, a:hover { background-color: transparent !important; color: #CAECFF; outline-color: transparent !important; text-decoration: underline; } 2 | dl dt { text-decoration: underline; } 3 | dl.class dt, dl.method dt { background-color: #444444; padding: 5px; text-decoration: none; } 4 | tt.descname { font-weight: normal; } 5 | dl.method dt span.optional { font-weight: normal; } 6 | div#header { margin-bottom: 0px; } 7 | div.document, div.related, div.footer { width: 900px; margin: 0 auto; } 8 | div.document { margin-top: 10px; } 9 | div.related { background-color: #262511; padding-left: 10px; padding-right: 10px; } 10 | div.documentwrapper { width:640px; float:left;} 11 | div.body h1, 12 | div.body h2, 13 | div.body h3, 14 | div.body h4, 15 | div.body h5, 16 | div.body h6 { 17 | background-color: #053211; 18 | font-weight: normal; 19 | border-bottom: 2px solid #262511; 20 | margin: 20px -20px 10px -20px; 21 | padding: 3px 0 3px 10px; 22 | } 23 | div.sphinxsidebar { width:220px; float:right;} 24 | div.sphinxsidebar ul { padding-left: 10px; } 25 | div.sphinxsidebar ul ul { padding-left: 10px; margin-left: 10px; } 26 | div.bodywrapper { margin: 0px; } 27 | div.highlight-python, div.highlight { background-color: #262511; margin-bottom: 10px; padding: 10px; } 28 | div.footer { background-color:#262511; font-size: 90%; padding: 10px; } 29 | table thead { background-color: #053211; border-bottom: 1px solid #262511; } -------------------------------------------------------------------------------- /docs/haystack_theme/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic -------------------------------------------------------------------------------- /docs/highlighting.rst: -------------------------------------------------------------------------------- 1 | .. _ref-highlighting: 2 | 3 | ============ 4 | Highlighting 5 | ============ 6 | 7 | Haystack supports two different methods of highlighting. You can either use 8 | ``SearchQuerySet.highlight`` or the built-in ``{% highlight %}`` template tag, 9 | which uses the ``Highlighter`` class. Each approach has advantages and 10 | disadvantages you need to weigh when deciding which to use. 11 | 12 | If you want portable, flexible, decently fast code, the 13 | ``{% highlight %}`` template tag (or manually using the underlying 14 | ``Highlighter`` class) is the way to go. On the other hand, if you care more 15 | about speed and will only ever be using one backend, 16 | ``SearchQuerySet.highlight`` may suit your needs better. 17 | 18 | Use of ``SearchQuerySet.highlight`` is documented in the 19 | :doc:`searchqueryset_api` documentation and the ``{% highlight %}`` tag is 20 | covered in the :doc:`templatetags` documentation, so the rest of this material 21 | will cover the ``Highlighter`` implementation. 22 | 23 | 24 | ``Highlighter`` 25 | --------------- 26 | 27 | The ``Highlighter`` class is a pure-Python implementation included with Haystack 28 | that's designed for flexibility. If you use the ``{% highlight %}`` template 29 | tag, you'll be automatically using this class. You can also use it manually in 30 | your code. For example:: 31 | 32 | >>> from haystack.utils.highlighting import Highlighter 33 | 34 | >>> my_text = 'This is a sample block that would be more meaningful in real life.' 35 | >>> my_query = 'block meaningful' 36 | 37 | >>> highlight = Highlighter(my_query) 38 | >>> highlight.highlight(my_text) 39 | u'...block that would be more meaningful in real life.' 40 | 41 | The default implementation takes three optional kwargs: ``html_tag``, 42 | ``css_class`` and ``max_length``. These allow for basic customizations to the 43 | output, like so:: 44 | 45 | >>> from haystack.utils.highlighting import Highlighter 46 | 47 | >>> my_text = 'This is a sample block that would be more meaningful in real life.' 48 | >>> my_query = 'block meaningful' 49 | 50 | >>> highlight = Highlighter(my_query, html_tag='div', css_class='found', max_length=35) 51 | >>> highlight.highlight(my_text) 52 | u'...
block
that would be more
meaningful
...' 53 | 54 | Further, if this implementation doesn't suit your needs, you can define your own 55 | custom highlighter class. As long as it implements the API you've just seen, it 56 | can highlight however you choose. For example:: 57 | 58 | # In ``myapp/utils.py``... 59 | from haystack.utils.highlighting import Highlighter 60 | 61 | class BorkHighlighter(Highlighter): 62 | def render_html(self, highlight_locations=None, start_offset=None, end_offset=None): 63 | highlighted_chunk = self.text_block[start_offset:end_offset] 64 | 65 | for word in self.query_words: 66 | highlighted_chunk = highlighted_chunk.replace(word, 'Bork!') 67 | 68 | return highlighted_chunk 69 | 70 | Then set the ``HAYSTACK_CUSTOM_HIGHLIGHTER`` setting to 71 | ``myapp.utils.BorkHighlighter``. Usage would then look like:: 72 | 73 | >>> highlight = BorkHighlighter(my_query) 74 | >>> highlight.highlight(my_text) 75 | u'Bork! that would be more Bork! in real life.' 76 | 77 | Now the ``{% highlight %}`` template tag will also use this highlighter. 78 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Haystack! 2 | ==================== 3 | 4 | Haystack provides modular search for Django. It features a unified, familiar 5 | API that allows you to plug in different search backends (such as Solr_, 6 | Elasticsearch_, Whoosh_, Xapian_, etc.) without having to modify your code. 7 | 8 | .. _Solr: http://lucene.apache.org/solr/ 9 | .. _Elasticsearch: http://elasticsearch.org/ 10 | .. _Whoosh: https://github.com/mchaput/whoosh/ 11 | .. _Xapian: http://xapian.org/ 12 | 13 | 14 | .. note:: 15 | 16 | This documentation represents the current version of Haystack. For old versions of the documentation: 17 | 18 | * v2.5.X: https://django-haystack.readthedocs.io/en/v2.5.1/ 19 | * v2.4.X: https://django-haystack.readthedocs.io/en/v2.4.1/ 20 | * v2.3.X: https://django-haystack.readthedocs.io/en/v2.3.0/ 21 | * v2.2.X: https://django-haystack.readthedocs.io/en/v2.2.0/ 22 | * v2.1.X: https://django-haystack.readthedocs.io/en/v2.1.0/ 23 | * v2.0.X: https://django-haystack.readthedocs.io/en/v2.0.0/ 24 | * v1.2.X: https://django-haystack.readthedocs.io/en/v1.2.7/ 25 | * v1.1.X: https://django-haystack.readthedocs.io/en/v1.1/ 26 | 27 | Getting Started 28 | --------------- 29 | 30 | If you're new to Haystack, you may want to start with these documents to get 31 | you up and running: 32 | 33 | .. toctree:: 34 | :maxdepth: 2 35 | 36 | tutorial 37 | 38 | .. toctree:: 39 | :maxdepth: 1 40 | 41 | views_and_forms 42 | templatetags 43 | glossary 44 | management_commands 45 | faq 46 | who_uses 47 | other_apps 48 | installing_search_engines 49 | debugging 50 | 51 | changelog 52 | contributing 53 | python3 54 | migration_from_1_to_2 55 | 56 | 57 | Advanced Uses 58 | ------------- 59 | 60 | Once you've got Haystack working, here are some of the more complex features 61 | you may want to include in your application. 62 | 63 | .. toctree:: 64 | :maxdepth: 1 65 | 66 | best_practices 67 | highlighting 68 | faceting 69 | autocomplete 70 | boost 71 | signal_processors 72 | multiple_index 73 | rich_content_extraction 74 | spatial 75 | admin 76 | 77 | 78 | Reference 79 | --------- 80 | 81 | If you're an experienced user and are looking for a reference, you may be 82 | looking for API documentation and advanced usage as detailed in: 83 | 84 | .. toctree:: 85 | :maxdepth: 2 86 | 87 | searchqueryset_api 88 | searchindex_api 89 | inputtypes 90 | searchfield_api 91 | searchresult_api 92 | searchquery_api 93 | searchbackend_api 94 | 95 | architecture_overview 96 | backend_support 97 | settings 98 | utils 99 | 100 | 101 | Developing 102 | ---------- 103 | 104 | Finally, if you're looking to help out with the development of Haystack, 105 | the following links should help guide you on running tests and creating 106 | additional backends: 107 | 108 | .. toctree:: 109 | :maxdepth: 1 110 | 111 | running_tests 112 | creating_new_backends 113 | 114 | 115 | Requirements 116 | ------------ 117 | 118 | Haystack has a relatively easily-met set of requirements. 119 | 120 | * Python 2.7+ or Python 3.3+ 121 | * A supported version of Django: https://www.djangoproject.com/download/#supported-versions 122 | 123 | Additionally, each backend has its own requirements. You should refer to 124 | :doc:`installing_search_engines` for more details. 125 | -------------------------------------------------------------------------------- /docs/other_apps.rst: -------------------------------------------------------------------------------- 1 | .. _ref-other_apps: 2 | 3 | ============================= 4 | Haystack-Related Applications 5 | ============================= 6 | 7 | Sub Apps 8 | ======== 9 | 10 | These are apps that build on top of the infrastructure provided by Haystack. 11 | Useful for essentially extending what Haystack can do. 12 | 13 | queued_search 14 | ------------- 15 | 16 | http://github.com/toastdriven/queued_search (2.X compatible) 17 | 18 | Provides a queue-based setup as an alternative to ``RealtimeSignalProcessor`` or 19 | constantly running the ``update_index`` command. Useful for high-load, short 20 | update time situations. 21 | 22 | celery-haystack 23 | --------------- 24 | 25 | https://github.com/jezdez/celery-haystack (1.X and 2.X compatible) 26 | 27 | Also provides a queue-based setup, this time centered around Celery. Useful 28 | for keeping the index fresh per model instance or with the included task 29 | to call the ``update_index`` management command instead. 30 | 31 | haystack-rqueue 32 | --------------- 33 | 34 | https://github.com/mandx/haystack-rqueue (2.X compatible) 35 | 36 | Also provides a queue-based setup, this time centered around RQ. Useful 37 | for keeping the index fresh using ``./manage.py rqworker``. 38 | 39 | django-celery-haystack 40 | ---------------------- 41 | 42 | https://github.com/mixcloud/django-celery-haystack-SearchIndex 43 | 44 | Another queue-based setup, also around Celery. Useful 45 | for keeping the index fresh. 46 | 47 | saved_searches 48 | -------------- 49 | 50 | http://github.com/toastdriven/saved_searches (2.X compatible) 51 | 52 | Adds personalization to search. Retains a history of queries run by the various 53 | users on the site (including anonymous users). This can be used to present the 54 | user with their search history and provide most popular/most recent queries 55 | on the site. 56 | 57 | saved-search 58 | ------------ 59 | 60 | https://github.com/DirectEmployers/saved-search 61 | 62 | An alternate take on persisting user searches, this has a stronger focus 63 | on locale-based searches as well as further integration. 64 | 65 | haystack-static-pages 66 | --------------------- 67 | 68 | http://github.com/trapeze/haystack-static-pages 69 | 70 | Provides a simple way to index flat (non-model-based) content on your site. 71 | By using the management command that comes with it, it can crawl all pertinent 72 | pages on your site and add them to search. 73 | 74 | django-tumbleweed 75 | ----------------- 76 | 77 | http://github.com/mcroydon/django-tumbleweed 78 | 79 | Provides a tumblelog-like view to any/all Haystack-enabled models on your 80 | site. Useful for presenting date-based views of search data. Attempts to avoid 81 | the database completely where possible. 82 | 83 | 84 | Haystack-Enabled Apps 85 | ===================== 86 | 87 | These are reusable apps that ship with ``SearchIndexes``, suitable for quick 88 | integration with Haystack. 89 | 90 | * django-faq (freq. asked questions app) - http://github.com/benspaulding/django-faq 91 | * django-essays (blog-like essay app) - http://github.com/bkeating/django-essays 92 | * gtalug (variety of apps) - http://github.com/myles/gtalug 93 | * sciencemuseum (science museum open data) - http://github.com/simonw/sciencemuseum 94 | * vz-wiki (wiki) - http://github.com/jobscry/vz-wiki 95 | * ffmff (events app) - http://github.com/stefreak/ffmff 96 | * Dinette (forums app) - http://github.com/uswaretech/Dinette 97 | * fiftystates_site (site) - http://github.com/sunlightlabs/fiftystates_site 98 | * Open-Knesset (site) - http://github.com/ofri/Open-Knesset 99 | -------------------------------------------------------------------------------- /docs/python3.rst: -------------------------------------------------------------------------------- 1 | .. _ref-python3: 2 | 3 | ================ 4 | Python 3 Support 5 | ================ 6 | 7 | As of Haystack v2.1.0, it has been ported to support both Python 2 & Python 3 8 | within the same codebase. This builds on top of what `six`_ & `Django`_ provide. 9 | 10 | No changes are required for anyone running an existing Haystack 11 | installation. The API is completely backward-compatible, so you should be able 12 | to run your existing software without modification. 13 | 14 | Virtually all tests pass under both Python 2 & 3, with a small number of 15 | expected failures under Python (typically related to ordering, see below). 16 | 17 | .. _`six`: http://pythonhosted.org/six/ 18 | .. _`Django`: https://docs.djangoproject.com/en/1.5/topics/python3/#str-and-unicode-methods 19 | 20 | 21 | Supported Backends 22 | ================== 23 | 24 | The following backends are fully supported under Python 3. However, you may 25 | need to update these dependencies if you have a pre-existing setup. 26 | 27 | * Solr (pysolr>=3.1.0) 28 | * Elasticsearch 29 | 30 | 31 | Notes 32 | ===== 33 | 34 | Testing 35 | ------- 36 | 37 | If you were testing things such as the query generated by a given 38 | ``SearchQuerySet`` or how your forms would render, under Python 3.3.2+, 39 | `hash randomization`_ is in effect, which means that the ordering of 40 | dictionaries is no longer consistent, even on the same platform. 41 | 42 | Haystack took the approach of abandoning making assertions about the entire 43 | structure. Instead, we either simply assert that the new object contains the 44 | right things or make a call to ``sorted(...)`` around it to ensure order. It is 45 | recommended you take a similar approach. 46 | 47 | .. _`hash randomization`: http://docs.python.org/3/whatsnew/3.3.html#builtin-functions-and-types 48 | -------------------------------------------------------------------------------- /docs/rich_content_extraction.rst: -------------------------------------------------------------------------------- 1 | .. _ref-rich_content_extraction: 2 | 3 | ======================= 4 | Rich Content Extraction 5 | ======================= 6 | 7 | For some projects it is desirable to index text content which is stored in 8 | structured files such as PDFs, Microsoft Office documents, images, etc. 9 | Currently only Solr's `ExtractingRequestHandler`_ is directly supported by 10 | Haystack but the approach below could be used with any backend which supports 11 | this feature. 12 | 13 | .. _`ExtractingRequestHandler`: http://wiki.apache.org/solr/ExtractingRequestHandler 14 | 15 | Extracting Content 16 | ================== 17 | 18 | :meth:`SearchBackend.extract_file_contents` accepts a file or file-like object 19 | and returns a dictionary containing two keys: ``metadata`` and ``contents``. The 20 | ``contents`` value will be a string containing all of the text which the backend 21 | managed to extract from the file contents. ``metadata`` will always be a 22 | dictionary but the keys and values will vary based on the underlying extraction 23 | engine and the type of file provided. 24 | 25 | Indexing Extracted Content 26 | ========================== 27 | 28 | Generally you will want to include the extracted text in your main document 29 | field along with everything else specified in your search template. This example 30 | shows how to override a hypothetical ``FileIndex``'s ``prepare`` method to 31 | include the extract content along with information retrieved from the database:: 32 | 33 | def prepare(self, obj): 34 | data = super(FileIndex, self).prepare(obj) 35 | 36 | # This could also be a regular Python open() call, a StringIO instance 37 | # or the result of opening a URL. Note that due to a library limitation 38 | # file_obj must have a .name attribute even if you need to set one 39 | # manually before calling extract_file_contents: 40 | file_obj = obj.the_file.open() 41 | 42 | extracted_data = self.get_backend().extract_file_contents(file_obj) 43 | 44 | # Now we'll finally perform the template processing to render the 45 | # text field with *all* of our metadata visible for templating: 46 | t = loader.select_template(('search/indexes/myapp/file_text.txt', )) 47 | data['text'] = t.render(Context({'object': obj, 48 | 'extracted': extracted_data})) 49 | 50 | return data 51 | 52 | This allows you to insert the extracted text at the appropriate place in your 53 | template, modified or intermixed with database content as appropriate: 54 | 55 | .. code-block:: html+django 56 | 57 | {{ object.title }} 58 | {{ object.owner.name }} 59 | 60 | … 61 | 62 | {% for k, v in extracted.metadata.items %} 63 | {% for val in v %} 64 | {{ k }}: {{ val|safe }} 65 | {% endfor %} 66 | {% endfor %} 67 | 68 | {{ extracted.contents|striptags|safe }} -------------------------------------------------------------------------------- /docs/running_tests.rst: -------------------------------------------------------------------------------- 1 | .. _ref-running-tests: 2 | 3 | ============= 4 | Running Tests 5 | ============= 6 | 7 | Everything 8 | ========== 9 | 10 | The simplest way to get up and running with Haystack's tests is to run:: 11 | 12 | python setup.py test 13 | 14 | This installs all of the backend libraries & all dependencies for getting the 15 | tests going and runs the tests. You will still have to setup search servers 16 | (for running Solr tests, the spatial Solr tests & the Elasticsearch tests). 17 | 18 | 19 | Cherry-Picked 20 | ============= 21 | 22 | If you'd rather not run all the tests, run only the backends you need since 23 | tests for backends that are not running will be skipped. 24 | 25 | ``Haystack`` is maintained with all tests passing at all times, so if you 26 | receive any errors during testing, please check your setup and file a report if 27 | the errors persist. 28 | 29 | To run just a portion of the tests you can use the script ``run_tests.py`` and 30 | just specify the files or directories you wish to run, for example:: 31 | 32 | cd test_haystack 33 | ./run_tests.py whoosh_tests test_loading.py 34 | 35 | The ``run_tests.py`` script is just a tiny wrapper around the nose_ library and 36 | any options you pass to it will be passed on; including ``--help`` to get a 37 | list of possible options:: 38 | 39 | cd test_haystack 40 | ./run_tests.py --help 41 | 42 | .. _nose: https://nose.readthedocs.io/en/latest/ 43 | 44 | Configuring Solr 45 | ================ 46 | 47 | Haystack assumes that you have a Solr server running on port ``9001`` which 48 | uses the schema and configuration provided in the 49 | ``test_haystack/solr_tests/server/`` directory. For convenience, a script is 50 | provided which will download, configure and start a test Solr server:: 51 | 52 | test_haystack/solr_tests/server/start-solr-test-server.sh 53 | 54 | If no server is found all solr-related tests will be skipped. 55 | 56 | Configuring Elasticsearch 57 | ========================= 58 | 59 | The test suite will try to connect to Elasticsearch on port ``9200``. If no 60 | server is found all elasticsearch tests will be skipped. Note that the tests 61 | are destructive - during the teardown phase they will wipe the cluster clean so 62 | make sure you don't run them against an instance with data you wish to keep. 63 | 64 | If you want to run the geo-django tests you may need to review the 65 | `GeoDjango GEOS and GDAL settings`_ before running these commands:: 66 | 67 | cd test_haystack 68 | ./run_tests.py elasticsearch_tests 69 | 70 | .. _GeoDjango GEOS and GDAL settings: https://docs.djangoproject.com/en/1.7/ref/contrib/gis/install/geolibs/#geos-library-path 71 | -------------------------------------------------------------------------------- /docs/searchbackend_api.rst: -------------------------------------------------------------------------------- 1 | .. _ref-searchbackend-api: 2 | 3 | ===================== 4 | ``SearchBackend`` API 5 | ===================== 6 | 7 | .. class:: SearchBackend(connection_alias, **connection_options) 8 | 9 | The ``SearchBackend`` class handles interaction directly with the backend. The 10 | search query it performs is usually fed to it from a ``SearchQuery`` class that 11 | has been built for that backend. 12 | 13 | This class must be at least partially implemented on a per-backend basis and 14 | is usually accompanied by a ``SearchQuery`` class within the same module. 15 | 16 | Unless you are writing a new backend, it is unlikely you need to directly 17 | access this class. 18 | 19 | 20 | Method Reference 21 | ================ 22 | 23 | ``update`` 24 | ---------- 25 | 26 | .. method:: SearchBackend.update(self, index, iterable) 27 | 28 | Updates the backend when given a ``SearchIndex`` and a collection of 29 | documents. 30 | 31 | This method MUST be implemented by each backend, as it will be highly 32 | specific to each one. 33 | 34 | ``remove`` 35 | ---------- 36 | 37 | .. method:: SearchBackend.remove(self, obj_or_string) 38 | 39 | Removes a document/object from the backend. Can be either a model 40 | instance or the identifier (i.e. ``app_name.model_name.id``) in the 41 | event the object no longer exists. 42 | 43 | This method MUST be implemented by each backend, as it will be highly 44 | specific to each one. 45 | 46 | ``clear`` 47 | --------- 48 | 49 | .. method:: SearchBackend.clear(self, models=[]) 50 | 51 | Clears the backend of all documents/objects for a collection of models. 52 | 53 | This method MUST be implemented by each backend, as it will be highly 54 | specific to each one. 55 | 56 | ``search`` 57 | ---------- 58 | 59 | .. method:: SearchBackend.search(self, query_string, sort_by=None, start_offset=0, end_offset=None, fields='', highlight=False, facets=None, date_facets=None, query_facets=None, narrow_queries=None, spelling_query=None, limit_to_registered_models=None, result_class=None, **kwargs) 60 | 61 | Takes a query to search on and returns a dictionary. 62 | 63 | The query should be a string that is appropriate syntax for the backend. 64 | 65 | The returned dictionary should contain the keys 'results' and 'hits'. 66 | The 'results' value should be an iterable of populated ``SearchResult`` 67 | objects. The 'hits' should be an integer count of the number of matched 68 | results the search backend found. 69 | 70 | This method MUST be implemented by each backend, as it will be highly 71 | specific to each one. 72 | 73 | ``extract_file_contents`` 74 | ------------------------- 75 | 76 | .. method:: SearchBackend.extract_file_contents(self, file_obj) 77 | 78 | Perform text extraction on the provided file or file-like object. Returns either 79 | None or a dictionary containing the keys ``contents`` and ``metadata``. The 80 | ``contents`` field will always contain the extracted text content returned by 81 | the underlying search engine but ``metadata`` may vary considerably based on 82 | the backend and the input file. 83 | 84 | ``prep_value`` 85 | -------------- 86 | 87 | .. method:: SearchBackend.prep_value(self, value) 88 | 89 | Hook to give the backend a chance to prep an attribute value before 90 | sending it to the search engine. 91 | 92 | By default, just force it to unicode. 93 | 94 | ``more_like_this`` 95 | ------------------ 96 | 97 | .. method:: SearchBackend.more_like_this(self, model_instance, additional_query_string=None, result_class=None) 98 | 99 | Takes a model object and returns results the backend thinks are similar. 100 | 101 | This method MUST be implemented by each backend, as it will be highly 102 | specific to each one. 103 | 104 | ``build_schema`` 105 | ---------------- 106 | 107 | .. method:: SearchBackend.build_schema(self, fields) 108 | 109 | Takes a dictionary of fields and returns schema information. 110 | 111 | This method MUST be implemented by each backend, as it will be highly 112 | specific to each one. 113 | 114 | ``build_models_list`` 115 | --------------------- 116 | 117 | .. method:: SearchBackend.build_models_list(self) 118 | 119 | Builds a list of models for searching. 120 | 121 | The ``search`` method should use this and the ``django_ct`` field to 122 | narrow the results (unless the user indicates not to). This helps ignore 123 | any results that are not currently handled models and ensures 124 | consistent caching. 125 | -------------------------------------------------------------------------------- /docs/searchresult_api.rst: -------------------------------------------------------------------------------- 1 | .. _ref-searchresult-api: 2 | 3 | ==================== 4 | ``SearchResult`` API 5 | ==================== 6 | 7 | .. class:: SearchResult(app_label, model_name, pk, score, **kwargs) 8 | 9 | The ``SearchResult`` class provides structure to the results that come back from 10 | the search index. These objects are what a ``SearchQuerySet`` will return when 11 | evaluated. 12 | 13 | 14 | Attribute Reference 15 | =================== 16 | 17 | The class exposes the following useful attributes/properties: 18 | 19 | * ``app_label`` - The application the model is attached to. 20 | * ``model_name`` - The model's name. 21 | * ``pk`` - The primary key of the model. 22 | * ``score`` - The score provided by the search engine. 23 | * ``object`` - The actual model instance (lazy loaded). 24 | * ``model`` - The model class. 25 | * ``verbose_name`` - A prettier version of the model's class name for display. 26 | * ``verbose_name_plural`` - A prettier version of the model's *plural* class name for display. 27 | * ``searchindex`` - Returns the ``SearchIndex`` class associated with this 28 | result. 29 | * ``distance`` - On geo-spatial queries, this returns a ``Distance`` object 30 | representing the distance the result was from the focused point. 31 | 32 | 33 | Method Reference 34 | ================ 35 | 36 | ``content_type`` 37 | ---------------- 38 | 39 | .. method:: SearchResult.content_type(self) 40 | 41 | Returns the content type for the result's model instance. 42 | 43 | ``get_additional_fields`` 44 | ------------------------- 45 | 46 | .. method:: SearchResult.get_additional_fields(self) 47 | 48 | Returns a dictionary of all of the fields from the raw result. 49 | 50 | Useful for serializing results. Only returns what was seen from the 51 | search engine, so it may have extra fields Haystack's indexes aren't 52 | aware of. 53 | 54 | ``get_stored_fields`` 55 | --------------------- 56 | 57 | .. method:: SearchResult.get_stored_fields(self) 58 | 59 | Returns a dictionary of all of the stored fields from the SearchIndex. 60 | 61 | Useful for serializing results. Only returns the fields Haystack's 62 | indexes are aware of as being 'stored'. 63 | -------------------------------------------------------------------------------- /docs/templatetags.rst: -------------------------------------------------------------------------------- 1 | .. _ref-templatetags: 2 | 3 | ============= 4 | Template Tags 5 | ============= 6 | 7 | Haystack comes with a couple common template tags to make using some of its 8 | special features available to templates. 9 | 10 | 11 | ``highlight`` 12 | ============= 13 | 14 | Takes a block of text and highlights words from a provided query within that 15 | block of text. Optionally accepts arguments to provide the HTML tag to wrap 16 | highlighted word in, a CSS class to use with the tag and a maximum length of 17 | the blurb in characters. 18 | 19 | The defaults are ``span`` for the HTML tag, ``highlighted`` for the CSS class 20 | and 200 characters for the excerpt. 21 | 22 | Syntax:: 23 | 24 | {% highlight with [css_class "class_name"] [html_tag "span"] [max_length 200] %} 25 | 26 | Example:: 27 | 28 | # Highlight summary with default behavior. 29 | {% highlight result.summary with query %} 30 | 31 | # Highlight summary but wrap highlighted words with a div and the 32 | # following CSS class. 33 | {% highlight result.summary with query html_tag "div" css_class "highlight_me_please" %} 34 | 35 | # Highlight summary but only show 40 words. 36 | {% highlight result.summary with query max_length 40 %} 37 | 38 | The highlighter used by this tag can be overridden as needed. See the 39 | :doc:`highlighting` documentation for more information. 40 | 41 | 42 | ``more_like_this`` 43 | ================== 44 | 45 | Fetches similar items from the search index to find content that is similar 46 | to the provided model's content. 47 | 48 | .. note:: 49 | 50 | This requires a backend that has More Like This built-in. 51 | 52 | Syntax:: 53 | 54 | {% more_like_this model_instance as varname [for app_label.model_name,app_label.model_name,...] [limit n] %} 55 | 56 | Example:: 57 | 58 | # Pull a full SearchQuerySet (lazy loaded) of similar content. 59 | {% more_like_this entry as related_content %} 60 | 61 | # Pull just the top 5 similar pieces of content. 62 | {% more_like_this entry as related_content limit 5 %} 63 | 64 | # Pull just the top 5 similar entries or comments. 65 | {% more_like_this entry as related_content for "blog.entry,comments.comment" limit 5 %} 66 | 67 | This tag behaves exactly like ``SearchQuerySet.more_like_this``, so all notes in 68 | that regard apply here as well. 69 | -------------------------------------------------------------------------------- /docs/toc.rst: -------------------------------------------------------------------------------- 1 | Table Of Contents 2 | ================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | index 8 | tutorial 9 | glossary 10 | views_and_forms 11 | templatetags 12 | management_commands 13 | architecture_overview 14 | backend_support 15 | installing_search_engines 16 | settings 17 | faq 18 | who_uses 19 | other_apps 20 | debugging 21 | 22 | migration_from_1_to_2 23 | python3 24 | contributing 25 | 26 | best_practices 27 | highlighting 28 | faceting 29 | autocomplete 30 | boost 31 | signal_processors 32 | multiple_index 33 | rich_content_extraction 34 | spatial 35 | 36 | searchqueryset_api 37 | searchindex_api 38 | inputtypes 39 | searchfield_api 40 | searchresult_api 41 | searchquery_api 42 | searchbackend_api 43 | 44 | running_tests 45 | creating_new_backends 46 | utils 47 | 48 | 49 | Indices and tables 50 | ================== 51 | 52 | * :ref:`search` 53 | 54 | -------------------------------------------------------------------------------- /docs/utils.rst: -------------------------------------------------------------------------------- 1 | .. _ref-utils: 2 | 3 | ========= 4 | Utilities 5 | ========= 6 | 7 | Included here are some of the general use bits included with Haystack. 8 | 9 | 10 | ``get_identifier`` 11 | ------------------ 12 | 13 | .. function:: get_identifier(obj_or_string) 14 | 15 | Gets an unique identifier for the object or a string representing the 16 | object. 17 | 18 | If not overridden, uses ``..``. 19 | -------------------------------------------------------------------------------- /example_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/example_project/__init__.py -------------------------------------------------------------------------------- /example_project/bare_bones_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/example_project/bare_bones_app/__init__.py -------------------------------------------------------------------------------- /example_project/bare_bones_app/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.db import models 4 | 5 | 6 | class Cat(models.Model): 7 | name = models.CharField(max_length=255) 8 | birth_date = models.DateField(default=datetime.date.today) 9 | bio = models.TextField(blank=True) 10 | created = models.DateTimeField(default=datetime.datetime.now) 11 | updated = models.DateTimeField(default=datetime.datetime.now) 12 | 13 | def __str__(self): 14 | return self.name 15 | 16 | @models.permalink 17 | def get_absolute_url(self): 18 | return ("cat_detail", [], {"id": self.id}) 19 | -------------------------------------------------------------------------------- /example_project/bare_bones_app/search_indexes.py: -------------------------------------------------------------------------------- 1 | from bare_bones_app.models import Cat 2 | 3 | from haystack import indexes 4 | 5 | 6 | # For the most basic usage, you can use a subclass of 7 | # `haystack.indexes.BasicSearchIndex`, whose only requirement will be that 8 | # you create a `search/indexes/bare_bones_app/cat_text.txt` data template 9 | # for indexing. 10 | class CatIndex(indexes.BasicSearchIndex, indexes.Indexable): 11 | def get_model(self): 12 | return Cat 13 | -------------------------------------------------------------------------------- /example_project/regular_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/example_project/regular_app/__init__.py -------------------------------------------------------------------------------- /example_project/regular_app/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.db import models 4 | 5 | BREED_CHOICES = [ 6 | ("collie", "Collie"), 7 | ("labrador", "Labrador"), 8 | ("pembroke", "Pembroke Corgi"), 9 | ("shetland", "Shetland Sheepdog"), 10 | ("border", "Border Collie"), 11 | ] 12 | 13 | 14 | class Dog(models.Model): 15 | breed = models.CharField(max_length=255, choices=BREED_CHOICES) 16 | name = models.CharField(max_length=255) 17 | owner_last_name = models.CharField(max_length=255, blank=True) 18 | birth_date = models.DateField(default=datetime.date.today) 19 | bio = models.TextField(blank=True) 20 | public = models.BooleanField(default=True) 21 | created = models.DateTimeField(default=datetime.datetime.now) 22 | updated = models.DateTimeField(default=datetime.datetime.now) 23 | 24 | def __str__(self): 25 | return self.full_name() 26 | 27 | @models.permalink 28 | def get_absolute_url(self): 29 | return ("dog_detail", [], {"id": self.id}) 30 | 31 | def full_name(self): 32 | if self.owner_last_name: 33 | return "%s %s" % (self.name, self.owner_last_name) 34 | 35 | return self.name 36 | 37 | 38 | class Toy(models.Model): 39 | dog = models.ForeignKey(Dog, related_name="toys") 40 | name = models.CharField(max_length=60) 41 | 42 | def __str__(self): 43 | return "%s's %s" % (self.dog.name, self.name) 44 | -------------------------------------------------------------------------------- /example_project/regular_app/search_indexes.py: -------------------------------------------------------------------------------- 1 | from regular_app.models import Dog 2 | 3 | from haystack import indexes 4 | 5 | 6 | # More typical usage involves creating a subclassed `SearchIndex`. This will 7 | # provide more control over how data is indexed, generally resulting in better 8 | # search. 9 | class DogIndex(indexes.SearchIndex, indexes.Indexable): 10 | text = indexes.CharField(document=True, use_template=True) 11 | # We can pull data straight out of the model via `model_attr`. 12 | breed = indexes.CharField(model_attr="breed") 13 | # Note that callables are also OK to use. 14 | name = indexes.CharField(model_attr="full_name") 15 | bio = indexes.CharField(model_attr="name") 16 | birth_date = indexes.DateField(model_attr="birth_date") 17 | # Note that we can't assign an attribute here. We'll manually prepare it instead. 18 | toys = indexes.MultiValueField() 19 | 20 | def get_model(self): 21 | return Dog 22 | 23 | def index_queryset(self, using=None): 24 | return self.get_model().objects.filter(public=True) 25 | 26 | def prepare_toys(self, obj): 27 | # Store a list of id's for filtering 28 | return [toy.id for toy in obj.toys.all()] 29 | 30 | # Alternatively, you could store the names if searching for toy names 31 | # is more useful. 32 | # return [toy.name for toy in obj.toys.all()] 33 | -------------------------------------------------------------------------------- /example_project/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf import settings 4 | 5 | SECRET_KEY = "CHANGE ME" 6 | 7 | # All the normal settings apply. What's included here are the bits you'll have 8 | # to customize. 9 | 10 | # Add Haystack to INSTALLED_APPS. You can do this by simply placing in your list. 11 | INSTALLED_APPS = settings.INSTALLED_APPS + ("haystack",) 12 | 13 | 14 | HAYSTACK_CONNECTIONS = { 15 | "default": { 16 | # For Solr: 17 | "ENGINE": "haystack.backends.solr_backend.SolrEngine", 18 | "URL": "http://localhost:9001/solr/example", 19 | "TIMEOUT": 60 * 5, 20 | "INCLUDE_SPELLING": True, 21 | }, 22 | "elasticsearch": { 23 | "ENGINE": "haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine", 24 | "URL": "http://localhost:9200", 25 | "INDEX_NAME": "example_project", 26 | }, 27 | "whoosh": { 28 | # For Whoosh: 29 | "ENGINE": "haystack.backends.whoosh_backend.WhooshEngine", 30 | "PATH": os.path.join(os.path.dirname(__file__), "whoosh_index"), 31 | "INCLUDE_SPELLING": True, 32 | }, 33 | "simple": { 34 | # For Simple: 35 | "ENGINE": "haystack.backends.simple_backend.SimpleEngine" 36 | }, 37 | # 'xapian': { 38 | # # For Xapian (requires the third-party install): 39 | # 'ENGINE': 'xapian_backend.XapianEngine', 40 | # 'PATH': os.path.join(os.path.dirname(__file__), 'xapian_index'), 41 | # } 42 | } 43 | -------------------------------------------------------------------------------- /example_project/templates/search/indexes/bare_bones_app/cat_text.txt: -------------------------------------------------------------------------------- 1 | {{ object.name }} 2 | {{ object.bio }} -------------------------------------------------------------------------------- /example_project/templates/search/indexes/regular_app/dog_text.txt: -------------------------------------------------------------------------------- 1 | {{ object.full_name }} 2 | {{ object.breed }} 3 | {{ object.bio }} 4 | 5 | {% for toy in object.toys.all %} 6 | {{ toy.name }} 7 | {% endfor %} -------------------------------------------------------------------------------- /haystack/__init__.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ImproperlyConfigured 3 | from pkg_resources import DistributionNotFound, get_distribution, parse_version 4 | 5 | from haystack.constants import DEFAULT_ALIAS 6 | from haystack.utils import loading 7 | 8 | __author__ = "Daniel Lindsley" 9 | 10 | try: 11 | pkg_distribution = get_distribution("django-haystack") 12 | __version__ = pkg_distribution.version 13 | version_info = pkg_distribution.parsed_version 14 | except DistributionNotFound: 15 | __version__ = "0.0.dev0" 16 | version_info = parse_version(__version__) 17 | 18 | default_app_config = "haystack.apps.HaystackConfig" 19 | 20 | 21 | # Help people clean up from 1.X. 22 | if hasattr(settings, "HAYSTACK_SITECONF"): 23 | raise ImproperlyConfigured( 24 | "The HAYSTACK_SITECONF setting is no longer used & can be removed." 25 | ) 26 | if hasattr(settings, "HAYSTACK_SEARCH_ENGINE"): 27 | raise ImproperlyConfigured( 28 | "The HAYSTACK_SEARCH_ENGINE setting has been replaced with HAYSTACK_CONNECTIONS." 29 | ) 30 | if hasattr(settings, "HAYSTACK_ENABLE_REGISTRATIONS"): 31 | raise ImproperlyConfigured( 32 | "The HAYSTACK_ENABLE_REGISTRATIONS setting is no longer used & can be removed." 33 | ) 34 | if hasattr(settings, "HAYSTACK_INCLUDE_SPELLING"): 35 | raise ImproperlyConfigured( 36 | "The HAYSTACK_INCLUDE_SPELLING setting is now a per-backend setting" 37 | " & belongs in HAYSTACK_CONNECTIONS." 38 | ) 39 | 40 | 41 | # Check the 2.X+ bits. 42 | if not hasattr(settings, "HAYSTACK_CONNECTIONS"): 43 | raise ImproperlyConfigured("The HAYSTACK_CONNECTIONS setting is required.") 44 | if DEFAULT_ALIAS not in settings.HAYSTACK_CONNECTIONS: 45 | raise ImproperlyConfigured( 46 | "The default alias '%s' must be included in the HAYSTACK_CONNECTIONS setting." 47 | % DEFAULT_ALIAS 48 | ) 49 | 50 | # Load the connections. 51 | connections = loading.ConnectionHandler(settings.HAYSTACK_CONNECTIONS) 52 | 53 | # Just check HAYSTACK_ROUTERS setting validity, routers will be loaded lazily 54 | if hasattr(settings, "HAYSTACK_ROUTERS"): 55 | if not isinstance(settings.HAYSTACK_ROUTERS, (list, tuple)): 56 | raise ImproperlyConfigured( 57 | "The HAYSTACK_ROUTERS setting must be either a list or tuple." 58 | ) 59 | 60 | # Load the router(s). 61 | connection_router = loading.ConnectionRouter() 62 | 63 | 64 | # Per-request, reset the ghetto query log. 65 | # Probably not extraordinarily thread-safe but should only matter when 66 | # DEBUG = True. 67 | def reset_search_queries(**kwargs): 68 | for conn in connections.all(): 69 | if conn: 70 | conn.reset_queries() 71 | 72 | 73 | if settings.DEBUG: 74 | from django.core import signals as django_signals 75 | 76 | django_signals.request_started.connect(reset_search_queries) 77 | -------------------------------------------------------------------------------- /haystack/apps.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.apps import AppConfig 4 | from django.conf import settings 5 | 6 | from haystack import connection_router, connections 7 | from haystack.utils import loading 8 | 9 | 10 | class HaystackConfig(AppConfig): 11 | name = "haystack" 12 | signal_processor = None 13 | stream = None 14 | 15 | def ready(self): 16 | # Setup default logging. 17 | log = logging.getLogger("haystack") 18 | self.stream = logging.StreamHandler() 19 | self.stream.setLevel(logging.INFO) 20 | log.addHandler(self.stream) 21 | 22 | # Setup the signal processor. 23 | if not self.signal_processor: 24 | signal_processor_path = getattr( 25 | settings, 26 | "HAYSTACK_SIGNAL_PROCESSOR", 27 | "haystack.signals.BaseSignalProcessor", 28 | ) 29 | signal_processor_class = loading.import_class(signal_processor_path) 30 | self.signal_processor = signal_processor_class( 31 | connections, connection_router 32 | ) 33 | -------------------------------------------------------------------------------- /haystack/backends/simple_backend.py: -------------------------------------------------------------------------------- 1 | """ 2 | A very basic, ORM-based backend for simple search during tests. 3 | """ 4 | from functools import reduce 5 | from warnings import warn 6 | 7 | from django.db.models import Q 8 | 9 | from haystack import connections 10 | from haystack.backends import ( 11 | BaseEngine, 12 | BaseSearchBackend, 13 | BaseSearchQuery, 14 | SearchNode, 15 | log_query, 16 | ) 17 | from haystack.inputs import PythonData 18 | from haystack.models import SearchResult 19 | from haystack.utils import get_model_ct_tuple 20 | 21 | 22 | class SimpleSearchBackend(BaseSearchBackend): 23 | def update(self, indexer, iterable, commit=True): 24 | warn("update is not implemented in this backend") 25 | 26 | def remove(self, obj, commit=True): 27 | warn("remove is not implemented in this backend") 28 | 29 | def clear(self, models=None, commit=True): 30 | warn("clear is not implemented in this backend") 31 | 32 | @log_query 33 | def search(self, query_string, **kwargs): 34 | hits = 0 35 | results = [] 36 | result_class = SearchResult 37 | models = ( 38 | connections[self.connection_alias].get_unified_index().get_indexed_models() 39 | ) 40 | 41 | if kwargs.get("result_class"): 42 | result_class = kwargs["result_class"] 43 | 44 | if kwargs.get("models"): 45 | models = kwargs["models"] 46 | 47 | if query_string: 48 | for model in models: 49 | if query_string == "*": 50 | qs = model.objects.all() 51 | else: 52 | for term in query_string.split(): 53 | queries = [] 54 | 55 | for field in model._meta.fields: 56 | if hasattr(field, "related"): 57 | continue 58 | 59 | if not field.get_internal_type() in ( 60 | "TextField", 61 | "CharField", 62 | "SlugField", 63 | ): 64 | continue 65 | 66 | queries.append(Q(**{"%s__icontains" % field.name: term})) 67 | 68 | if queries: 69 | qs = model.objects.filter( 70 | reduce(lambda x, y: x | y, queries) 71 | ) 72 | else: 73 | qs = [] 74 | 75 | hits += len(qs) 76 | 77 | for match in qs: 78 | match.__dict__.pop("score", None) 79 | app_label, model_name = get_model_ct_tuple(match) 80 | result = result_class( 81 | app_label, model_name, match.pk, 0, **match.__dict__ 82 | ) 83 | # For efficiency. 84 | result._model = match.__class__ 85 | result._object = match 86 | results.append(result) 87 | 88 | return {"results": results, "hits": hits} 89 | 90 | def prep_value(self, db_field, value): 91 | return value 92 | 93 | def more_like_this( 94 | self, 95 | model_instance, 96 | additional_query_string=None, 97 | start_offset=0, 98 | end_offset=None, 99 | limit_to_registered_models=None, 100 | result_class=None, 101 | **kwargs 102 | ): 103 | return {"results": [], "hits": 0} 104 | 105 | 106 | class SimpleSearchQuery(BaseSearchQuery): 107 | def build_query(self): 108 | if not self.query_filter: 109 | return "*" 110 | 111 | return self._build_sub_query(self.query_filter) 112 | 113 | def _build_sub_query(self, search_node): 114 | term_list = [] 115 | 116 | for child in search_node.children: 117 | if isinstance(child, SearchNode): 118 | term_list.append(self._build_sub_query(child)) 119 | else: 120 | value = child[1] 121 | 122 | if not hasattr(value, "input_type_name"): 123 | value = PythonData(value) 124 | 125 | term_list.append(value.prepare(self)) 126 | 127 | return (" ").join(map(str, term_list)) 128 | 129 | 130 | class SimpleEngine(BaseEngine): 131 | backend = SimpleSearchBackend 132 | query = SimpleSearchQuery 133 | -------------------------------------------------------------------------------- /haystack/constants.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | DEFAULT_ALIAS = "default" 4 | 5 | # Reserved field names 6 | ID = getattr(settings, "HAYSTACK_ID_FIELD", "id") 7 | DJANGO_CT = getattr(settings, "HAYSTACK_DJANGO_CT_FIELD", "django_ct") 8 | DJANGO_ID = getattr(settings, "HAYSTACK_DJANGO_ID_FIELD", "django_id") 9 | DOCUMENT_FIELD = getattr(settings, "HAYSTACK_DOCUMENT_FIELD", "text") 10 | 11 | # Default operator. Valid options are AND/OR. 12 | DEFAULT_OPERATOR = getattr(settings, "HAYSTACK_DEFAULT_OPERATOR", "AND") 13 | 14 | # Default values on elasticsearch 15 | FUZZINESS = getattr(settings, "HAYSTACK_FUZZINESS", "AUTO") 16 | FUZZY_MIN_SIM = getattr(settings, "HAYSTACK_FUZZY_MIN_SIM", 0.5) 17 | FUZZY_MAX_EXPANSIONS = getattr(settings, "HAYSTACK_FUZZY_MAX_EXPANSIONS", 50) 18 | 19 | # Default values on whoosh 20 | FUZZY_WHOOSH_MIN_PREFIX = getattr(settings, "HAYSTACK_FUZZY_WHOOSH_MIN_PREFIX", 3) 21 | FUZZY_WHOOSH_MAX_EDITS = getattr(settings, "HAYSTACK_FUZZY_WHOOSH_MAX_EDITS", 2) 22 | 23 | # Valid expression extensions. 24 | VALID_FILTERS = { 25 | "contains", 26 | "exact", 27 | "gt", 28 | "gte", 29 | "lt", 30 | "lte", 31 | "in", 32 | "startswith", 33 | "range", 34 | "endswith", 35 | "content", 36 | "fuzzy", 37 | } 38 | 39 | 40 | FILTER_SEPARATOR = "__" 41 | 42 | # The maximum number of items to display in a SearchQuerySet.__repr__ 43 | REPR_OUTPUT_SIZE = 20 44 | 45 | # Number of SearchResults to load at a time. 46 | ITERATOR_LOAD_PER_QUERY = getattr(settings, "HAYSTACK_ITERATOR_LOAD_PER_QUERY", 10) 47 | 48 | 49 | # A marker class in the hierarchy to indicate that it handles search data. 50 | class Indexable(object): 51 | haystack_use_for_indexing = True 52 | 53 | 54 | # For the geo bits, since that's what Solr & Elasticsearch seem to silently 55 | # assume... 56 | WGS_84_SRID = 4326 57 | -------------------------------------------------------------------------------- /haystack/exceptions.py: -------------------------------------------------------------------------------- 1 | class HaystackError(Exception): 2 | """A generic exception for all others to extend.""" 3 | 4 | pass 5 | 6 | 7 | class SearchBackendError(HaystackError): 8 | """Raised when a backend can not be found.""" 9 | 10 | pass 11 | 12 | 13 | class SearchFieldError(HaystackError): 14 | """Raised when a field encounters an error.""" 15 | 16 | pass 17 | 18 | 19 | class MissingDependency(HaystackError): 20 | """Raised when a library a backend depends on can not be found.""" 21 | 22 | pass 23 | 24 | 25 | class NotHandled(HaystackError): 26 | """Raised when a model is not handled by the router setup.""" 27 | 28 | pass 29 | 30 | 31 | class MoreLikeThisError(HaystackError): 32 | """Raised when a model instance has not been provided for More Like This.""" 33 | 34 | pass 35 | 36 | 37 | class FacetingError(HaystackError): 38 | """Raised when incorrect arguments have been provided for faceting.""" 39 | 40 | pass 41 | 42 | 43 | class SpatialError(HaystackError): 44 | """Raised when incorrect arguments have been provided for spatial.""" 45 | 46 | pass 47 | 48 | 49 | class StatsError(HaystackError): 50 | "Raised when incorrect arguments have been provided for stats" 51 | pass 52 | 53 | 54 | class SkipDocument(HaystackError): 55 | """Raised when a document should be skipped while updating""" 56 | 57 | pass 58 | -------------------------------------------------------------------------------- /haystack/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.encoding import smart_text 3 | from django.utils.text import capfirst 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from haystack import connections 7 | from haystack.constants import DEFAULT_ALIAS 8 | from haystack.query import EmptySearchQuerySet, SearchQuerySet 9 | from haystack.utils import get_model_ct 10 | from haystack.utils.app_loading import haystack_get_model 11 | 12 | 13 | def model_choices(using=DEFAULT_ALIAS): 14 | choices = [ 15 | (get_model_ct(m), capfirst(smart_text(m._meta.verbose_name_plural))) 16 | for m in connections[using].get_unified_index().get_indexed_models() 17 | ] 18 | return sorted(choices, key=lambda x: x[1]) 19 | 20 | 21 | class SearchForm(forms.Form): 22 | q = forms.CharField( 23 | required=False, 24 | label=_("Search"), 25 | widget=forms.TextInput(attrs={"type": "search"}), 26 | ) 27 | 28 | def __init__(self, *args, **kwargs): 29 | self.searchqueryset = kwargs.pop("searchqueryset", None) 30 | self.load_all = kwargs.pop("load_all", False) 31 | 32 | if self.searchqueryset is None: 33 | self.searchqueryset = SearchQuerySet() 34 | 35 | super().__init__(*args, **kwargs) 36 | 37 | def no_query_found(self): 38 | """ 39 | Determines the behavior when no query was found. 40 | 41 | By default, no results are returned (``EmptySearchQuerySet``). 42 | 43 | Should you want to show all results, override this method in your 44 | own ``SearchForm`` subclass and do ``return self.searchqueryset.all()``. 45 | """ 46 | return EmptySearchQuerySet() 47 | 48 | def search(self): 49 | if not self.is_valid(): 50 | return self.no_query_found() 51 | 52 | if not self.cleaned_data.get("q"): 53 | return self.no_query_found() 54 | 55 | sqs = self.searchqueryset.auto_query(self.cleaned_data["q"]) 56 | 57 | if self.load_all: 58 | sqs = sqs.load_all() 59 | 60 | return sqs 61 | 62 | def get_suggestion(self): 63 | if not self.is_valid(): 64 | return None 65 | 66 | return self.searchqueryset.spelling_suggestion(self.cleaned_data["q"]) 67 | 68 | 69 | class HighlightedSearchForm(SearchForm): 70 | def search(self): 71 | return super().search().highlight() 72 | 73 | 74 | class FacetedSearchForm(SearchForm): 75 | def __init__(self, *args, **kwargs): 76 | self.selected_facets = kwargs.pop("selected_facets", []) 77 | super().__init__(*args, **kwargs) 78 | 79 | def search(self): 80 | sqs = super().search() 81 | 82 | # We need to process each facet to ensure that the field name and the 83 | # value are quoted correctly and separately: 84 | for facet in self.selected_facets: 85 | if ":" not in facet: 86 | continue 87 | 88 | field, value = facet.split(":", 1) 89 | 90 | if value: 91 | sqs = sqs.narrow('%s:"%s"' % (field, sqs.query.clean(value))) 92 | 93 | return sqs 94 | 95 | 96 | class ModelSearchForm(SearchForm): 97 | def __init__(self, *args, **kwargs): 98 | super().__init__(*args, **kwargs) 99 | self.fields["models"] = forms.MultipleChoiceField( 100 | choices=model_choices(), 101 | required=False, 102 | label=_("Search In"), 103 | widget=forms.CheckboxSelectMultiple, 104 | ) 105 | 106 | def get_models(self): 107 | """Return a list of the selected models.""" 108 | search_models = [] 109 | 110 | if self.is_valid(): 111 | for model in self.cleaned_data["models"]: 112 | search_models.append(haystack_get_model(*model.split("."))) 113 | 114 | return search_models 115 | 116 | def search(self): 117 | sqs = super().search() 118 | return sqs.models(*self.get_models()) 119 | 120 | 121 | class HighlightedModelSearchForm(ModelSearchForm): 122 | def search(self): 123 | return super().search().highlight() 124 | 125 | 126 | class FacetedModelSearchForm(ModelSearchForm): 127 | selected_facets = forms.CharField(required=False, widget=forms.HiddenInput) 128 | 129 | def search(self): 130 | sqs = super().search() 131 | 132 | if hasattr(self, "cleaned_data") and self.cleaned_data["selected_facets"]: 133 | sqs = sqs.narrow(self.cleaned_data["selected_facets"]) 134 | 135 | return sqs.models(*self.get_models()) 136 | -------------------------------------------------------------------------------- /haystack/generic_views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.paginator import Paginator 3 | from django.views.generic import FormView 4 | from django.views.generic.edit import FormMixin 5 | from django.views.generic.list import MultipleObjectMixin 6 | 7 | from .forms import FacetedSearchForm, ModelSearchForm 8 | from .query import SearchQuerySet 9 | 10 | RESULTS_PER_PAGE = getattr(settings, "HAYSTACK_SEARCH_RESULTS_PER_PAGE", 20) 11 | 12 | 13 | class SearchMixin(MultipleObjectMixin, FormMixin): 14 | """ 15 | A mixin that allows adding in Haystacks search functionality into 16 | another view class. 17 | 18 | This mixin exhibits similar end functionality as the base Haystack search 19 | view, but with some important distinctions oriented around greater 20 | compatibility with Django's built-in class based views and mixins. 21 | 22 | Normal flow: 23 | 24 | self.request = request 25 | 26 | self.form = self.build_form() 27 | self.query = self.get_query() 28 | self.results = self.get_results() 29 | 30 | return self.create_response() 31 | 32 | This mixin should: 33 | 34 | 1. Make the form 35 | 2. Get the queryset 36 | 3. Return the paginated queryset 37 | 38 | """ 39 | 40 | template_name = "search/search.html" 41 | load_all = True 42 | form_class = ModelSearchForm 43 | context_object_name = None 44 | paginate_by = RESULTS_PER_PAGE 45 | paginate_orphans = 0 46 | paginator_class = Paginator 47 | page_kwarg = "page" 48 | form_name = "form" 49 | search_field = "q" 50 | object_list = None 51 | 52 | def get_queryset(self): 53 | if self.queryset is None: 54 | self.queryset = SearchQuerySet() 55 | return self.queryset 56 | 57 | def get_form_kwargs(self): 58 | """ 59 | Returns the keyword arguments for instantiating the form. 60 | """ 61 | kwargs = {"initial": self.get_initial()} 62 | if self.request.method == "GET": 63 | kwargs.update({"data": self.request.GET}) 64 | kwargs.update( 65 | {"searchqueryset": self.get_queryset(), "load_all": self.load_all} 66 | ) 67 | return kwargs 68 | 69 | def form_invalid(self, form): 70 | context = self.get_context_data( 71 | **{self.form_name: form, "object_list": self.get_queryset()} 72 | ) 73 | return self.render_to_response(context) 74 | 75 | def form_valid(self, form): 76 | self.queryset = form.search() 77 | context = self.get_context_data( 78 | **{ 79 | self.form_name: form, 80 | "query": form.cleaned_data.get(self.search_field), 81 | "object_list": self.queryset, 82 | } 83 | ) 84 | return self.render_to_response(context) 85 | 86 | 87 | class FacetedSearchMixin(SearchMixin): 88 | """ 89 | A mixin that allows adding in a Haystack search functionality with search 90 | faceting. 91 | """ 92 | 93 | form_class = FacetedSearchForm 94 | facet_fields = None 95 | 96 | def get_form_kwargs(self): 97 | kwargs = super().get_form_kwargs() 98 | kwargs.update({"selected_facets": self.request.GET.getlist("selected_facets")}) 99 | return kwargs 100 | 101 | def get_context_data(self, **kwargs): 102 | context = super().get_context_data(**kwargs) 103 | context.update({"facets": self.queryset.facet_counts()}) 104 | return context 105 | 106 | def get_queryset(self): 107 | qs = super().get_queryset() 108 | for field in self.facet_fields: 109 | qs = qs.facet(field) 110 | return qs 111 | 112 | 113 | class SearchView(SearchMixin, FormView): 114 | """A view class for searching a Haystack managed search index""" 115 | 116 | def get(self, request, *args, **kwargs): 117 | """ 118 | Handles GET requests and instantiates a blank version of the form. 119 | """ 120 | form_class = self.get_form_class() 121 | form = self.get_form(form_class) 122 | 123 | if form.is_valid(): 124 | return self.form_valid(form) 125 | else: 126 | return self.form_invalid(form) 127 | 128 | 129 | class FacetedSearchView(FacetedSearchMixin, SearchView): 130 | """ 131 | A view class for searching a Haystack managed search index with 132 | facets 133 | """ 134 | 135 | pass 136 | -------------------------------------------------------------------------------- /haystack/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/haystack/management/__init__.py -------------------------------------------------------------------------------- /haystack/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/haystack/management/commands/__init__.py -------------------------------------------------------------------------------- /haystack/management/commands/clear_index.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from haystack import connections 4 | 5 | 6 | class Command(BaseCommand): 7 | help = "Clears out the search index completely." # noqa A003 8 | 9 | def add_arguments(self, parser): 10 | parser.add_argument( 11 | "--noinput", 12 | action="store_false", 13 | dest="interactive", 14 | default=True, 15 | help="If provided, no prompts will be issued to the user and the data will be wiped out.", 16 | ) 17 | parser.add_argument( 18 | "-u", 19 | "--using", 20 | action="append", 21 | default=[], 22 | help="Update only the named backend (can be used multiple times). " 23 | "By default all backends will be updated.", 24 | ) 25 | parser.add_argument( 26 | "--nocommit", 27 | action="store_false", 28 | dest="commit", 29 | default=True, 30 | help="Will pass commit=False to the backend.", 31 | ) 32 | 33 | def handle(self, **options): 34 | """Clears out the search index completely.""" 35 | self.verbosity = int(options.get("verbosity", 1)) 36 | self.commit = options.get("commit", True) 37 | 38 | using = options.get("using") 39 | if not using: 40 | using = connections.connections_info.keys() 41 | 42 | if options.get("interactive", True): 43 | self.stdout.write( 44 | "WARNING: This will irreparably remove EVERYTHING from your search index in connection '%s'." 45 | % "', '".join(using) 46 | ) 47 | self.stdout.write( 48 | "Your choices after this are to restore from backups or rebuild via the `rebuild_index` command." 49 | ) 50 | 51 | yes_or_no = input("Are you sure you wish to continue? [y/N] ") 52 | 53 | if not yes_or_no.lower().startswith("y"): 54 | self.stdout.write("No action taken.") 55 | return 56 | 57 | if self.verbosity >= 1: 58 | self.stdout.write( 59 | "Removing all documents from your index because you said so." 60 | ) 61 | 62 | for backend_name in using: 63 | backend = connections[backend_name].get_backend() 64 | backend.clear(commit=self.commit) 65 | 66 | if self.verbosity >= 1: 67 | self.stdout.write("All documents removed.") 68 | -------------------------------------------------------------------------------- /haystack/management/commands/haystack_info.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from haystack import connections 4 | from haystack.constants import DEFAULT_ALIAS 5 | 6 | 7 | class Command(BaseCommand): 8 | help = "Provides feedback about the current Haystack setup." # noqa A003 9 | 10 | def handle(self, **options): 11 | """Provides feedback about the current Haystack setup.""" 12 | 13 | unified_index = connections[DEFAULT_ALIAS].get_unified_index() 14 | indexed = unified_index.get_indexed_models() 15 | index_count = len(indexed) 16 | self.stdout.write("Number of handled %s index(es)." % index_count) 17 | 18 | for index in indexed: 19 | self.stdout.write( 20 | " - Model: %s by Index: %s" 21 | % (index.__name__, unified_index.get_indexes()[index]) 22 | ) 23 | -------------------------------------------------------------------------------- /haystack/management/commands/rebuild_index.py: -------------------------------------------------------------------------------- 1 | from django.core.management import call_command 2 | from django.core.management.base import BaseCommand 3 | 4 | from .update_index import DEFAULT_MAX_RETRIES 5 | 6 | 7 | class Command(BaseCommand): 8 | help = "Completely rebuilds the search index by removing the old data and then updating." # noqa A003 9 | 10 | def add_arguments(self, parser): 11 | parser.add_argument( 12 | "--noinput", 13 | action="store_false", 14 | dest="interactive", 15 | default=True, 16 | help="If provided, no prompts will be issued to the user and the data will be wiped out.", 17 | ) 18 | parser.add_argument( 19 | "-u", 20 | "--using", 21 | action="append", 22 | default=[], 23 | help="Update only the named backend (can be used multiple times). " 24 | "By default all backends will be updated.", 25 | ) 26 | parser.add_argument( 27 | "-k", 28 | "--workers", 29 | default=0, 30 | type=int, 31 | help="Allows for the use multiple workers to parallelize indexing. Requires multiprocessing.", 32 | ) 33 | parser.add_argument( 34 | "--nocommit", 35 | action="store_false", 36 | dest="commit", 37 | default=True, 38 | help="Will pass commit=False to the backend.", 39 | ) 40 | parser.add_argument( 41 | "-b", 42 | "--batch-size", 43 | dest="batchsize", 44 | type=int, 45 | help="Number of items to index at once.", 46 | ) 47 | parser.add_argument( 48 | "-t", 49 | "--max-retries", 50 | action="store", 51 | dest="max_retries", 52 | type=int, 53 | default=DEFAULT_MAX_RETRIES, 54 | help="Maximum number of attempts to write to the backend when an error occurs.", 55 | ) 56 | 57 | def handle(self, **options): 58 | clear_options = options.copy() 59 | update_options = options.copy() 60 | for key in ("batchsize", "workers", "max_retries"): 61 | del clear_options[key] 62 | for key in ("interactive",): 63 | del update_options[key] 64 | call_command("clear_index", **clear_options) 65 | call_command("update_index", **update_options) 66 | -------------------------------------------------------------------------------- /haystack/manager.py: -------------------------------------------------------------------------------- 1 | from haystack.query import EmptySearchQuerySet, SearchQuerySet 2 | 3 | 4 | class SearchIndexManager(object): 5 | def __init__(self, using=None): 6 | super().__init__() 7 | self.using = using 8 | 9 | def get_search_queryset(self): 10 | """Returns a new SearchQuerySet object. Subclasses can override this method 11 | to easily customize the behavior of the Manager. 12 | """ 13 | return SearchQuerySet(using=self.using) 14 | 15 | def get_empty_query_set(self): 16 | return EmptySearchQuerySet(using=self.using) 17 | 18 | def all(self): # noqa A003 19 | return self.get_search_queryset() 20 | 21 | def none(self): 22 | return self.get_empty_query_set() 23 | 24 | def filter(self, *args, **kwargs): # noqa A003 25 | return self.get_search_queryset().filter(*args, **kwargs) 26 | 27 | def exclude(self, *args, **kwargs): 28 | return self.get_search_queryset().exclude(*args, **kwargs) 29 | 30 | def filter_and(self, *args, **kwargs): 31 | return self.get_search_queryset().filter_and(*args, **kwargs) 32 | 33 | def filter_or(self, *args, **kwargs): 34 | return self.get_search_queryset().filter_or(*args, **kwargs) 35 | 36 | def order_by(self, *args): 37 | return self.get_search_queryset().order_by(*args) 38 | 39 | def highlight(self): 40 | return self.get_search_queryset().highlight() 41 | 42 | def boost(self, term, boost): 43 | return self.get_search_queryset().boost(term, boost) 44 | 45 | def facet(self, field): 46 | return self.get_search_queryset().facet(field) 47 | 48 | def within(self, field, point_1, point_2): 49 | return self.get_search_queryset().within(field, point_1, point_2) 50 | 51 | def dwithin(self, field, point, distance): 52 | return self.get_search_queryset().dwithin(field, point, distance) 53 | 54 | def distance(self, field, point): 55 | return self.get_search_queryset().distance(field, point) 56 | 57 | def date_facet(self, field, start_date, end_date, gap_by, gap_amount=1): 58 | return self.get_search_queryset().date_facet( 59 | field, start_date, end_date, gap_by, gap_amount=1 60 | ) 61 | 62 | def query_facet(self, field, query): 63 | return self.get_search_queryset().query_facet(field, query) 64 | 65 | def narrow(self, query): 66 | return self.get_search_queryset().narrow(query) 67 | 68 | def raw_search(self, query_string, **kwargs): 69 | return self.get_search_queryset().raw_search(query_string, **kwargs) 70 | 71 | def load_all(self): 72 | return self.get_search_queryset().load_all() 73 | 74 | def auto_query(self, query_string, fieldname="content"): 75 | return self.get_search_queryset().auto_query(query_string, fieldname=fieldname) 76 | 77 | def autocomplete(self, **kwargs): 78 | return self.get_search_queryset().autocomplete(**kwargs) 79 | 80 | def using(self, connection_name): 81 | return self.get_search_queryset().using(connection_name) 82 | 83 | def count(self): 84 | return self.get_search_queryset().count() 85 | 86 | def best_match(self): 87 | return self.get_search_queryset().best_match() 88 | 89 | def latest(self, date_field): 90 | return self.get_search_queryset().latest(date_field) 91 | 92 | def more_like_this(self, model_instance): 93 | return self.get_search_queryset().more_like_this(model_instance) 94 | 95 | def facet_counts(self): 96 | return self.get_search_queryset().facet_counts() 97 | 98 | def spelling_suggestion(self, preferred_query=None): 99 | return self.get_search_queryset().spelling_suggestion(preferred_query=None) 100 | 101 | def values(self, *fields): 102 | return self.get_search_queryset().values(*fields) 103 | 104 | def values_list(self, *fields, **kwargs): 105 | return self.get_search_queryset().values_list(*fields, **kwargs) 106 | -------------------------------------------------------------------------------- /haystack/panels.py: -------------------------------------------------------------------------------- 1 | from debug_toolbar.panels import DebugPanel 2 | from django.template.loader import render_to_string 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from haystack import connections 6 | 7 | 8 | class HaystackDebugPanel(DebugPanel): 9 | """ 10 | Panel that displays information about the Haystack queries run while 11 | processing the request. 12 | """ 13 | 14 | name = "Haystack" 15 | has_content = True 16 | 17 | def __init__(self, *args, **kwargs): 18 | super().__init__(*args, **kwargs) 19 | self._offset = { 20 | alias: len(connections[alias].queries) 21 | for alias in connections.connections_info.keys() 22 | } 23 | self._search_time = 0 24 | self._queries = [] 25 | self._backends = {} 26 | 27 | def nav_title(self): 28 | return _("Haystack") 29 | 30 | def nav_subtitle(self): 31 | self._queries = [] 32 | self._backends = {} 33 | 34 | for alias in connections.connections_info.keys(): 35 | search_queries = connections[alias].queries[self._offset[alias] :] 36 | self._backends[alias] = { 37 | "time_spent": sum(float(q["time"]) for q in search_queries), 38 | "queries": len(search_queries), 39 | } 40 | self._queries.extend([(alias, q) for q in search_queries]) 41 | 42 | self._queries.sort(key=lambda x: x[1]["start"]) 43 | self._search_time = sum([d["time_spent"] for d in self._backends.values()]) 44 | num_queries = len(self._queries) 45 | return "%d %s in %.2fms" % ( 46 | num_queries, 47 | (num_queries == 1) and "query" or "queries", 48 | self._search_time, 49 | ) 50 | 51 | def title(self): 52 | return _("Search Queries") 53 | 54 | def url(self): 55 | return "" 56 | 57 | def content(self): 58 | width_ratio_tally = 0 59 | 60 | for alias, query in self._queries: 61 | query["alias"] = alias 62 | query["query"] = query["query_string"] 63 | 64 | if query.get("additional_kwargs"): 65 | if query["additional_kwargs"].get("result_class"): 66 | query["additional_kwargs"]["result_class"] = str( 67 | query["additional_kwargs"]["result_class"] 68 | ) 69 | 70 | try: 71 | query["width_ratio"] = (float(query["time"]) / self._search_time) * 100 72 | except ZeroDivisionError: 73 | query["width_ratio"] = 0 74 | 75 | query["start_offset"] = width_ratio_tally 76 | width_ratio_tally += query["width_ratio"] 77 | 78 | context = self.context.copy() 79 | context.update( 80 | { 81 | "backends": sorted( 82 | self._backends.items(), key=lambda x: -x[1]["time_spent"] 83 | ), 84 | "queries": [q for a, q in self._queries], 85 | "sql_time": self._search_time, 86 | } 87 | ) 88 | 89 | return render_to_string("panels/haystack.html", context) 90 | -------------------------------------------------------------------------------- /haystack/routers.py: -------------------------------------------------------------------------------- 1 | from haystack.constants import DEFAULT_ALIAS 2 | 3 | 4 | class BaseRouter(object): 5 | # Reserved for future extension. 6 | pass 7 | 8 | 9 | class DefaultRouter(BaseRouter): 10 | def for_read(self, **hints): 11 | return DEFAULT_ALIAS 12 | 13 | def for_write(self, **hints): 14 | return DEFAULT_ALIAS 15 | -------------------------------------------------------------------------------- /haystack/signals.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from haystack.exceptions import NotHandled 4 | 5 | 6 | class BaseSignalProcessor(object): 7 | """ 8 | A convenient way to attach Haystack to Django's signals & cause things to 9 | index. 10 | 11 | By default, does nothing with signals but provides underlying functionality. 12 | """ 13 | 14 | def __init__(self, connections, connection_router): 15 | self.connections = connections 16 | self.connection_router = connection_router 17 | self.setup() 18 | 19 | def setup(self): 20 | """ 21 | A hook for setting up anything necessary for 22 | ``handle_save/handle_delete`` to be executed. 23 | 24 | Default behavior is to do nothing (``pass``). 25 | """ 26 | # Do nothing. 27 | pass 28 | 29 | def teardown(self): 30 | """ 31 | A hook for tearing down anything necessary for 32 | ``handle_save/handle_delete`` to no longer be executed. 33 | 34 | Default behavior is to do nothing (``pass``). 35 | """ 36 | # Do nothing. 37 | pass 38 | 39 | def handle_save(self, sender, instance, **kwargs): 40 | """ 41 | Given an individual model instance, determine which backends the 42 | update should be sent to & update the object on those backends. 43 | """ 44 | using_backends = self.connection_router.for_write(instance=instance) 45 | 46 | for using in using_backends: 47 | try: 48 | index = self.connections[using].get_unified_index().get_index(sender) 49 | index.update_object(instance, using=using) 50 | except NotHandled: 51 | # TODO: Maybe log it or let the exception bubble? 52 | pass 53 | 54 | def handle_delete(self, sender, instance, **kwargs): 55 | """ 56 | Given an individual model instance, determine which backends the 57 | delete should be sent to & delete the object on those backends. 58 | """ 59 | using_backends = self.connection_router.for_write(instance=instance) 60 | 61 | for using in using_backends: 62 | try: 63 | index = self.connections[using].get_unified_index().get_index(sender) 64 | index.remove_object(instance, using=using) 65 | except NotHandled: 66 | # TODO: Maybe log it or let the exception bubble? 67 | pass 68 | 69 | 70 | class RealtimeSignalProcessor(BaseSignalProcessor): 71 | """ 72 | Allows for observing when saves/deletes fire & automatically updates the 73 | search engine appropriately. 74 | """ 75 | 76 | def setup(self): 77 | # Naive (listen to all model saves). 78 | models.signals.post_save.connect(self.handle_save) 79 | models.signals.post_delete.connect(self.handle_delete) 80 | # Efficient would be going through all backends & collecting all models 81 | # being used, then hooking up signals only for those. 82 | 83 | def teardown(self): 84 | # Naive (listen to all model saves). 85 | models.signals.post_save.disconnect(self.handle_save) 86 | models.signals.post_delete.disconnect(self.handle_delete) 87 | # Efficient would be going through all backends & collecting all models 88 | # being used, then disconnecting signals only for those. 89 | -------------------------------------------------------------------------------- /haystack/templates/panels/haystack.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% for query in queries %} 14 | 15 | 20 | 21 | 24 | 25 | 30 | 31 | {% endfor %} 32 | 33 |
{% trans 'Query' %}{% trans 'Backend Alias' %}{% trans 'Timeline' %}{% trans 'Time' %} (ms){% trans 'Kwargs' %}
16 |
17 |
{{ query.query_string|safe }}
18 |
19 |
{{ query.alias }} 22 |   23 | {{ query.time }} 26 | {% for key, value in query.additional_kwargs.items %} 27 | '{{ key }}': {{ value|stringformat:"r" }}
28 | {% endfor %} 29 |
34 | -------------------------------------------------------------------------------- /haystack/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/haystack/templatetags/__init__.py -------------------------------------------------------------------------------- /haystack/templatetags/more_like_this.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django import template 4 | 5 | from haystack.query import SearchQuerySet 6 | from haystack.utils.app_loading import haystack_get_model 7 | 8 | register = template.Library() 9 | 10 | 11 | class MoreLikeThisNode(template.Node): 12 | def __init__(self, model, varname, for_types=None, limit=None): 13 | self.model = template.Variable(model) 14 | self.varname = varname 15 | self.for_types = for_types 16 | self.limit = limit 17 | 18 | if self.limit is not None: 19 | self.limit = int(self.limit) 20 | 21 | def render(self, context): 22 | try: 23 | model_instance = self.model.resolve(context) 24 | sqs = SearchQuerySet() 25 | 26 | if self.for_types is not None: 27 | intermediate = template.Variable(self.for_types) 28 | for_types = intermediate.resolve(context).split(",") 29 | search_models = [] 30 | 31 | for model in for_types: 32 | model_class = haystack_get_model(*model.split(".")) 33 | 34 | if model_class: 35 | search_models.append(model_class) 36 | 37 | sqs = sqs.models(*search_models) 38 | 39 | sqs = sqs.more_like_this(model_instance) 40 | 41 | if self.limit is not None: 42 | sqs = sqs[: self.limit] 43 | 44 | context[self.varname] = sqs 45 | except Exception as exc: 46 | logging.warning( 47 | "Unhandled exception rendering %r: %s", self, exc, exc_info=True 48 | ) 49 | 50 | return "" 51 | 52 | 53 | @register.tag 54 | def more_like_this(parser, token): 55 | """ 56 | Fetches similar items from the search index to find content that is similar 57 | to the provided model's content. 58 | 59 | Syntax:: 60 | 61 | {% more_like_this model_instance as varname [for app_label.model_name,app_label.model_name,...] [limit n] %} 62 | 63 | Example:: 64 | 65 | # Pull a full SearchQuerySet (lazy loaded) of similar content. 66 | {% more_like_this entry as related_content %} 67 | 68 | # Pull just the top 5 similar pieces of content. 69 | {% more_like_this entry as related_content limit 5 %} 70 | 71 | # Pull just the top 5 similar entries or comments. 72 | {% more_like_this entry as related_content for "blog.entry,comments.comment" limit 5 %} 73 | """ 74 | bits = token.split_contents() 75 | 76 | if not len(bits) in (4, 6, 8): 77 | raise template.TemplateSyntaxError( 78 | "'%s' tag requires either 3, 5 or 7 arguments." % bits[0] 79 | ) 80 | 81 | model = bits[1] 82 | 83 | if bits[2] != "as": 84 | raise template.TemplateSyntaxError( 85 | "'%s' tag's second argument should be 'as'." % bits[0] 86 | ) 87 | 88 | varname = bits[3] 89 | limit = None 90 | for_types = None 91 | 92 | if len(bits) == 6: 93 | if bits[4] != "limit" and bits[4] != "for": 94 | raise template.TemplateSyntaxError( 95 | "'%s' tag's fourth argument should be either 'limit' or 'for'." 96 | % bits[0] 97 | ) 98 | 99 | if bits[4] == "limit": 100 | limit = bits[5] 101 | else: 102 | for_types = bits[5] 103 | 104 | if len(bits) == 8: 105 | if bits[4] != "for": 106 | raise template.TemplateSyntaxError( 107 | "'%s' tag's fourth argument should be 'for'." % bits[0] 108 | ) 109 | 110 | for_types = bits[5] 111 | 112 | if bits[6] != "limit": 113 | raise template.TemplateSyntaxError( 114 | "'%s' tag's sixth argument should be 'limit'." % bits[0] 115 | ) 116 | 117 | limit = bits[7] 118 | 119 | return MoreLikeThisNode(model, varname, for_types, limit) 120 | -------------------------------------------------------------------------------- /haystack/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from haystack.views import SearchView 4 | 5 | urlpatterns = [path("", SearchView(), name="haystack_search")] 6 | -------------------------------------------------------------------------------- /haystack/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import re 3 | 4 | from django.conf import settings 5 | 6 | from haystack.constants import DJANGO_CT, DJANGO_ID, ID 7 | 8 | IDENTIFIER_REGEX = re.compile(r"^[\w\d_]+\.[\w\d_]+\.[\w\d-]+$") 9 | 10 | 11 | def default_get_identifier(obj_or_string): 12 | """ 13 | Get an unique identifier for the object or a string representing the 14 | object. 15 | 16 | If not overridden, uses ... 17 | """ 18 | if isinstance(obj_or_string, str): 19 | if not IDENTIFIER_REGEX.match(obj_or_string): 20 | raise AttributeError( 21 | "Provided string '%s' is not a valid identifier." % obj_or_string 22 | ) 23 | 24 | return obj_or_string 25 | 26 | return "%s.%s" % (get_model_ct(obj_or_string), obj_or_string._get_pk_val()) 27 | 28 | 29 | def _lookup_identifier_method(): 30 | """ 31 | If the user has set HAYSTACK_IDENTIFIER_METHOD, import it and return the method uncalled. 32 | If HAYSTACK_IDENTIFIER_METHOD is not defined, return haystack.utils.default_get_identifier. 33 | 34 | This always runs at module import time. We keep the code in a function 35 | so that it can be called from unit tests, in order to simulate the re-loading 36 | of this module. 37 | """ 38 | if not hasattr(settings, "HAYSTACK_IDENTIFIER_METHOD"): 39 | return default_get_identifier 40 | 41 | module_path, method_name = settings.HAYSTACK_IDENTIFIER_METHOD.rsplit(".", 1) 42 | 43 | try: 44 | module = importlib.import_module(module_path) 45 | except ImportError: 46 | raise ImportError( 47 | "Unable to import module '%s' provided for HAYSTACK_IDENTIFIER_METHOD." 48 | % module_path 49 | ) 50 | 51 | identifier_method = getattr(module, method_name, None) 52 | 53 | if not identifier_method: 54 | raise AttributeError( 55 | "Provided method '%s' for HAYSTACK_IDENTIFIER_METHOD does not exist in '%s'." 56 | % (method_name, module_path) 57 | ) 58 | 59 | return identifier_method 60 | 61 | 62 | get_identifier = _lookup_identifier_method() 63 | 64 | 65 | def get_model_ct_tuple(model): 66 | # Deferred models should be identified as if they were the underlying model. 67 | model_name = ( 68 | model._meta.concrete_model._meta.model_name 69 | if hasattr(model, "_deferred") and model._deferred 70 | else model._meta.model_name 71 | ) 72 | return (model._meta.app_label, model_name) 73 | 74 | 75 | def get_model_ct(model): 76 | return "%s.%s" % get_model_ct_tuple(model) 77 | 78 | 79 | def get_facet_field_name(fieldname): 80 | if fieldname in [ID, DJANGO_ID, DJANGO_CT]: 81 | return fieldname 82 | 83 | return "%s_exact" % fieldname 84 | -------------------------------------------------------------------------------- /haystack/utils/app_loading.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | from django.core.exceptions import ImproperlyConfigured 3 | 4 | __all__ = ["haystack_get_models", "haystack_load_apps"] 5 | 6 | APP = "app" 7 | MODEL = "model" 8 | 9 | 10 | def haystack_get_app_modules(): 11 | """Return the Python module for each installed app""" 12 | return [i.module for i in apps.get_app_configs()] 13 | 14 | 15 | def haystack_load_apps(): 16 | """Return a list of app labels for all installed applications which have models""" 17 | return [i.label for i in apps.get_app_configs() if i.models_module is not None] 18 | 19 | 20 | def haystack_get_models(label): 21 | try: 22 | app_mod = apps.get_app_config(label) 23 | return app_mod.get_models() 24 | except LookupError: 25 | if "." not in label: 26 | raise ImproperlyConfigured("Unknown application label {}".format(label)) 27 | app_label, model_name = label.rsplit(".", 1) 28 | return [apps.get_model(app_label, model_name)] 29 | except ImproperlyConfigured: 30 | pass 31 | 32 | 33 | def haystack_get_model(app_label, model_name): 34 | return apps.get_model(app_label, model_name) 35 | -------------------------------------------------------------------------------- /haystack/utils/geo.py: -------------------------------------------------------------------------------- 1 | from haystack.constants import WGS_84_SRID 2 | from haystack.exceptions import SpatialError 3 | 4 | 5 | def ensure_geometry(geom): 6 | """ 7 | Makes sure the parameter passed in looks like a GEOS ``GEOSGeometry``. 8 | """ 9 | if not hasattr(geom, "geom_type"): 10 | raise SpatialError("Point '%s' doesn't appear to be a GEOS geometry." % geom) 11 | 12 | return geom 13 | 14 | 15 | def ensure_point(geom): 16 | """ 17 | Makes sure the parameter passed in looks like a GEOS ``Point``. 18 | """ 19 | ensure_geometry(geom) 20 | 21 | if geom.geom_type != "Point": 22 | raise SpatialError("Provided geometry '%s' is not a 'Point'." % geom) 23 | 24 | return geom 25 | 26 | 27 | def ensure_wgs84(point): 28 | """ 29 | Ensures the point passed in is a GEOS ``Point`` & returns that point's 30 | data is in the WGS-84 spatial reference. 31 | """ 32 | ensure_point(point) 33 | # Clone it so we don't alter the original, in case they're using it for 34 | # something else. 35 | new_point = point.clone() 36 | 37 | if not new_point.srid: 38 | # It has no spatial reference id. Assume WGS-84. 39 | new_point.srid = WGS_84_SRID 40 | elif new_point.srid != WGS_84_SRID: 41 | # Transform it to get to the right system. 42 | new_point.transform(WGS_84_SRID) 43 | 44 | return new_point 45 | 46 | 47 | def ensure_distance(dist): 48 | """ 49 | Makes sure the parameter passed in is a 'Distance' object. 50 | """ 51 | try: 52 | # Since we mostly only care about the ``.km`` attribute, make sure 53 | # it's there. 54 | dist.km 55 | except AttributeError: 56 | raise SpatialError("'%s' does not appear to be a 'Distance' object." % dist) 57 | 58 | return dist 59 | 60 | 61 | def generate_bounding_box(bottom_left, top_right): 62 | """ 63 | Takes two opposite corners of a bounding box (order matters!) & generates 64 | a two-tuple of the correct coordinates for the bounding box. 65 | 66 | The two-tuple is in the form ``((min_lat, min_lng), (max_lat, max_lng))``. 67 | """ 68 | west, lat_1 = bottom_left.coords 69 | east, lat_2 = top_right.coords 70 | min_lat, max_lat = min(lat_1, lat_2), max(lat_1, lat_2) 71 | return ((min_lat, west), (max_lat, east)) 72 | -------------------------------------------------------------------------------- /haystack/utils/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | 5 | 6 | def getLogger(name): 7 | real_logger = logging.getLogger(name) 8 | return LoggingFacade(real_logger) 9 | 10 | 11 | class LoggingFacade(object): 12 | def __init__(self, real_logger): 13 | self.real_logger = real_logger 14 | 15 | def noop(self, *args, **kwargs): 16 | pass 17 | 18 | def __getattr__(self, attr): 19 | if getattr(settings, "HAYSTACK_LOGGING", True): 20 | return getattr(self.real_logger, attr) 21 | return self.noop 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line_length=88 3 | 4 | [tool.isort] 5 | known_first_party = ["haystack", "test_haystack"] 6 | profile = "black" 7 | multi_line_output = 3 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pep8] 2 | line_length=88 3 | exclude=docs 4 | 5 | [flake8] 6 | line_length=88 7 | exclude=docs,tests 8 | ignore=E203, E501, W503, D 9 | 10 | [options] 11 | setup_requires = 12 | setuptools_scm 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | try: 5 | from setuptools import setup 6 | except ImportError: 7 | from ez_setup import use_setuptools 8 | 9 | use_setuptools() 10 | from setuptools import setup 11 | 12 | install_requires = ["Django>=2.2"] 13 | 14 | tests_require = [ 15 | "pysolr>=3.7.0", 16 | "whoosh>=2.5.4,<3.0", 17 | "python-dateutil", 18 | "geopy==2.0.0", 19 | "nose", 20 | "coverage", 21 | "requests", 22 | ] 23 | 24 | setup( 25 | name="django-haystack", 26 | use_scm_version=True, 27 | description="Pluggable search for Django.", 28 | author="Daniel Lindsley", 29 | author_email="daniel@toastdriven.com", 30 | long_description=open("README.rst", "r").read(), 31 | url="http://haystacksearch.org/", 32 | packages=[ 33 | "haystack", 34 | "haystack.backends", 35 | "haystack.management", 36 | "haystack.management.commands", 37 | "haystack.templatetags", 38 | "haystack.utils", 39 | ], 40 | package_data={ 41 | "haystack": ["templates/panels/*", "templates/search_configuration/*"] 42 | }, 43 | classifiers=[ 44 | "Development Status :: 5 - Production/Stable", 45 | "Environment :: Web Environment", 46 | "Framework :: Django", 47 | "Framework :: Django :: 2.2", 48 | "Framework :: Django :: 3.0", 49 | "Intended Audience :: Developers", 50 | "License :: OSI Approved :: BSD License", 51 | "Operating System :: OS Independent", 52 | "Programming Language :: Python", 53 | "Programming Language :: Python :: 3", 54 | "Programming Language :: Python :: 3.5", 55 | "Programming Language :: Python :: 3.6", 56 | "Programming Language :: Python :: 3.7", 57 | "Programming Language :: Python :: 3.8", 58 | "Topic :: Utilities", 59 | ], 60 | zip_safe=False, 61 | install_requires=install_requires, 62 | tests_require=tests_require, 63 | extras_require={ 64 | "elasticsearch": ["elasticsearch>=5,<6"], 65 | }, 66 | test_suite="test_haystack.run_tests.run_all", 67 | ) 68 | -------------------------------------------------------------------------------- /test_haystack/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | test_runner = None 4 | old_config = None 5 | 6 | os.environ["DJANGO_SETTINGS_MODULE"] = "test_haystack.settings" 7 | 8 | 9 | import django 10 | 11 | django.setup() 12 | 13 | 14 | def setup(): 15 | global test_runner 16 | global old_config 17 | 18 | from django.test.runner import DiscoverRunner 19 | 20 | test_runner = DiscoverRunner() 21 | test_runner.setup_test_environment() 22 | old_config = test_runner.setup_databases() 23 | 24 | 25 | def teardown(): 26 | test_runner.teardown_databases(old_config) 27 | test_runner.teardown_test_environment() 28 | -------------------------------------------------------------------------------- /test_haystack/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/test_haystack/core/__init__.py -------------------------------------------------------------------------------- /test_haystack/core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from haystack.admin import SearchModelAdmin 4 | 5 | from .models import MockModel 6 | 7 | 8 | class MockModelAdmin(SearchModelAdmin): 9 | haystack_connection = "solr" 10 | date_hierarchy = "pub_date" 11 | list_display = ("author", "pub_date") 12 | 13 | 14 | admin.site.register(MockModel, MockModelAdmin) 15 | -------------------------------------------------------------------------------- /test_haystack/core/custom_identifier.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | 4 | def get_identifier_method(key): 5 | """ 6 | Custom get_identifier method used for testing the 7 | setting HAYSTACK_IDENTIFIER_MODULE 8 | """ 9 | 10 | if hasattr(key, "get_custom_haystack_id"): 11 | return key.get_custom_haystack_id() 12 | else: 13 | key_bytes = key.encode("utf-8") 14 | return hashlib.md5(key_bytes).hexdigest() 15 | -------------------------------------------------------------------------------- /test_haystack/core/fixtures/base_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "core.mocktag", 5 | "fields": { 6 | "name": "primary" 7 | } 8 | }, 9 | { 10 | "pk": 2, 11 | "model": "core.mocktag", 12 | "fields": { 13 | "name": "secondary" 14 | } 15 | }, 16 | { 17 | "pk": 1, 18 | "model": "core.mockmodel", 19 | "fields": { 20 | "author": "daniel1", 21 | "foo": "bar", 22 | "pub_date": "2009-03-17 06:00:00", 23 | "tag": 1 24 | } 25 | }, 26 | { 27 | "pk": 2, 28 | "model": "core.mockmodel", 29 | "fields": { 30 | "author": "daniel2", 31 | "foo": "bar", 32 | "pub_date": "2009-03-17 07:00:00", 33 | "tag": 1 34 | } 35 | }, 36 | { 37 | "pk": 3, 38 | "model": "core.mockmodel", 39 | "fields": { 40 | "author": "daniel3", 41 | "foo": "bar", 42 | "pub_date": "2009-03-17 08:00:00", 43 | "tag": 2 44 | } 45 | }, 46 | { 47 | "pk": "sometext", 48 | "model": "core.charpkmockmodel", 49 | "fields": { 50 | } 51 | }, 52 | { 53 | "pk": "1234", 54 | "model": "core.charpkmockmodel", 55 | "fields": { 56 | } 57 | }, 58 | { 59 | "pk": 1, 60 | "model": "core.afifthmockmodel", 61 | "fields": { 62 | "author": "sam1", 63 | "deleted": false 64 | } 65 | }, 66 | { 67 | "pk": 2, 68 | "model": "core.afifthmockmodel", 69 | "fields": { 70 | "author": "sam2", 71 | "deleted": true 72 | } 73 | }, 74 | { 75 | "pk": "53554c58-7051-4350-bcc9-dad75eb248a9", 76 | "model": "core.uuidmockmodel", 77 | "fields": { 78 | "characteristics": "some text that was indexed" 79 | } 80 | }, 81 | { 82 | "pk": "77554c58-7051-4350-bcc9-dad75eb24888", 83 | "model": "core.uuidmockmodel", 84 | "fields": { 85 | "characteristics": "more text that was indexed" 86 | } 87 | } 88 | ] 89 | -------------------------------------------------------------------------------- /test_haystack/core/models.py: -------------------------------------------------------------------------------- 1 | # A couple models for Haystack to test with. 2 | import datetime 3 | import uuid 4 | 5 | from django.db import models 6 | 7 | 8 | class MockTag(models.Model): 9 | name = models.CharField(max_length=32) 10 | 11 | def __str__(self): 12 | return self.name 13 | 14 | 15 | class MockModel(models.Model): 16 | author = models.CharField(max_length=255) 17 | foo = models.CharField(max_length=255, blank=True) 18 | pub_date = models.DateTimeField(default=datetime.datetime.now) 19 | tag = models.ForeignKey(MockTag, models.CASCADE) 20 | 21 | def __str__(self): 22 | return self.author 23 | 24 | def hello(self): 25 | return "World!" 26 | 27 | 28 | class UUIDMockModel(models.Model): 29 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 30 | characteristics = models.TextField() 31 | 32 | def __str__(self): 33 | return str(self.id) 34 | 35 | 36 | class AnotherMockModel(models.Model): 37 | author = models.CharField(max_length=255) 38 | pub_date = models.DateTimeField(default=datetime.datetime.now) 39 | 40 | def __str__(self): 41 | return self.author 42 | 43 | 44 | class AThirdMockModel(AnotherMockModel): 45 | average_delay = models.FloatField(default=0.0) 46 | view_count = models.PositiveIntegerField(default=0) 47 | 48 | 49 | class CharPKMockModel(models.Model): 50 | key = models.CharField(primary_key=True, max_length=10) 51 | 52 | 53 | class AFourthMockModel(models.Model): 54 | author = models.CharField(max_length=255) 55 | editor = models.CharField(max_length=255) 56 | pub_date = models.DateTimeField(default=datetime.datetime.now) 57 | 58 | def __str__(self): 59 | return self.author 60 | 61 | 62 | class SoftDeleteManager(models.Manager): 63 | def get_queryset(self): 64 | return super().get_queryset().filter(deleted=False) 65 | 66 | def complete_set(self): 67 | return super().get_queryset() 68 | 69 | 70 | class AFifthMockModel(models.Model): 71 | author = models.CharField(max_length=255) 72 | deleted = models.BooleanField(default=False) 73 | 74 | objects = SoftDeleteManager() 75 | 76 | def __str__(self): 77 | return self.author 78 | 79 | 80 | class ASixthMockModel(models.Model): 81 | name = models.CharField(max_length=255) 82 | lat = models.FloatField() 83 | lon = models.FloatField() 84 | 85 | def __str__(self): 86 | return self.name 87 | 88 | 89 | class ScoreMockModel(models.Model): 90 | score = models.CharField(max_length=10) 91 | 92 | def __str__(self): 93 | return self.score 94 | 95 | 96 | class ManyToManyLeftSideModel(models.Model): 97 | related_models = models.ManyToManyField("ManyToManyRightSideModel") 98 | 99 | 100 | class ManyToManyRightSideModel(models.Model): 101 | name = models.CharField(max_length=32, default="Default name") 102 | 103 | def __str__(self): 104 | return self.name 105 | 106 | 107 | class OneToManyLeftSideModel(models.Model): 108 | pass 109 | 110 | 111 | class OneToManyRightSideModel(models.Model): 112 | left_side = models.ForeignKey( 113 | OneToManyLeftSideModel, models.CASCADE, related_name="right_side" 114 | ) 115 | -------------------------------------------------------------------------------- /test_haystack/core/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} -------------------------------------------------------------------------------- /test_haystack/core/templates/base.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/test_haystack/core/templates/base.html -------------------------------------------------------------------------------- /test_haystack/core/templates/search/indexes/bar.txt: -------------------------------------------------------------------------------- 1 | BAR! 2 | -------------------------------------------------------------------------------- /test_haystack/core/templates/search/indexes/core/mockmodel_content.txt: -------------------------------------------------------------------------------- 1 | Indexed! 2 | {{ object.pk }} -------------------------------------------------------------------------------- /test_haystack/core/templates/search/indexes/core/mockmodel_extra.txt: -------------------------------------------------------------------------------- 1 | Stored! 2 | {{ object.pk }} -------------------------------------------------------------------------------- /test_haystack/core/templates/search/indexes/core/mockmodel_template.txt: -------------------------------------------------------------------------------- 1 | Indexed! 2 | {{ object.pk }} -------------------------------------------------------------------------------- /test_haystack/core/templates/search/indexes/core/mockmodel_text.txt: -------------------------------------------------------------------------------- 1 | Indexed! 2 | {{ object.pk }} -------------------------------------------------------------------------------- /test_haystack/core/templates/search/indexes/foo.txt: -------------------------------------------------------------------------------- 1 | FOO! 2 | -------------------------------------------------------------------------------- /test_haystack/core/templates/search/search.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} -------------------------------------------------------------------------------- /test_haystack/core/templates/test_suggestion.html: -------------------------------------------------------------------------------- 1 | Suggestion: {{ suggestion }} -------------------------------------------------------------------------------- /test_haystack/core/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | 4 | from haystack.forms import FacetedSearchForm 5 | from haystack.query import SearchQuerySet 6 | from haystack.views import FacetedSearchView, SearchView, basic_search 7 | 8 | admin.autodiscover() 9 | 10 | 11 | urlpatterns = [ 12 | path("", SearchView(load_all=False), name="haystack_search"), 13 | path("admin/", admin.site.urls), 14 | path("basic/", basic_search, {"load_all": False}, name="haystack_basic_search"), 15 | path( 16 | "faceted/", 17 | FacetedSearchView( 18 | searchqueryset=SearchQuerySet().facet("author"), 19 | form_class=FacetedSearchForm, 20 | ), 21 | name="haystack_faceted_search", 22 | ), 23 | ] 24 | 25 | urlpatterns += [ 26 | path( 27 | "", 28 | include(("test_haystack.test_app_without_models.urls", "app-without-models")), 29 | ) 30 | ] 31 | -------------------------------------------------------------------------------- /test_haystack/discovery/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/test_haystack/discovery/__init__.py -------------------------------------------------------------------------------- /test_haystack/discovery/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Foo(models.Model): 5 | title = models.CharField(max_length=255) 6 | body = models.TextField() 7 | 8 | def __str__(self): 9 | return self.title 10 | 11 | 12 | class Bar(models.Model): 13 | author = models.CharField(max_length=255) 14 | content = models.TextField() 15 | 16 | def __str__(self): 17 | return self.author 18 | -------------------------------------------------------------------------------- /test_haystack/discovery/search_indexes.py: -------------------------------------------------------------------------------- 1 | from haystack import indexes 2 | from test_haystack.discovery.models import Bar, Foo 3 | 4 | 5 | class FooIndex(indexes.SearchIndex, indexes.Indexable): 6 | text = indexes.CharField(document=True, model_attr="body") 7 | 8 | def get_model(self): 9 | return Foo 10 | 11 | 12 | class BarIndex(indexes.SearchIndex, indexes.Indexable): 13 | text = indexes.CharField(document=True) 14 | 15 | def get_model(self): 16 | return Bar 17 | -------------------------------------------------------------------------------- /test_haystack/discovery/templates/search/indexes/bar_text.txt: -------------------------------------------------------------------------------- 1 | {{ object.title }} 2 | {{ object.body }} -------------------------------------------------------------------------------- /test_haystack/elasticsearch2_tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import warnings 3 | 4 | from django.conf import settings 5 | 6 | from haystack.utils import log as logging 7 | 8 | warnings.simplefilter("ignore", Warning) 9 | 10 | 11 | def setup(): 12 | log = logging.getLogger("haystack") 13 | try: 14 | import elasticsearch 15 | 16 | if not ((2, 0, 0) <= elasticsearch.__version__ < (3, 0, 0)): 17 | raise ImportError 18 | from elasticsearch import Elasticsearch, exceptions 19 | except ImportError: 20 | log.error( 21 | "Skipping ElasticSearch 2 tests: 'elasticsearch>=2.0.0,<3.0.0' not installed." 22 | ) 23 | raise unittest.SkipTest("'elasticsearch>=2.0.0,<3.0.0' not installed.") 24 | 25 | url = settings.HAYSTACK_CONNECTIONS["elasticsearch"]["URL"] 26 | es = Elasticsearch(url) 27 | try: 28 | es.info() 29 | except exceptions.ConnectionError as e: 30 | log.error("elasticsearch not running on %r" % url, exc_info=True) 31 | raise unittest.SkipTest("elasticsearch not running on %r" % url, e) 32 | -------------------------------------------------------------------------------- /test_haystack/elasticsearch2_tests/test_inputs.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from haystack import connections, inputs 4 | 5 | 6 | class Elasticsearch2InputTestCase(TestCase): 7 | def setUp(self): 8 | super().setUp() 9 | self.query_obj = connections["elasticsearch"].get_query() 10 | 11 | def test_raw_init(self): 12 | raw = inputs.Raw("hello OR there, :you") 13 | self.assertEqual(raw.query_string, "hello OR there, :you") 14 | self.assertEqual(raw.kwargs, {}) 15 | self.assertEqual(raw.post_process, False) 16 | 17 | raw = inputs.Raw("hello OR there, :you", test="really") 18 | self.assertEqual(raw.query_string, "hello OR there, :you") 19 | self.assertEqual(raw.kwargs, {"test": "really"}) 20 | self.assertEqual(raw.post_process, False) 21 | 22 | def test_raw_prepare(self): 23 | raw = inputs.Raw("hello OR there, :you") 24 | self.assertEqual(raw.prepare(self.query_obj), "hello OR there, :you") 25 | 26 | def test_clean_init(self): 27 | clean = inputs.Clean("hello OR there, :you") 28 | self.assertEqual(clean.query_string, "hello OR there, :you") 29 | self.assertEqual(clean.post_process, True) 30 | 31 | def test_clean_prepare(self): 32 | clean = inputs.Clean("hello OR there, :you") 33 | self.assertEqual(clean.prepare(self.query_obj), "hello or there, \\:you") 34 | 35 | def test_exact_init(self): 36 | exact = inputs.Exact("hello OR there, :you") 37 | self.assertEqual(exact.query_string, "hello OR there, :you") 38 | self.assertEqual(exact.post_process, True) 39 | 40 | def test_exact_prepare(self): 41 | exact = inputs.Exact("hello OR there, :you") 42 | self.assertEqual(exact.prepare(self.query_obj), '"hello OR there, :you"') 43 | 44 | exact = inputs.Exact("hello OR there, :you", clean=True) 45 | self.assertEqual(exact.prepare(self.query_obj), '"hello or there, \\:you"') 46 | 47 | def test_not_init(self): 48 | not_it = inputs.Not("hello OR there, :you") 49 | self.assertEqual(not_it.query_string, "hello OR there, :you") 50 | self.assertEqual(not_it.post_process, True) 51 | 52 | def test_not_prepare(self): 53 | not_it = inputs.Not("hello OR there, :you") 54 | self.assertEqual(not_it.prepare(self.query_obj), "NOT (hello or there, \\:you)") 55 | 56 | def test_autoquery_init(self): 57 | autoquery = inputs.AutoQuery('panic -don\'t "froody dude"') 58 | self.assertEqual(autoquery.query_string, 'panic -don\'t "froody dude"') 59 | self.assertEqual(autoquery.post_process, False) 60 | 61 | def test_autoquery_prepare(self): 62 | autoquery = inputs.AutoQuery('panic -don\'t "froody dude"') 63 | self.assertEqual( 64 | autoquery.prepare(self.query_obj), 'panic NOT don\'t "froody dude"' 65 | ) 66 | 67 | def test_altparser_init(self): 68 | altparser = inputs.AltParser("dismax") 69 | self.assertEqual(altparser.parser_name, "dismax") 70 | self.assertEqual(altparser.query_string, "") 71 | self.assertEqual(altparser.kwargs, {}) 72 | self.assertEqual(altparser.post_process, False) 73 | 74 | altparser = inputs.AltParser("dismax", "douglas adams", qf="author", mm=1) 75 | self.assertEqual(altparser.parser_name, "dismax") 76 | self.assertEqual(altparser.query_string, "douglas adams") 77 | self.assertEqual(altparser.kwargs, {"mm": 1, "qf": "author"}) 78 | self.assertEqual(altparser.post_process, False) 79 | 80 | def test_altparser_prepare(self): 81 | altparser = inputs.AltParser("dismax", "douglas adams", qf="author", mm=1) 82 | self.assertEqual( 83 | altparser.prepare(self.query_obj), 84 | """{!dismax mm=1 qf=author v='douglas adams'}""", 85 | ) 86 | -------------------------------------------------------------------------------- /test_haystack/elasticsearch5_tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import warnings 3 | 4 | from django.conf import settings 5 | 6 | from haystack.utils import log as logging 7 | 8 | warnings.simplefilter("ignore", Warning) 9 | 10 | 11 | def setup(): 12 | log = logging.getLogger("haystack") 13 | try: 14 | import elasticsearch 15 | 16 | if not ((5, 0, 0) <= elasticsearch.__version__ < (6, 0, 0)): 17 | raise ImportError 18 | from elasticsearch import Elasticsearch, exceptions 19 | except ImportError: 20 | log.error( 21 | "Skipping ElasticSearch 5 tests: 'elasticsearch>=5.0.0,<6.0.0' not installed." 22 | ) 23 | raise unittest.SkipTest("'elasticsearch>=5.0.0,<6.0.0' not installed.") 24 | 25 | url = settings.HAYSTACK_CONNECTIONS["elasticsearch"]["URL"] 26 | es = Elasticsearch(url) 27 | try: 28 | es.info() 29 | except exceptions.ConnectionError as e: 30 | log.error("elasticsearch not running on %r" % url, exc_info=True) 31 | raise unittest.SkipTest("elasticsearch not running on %r" % url, e) 32 | -------------------------------------------------------------------------------- /test_haystack/elasticsearch5_tests/test_inputs.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from haystack import connections, inputs 4 | 5 | 6 | class Elasticsearch5InputTestCase(TestCase): 7 | def setUp(self): 8 | super().setUp() 9 | self.query_obj = connections["elasticsearch"].get_query() 10 | 11 | def test_raw_init(self): 12 | raw = inputs.Raw("hello OR there, :you") 13 | self.assertEqual(raw.query_string, "hello OR there, :you") 14 | self.assertEqual(raw.kwargs, {}) 15 | self.assertEqual(raw.post_process, False) 16 | 17 | raw = inputs.Raw("hello OR there, :you", test="really") 18 | self.assertEqual(raw.query_string, "hello OR there, :you") 19 | self.assertEqual(raw.kwargs, {"test": "really"}) 20 | self.assertEqual(raw.post_process, False) 21 | 22 | def test_raw_prepare(self): 23 | raw = inputs.Raw("hello OR there, :you") 24 | self.assertEqual(raw.prepare(self.query_obj), "hello OR there, :you") 25 | 26 | def test_clean_init(self): 27 | clean = inputs.Clean("hello OR there, :you") 28 | self.assertEqual(clean.query_string, "hello OR there, :you") 29 | self.assertEqual(clean.post_process, True) 30 | 31 | def test_clean_prepare(self): 32 | clean = inputs.Clean("hello OR there, :you") 33 | self.assertEqual(clean.prepare(self.query_obj), "hello or there, \\:you") 34 | 35 | def test_exact_init(self): 36 | exact = inputs.Exact("hello OR there, :you") 37 | self.assertEqual(exact.query_string, "hello OR there, :you") 38 | self.assertEqual(exact.post_process, True) 39 | 40 | def test_exact_prepare(self): 41 | exact = inputs.Exact("hello OR there, :you") 42 | self.assertEqual(exact.prepare(self.query_obj), '"hello OR there, :you"') 43 | 44 | exact = inputs.Exact("hello OR there, :you", clean=True) 45 | self.assertEqual(exact.prepare(self.query_obj), '"hello or there, \\:you"') 46 | 47 | def test_not_init(self): 48 | not_it = inputs.Not("hello OR there, :you") 49 | self.assertEqual(not_it.query_string, "hello OR there, :you") 50 | self.assertEqual(not_it.post_process, True) 51 | 52 | def test_not_prepare(self): 53 | not_it = inputs.Not("hello OR there, :you") 54 | self.assertEqual(not_it.prepare(self.query_obj), "NOT (hello or there, \\:you)") 55 | 56 | def test_autoquery_init(self): 57 | autoquery = inputs.AutoQuery('panic -don\'t "froody dude"') 58 | self.assertEqual(autoquery.query_string, 'panic -don\'t "froody dude"') 59 | self.assertEqual(autoquery.post_process, False) 60 | 61 | def test_autoquery_prepare(self): 62 | autoquery = inputs.AutoQuery('panic -don\'t "froody dude"') 63 | self.assertEqual( 64 | autoquery.prepare(self.query_obj), 'panic NOT don\'t "froody dude"' 65 | ) 66 | 67 | def test_altparser_init(self): 68 | altparser = inputs.AltParser("dismax") 69 | self.assertEqual(altparser.parser_name, "dismax") 70 | self.assertEqual(altparser.query_string, "") 71 | self.assertEqual(altparser.kwargs, {}) 72 | self.assertEqual(altparser.post_process, False) 73 | 74 | altparser = inputs.AltParser("dismax", "douglas adams", qf="author", mm=1) 75 | self.assertEqual(altparser.parser_name, "dismax") 76 | self.assertEqual(altparser.query_string, "douglas adams") 77 | self.assertEqual(altparser.kwargs, {"mm": 1, "qf": "author"}) 78 | self.assertEqual(altparser.post_process, False) 79 | 80 | def test_altparser_prepare(self): 81 | altparser = inputs.AltParser("dismax", "douglas adams", qf="author", mm=1) 82 | self.assertEqual( 83 | altparser.prepare(self.query_obj), 84 | """{!dismax mm=1 qf=author v='douglas adams'}""", 85 | ) 86 | -------------------------------------------------------------------------------- /test_haystack/elasticsearch_tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import warnings 3 | 4 | from django.conf import settings 5 | 6 | from haystack.utils import log as logging 7 | 8 | warnings.simplefilter("ignore", Warning) 9 | 10 | 11 | def setup(): 12 | log = logging.getLogger("haystack") 13 | try: 14 | import elasticsearch 15 | 16 | if not ((1, 0, 0) <= elasticsearch.__version__ < (2, 0, 0)): 17 | raise ImportError 18 | from elasticsearch import Elasticsearch, ElasticsearchException 19 | except ImportError: 20 | log.error( 21 | "Skipping ElasticSearch 1 tests: 'elasticsearch>=1.0.0,<2.0.0' not installed." 22 | ) 23 | raise unittest.SkipTest("'elasticsearch>=1.0.0,<2.0.0' not installed.") 24 | 25 | es = Elasticsearch(settings.HAYSTACK_CONNECTIONS["elasticsearch"]["URL"]) 26 | try: 27 | es.info() 28 | except ElasticsearchException as e: 29 | log.error( 30 | "elasticsearch not running on %r" 31 | % settings.HAYSTACK_CONNECTIONS["elasticsearch"]["URL"], 32 | exc_info=True, 33 | ) 34 | raise unittest.SkipTest( 35 | "elasticsearch not running on %r" 36 | % settings.HAYSTACK_CONNECTIONS["elasticsearch"]["URL"], 37 | e, 38 | ) 39 | -------------------------------------------------------------------------------- /test_haystack/elasticsearch_tests/test_inputs.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from haystack import connections, inputs 4 | 5 | 6 | class ElasticsearchInputTestCase(TestCase): 7 | def setUp(self): 8 | super().setUp() 9 | self.query_obj = connections["elasticsearch"].get_query() 10 | 11 | def test_raw_init(self): 12 | raw = inputs.Raw("hello OR there, :you") 13 | self.assertEqual(raw.query_string, "hello OR there, :you") 14 | self.assertEqual(raw.kwargs, {}) 15 | self.assertEqual(raw.post_process, False) 16 | 17 | raw = inputs.Raw("hello OR there, :you", test="really") 18 | self.assertEqual(raw.query_string, "hello OR there, :you") 19 | self.assertEqual(raw.kwargs, {"test": "really"}) 20 | self.assertEqual(raw.post_process, False) 21 | 22 | def test_raw_prepare(self): 23 | raw = inputs.Raw("hello OR there, :you") 24 | self.assertEqual(raw.prepare(self.query_obj), "hello OR there, :you") 25 | 26 | def test_clean_init(self): 27 | clean = inputs.Clean("hello OR there, :you") 28 | self.assertEqual(clean.query_string, "hello OR there, :you") 29 | self.assertEqual(clean.post_process, True) 30 | 31 | def test_clean_prepare(self): 32 | clean = inputs.Clean("hello OR there, :you") 33 | self.assertEqual(clean.prepare(self.query_obj), "hello or there, \\:you") 34 | 35 | def test_exact_init(self): 36 | exact = inputs.Exact("hello OR there, :you") 37 | self.assertEqual(exact.query_string, "hello OR there, :you") 38 | self.assertEqual(exact.post_process, True) 39 | 40 | def test_exact_prepare(self): 41 | exact = inputs.Exact("hello OR there, :you") 42 | self.assertEqual(exact.prepare(self.query_obj), '"hello OR there, :you"') 43 | 44 | exact = inputs.Exact("hello OR there, :you", clean=True) 45 | self.assertEqual(exact.prepare(self.query_obj), '"hello or there, \\:you"') 46 | 47 | def test_not_init(self): 48 | not_it = inputs.Not("hello OR there, :you") 49 | self.assertEqual(not_it.query_string, "hello OR there, :you") 50 | self.assertEqual(not_it.post_process, True) 51 | 52 | def test_not_prepare(self): 53 | not_it = inputs.Not("hello OR there, :you") 54 | self.assertEqual(not_it.prepare(self.query_obj), "NOT (hello or there, \\:you)") 55 | 56 | def test_autoquery_init(self): 57 | autoquery = inputs.AutoQuery('panic -don\'t "froody dude"') 58 | self.assertEqual(autoquery.query_string, 'panic -don\'t "froody dude"') 59 | self.assertEqual(autoquery.post_process, False) 60 | 61 | def test_autoquery_prepare(self): 62 | autoquery = inputs.AutoQuery('panic -don\'t "froody dude"') 63 | self.assertEqual( 64 | autoquery.prepare(self.query_obj), 'panic NOT don\'t "froody dude"' 65 | ) 66 | 67 | def test_altparser_init(self): 68 | altparser = inputs.AltParser("dismax") 69 | self.assertEqual(altparser.parser_name, "dismax") 70 | self.assertEqual(altparser.query_string, "") 71 | self.assertEqual(altparser.kwargs, {}) 72 | self.assertEqual(altparser.post_process, False) 73 | 74 | altparser = inputs.AltParser("dismax", "douglas adams", qf="author", mm=1) 75 | self.assertEqual(altparser.parser_name, "dismax") 76 | self.assertEqual(altparser.query_string, "douglas adams") 77 | self.assertEqual(altparser.kwargs, {"mm": 1, "qf": "author"}) 78 | self.assertEqual(altparser.post_process, False) 79 | 80 | def test_altparser_prepare(self): 81 | altparser = inputs.AltParser("dismax", "douglas adams", qf="author", mm=1) 82 | self.assertEqual( 83 | altparser.prepare(self.query_obj), 84 | """{!dismax mm=1 qf=author v='douglas adams'}""", 85 | ) 86 | -------------------------------------------------------------------------------- /test_haystack/multipleindex/__init__.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | 3 | import haystack 4 | from haystack.signals import RealtimeSignalProcessor 5 | 6 | from ..utils import check_solr 7 | 8 | _old_sp = None 9 | 10 | 11 | def setup(): 12 | check_solr() 13 | global _old_sp 14 | config = apps.get_app_config("haystack") 15 | _old_sp = config.signal_processor 16 | config.signal_processor = RealtimeSignalProcessor( 17 | haystack.connections, haystack.connection_router 18 | ) 19 | 20 | 21 | def teardown(): 22 | config = apps.get_app_config("haystack") 23 | config.signal_processor.teardown() 24 | config.signal_processor = _old_sp 25 | -------------------------------------------------------------------------------- /test_haystack/multipleindex/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Foo(models.Model): 5 | title = models.CharField(max_length=255) 6 | body = models.TextField() 7 | 8 | def __str__(self): 9 | return self.title 10 | 11 | 12 | class Bar(models.Model): 13 | author = models.CharField(max_length=255) 14 | content = models.TextField() 15 | 16 | def __str__(self): 17 | return self.author 18 | -------------------------------------------------------------------------------- /test_haystack/multipleindex/routers.py: -------------------------------------------------------------------------------- 1 | from haystack.routers import BaseRouter 2 | 3 | 4 | class MultipleIndexRouter(BaseRouter): 5 | def for_write(self, instance=None, **hints): 6 | if instance and instance._meta.app_label == "multipleindex": 7 | return "solr" 8 | -------------------------------------------------------------------------------- /test_haystack/multipleindex/search_indexes.py: -------------------------------------------------------------------------------- 1 | from haystack import indexes 2 | from haystack.indexes import Indexable, SearchIndex 3 | 4 | from .models import Bar, Foo 5 | 6 | 7 | # To test additional ignores... 8 | class BaseIndex(indexes.SearchIndex): 9 | text = indexes.CharField(document=True, model_attr="body") 10 | 11 | def get_model(self): 12 | return Foo 13 | 14 | 15 | class FooIndex(BaseIndex, indexes.Indexable): 16 | def index_queryset(self, using=None): 17 | qs = super().index_queryset(using=using) 18 | if using == "filtered_whoosh": 19 | qs = qs.filter(body__contains="1") 20 | return qs 21 | 22 | 23 | # Import the old way & make sure things don't explode. 24 | 25 | 26 | class BarIndex(SearchIndex, Indexable): 27 | text = indexes.CharField(document=True) 28 | 29 | def get_model(self): 30 | return Bar 31 | 32 | def prepare_text(self, obj): 33 | return "%s\n%s" % (obj.author, obj.content) 34 | -------------------------------------------------------------------------------- /test_haystack/results_per_page_urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from haystack.views import SearchView 4 | 5 | 6 | class CustomPerPage(SearchView): 7 | results_per_page = 1 8 | 9 | 10 | urlpatterns = [ 11 | url(r"^search/$", CustomPerPage(load_all=False), name="haystack_search"), 12 | url( 13 | r"^search2/$", 14 | CustomPerPage(load_all=False, results_per_page=2), 15 | name="haystack_search", 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /test_haystack/run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | from os.path import abspath, dirname 4 | 5 | import nose 6 | 7 | 8 | def run_all(argv=None): 9 | sys.exitfunc = lambda: sys.stderr.write("Shutting down....\n") 10 | 11 | # always insert coverage when running tests through setup.py 12 | if argv is None: 13 | argv = [ 14 | "nosetests", 15 | "--with-coverage", 16 | "--cover-package=haystack", 17 | "--cover-erase", 18 | "--verbose", 19 | ] 20 | 21 | nose.run_exit(argv=argv, defaultTest=abspath(dirname(__file__))) 22 | 23 | 24 | if __name__ == "__main__": 25 | run_all(sys.argv) 26 | -------------------------------------------------------------------------------- /test_haystack/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from tempfile import mkdtemp 3 | 4 | SECRET_KEY = "Please do not spew DeprecationWarnings" 5 | 6 | # Haystack settings for running tests. 7 | DATABASES = { 8 | "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "haystack_tests.db"} 9 | } 10 | 11 | INSTALLED_APPS = [ 12 | "django.contrib.admin", 13 | "django.contrib.auth", 14 | "django.contrib.contenttypes", 15 | "django.contrib.sessions", 16 | "django.contrib.messages", 17 | "haystack", 18 | "test_haystack.discovery", 19 | "test_haystack.core", 20 | "test_haystack.spatial", 21 | "test_haystack.multipleindex", 22 | # This app exists to confirm that nothing breaks when INSTALLED_APPS has an app without models.py 23 | # which is common in some cases for things like admin extensions, reporting, etc. 24 | "test_haystack.test_app_without_models", 25 | # Confirm that everything works with app labels which have more than one level of hierarchy 26 | # as reported in https://github.com/django-haystack/django-haystack/issues/1152 27 | "test_haystack.test_app_with_hierarchy.contrib.django.hierarchal_app_django", 28 | "test_haystack.test_app_using_appconfig.apps.SimpleTestAppConfig", 29 | ] 30 | 31 | TEMPLATES = [ 32 | { 33 | "BACKEND": "django.template.backends.django.DjangoTemplates", 34 | "APP_DIRS": True, 35 | "OPTIONS": { 36 | "context_processors": [ 37 | "django.contrib.auth.context_processors.auth", 38 | "django.contrib.messages.context_processors.messages", 39 | ] 40 | }, 41 | } 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | "django.middleware.common.CommonMiddleware", 46 | "django.contrib.sessions.middleware.SessionMiddleware", 47 | "django.middleware.csrf.CsrfViewMiddleware", 48 | "django.contrib.auth.middleware.AuthenticationMiddleware", 49 | "django.contrib.messages.middleware.MessageMiddleware", 50 | ] 51 | 52 | ROOT_URLCONF = "test_haystack.core.urls" 53 | 54 | HAYSTACK_ROUTERS = [ 55 | "haystack.routers.DefaultRouter", 56 | "test_haystack.multipleindex.routers.MultipleIndexRouter", 57 | ] 58 | 59 | HAYSTACK_CONNECTIONS = { 60 | "default": {"ENGINE": "test_haystack.mocks.MockEngine"}, 61 | "whoosh": { 62 | "ENGINE": "haystack.backends.whoosh_backend.WhooshEngine", 63 | "PATH": mkdtemp(prefix="test_whoosh_query"), 64 | "INCLUDE_SPELLING": True, 65 | }, 66 | "filtered_whoosh": { 67 | "ENGINE": "haystack.backends.whoosh_backend.WhooshEngine", 68 | "PATH": mkdtemp(prefix="haystack-multipleindex-filtered-whoosh-tests-"), 69 | "EXCLUDED_INDEXES": ["test_haystack.multipleindex.search_indexes.BarIndex"], 70 | }, 71 | "elasticsearch": { 72 | "ENGINE": "haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine", 73 | "URL": os.environ.get("TEST_ELASTICSEARCH_1_URL", "http://localhost:9200/"), 74 | "INDEX_NAME": "test_default", 75 | "INCLUDE_SPELLING": True, 76 | }, 77 | "simple": {"ENGINE": "haystack.backends.simple_backend.SimpleEngine"}, 78 | "solr": { 79 | "ENGINE": "haystack.backends.solr_backend.SolrEngine", 80 | "URL": os.environ.get( 81 | "TEST_SOLR_URL", "http://localhost:9001/solr/collection1" 82 | ), 83 | "ADMIN_URL": os.environ.get( 84 | "TEST_SOLR_ADMIN_URL", "http://localhost:9001/solr/admin/cores" 85 | ), 86 | "INCLUDE_SPELLING": True, 87 | }, 88 | } 89 | 90 | if "elasticsearch" in HAYSTACK_CONNECTIONS: 91 | try: 92 | import elasticsearch 93 | 94 | if (2,) <= elasticsearch.__version__ <= (3,): 95 | HAYSTACK_CONNECTIONS["elasticsearch"].update( 96 | { 97 | "ENGINE": "haystack.backends.elasticsearch2_backend.Elasticsearch2SearchEngine" 98 | } 99 | ) 100 | elif (5,) <= elasticsearch.__version__ <= (6,): 101 | HAYSTACK_CONNECTIONS["elasticsearch"].update( 102 | { 103 | "ENGINE": "haystack.backends.elasticsearch5_backend.Elasticsearch5SearchEngine" 104 | } 105 | ) 106 | except ImportError: 107 | del HAYSTACK_CONNECTIONS["elasticsearch"] 108 | -------------------------------------------------------------------------------- /test_haystack/simple_tests/__init__.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | warnings.simplefilter("ignore", Warning) 4 | -------------------------------------------------------------------------------- /test_haystack/simple_tests/search_indexes.py: -------------------------------------------------------------------------------- 1 | from haystack import indexes 2 | 3 | from ..core.models import MockModel, ScoreMockModel 4 | 5 | 6 | class SimpleMockSearchIndex(indexes.SearchIndex, indexes.Indexable): 7 | text = indexes.CharField(document=True, use_template=True) 8 | name = indexes.CharField(model_attr="author") 9 | pub_date = indexes.DateTimeField(model_attr="pub_date") 10 | 11 | def get_model(self): 12 | return MockModel 13 | 14 | 15 | class SimpleMockScoreIndex(indexes.SearchIndex, indexes.Indexable): 16 | text = indexes.CharField(document=True, use_template=True) 17 | score = indexes.CharField(model_attr="score") 18 | 19 | def get_model(self): 20 | return ScoreMockModel 21 | 22 | 23 | class SimpleMockUUIDModelIndex(indexes.SearchIndex, indexes.Indexable): 24 | text = indexes.CharField(document=True, model_attr="characteristics") 25 | -------------------------------------------------------------------------------- /test_haystack/simple_tests/test_simple_query.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from haystack import connections 4 | from haystack.models import SearchResult 5 | from haystack.query import SQ 6 | 7 | 8 | class SimpleSearchQueryTestCase(TestCase): 9 | def setUp(self): 10 | super().setUp() 11 | self.sq = connections["simple"].get_query() 12 | 13 | def test_build_query_all(self): 14 | self.assertEqual(self.sq.build_query(), "*") 15 | 16 | def test_build_query_single_word(self): 17 | self.sq.add_filter(SQ(content="hello")) 18 | self.assertEqual(self.sq.build_query(), "hello") 19 | 20 | def test_build_query_multiple_word(self): 21 | self.sq.add_filter(SQ(name="foo")) 22 | self.sq.add_filter(SQ(name="bar")) 23 | self.assertEqual(self.sq.build_query(), "foo bar") 24 | 25 | def test_set_result_class(self): 26 | # Assert that we're defaulting to ``SearchResult``. 27 | self.assertTrue(issubclass(self.sq.result_class, SearchResult)) 28 | 29 | # Custom class. 30 | class IttyBittyResult(object): 31 | pass 32 | 33 | self.sq.set_result_class(IttyBittyResult) 34 | self.assertTrue(issubclass(self.sq.result_class, IttyBittyResult)) 35 | 36 | # Reset to default. 37 | self.sq.set_result_class(None) 38 | self.assertTrue(issubclass(self.sq.result_class, SearchResult)) 39 | -------------------------------------------------------------------------------- /test_haystack/solr_tests/__init__.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | warnings.simplefilter("ignore", Warning) 4 | 5 | from ..utils import check_solr 6 | 7 | 8 | def setup(): 9 | check_solr() 10 | -------------------------------------------------------------------------------- /test_haystack/solr_tests/content_extraction/test.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/test_haystack/solr_tests/content_extraction/test.pdf -------------------------------------------------------------------------------- /test_haystack/solr_tests/server/.gitignore: -------------------------------------------------------------------------------- 1 | solr-*.tgz 2 | -------------------------------------------------------------------------------- /test_haystack/solr_tests/server/get-solr-download-url.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | from itertools import chain 4 | from urllib.parse import urljoin 5 | 6 | import requests 7 | 8 | if len(sys.argv) != 2: 9 | print("Usage: %s SOLR_VERSION" % sys.argv[0], file=sys.stderr) 10 | sys.exit(1) 11 | 12 | solr_version = sys.argv[1] 13 | tarball = "solr-{0}.tgz".format(solr_version) 14 | dist_path = "lucene/solr/{0}/{1}".format(solr_version, tarball) 15 | 16 | download_url = urljoin("https://archive.apache.org/dist/", dist_path) 17 | mirror_response = requests.get( 18 | "https://www.apache.org/dyn/mirrors/mirrors.cgi/%s?asjson=1" % dist_path 19 | ) 20 | 21 | if not mirror_response.ok: 22 | print( 23 | "Apache mirror request returned HTTP %d" % mirror_response.status_code, 24 | file=sys.stderr, 25 | ) 26 | sys.exit(1) 27 | 28 | mirror_data = mirror_response.json() 29 | 30 | # Since the Apache mirrors are often unreliable and releases may disappear without notice we'll 31 | # try the preferred mirror, all of the alternates and backups, and fall back to the main Apache 32 | # archive server: 33 | for base_url in chain( 34 | (mirror_data["preferred"],), 35 | mirror_data["http"], 36 | mirror_data["backup"], 37 | ("https://archive.apache.org/dist/",), 38 | ): 39 | test_url = urljoin(base_url, mirror_data["path_info"]) 40 | 41 | # The Apache mirror script's response format has recently changed to exclude the actual file paths: 42 | if not test_url.endswith(tarball): 43 | test_url = urljoin(test_url, dist_path) 44 | 45 | try: 46 | if requests.head(test_url, allow_redirects=True).status_code == 200: 47 | download_url = test_url 48 | break 49 | except requests.exceptions.ConnectionError: 50 | continue 51 | else: 52 | print("None of the Apache mirrors have %s" % dist_path, file=sys.stderr) 53 | sys.exit(1) 54 | 55 | print(download_url) 56 | -------------------------------------------------------------------------------- /test_haystack/solr_tests/server/start-solr-test-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SOLR_VERSION=6.6.4 6 | SOLR_DIR=solr 7 | 8 | 9 | SOLR_PORT=9001 10 | 11 | cd $(dirname $0) 12 | 13 | export TEST_ROOT=$(pwd) 14 | 15 | export SOLR_ARCHIVE="${SOLR_VERSION}.tgz" 16 | 17 | if [ -d "${HOME}/download-cache/" ]; then 18 | export SOLR_ARCHIVE="${HOME}/download-cache/${SOLR_ARCHIVE}" 19 | fi 20 | 21 | if [ -f ${SOLR_ARCHIVE} ]; then 22 | # If the tarball doesn't extract cleanly, remove it so it'll download again: 23 | tar -tf ${SOLR_ARCHIVE} > /dev/null || rm ${SOLR_ARCHIVE} 24 | fi 25 | 26 | if [ ! -f ${SOLR_ARCHIVE} ]; then 27 | SOLR_DOWNLOAD_URL=$(python get-solr-download-url.py $SOLR_VERSION) 28 | curl -Lo $SOLR_ARCHIVE ${SOLR_DOWNLOAD_URL} || (echo "Unable to download ${SOLR_DOWNLOAD_URL}"; exit 2) 29 | fi 30 | 31 | echo "Extracting Solr ${SOLR_ARCHIVE} to ${TEST_ROOT}/${SOLR_DIR}" 32 | rm -rf ${SOLR_DIR} 33 | mkdir ${SOLR_DIR} 34 | FULL_SOLR_DIR=$(readlink -f ./${SOLR_DIR}) 35 | tar -C ${SOLR_DIR} -xf ${SOLR_ARCHIVE} --strip-components=1 36 | 37 | # These tuning options will break on Java 10 and for testing we don't care about 38 | # production server optimizations: 39 | export GC_LOG_OPTS="" 40 | export GC_TUNE="" 41 | 42 | export SOLR_LOGS_DIR="${FULL_SOLR_DIR}/logs" 43 | 44 | install -d ${SOLR_LOGS_DIR} 45 | 46 | echo "Changing into ${FULL_SOLR_DIR} " 47 | 48 | cd ${FULL_SOLR_DIR} 49 | 50 | echo "Creating Solr Core" 51 | ./bin/solr start -p ${SOLR_PORT} 52 | ./bin/solr create -c collection1 -p ${SOLR_PORT} -n basic_config 53 | ./bin/solr create -c mgmnt -p ${SOLR_PORT} 54 | 55 | echo "Solr system information:" 56 | curl --fail --silent 'http://localhost:9001/solr/admin/info/system?wt=json&indent=on' | python -m json.tool 57 | ./bin/solr stop -p ${SOLR_PORT} 58 | 59 | CONF_DIR=${TEST_ROOT}/confdir 60 | CORE_DIR=${FULL_SOLR_DIR}/server/solr/collection1 61 | mv ${CORE_DIR}/conf/managed-schema ${CORE_DIR}/conf/managed-schema.old 62 | cp ${CONF_DIR}/* ${CORE_DIR}/conf/ 63 | 64 | echo 'Starting server' 65 | cd server 66 | # We use exec to allow process monitors to correctly kill the 67 | # actual Java process rather than this launcher script: 68 | export CMD="java -Djetty.port=${SOLR_PORT} -Djava.awt.headless=true -Dapple.awt.UIElement=true -jar start.jar --module=http -Dsolr.install.dir=${FULL_SOLR_DIR} -Dsolr.log.dir=${SOLR_LOGS_DIR}" 69 | 70 | if [ -z "${BACKGROUND_SOLR}" ]; then 71 | exec $CMD 72 | else 73 | exec $CMD >/dev/null & 74 | fi 75 | -------------------------------------------------------------------------------- /test_haystack/solr_tests/server/wait-for-solr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Simple throttle to wait for Solr to start on busy test servers""" 3 | import sys 4 | import time 5 | 6 | import requests 7 | 8 | max_retries = 100 9 | retry_count = 0 10 | retry_delay = 15 11 | status_url = 'http://localhost:9001/solr/collection1/admin/ping' 12 | 13 | 14 | while retry_count < max_retries: 15 | status_code = 0 16 | 17 | try: 18 | r = requests.get(status_url) 19 | status_code = r.status_code 20 | if status_code == 200: 21 | sys.exit(0) 22 | except Exception as exc: 23 | print('Unhandled exception requesting %s: %s' % (status_url, exc), file=sys.stderr) 24 | 25 | retry_count += 1 26 | 27 | print('Waiting {0} seconds for Solr to start (retry #{1}, status {2})'.format(retry_delay, 28 | retry_count, 29 | status_code), 30 | file=sys.stderr) 31 | time.sleep(retry_delay) 32 | 33 | 34 | print("Solr took too long to start (#%d retries)" % retry_count, file=sys.stderr) 35 | sys.exit(1) 36 | -------------------------------------------------------------------------------- /test_haystack/solr_tests/test_admin.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth.models import User 3 | from django.test import TestCase 4 | from django.test.utils import override_settings 5 | from django.urls import reverse 6 | 7 | from haystack import connections, reset_search_queries 8 | from haystack.utils.loading import UnifiedIndex 9 | 10 | from ..core.models import MockModel 11 | from .test_solr_backend import SolrMockModelSearchIndex, clear_solr_index 12 | 13 | 14 | @override_settings(DEBUG=True) 15 | class SearchModelAdminTestCase(TestCase): 16 | fixtures = ["base_data.json", "bulk_data.json"] 17 | 18 | def setUp(self): 19 | super().setUp() 20 | 21 | # With the models setup, you get the proper bits. 22 | # Stow. 23 | self.old_ui = connections["solr"].get_unified_index() 24 | self.ui = UnifiedIndex() 25 | smmsi = SolrMockModelSearchIndex() 26 | self.ui.build(indexes=[smmsi]) 27 | connections["solr"]._index = self.ui 28 | 29 | # Wipe it clean. 30 | clear_solr_index() 31 | 32 | # Force indexing of the content. 33 | smmsi.update(using="solr") 34 | 35 | superuser = User.objects.create_superuser( 36 | username="superuser", password="password", email="super@user.com" 37 | ) 38 | 39 | def tearDown(self): 40 | # Restore. 41 | connections["solr"]._index = self.old_ui 42 | super().tearDown() 43 | 44 | def test_usage(self): 45 | reset_search_queries() 46 | self.assertEqual(len(connections["solr"].queries), 0) 47 | 48 | self.assertEqual( 49 | self.client.login(username="superuser", password="password"), True 50 | ) 51 | 52 | # First, non-search behavior. 53 | resp = self.client.get("/admin/core/mockmodel/") 54 | self.assertEqual(resp.status_code, 200) 55 | self.assertEqual(len(connections["solr"].queries), 0) 56 | self.assertEqual(resp.context["cl"].full_result_count, 23) 57 | 58 | # Then search behavior. 59 | resp = self.client.get("/admin/core/mockmodel/", data={"q": "Haystack"}) 60 | self.assertEqual(resp.status_code, 200) 61 | self.assertEqual(len(connections["solr"].queries), 3) 62 | self.assertEqual(resp.context["cl"].full_result_count, 23) 63 | # Ensure they aren't search results. 64 | self.assertEqual(isinstance(resp.context["cl"].result_list[0], MockModel), True) 65 | 66 | result_pks = [i.pk for i in resp.context["cl"].result_list] 67 | self.assertIn(5, result_pks) 68 | 69 | # Make sure only changelist is affected. 70 | resp = self.client.get(reverse("admin:core_mockmodel_change", args=(1,))) 71 | self.assertEqual(resp.status_code, 200) 72 | self.assertEqual(resp.context["original"].id, 1) 73 | self.assertTemplateUsed(resp, "admin/change_form.html") 74 | 75 | # The Solr query count should be unchanged: 76 | self.assertEqual(len(connections["solr"].queries), 3) 77 | -------------------------------------------------------------------------------- /test_haystack/solr_tests/test_inputs.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from haystack import connections, inputs 4 | 5 | 6 | class SolrInputTestCase(TestCase): 7 | def setUp(self): 8 | super().setUp() 9 | self.query_obj = connections["solr"].get_query() 10 | 11 | def test_raw_init(self): 12 | raw = inputs.Raw("hello OR there, :you") 13 | self.assertEqual(raw.query_string, "hello OR there, :you") 14 | self.assertEqual(raw.kwargs, {}) 15 | self.assertEqual(raw.post_process, False) 16 | 17 | raw = inputs.Raw("hello OR there, :you", test="really") 18 | self.assertEqual(raw.query_string, "hello OR there, :you") 19 | self.assertEqual(raw.kwargs, {"test": "really"}) 20 | self.assertEqual(raw.post_process, False) 21 | 22 | def test_raw_prepare(self): 23 | raw = inputs.Raw("hello OR there, :you") 24 | self.assertEqual(raw.prepare(self.query_obj), "hello OR there, :you") 25 | 26 | def test_clean_init(self): 27 | clean = inputs.Clean("hello OR there, :you") 28 | self.assertEqual(clean.query_string, "hello OR there, :you") 29 | self.assertEqual(clean.post_process, True) 30 | 31 | def test_clean_prepare(self): 32 | clean = inputs.Clean("hello OR there, :you") 33 | self.assertEqual(clean.prepare(self.query_obj), "hello or there, \\:you") 34 | 35 | def test_exact_init(self): 36 | exact = inputs.Exact("hello OR there, :you") 37 | self.assertEqual(exact.query_string, "hello OR there, :you") 38 | self.assertEqual(exact.post_process, True) 39 | 40 | def test_exact_prepare(self): 41 | exact = inputs.Exact("hello OR there, :you") 42 | self.assertEqual(exact.prepare(self.query_obj), '"hello OR there, :you"') 43 | 44 | exact = inputs.Exact("hello OR there, :you", clean=True) 45 | self.assertEqual(exact.prepare(self.query_obj), '"hello or there, \\:you"') 46 | 47 | def test_not_init(self): 48 | not_it = inputs.Not("hello OR there, :you") 49 | self.assertEqual(not_it.query_string, "hello OR there, :you") 50 | self.assertEqual(not_it.post_process, True) 51 | 52 | def test_not_prepare(self): 53 | not_it = inputs.Not("hello OR there, :you") 54 | self.assertEqual(not_it.prepare(self.query_obj), "NOT (hello or there, \\:you)") 55 | 56 | def test_autoquery_init(self): 57 | autoquery = inputs.AutoQuery('panic -don\'t "froody dude"') 58 | self.assertEqual(autoquery.query_string, 'panic -don\'t "froody dude"') 59 | self.assertEqual(autoquery.post_process, False) 60 | 61 | def test_autoquery_prepare(self): 62 | autoquery = inputs.AutoQuery('panic -don\'t "froody dude"') 63 | self.assertEqual( 64 | autoquery.prepare(self.query_obj), 'panic NOT don\'t "froody dude"' 65 | ) 66 | 67 | def test_altparser_init(self): 68 | altparser = inputs.AltParser("dismax") 69 | self.assertEqual(altparser.parser_name, "dismax") 70 | self.assertEqual(altparser.query_string, "") 71 | self.assertEqual(altparser.kwargs, {}) 72 | self.assertEqual(altparser.post_process, False) 73 | 74 | altparser = inputs.AltParser("dismax", "douglas adams", qf="author", mm=1) 75 | self.assertEqual(altparser.parser_name, "dismax") 76 | self.assertEqual(altparser.query_string, "douglas adams") 77 | self.assertEqual(altparser.kwargs, {"mm": 1, "qf": "author"}) 78 | self.assertEqual(altparser.post_process, False) 79 | 80 | def test_altparser_prepare(self): 81 | altparser = inputs.AltParser("dismax", "douglas adams", qf="author", mm=1) 82 | self.assertEqual( 83 | altparser.prepare(self.query_obj), 84 | """_query_:"{!dismax mm=1 qf=author}douglas adams\"""", 85 | ) 86 | 87 | altparser = inputs.AltParser("dismax", "Don't panic", qf="text author", mm=1) 88 | self.assertEqual( 89 | altparser.prepare(self.query_obj), 90 | """_query_:"{!dismax mm=1 qf='text author'}Don't panic\"""", 91 | ) 92 | -------------------------------------------------------------------------------- /test_haystack/solr_tests/test_templatetags.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import call, patch 3 | 4 | from django.template import Context, Template 5 | from django.test import TestCase 6 | 7 | from ..core.models import MockModel 8 | 9 | 10 | @patch("haystack.templatetags.more_like_this.SearchQuerySet") 11 | class MoreLikeThisTagTestCase(TestCase): 12 | fixtures = ["base_data"] 13 | 14 | def render(self, template, context): 15 | # Why on Earth does Django not have a TemplateTestCase yet? 16 | t = Template(template) 17 | c = Context(context) 18 | return t.render(c) 19 | 20 | def test_more_like_this_without_limit(self, mock_sqs): 21 | mock_model = MockModel.objects.get(pk=3) 22 | template = """{% load more_like_this %}{% more_like_this entry as related_content %}{% for rc in related_content %}{{ rc.id }}{% endfor %}""" 23 | context = {"entry": mock_model} 24 | 25 | mlt = mock_sqs.return_value.more_like_this 26 | mlt.return_value = [{"id": "test_id"}] 27 | 28 | self.assertEqual("test_id", self.render(template, context)) 29 | 30 | mlt.assert_called_once_with(mock_model) 31 | 32 | def test_more_like_this_with_limit(self, mock_sqs): 33 | mock_model = MockModel.objects.get(pk=3) 34 | template = """{% load more_like_this %}{% more_like_this entry as related_content limit 5 %}{% for rc in related_content %}{{ rc.id }}{% endfor %}""" 35 | context = {"entry": mock_model} 36 | 37 | mlt = mock_sqs.return_value.more_like_this 38 | mlt.return_value.__getitem__.return_value = [{"id": "test_id"}] 39 | 40 | self.assertEqual("test_id", self.render(template, context)) 41 | 42 | mlt.assert_called_once_with(mock_model) 43 | 44 | mock_sqs.assert_has_calls( 45 | [ 46 | call().more_like_this(mock_model), 47 | call().more_like_this().__getitem__(slice(None, 5)), 48 | ], 49 | any_order=True, 50 | ) 51 | 52 | # FIXME: https://github.com/toastdriven/django-haystack/issues/1069 53 | @unittest.expectedFailure 54 | def test_more_like_this_for_model(self, mock_sqs): 55 | mock_model = MockModel.objects.get(pk=3) 56 | template = """{% load more_like_this %}{% more_like_this entry as related_content for "core.mock" limit 5 %}{% for rc in related_content %}{{ rc.id }}{% endfor %}""" 57 | context = {"entry": mock_model} 58 | 59 | self.render(template, context) 60 | 61 | mock_sqs.assert_has_calls( 62 | [ 63 | call().models().more_like_this(mock_model), 64 | call().models().more_like_this().__getitem__(slice(None, 5)), 65 | ], 66 | any_order=True, 67 | ) 68 | -------------------------------------------------------------------------------- /test_haystack/spatial/__init__.py: -------------------------------------------------------------------------------- 1 | from ..utils import check_solr 2 | 3 | 4 | def setup(): 5 | check_solr() 6 | -------------------------------------------------------------------------------- /test_haystack/spatial/fixtures/sample_spatial_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "spatial.checkin", 5 | "fields": { 6 | "username": "daniel", 7 | "latitude": 38.971955031423384, 8 | "longitude": -95.23573637008667, 9 | "comment": "Man, I love the coffee at LPT!", 10 | "created": "2011-12-13 09:12:23" 11 | } 12 | }, 13 | { 14 | "pk": 2, 15 | "model": "spatial.checkin", 16 | "fields": { 17 | "username": "daniel", 18 | "latitude": 38.967667537449294, 19 | "longitude": -95.23528575897217, 20 | "comment": "At the Pig for coffee. No one is here.", 21 | "created": "2011-12-13 10:21:23" 22 | } 23 | }, 24 | { 25 | "pk": 3, 26 | "model": "spatial.checkin", 27 | "fields": { 28 | "username": "daniel", 29 | "latitude": 38.971955031423384, 30 | "longitude": -95.23573637008667, 31 | "comment": "Back to LPT's coffee.", 32 | "created": "2011-12-14 14:53:23" 33 | } 34 | }, 35 | { 36 | "pk": 4, 37 | "model": "spatial.checkin", 38 | "fields": { 39 | "username": "daniel", 40 | "latitude": 38.92776639117804, 41 | "longitude": -95.2584171295166, 42 | "comment": "I hate the lines at the post office.", 43 | "created": "2011-12-14 10:01:23" 44 | } 45 | }, 46 | { 47 | "pk": 5, 48 | "model": "spatial.checkin", 49 | "fields": { 50 | "username": "daniel", 51 | "latitude": 38.96531514451104, 52 | "longitude": -95.23622989654541, 53 | "comment": "ZOMGEncore!", 54 | "created": "2011-12-14 12:30:23" 55 | } 56 | }, 57 | { 58 | "pk": 6, 59 | "model": "spatial.checkin", 60 | "fields": { 61 | "username": "daniel", 62 | "latitude": 38.97110422641184, 63 | "longitude": -95.23511409759521, 64 | "comment": "Trying a little Java Break coffee to get the day going.", 65 | "created": "2011-12-15 08:44:23" 66 | } 67 | }, 68 | { 69 | "pk": 7, 70 | "model": "spatial.checkin", 71 | "fields": { 72 | "username": "daniel", 73 | "latitude": 38.9128152, 74 | "longitude": -94.6373083, 75 | "comment": "Apple Store! And they have coffee!", 76 | "created": "2011-12-15 11:05:23" 77 | } 78 | }, 79 | { 80 | "pk": 8, 81 | "model": "spatial.checkin", 82 | "fields": { 83 | "username": "daniel", 84 | "latitude": 38.97143787665407, 85 | "longitude": -95.23622989654541, 86 | "comment": "4bucks coffee run. :/", 87 | "created": "2011-12-16 10:10:23" 88 | } 89 | }, 90 | { 91 | "pk": 9, 92 | "model": "spatial.checkin", 93 | "fields": { 94 | "username": "daniel", 95 | "latitude": 38.97080393984995, 96 | "longitude": -95.23573637008667, 97 | "comment": "Time for lunch at Rudy's.", 98 | "created": "2011-12-16 01:23:23" 99 | } 100 | }, 101 | { 102 | "pk": 10, 103 | "model": "spatial.checkin", 104 | "fields": { 105 | "username": "daniel", 106 | "latitude": 38.92588008485826, 107 | "longitude": -95.2640175819397, 108 | "comment": "At Target. Again.", 109 | "created": "2011-12-16 19:51:23" 110 | } 111 | } 112 | ] 113 | -------------------------------------------------------------------------------- /test_haystack/spatial/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.db import models 4 | 5 | 6 | class Checkin(models.Model): 7 | username = models.CharField(max_length=255) 8 | # We're going to do some non-GeoDjango action, since the setup is 9 | # complex enough. You could just as easily do: 10 | # 11 | # location = models.PointField() 12 | # 13 | # ...and your ``search_indexes.py`` could be less complex. 14 | latitude = models.FloatField() 15 | longitude = models.FloatField() 16 | comment = models.CharField( 17 | max_length=140, blank=True, default="", help_text="Say something pithy." 18 | ) 19 | created = models.DateTimeField(default=datetime.datetime.now) 20 | 21 | class Meta: 22 | ordering = ["-created"] 23 | 24 | # Again, with GeoDjango, this would be unnecessary. 25 | def get_location(self): 26 | # Nothing special about this Point, but ensure that's we don't have to worry 27 | # about import paths. 28 | from django.contrib.gis.geos import Point 29 | 30 | pnt = Point(self.longitude, self.latitude) 31 | return pnt 32 | -------------------------------------------------------------------------------- /test_haystack/spatial/search_indexes.py: -------------------------------------------------------------------------------- 1 | from haystack import indexes 2 | 3 | from .models import Checkin 4 | 5 | 6 | class CheckinSearchIndex(indexes.SearchIndex, indexes.Indexable): 7 | text = indexes.CharField(document=True) 8 | username = indexes.CharField(model_attr="username") 9 | comment = indexes.CharField(model_attr="comment") 10 | # Again, if you were using GeoDjango, this could be just: 11 | # location = indexes.LocationField(model_attr='location') 12 | location = indexes.LocationField(model_attr="get_location") 13 | created = indexes.DateTimeField(model_attr="created") 14 | 15 | def get_model(self): 16 | return Checkin 17 | 18 | def prepare_text(self, obj): 19 | # Because I don't feel like creating a template just for this. 20 | return "\n".join([obj.comment, obj.username]) 21 | -------------------------------------------------------------------------------- /test_haystack/test_altered_internal_names.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.test import TestCase 3 | 4 | from haystack import connection_router, connections, constants, indexes 5 | from haystack.management.commands.build_solr_schema import Command 6 | from haystack.query import SQ 7 | from haystack.utils.loading import UnifiedIndex 8 | from test_haystack.core.models import AnotherMockModel, MockModel 9 | from test_haystack.utils import check_solr 10 | 11 | 12 | class MockModelSearchIndex(indexes.SearchIndex, indexes.Indexable): 13 | text = indexes.CharField(model_attr="foo", document=True) 14 | name = indexes.CharField(model_attr="author") 15 | pub_date = indexes.DateTimeField(model_attr="pub_date") 16 | 17 | def get_model(self): 18 | return MockModel 19 | 20 | 21 | class AlteredInternalNamesTestCase(TestCase): 22 | def setUp(self): 23 | check_solr() 24 | super().setUp() 25 | 26 | self.old_ui = connections["solr"].get_unified_index() 27 | ui = UnifiedIndex() 28 | ui.build(indexes=[MockModelSearchIndex()]) 29 | connections["solr"]._index = ui 30 | 31 | constants.ID = "my_id" 32 | constants.DJANGO_CT = "my_django_ct" 33 | constants.DJANGO_ID = "my_django_id" 34 | 35 | def tearDown(self): 36 | constants.ID = "id" 37 | constants.DJANGO_CT = "django_ct" 38 | constants.DJANGO_ID = "django_id" 39 | connections["solr"]._index = self.old_ui 40 | super().tearDown() 41 | 42 | def test_altered_names(self): 43 | sq = connections["solr"].get_query() 44 | 45 | sq.add_filter(SQ(content="hello")) 46 | sq.add_model(MockModel) 47 | self.assertEqual(sq.build_query(), "(hello)") 48 | 49 | sq.add_model(AnotherMockModel) 50 | self.assertEqual(sq.build_query(), "(hello)") 51 | 52 | def test_solr_schema(self): 53 | command = Command() 54 | context_data = command.build_context(using="solr") 55 | self.assertEqual(len(context_data), 6) 56 | self.assertEqual(context_data["DJANGO_ID"], "my_django_id") 57 | self.assertEqual(context_data["content_field_name"], "text") 58 | self.assertEqual(context_data["DJANGO_CT"], "my_django_ct") 59 | self.assertEqual(context_data["default_operator"], "AND") 60 | self.assertEqual(context_data["ID"], "my_id") 61 | self.assertEqual(len(context_data["fields"]), 3) 62 | self.assertEqual( 63 | sorted(context_data["fields"], key=lambda x: x["field_name"]), 64 | [ 65 | { 66 | "indexed": "true", 67 | "type": "text_en", 68 | "stored": "true", 69 | "field_name": "name", 70 | "multi_valued": "false", 71 | }, 72 | { 73 | "indexed": "true", 74 | "type": "date", 75 | "stored": "true", 76 | "field_name": "pub_date", 77 | "multi_valued": "false", 78 | }, 79 | { 80 | "indexed": "true", 81 | "type": "text_en", 82 | "stored": "true", 83 | "field_name": "text", 84 | "multi_valued": "false", 85 | }, 86 | ], 87 | ) 88 | 89 | schema_xml = command.build_template(using="solr") 90 | self.assertTrue("my_id" in schema_xml) 91 | self.assertTrue( 92 | '' 93 | in schema_xml 94 | ) 95 | self.assertTrue( 96 | '' 97 | in schema_xml 98 | ) 99 | -------------------------------------------------------------------------------- /test_haystack/test_app_loading.py: -------------------------------------------------------------------------------- 1 | from types import GeneratorType, ModuleType 2 | 3 | from django.test import TestCase 4 | from django.urls import reverse 5 | 6 | from haystack.utils import app_loading 7 | 8 | 9 | class AppLoadingTests(TestCase): 10 | def test_load_apps(self): 11 | apps = app_loading.haystack_load_apps() 12 | self.assertIsInstance(apps, (list, GeneratorType)) 13 | 14 | self.assertIn("hierarchal_app_django", apps) 15 | 16 | self.assertNotIn( 17 | "test_app_without_models", 18 | apps, 19 | msg="haystack_load_apps should exclude apps without defined models", 20 | ) 21 | 22 | def test_get_app_modules(self): 23 | app_modules = app_loading.haystack_get_app_modules() 24 | self.assertIsInstance(app_modules, (list, GeneratorType)) 25 | 26 | for i in app_modules: 27 | self.assertIsInstance(i, ModuleType) 28 | 29 | def test_get_models_all(self): 30 | models = app_loading.haystack_get_models("core") 31 | self.assertIsInstance(models, (list, GeneratorType)) 32 | 33 | def test_get_models_specific(self): 34 | from test_haystack.core.models import MockModel 35 | 36 | models = app_loading.haystack_get_models("core.MockModel") 37 | self.assertIsInstance(models, (list, GeneratorType)) 38 | self.assertListEqual(models, [MockModel]) 39 | 40 | def test_hierarchal_app_get_models(self): 41 | models = app_loading.haystack_get_models("hierarchal_app_django") 42 | self.assertIsInstance(models, (list, GeneratorType)) 43 | self.assertSetEqual( 44 | set(str(i._meta) for i in models), 45 | set( 46 | ( 47 | "hierarchal_app_django.hierarchalappsecondmodel", 48 | "hierarchal_app_django.hierarchalappmodel", 49 | ) 50 | ), 51 | ) 52 | 53 | def test_hierarchal_app_specific_model(self): 54 | models = app_loading.haystack_get_models( 55 | "hierarchal_app_django.HierarchalAppModel" 56 | ) 57 | self.assertIsInstance(models, (list, GeneratorType)) 58 | self.assertSetEqual( 59 | set(str(i._meta) for i in models), 60 | set(("hierarchal_app_django.hierarchalappmodel",)), 61 | ) 62 | 63 | 64 | class AppWithoutModelsTests(TestCase): 65 | # Confirm that everything works if an app is enabled 66 | 67 | def test_simple_view(self): 68 | url = reverse("app-without-models:simple-view") 69 | resp = self.client.get(url) 70 | self.assertEqual(resp.content.decode("utf-8"), "OK") 71 | -------------------------------------------------------------------------------- /test_haystack/test_app_using_appconfig/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "test_app_using_appconfig.apps.SimpleTestAppConfig" 2 | -------------------------------------------------------------------------------- /test_haystack/test_app_using_appconfig/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SimpleTestAppConfig(AppConfig): 5 | name = "test_haystack.test_app_using_appconfig" 6 | verbose_name = "Simple test app using AppConfig" 7 | -------------------------------------------------------------------------------- /test_haystack/test_app_using_appconfig/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [] 7 | 8 | operations = [ 9 | migrations.CreateModel( 10 | name="MicroBlogPost", 11 | fields=[ 12 | ( 13 | "id", 14 | models.AutoField( 15 | verbose_name="ID", 16 | serialize=False, 17 | auto_created=True, 18 | primary_key=True, 19 | ), 20 | ), 21 | ("text", models.CharField(max_length=140)), 22 | ], 23 | options={}, 24 | bases=(models.Model,), 25 | ) 26 | ] 27 | -------------------------------------------------------------------------------- /test_haystack/test_app_using_appconfig/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/test_haystack/test_app_using_appconfig/migrations/__init__.py -------------------------------------------------------------------------------- /test_haystack/test_app_using_appconfig/models.py: -------------------------------------------------------------------------------- 1 | from django.db.models import CharField, Model 2 | 3 | 4 | class MicroBlogPost(Model): 5 | text = CharField(max_length=140) 6 | -------------------------------------------------------------------------------- /test_haystack/test_app_using_appconfig/search_indexes.py: -------------------------------------------------------------------------------- 1 | from haystack import indexes 2 | 3 | from .models import MicroBlogPost 4 | 5 | 6 | class MicroBlogSearchIndex(indexes.SearchIndex, indexes.Indexable): 7 | text = indexes.CharField(document=True, use_template=False, model_attr="text") 8 | 9 | def get_model(self): 10 | return MicroBlogPost 11 | -------------------------------------------------------------------------------- /test_haystack/test_app_using_appconfig/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from .models import MicroBlogPost 4 | 5 | 6 | class AppConfigTests(TestCase): 7 | def test_index_collection(self): 8 | from haystack import connections 9 | 10 | unified_index = connections["default"].get_unified_index() 11 | models = unified_index.get_indexed_models() 12 | 13 | self.assertIn(MicroBlogPost, models) 14 | -------------------------------------------------------------------------------- /test_haystack/test_app_with_hierarchy/__init__.py: -------------------------------------------------------------------------------- 1 | """Test app with multiple hierarchy levels above the actual models.py file""" 2 | -------------------------------------------------------------------------------- /test_haystack/test_app_with_hierarchy/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/test_haystack/test_app_with_hierarchy/contrib/__init__.py -------------------------------------------------------------------------------- /test_haystack/test_app_with_hierarchy/contrib/django/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/test_haystack/test_app_with_hierarchy/contrib/django/__init__.py -------------------------------------------------------------------------------- /test_haystack/test_app_with_hierarchy/contrib/django/hierarchal_app_django/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/test_haystack/test_app_with_hierarchy/contrib/django/hierarchal_app_django/__init__.py -------------------------------------------------------------------------------- /test_haystack/test_app_with_hierarchy/contrib/django/hierarchal_app_django/models.py: -------------------------------------------------------------------------------- 1 | from django.db.models import BooleanField, CharField, Model 2 | 3 | 4 | class HierarchalAppModel(Model): 5 | enabled = BooleanField(default=True) 6 | 7 | 8 | class HierarchalAppSecondModel(Model): 9 | title = CharField(max_length=16) 10 | -------------------------------------------------------------------------------- /test_haystack/test_app_without_models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-haystack/8ab14a273d8f438fe3542d988954e41bddc13fc8/test_haystack/test_app_without_models/__init__.py -------------------------------------------------------------------------------- /test_haystack/test_app_without_models/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import simple_view 4 | 5 | urlpatterns = [path("simple-view", simple_view, name="simple-view")] 6 | -------------------------------------------------------------------------------- /test_haystack/test_app_without_models/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | 4 | def simple_view(request): 5 | return HttpResponse("OK") 6 | -------------------------------------------------------------------------------- /test_haystack/test_backends.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from django.core.exceptions import ImproperlyConfigured 4 | from django.test import TestCase 5 | 6 | from haystack.utils import loading 7 | 8 | 9 | class LoadBackendTestCase(TestCase): 10 | def test_load_solr(self): 11 | try: 12 | import pysolr 13 | except ImportError: 14 | warnings.warn( 15 | "Pysolr doesn't appear to be installed. Unable to test loading the Solr backend." 16 | ) 17 | return 18 | 19 | backend = loading.load_backend("haystack.backends.solr_backend.SolrEngine") 20 | self.assertEqual(backend.__name__, "SolrEngine") 21 | 22 | def test_load_whoosh(self): 23 | try: 24 | import whoosh 25 | except ImportError: 26 | warnings.warn( 27 | "Whoosh doesn't appear to be installed. Unable to test loading the Whoosh backend." 28 | ) 29 | return 30 | 31 | backend = loading.load_backend("haystack.backends.whoosh_backend.WhooshEngine") 32 | self.assertEqual(backend.__name__, "WhooshEngine") 33 | 34 | def test_load_elasticsearch(self): 35 | try: 36 | import elasticsearch 37 | except ImportError: 38 | warnings.warn( 39 | "elasticsearch-py doesn't appear to be installed. Unable to test loading the ElasticSearch backend." 40 | ) 41 | return 42 | 43 | backend = loading.load_backend( 44 | "haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine" 45 | ) 46 | self.assertEqual(backend.__name__, "ElasticsearchSearchEngine") 47 | 48 | def test_load_simple(self): 49 | backend = loading.load_backend("haystack.backends.simple_backend.SimpleEngine") 50 | self.assertEqual(backend.__name__, "SimpleEngine") 51 | 52 | def test_load_nonexistent(self): 53 | try: 54 | backend = loading.load_backend("foobar") 55 | self.fail() 56 | except ImproperlyConfigured as e: 57 | self.assertEqual( 58 | str(e), 59 | "The provided backend 'foobar' is not a complete Python path to a BaseEngine subclass.", 60 | ) 61 | 62 | try: 63 | backend = loading.load_backend("foobar.FooEngine") 64 | self.fail() 65 | except ImportError as e: 66 | pass 67 | 68 | try: 69 | backend = loading.load_backend("haystack.backends.simple_backend.FooEngine") 70 | self.fail() 71 | except ImportError as e: 72 | self.assertEqual( 73 | str(e), 74 | "The Python module 'haystack.backends.simple_backend' has no 'FooEngine' class.", 75 | ) 76 | -------------------------------------------------------------------------------- /test_haystack/test_discovery.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from haystack import connections 4 | from haystack.utils.loading import UnifiedIndex 5 | from test_haystack.discovery.search_indexes import FooIndex 6 | 7 | EXPECTED_INDEX_MODEL_COUNT = 6 8 | 9 | 10 | class ManualDiscoveryTestCase(TestCase): 11 | def test_discovery(self): 12 | old_ui = connections["default"].get_unified_index() 13 | connections["default"]._index = UnifiedIndex() 14 | ui = connections["default"].get_unified_index() 15 | self.assertEqual(len(ui.get_indexed_models()), EXPECTED_INDEX_MODEL_COUNT) 16 | 17 | ui.build(indexes=[FooIndex()]) 18 | 19 | self.assertListEqual( 20 | ["discovery.foo"], [str(i._meta) for i in ui.get_indexed_models()] 21 | ) 22 | 23 | ui.build(indexes=[]) 24 | 25 | self.assertListEqual([], ui.get_indexed_models()) 26 | connections["default"]._index = old_ui 27 | 28 | 29 | class AutomaticDiscoveryTestCase(TestCase): 30 | def test_discovery(self): 31 | old_ui = connections["default"].get_unified_index() 32 | connections["default"]._index = UnifiedIndex() 33 | ui = connections["default"].get_unified_index() 34 | self.assertEqual(len(ui.get_indexed_models()), EXPECTED_INDEX_MODEL_COUNT) 35 | 36 | # Test exclusions. 37 | ui.excluded_indexes = ["test_haystack.discovery.search_indexes.BarIndex"] 38 | ui.build() 39 | 40 | indexed_model_names = [str(i._meta) for i in ui.get_indexed_models()] 41 | self.assertIn("multipleindex.foo", indexed_model_names) 42 | self.assertIn("multipleindex.bar", indexed_model_names) 43 | self.assertNotIn("discovery.bar", indexed_model_names) 44 | 45 | ui.excluded_indexes = [ 46 | "test_haystack.discovery.search_indexes.BarIndex", 47 | "test_haystack.discovery.search_indexes.FooIndex", 48 | ] 49 | ui.build() 50 | 51 | indexed_model_names = [str(i._meta) for i in ui.get_indexed_models()] 52 | self.assertIn("multipleindex.foo", indexed_model_names) 53 | self.assertIn("multipleindex.bar", indexed_model_names) 54 | self.assertListEqual( 55 | [], [i for i in indexed_model_names if i.startswith("discovery")] 56 | ) 57 | connections["default"]._index = old_ui 58 | -------------------------------------------------------------------------------- /test_haystack/test_generic_views.py: -------------------------------------------------------------------------------- 1 | from django.test.client import RequestFactory 2 | from django.test.testcases import TestCase 3 | 4 | from haystack.forms import ModelSearchForm 5 | from haystack.generic_views import SearchView 6 | 7 | 8 | class GenericSearchViewsTestCase(TestCase): 9 | """Test case for the generic search views.""" 10 | 11 | def setUp(self): 12 | super().setUp() 13 | self.query = "haystack" 14 | self.request = self.get_request(url="/some/random/url?q={0}".format(self.query)) 15 | 16 | def test_get_form_kwargs(self): 17 | """Test getting the search view form kwargs.""" 18 | v = SearchView() 19 | v.request = self.request 20 | 21 | form_kwargs = v.get_form_kwargs() 22 | self.assertEqual(form_kwargs.get("data").get("q"), self.query) 23 | self.assertEqual(form_kwargs.get("initial"), {}) 24 | self.assertTrue("searchqueryset" in form_kwargs) 25 | self.assertTrue("load_all" in form_kwargs) 26 | 27 | def test_search_view_response(self): 28 | """Test the generic SearchView response.""" 29 | response = SearchView.as_view()(request=self.request) 30 | 31 | context = response.context_data 32 | self.assertEqual(context["query"], self.query) 33 | self.assertEqual(context.get("view").__class__, SearchView) 34 | self.assertEqual(context.get("form").__class__, ModelSearchForm) 35 | self.assertIn("page_obj", context) 36 | self.assertNotIn("page", context) 37 | 38 | def test_search_view_form_valid(self): 39 | """Test the generic SearchView form is valid.""" 40 | v = SearchView() 41 | v.kwargs = {} 42 | v.request = self.request 43 | 44 | form = v.get_form(v.get_form_class()) 45 | response = v.form_valid(form) 46 | context = response.context_data 47 | 48 | self.assertEqual(context["query"], self.query) 49 | 50 | def test_search_view_form_invalid(self): 51 | """Test the generic SearchView form is invalid.""" 52 | v = SearchView() 53 | v.kwargs = {} 54 | v.request = self.request 55 | 56 | form = v.get_form(v.get_form_class()) 57 | response = v.form_invalid(form) 58 | context = response.context_data 59 | 60 | self.assertTrue("query" not in context) 61 | 62 | def get_request(self, url, method="get", data=None, **kwargs): 63 | """Gets the request object for the view. 64 | 65 | :param url: a mock url to use for the request 66 | :param method: the http method to use for the request ('get', 'post', 67 | etc). 68 | """ 69 | factory = RequestFactory() 70 | factory_func = getattr(factory, method) 71 | 72 | request = factory_func(url, data=data or {}, **kwargs) 73 | return request 74 | -------------------------------------------------------------------------------- /test_haystack/test_inputs.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from haystack import connections, inputs 4 | 5 | 6 | class InputTestCase(TestCase): 7 | def setUp(self): 8 | super().setUp() 9 | self.query_obj = connections["default"].get_query() 10 | 11 | def test_raw_init(self): 12 | raw = inputs.Raw("hello OR there, :you") 13 | self.assertEqual(raw.query_string, "hello OR there, :you") 14 | self.assertEqual(raw.kwargs, {}) 15 | self.assertEqual(raw.post_process, False) 16 | 17 | raw = inputs.Raw("hello OR there, :you", test="really") 18 | self.assertEqual(raw.query_string, "hello OR there, :you") 19 | self.assertEqual(raw.kwargs, {"test": "really"}) 20 | self.assertEqual(raw.post_process, False) 21 | 22 | def test_raw_prepare(self): 23 | raw = inputs.Raw("hello OR there, :you") 24 | self.assertEqual(raw.prepare(self.query_obj), "hello OR there, :you") 25 | 26 | def test_clean_init(self): 27 | clean = inputs.Clean("hello OR there, :you") 28 | self.assertEqual(clean.query_string, "hello OR there, :you") 29 | self.assertEqual(clean.post_process, True) 30 | 31 | def test_clean_prepare(self): 32 | clean = inputs.Clean("hello OR there, :you") 33 | self.assertEqual(clean.prepare(self.query_obj), "hello OR there, :you") 34 | 35 | def test_exact_init(self): 36 | exact = inputs.Exact("hello OR there, :you") 37 | self.assertEqual(exact.query_string, "hello OR there, :you") 38 | self.assertEqual(exact.post_process, True) 39 | 40 | def test_exact_prepare(self): 41 | exact = inputs.Exact("hello OR there, :you") 42 | self.assertEqual(exact.prepare(self.query_obj), '"hello OR there, :you"') 43 | 44 | # Incorrect, but the backend doesn't implement much of anything useful. 45 | exact = inputs.Exact("hello OR there, :you", clean=True) 46 | self.assertEqual(exact.prepare(self.query_obj), '"hello OR there, :you"') 47 | 48 | def test_not_init(self): 49 | not_it = inputs.Not("hello OR there, :you") 50 | self.assertEqual(not_it.query_string, "hello OR there, :you") 51 | self.assertEqual(not_it.post_process, True) 52 | 53 | def test_not_prepare(self): 54 | not_it = inputs.Not("hello OR there, :you") 55 | self.assertEqual(not_it.prepare(self.query_obj), "NOT (hello OR there, :you)") 56 | 57 | def test_autoquery_init(self): 58 | autoquery = inputs.AutoQuery('panic -don\'t "froody dude"') 59 | self.assertEqual(autoquery.query_string, 'panic -don\'t "froody dude"') 60 | self.assertEqual(autoquery.post_process, False) 61 | 62 | def test_autoquery_prepare(self): 63 | autoquery = inputs.AutoQuery('panic -don\'t "froody dude"') 64 | self.assertEqual( 65 | autoquery.prepare(self.query_obj), 'panic NOT don\'t "froody dude"' 66 | ) 67 | 68 | def test_altparser_init(self): 69 | altparser = inputs.AltParser("dismax") 70 | self.assertEqual(altparser.parser_name, "dismax") 71 | self.assertEqual(altparser.query_string, "") 72 | self.assertEqual(altparser.kwargs, {}) 73 | self.assertEqual(altparser.post_process, False) 74 | 75 | altparser = inputs.AltParser("dismax", "douglas adams", qf="author", mm=1) 76 | self.assertEqual(altparser.parser_name, "dismax") 77 | self.assertEqual(altparser.query_string, "douglas adams") 78 | self.assertEqual(altparser.kwargs, {"mm": 1, "qf": "author"}) 79 | self.assertEqual(altparser.post_process, False) 80 | 81 | def test_altparser_prepare(self): 82 | altparser = inputs.AltParser("dismax", "douglas adams", qf="author", mm=1) 83 | # Not supported on that backend. 84 | self.assertEqual(altparser.prepare(self.query_obj), "") 85 | -------------------------------------------------------------------------------- /test_haystack/utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from django.conf import settings 4 | 5 | 6 | def check_solr(using="solr"): 7 | try: 8 | from pysolr import Solr, SolrError 9 | except ImportError: 10 | raise unittest.SkipTest("pysolr not installed.") 11 | 12 | solr = Solr(settings.HAYSTACK_CONNECTIONS[using]["URL"]) 13 | try: 14 | solr.search("*:*") 15 | except SolrError as e: 16 | raise unittest.SkipTest( 17 | "solr not running on %r" % settings.HAYSTACK_CONNECTIONS[using]["URL"], e 18 | ) 19 | -------------------------------------------------------------------------------- /test_haystack/whoosh_tests/__init__.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | warnings.simplefilter("ignore", Warning) 4 | -------------------------------------------------------------------------------- /test_haystack/whoosh_tests/test_forms.py: -------------------------------------------------------------------------------- 1 | """Tests for Whoosh spelling suggestions""" 2 | from django.conf import settings 3 | from django.http import HttpRequest 4 | 5 | from haystack.forms import SearchForm 6 | from haystack.query import SearchQuerySet 7 | from haystack.views import SearchView 8 | 9 | from .test_whoosh_backend import LiveWhooshRoundTripTestCase 10 | 11 | 12 | class SpellingSuggestionTestCase(LiveWhooshRoundTripTestCase): 13 | fixtures = ["base_data"] 14 | 15 | def setUp(self): 16 | self.old_spelling_setting = settings.HAYSTACK_CONNECTIONS["whoosh"].get( 17 | "INCLUDE_SPELLING", False 18 | ) 19 | settings.HAYSTACK_CONNECTIONS["whoosh"]["INCLUDE_SPELLING"] = True 20 | 21 | super().setUp() 22 | 23 | def tearDown(self): 24 | settings.HAYSTACK_CONNECTIONS["whoosh"][ 25 | "INCLUDE_SPELLING" 26 | ] = self.old_spelling_setting 27 | super().tearDown() 28 | 29 | def test_form_suggestion(self): 30 | form = SearchForm({"q": "exampl"}, searchqueryset=SearchQuerySet("whoosh")) 31 | self.assertEqual(form.get_suggestion(), "example") 32 | 33 | def test_view_suggestion(self): 34 | view = SearchView( 35 | template="test_suggestion.html", searchqueryset=SearchQuerySet("whoosh") 36 | ) 37 | mock = HttpRequest() 38 | mock.GET["q"] = "exampl" 39 | resp = view(mock) 40 | self.assertEqual(resp.content, b"Suggestion: example") 41 | -------------------------------------------------------------------------------- /test_haystack/whoosh_tests/test_inputs.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from haystack import connections, inputs 4 | 5 | 6 | class WhooshInputTestCase(TestCase): 7 | def setUp(self): 8 | super().setUp() 9 | self.query_obj = connections["whoosh"].get_query() 10 | 11 | def test_raw_init(self): 12 | raw = inputs.Raw("hello OR there, :you") 13 | self.assertEqual(raw.query_string, "hello OR there, :you") 14 | self.assertEqual(raw.kwargs, {}) 15 | self.assertEqual(raw.post_process, False) 16 | 17 | raw = inputs.Raw("hello OR there, :you", test="really") 18 | self.assertEqual(raw.query_string, "hello OR there, :you") 19 | self.assertEqual(raw.kwargs, {"test": "really"}) 20 | self.assertEqual(raw.post_process, False) 21 | 22 | def test_raw_prepare(self): 23 | raw = inputs.Raw("hello OR there, :you") 24 | self.assertEqual(raw.prepare(self.query_obj), "hello OR there, :you") 25 | 26 | def test_clean_init(self): 27 | clean = inputs.Clean("hello OR there, :you") 28 | self.assertEqual(clean.query_string, "hello OR there, :you") 29 | self.assertEqual(clean.post_process, True) 30 | 31 | def test_clean_prepare(self): 32 | clean = inputs.Clean("hello OR there, :you") 33 | self.assertEqual(clean.prepare(self.query_obj), "hello or there, ':you'") 34 | 35 | def test_exact_init(self): 36 | exact = inputs.Exact("hello OR there, :you") 37 | self.assertEqual(exact.query_string, "hello OR there, :you") 38 | self.assertEqual(exact.post_process, True) 39 | 40 | def test_exact_prepare(self): 41 | exact = inputs.Exact("hello OR there, :you") 42 | self.assertEqual(exact.prepare(self.query_obj), '"hello OR there, :you"') 43 | 44 | def test_not_init(self): 45 | not_it = inputs.Not("hello OR there, :you") 46 | self.assertEqual(not_it.query_string, "hello OR there, :you") 47 | self.assertEqual(not_it.post_process, True) 48 | 49 | def test_not_prepare(self): 50 | not_it = inputs.Not("hello OR there, :you") 51 | self.assertEqual(not_it.prepare(self.query_obj), "NOT (hello or there, ':you')") 52 | 53 | def test_autoquery_init(self): 54 | autoquery = inputs.AutoQuery('panic -don\'t "froody dude"') 55 | self.assertEqual(autoquery.query_string, 'panic -don\'t "froody dude"') 56 | self.assertEqual(autoquery.post_process, False) 57 | 58 | def test_autoquery_prepare(self): 59 | autoquery = inputs.AutoQuery('panic -don\'t "froody dude"') 60 | self.assertEqual( 61 | autoquery.prepare(self.query_obj), 'panic NOT don\'t "froody dude"' 62 | ) 63 | 64 | def test_altparser_init(self): 65 | altparser = inputs.AltParser("dismax") 66 | self.assertEqual(altparser.parser_name, "dismax") 67 | self.assertEqual(altparser.query_string, "") 68 | self.assertEqual(altparser.kwargs, {}) 69 | self.assertEqual(altparser.post_process, False) 70 | 71 | altparser = inputs.AltParser("dismax", "douglas adams", qf="author", mm=1) 72 | self.assertEqual(altparser.parser_name, "dismax") 73 | self.assertEqual(altparser.query_string, "douglas adams") 74 | self.assertEqual(altparser.kwargs, {"mm": 1, "qf": "author"}) 75 | self.assertEqual(altparser.post_process, False) 76 | 77 | def test_altparser_prepare(self): 78 | altparser = inputs.AltParser("hello OR there, :you") 79 | # Not supported on that backend. 80 | self.assertEqual(altparser.prepare(self.query_obj), "") 81 | -------------------------------------------------------------------------------- /test_haystack/whoosh_tests/testcases.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from django.conf import settings 5 | from django.test import TestCase 6 | 7 | 8 | class WhooshTestCase(TestCase): 9 | fixtures = ["base_data"] 10 | 11 | @classmethod 12 | def setUpClass(cls): 13 | for name, conn_settings in settings.HAYSTACK_CONNECTIONS.items(): 14 | if ( 15 | conn_settings["ENGINE"] 16 | != "haystack.backends.whoosh_backend.WhooshEngine" 17 | ): 18 | continue 19 | 20 | if "STORAGE" in conn_settings and conn_settings["STORAGE"] != "file": 21 | continue 22 | 23 | # Start clean 24 | if os.path.exists(conn_settings["PATH"]): 25 | shutil.rmtree(conn_settings["PATH"]) 26 | 27 | from haystack import connections 28 | 29 | connections[name].get_backend().setup() 30 | 31 | super(WhooshTestCase, cls).setUpClass() 32 | 33 | @classmethod 34 | def tearDownClass(cls): 35 | for conn in settings.HAYSTACK_CONNECTIONS.values(): 36 | if conn["ENGINE"] != "haystack.backends.whoosh_backend.WhooshEngine": 37 | continue 38 | 39 | if "STORAGE" in conn and conn["STORAGE"] != "file": 40 | continue 41 | 42 | # Start clean 43 | if os.path.exists(conn["PATH"]): 44 | shutil.rmtree(conn["PATH"]) 45 | 46 | super(WhooshTestCase, cls).tearDownClass() 47 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | docs 4 | py35-django2.2-es{1.x,2.x,5.x} 5 | py{36,37,38,py}-django{2.2,3.0}-es{1.x,2.x,5.x} 6 | 7 | 8 | [testenv] 9 | commands = 10 | python test_haystack/solr_tests/server/wait-for-solr 11 | python {toxinidir}/setup.py test 12 | deps = 13 | requests 14 | django2.2: Django>=2.2,<3.0 15 | django3.0: Django>=3.0,<3.1 16 | es1.x: elasticsearch>=1,<2 17 | es2.x: elasticsearch>=2,<3 18 | es5.x: elasticsearch>=5,<6 19 | setenv = 20 | es1.x: VERSION_ES=>=1,<2 21 | es2.x: VERSION_ES=>=2,<3 22 | es5.x: VERSION_ES=>=5,<6 23 | 24 | 25 | [testenv:docs] 26 | changedir = docs 27 | deps = 28 | sphinx 29 | commands = 30 | sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 31 | --------------------------------------------------------------------------------