django-pitchfork 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_pitchfork-0.1.0/LICENSE +30 -0
- django_pitchfork-0.1.0/PKG-INFO +153 -0
- django_pitchfork-0.1.0/README.md +118 -0
- django_pitchfork-0.1.0/django_pitchfork/__init__.py +47 -0
- django_pitchfork-0.1.0/django_pitchfork/admin.py +167 -0
- django_pitchfork-0.1.0/django_pitchfork/apps.py +38 -0
- django_pitchfork-0.1.0/django_pitchfork/backends/__init__.py +17 -0
- django_pitchfork-0.1.0/django_pitchfork/backends/base.py +15 -0
- django_pitchfork-0.1.0/django_pitchfork/backends/caps.py +402 -0
- django_pitchfork-0.1.0/django_pitchfork/backends/elasticsearch.py +1733 -0
- django_pitchfork-0.1.0/django_pitchfork/backends/pitchfork.py +44 -0
- django_pitchfork-0.1.0/django_pitchfork/backends/results.py +12 -0
- django_pitchfork-0.1.0/django_pitchfork/backends/simple.py +14 -0
- django_pitchfork-0.1.0/django_pitchfork/backends/solr.py +1007 -0
- django_pitchfork-0.1.0/django_pitchfork/backends/whoosh.py +1144 -0
- django_pitchfork-0.1.0/django_pitchfork/backends/xapian.py +1719 -0
- django_pitchfork-0.1.0/django_pitchfork/compat/__init__.py +9 -0
- django_pitchfork-0.1.0/django_pitchfork/compat/haystack/__init__.py +90 -0
- django_pitchfork-0.1.0/django_pitchfork/compat/haystack/admin.py +3 -0
- django_pitchfork-0.1.0/django_pitchfork/compat/haystack/backends.py +8 -0
- django_pitchfork-0.1.0/django_pitchfork/compat/haystack/constants.py +3 -0
- django_pitchfork-0.1.0/django_pitchfork/compat/haystack/exceptions.py +3 -0
- django_pitchfork-0.1.0/django_pitchfork/compat/haystack/fields.py +3 -0
- django_pitchfork-0.1.0/django_pitchfork/compat/haystack/indexes.py +3 -0
- django_pitchfork-0.1.0/django_pitchfork/compat/haystack/inputs.py +3 -0
- django_pitchfork-0.1.0/django_pitchfork/compat/haystack/manager.py +3 -0
- django_pitchfork-0.1.0/django_pitchfork/compat/haystack/models.py +3 -0
- django_pitchfork-0.1.0/django_pitchfork/compat/haystack/query.py +3 -0
- django_pitchfork-0.1.0/django_pitchfork/compat/haystack/routers.py +3 -0
- django_pitchfork-0.1.0/django_pitchfork/compat/haystack/signals.py +3 -0
- django_pitchfork-0.1.0/django_pitchfork/compat/haystack/urls.py +3 -0
- django_pitchfork-0.1.0/django_pitchfork/compat/haystack/utils.py +4 -0
- django_pitchfork-0.1.0/django_pitchfork/constants.py +40 -0
- django_pitchfork-0.1.0/django_pitchfork/exceptions.py +18 -0
- django_pitchfork-0.1.0/django_pitchfork/fields.py +29 -0
- django_pitchfork-0.1.0/django_pitchfork/forms.py +135 -0
- django_pitchfork-0.1.0/django_pitchfork/generic_views.py +140 -0
- django_pitchfork-0.1.0/django_pitchfork/indexes.py +37 -0
- django_pitchfork-0.1.0/django_pitchfork/inputs.py +12 -0
- django_pitchfork-0.1.0/django_pitchfork/management/__init__.py +0 -0
- django_pitchfork-0.1.0/django_pitchfork/management/commands/__init__.py +0 -0
- django_pitchfork-0.1.0/django_pitchfork/management/commands/build_solr_schema.py +212 -0
- django_pitchfork-0.1.0/django_pitchfork/management/commands/clear_index.py +67 -0
- django_pitchfork-0.1.0/django_pitchfork/management/commands/haystack_info.py +22 -0
- django_pitchfork-0.1.0/django_pitchfork/management/commands/rebuild_index.py +65 -0
- django_pitchfork-0.1.0/django_pitchfork/management/commands/update_index.py +431 -0
- django_pitchfork-0.1.0/django_pitchfork/manager.py +3 -0
- django_pitchfork-0.1.0/django_pitchfork/models.py +3 -0
- django_pitchfork-0.1.0/django_pitchfork/panels.py +102 -0
- django_pitchfork-0.1.0/django_pitchfork/query.py +14 -0
- django_pitchfork-0.1.0/django_pitchfork/routers.py +17 -0
- django_pitchfork-0.1.0/django_pitchfork/signals.py +3 -0
- django_pitchfork-0.1.0/django_pitchfork/templates/panels/haystack.html +33 -0
- django_pitchfork-0.1.0/django_pitchfork/templatetags/__init__.py +0 -0
- django_pitchfork-0.1.0/django_pitchfork/templatetags/highlight.py +133 -0
- django_pitchfork-0.1.0/django_pitchfork/templatetags/more_like_this.py +121 -0
- django_pitchfork-0.1.0/django_pitchfork/urls.py +5 -0
- django_pitchfork-0.1.0/django_pitchfork/utils/__init__.py +20 -0
- django_pitchfork-0.1.0/django_pitchfork/utils/app_loading.py +10 -0
- django_pitchfork-0.1.0/django_pitchfork/utils/geo.py +9 -0
- django_pitchfork-0.1.0/django_pitchfork/utils/highlighting.py +5 -0
- django_pitchfork-0.1.0/django_pitchfork/utils/loading.py +167 -0
- django_pitchfork-0.1.0/django_pitchfork/utils/log.py +3 -0
- django_pitchfork-0.1.0/django_pitchfork/views.py +252 -0
- django_pitchfork-0.1.0/django_pitchfork.egg-info/PKG-INFO +153 -0
- django_pitchfork-0.1.0/django_pitchfork.egg-info/SOURCES.txt +88 -0
- django_pitchfork-0.1.0/django_pitchfork.egg-info/dependency_links.txt +1 -0
- django_pitchfork-0.1.0/django_pitchfork.egg-info/requires.txt +11 -0
- django_pitchfork-0.1.0/django_pitchfork.egg-info/top_level.txt +1 -0
- django_pitchfork-0.1.0/pyproject.toml +63 -0
- django_pitchfork-0.1.0/setup.cfg +4 -0
- django_pitchfork-0.1.0/tests/test_altered_internal_names.py +98 -0
- django_pitchfork-0.1.0/tests/test_app_loading.py +70 -0
- django_pitchfork-0.1.0/tests/test_backends.py +75 -0
- django_pitchfork-0.1.0/tests/test_compat_shim.py +148 -0
- django_pitchfork-0.1.0/tests/test_discovery.py +60 -0
- django_pitchfork-0.1.0/tests/test_django_config_detection.py +15 -0
- django_pitchfork-0.1.0/tests/test_fields.py +781 -0
- django_pitchfork-0.1.0/tests/test_forms.py +184 -0
- django_pitchfork-0.1.0/tests/test_generic_views.py +81 -0
- django_pitchfork-0.1.0/tests/test_indexes.py +926 -0
- django_pitchfork-0.1.0/tests/test_inputs.py +84 -0
- django_pitchfork-0.1.0/tests/test_loading.py +423 -0
- django_pitchfork-0.1.0/tests/test_management_commands.py +162 -0
- django_pitchfork-0.1.0/tests/test_managers.py +269 -0
- django_pitchfork-0.1.0/tests/test_models.py +205 -0
- django_pitchfork-0.1.0/tests/test_query.py +1084 -0
- django_pitchfork-0.1.0/tests/test_templatetags.py +102 -0
- django_pitchfork-0.1.0/tests/test_utils.py +337 -0
- django_pitchfork-0.1.0/tests/test_views.py +308 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, the django-pitchfork authors.
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
10
|
+
this list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
23
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
24
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
25
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
26
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
27
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
28
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
29
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
30
|
+
POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-pitchfork
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Rust-powered search for Django: the haystack-compatible search layer on pitchfork-engine (native Tantivy, Whoosh, Solr, Elasticsearch, Xapian).
|
|
5
|
+
License: BSD-3-Clause
|
|
6
|
+
Classifier: Development Status :: 4 - Beta
|
|
7
|
+
Classifier: Environment :: Web Environment
|
|
8
|
+
Classifier: Framework :: Django
|
|
9
|
+
Classifier: Framework :: Django :: 4.2
|
|
10
|
+
Classifier: Framework :: Django :: 5.0
|
|
11
|
+
Classifier: Framework :: Django :: 5.1
|
|
12
|
+
Classifier: Framework :: Django :: 6.0
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: django>=4.2
|
|
27
|
+
Requires-Dist: pitchfork-engine>=0.1.0
|
|
28
|
+
Provides-Extra: whoosh
|
|
29
|
+
Requires-Dist: whoosh<3,>=2.7; extra == "whoosh"
|
|
30
|
+
Provides-Extra: solr
|
|
31
|
+
Requires-Dist: pysolr<5,>=3.9; extra == "solr"
|
|
32
|
+
Provides-Extra: elasticsearch
|
|
33
|
+
Requires-Dist: elasticsearch<8,>=7.17; extra == "elasticsearch"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# django-pitchfork
|
|
37
|
+
|
|
38
|
+
**Full-text search for Django, powered by Rust.** `django-pitchfork` is a
|
|
39
|
+
drop-in replacement for [django-haystack](https://github.com/django-haystack/django-haystack):
|
|
40
|
+
the same public API your templates, views and management commands already
|
|
41
|
+
speak — `SearchQuerySet`, `SearchIndex`, fields, inputs, views, forms,
|
|
42
|
+
templatetags, management commands — backed by a fast Rust/Tantivy engine
|
|
43
|
+
instead of Python-only indexes.
|
|
44
|
+
|
|
45
|
+
It is the Django-facing search layer on top of
|
|
46
|
+
[pitchfork-engine](https://gitlab.com/co-plus-plus/pitchfork-engine) (the
|
|
47
|
+
search engine itself: a standalone Tantivy-backed HTTP server, a PyO3
|
|
48
|
+
in-process extension, and the haystack backend machinery).
|
|
49
|
+
|
|
50
|
+
## Why
|
|
51
|
+
|
|
52
|
+
- **Same API, faster core.** Your code keeps using the haystack API; queries
|
|
53
|
+
and indexing run against Rust's Tantivy (GIL released, native query AST).
|
|
54
|
+
- **Escape hatch from unmaintained backends.** Upstream haystack's
|
|
55
|
+
Elasticsearch support tracks ES 7 only; here every backend is tested
|
|
56
|
+
against a live fixture in CI, and gaps are documented, not silent.
|
|
57
|
+
- **One-way dependency.** `django_pitchfork` imports `pitchfork` (the
|
|
58
|
+
engine); the engine never imports the replacement.
|
|
59
|
+
|
|
60
|
+
## Backends
|
|
61
|
+
|
|
62
|
+
| Backend | Transport | Notes |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| **pitchfork** (native) | HTTP server or in-process PyO3 | The perf baseline; Rust/Tantivy |
|
|
65
|
+
| Whoosh | file-based index | No service needed |
|
|
66
|
+
| Solr | HTTP (pysolr) | Solr 6 fixture semantics; managed-schema generation |
|
|
67
|
+
| Elasticsearch | HTTP (elasticsearch-py) | 7.x tested; 8.x is a documented gap |
|
|
68
|
+
| Xapian | in-process bindings | Bindings not on PyPI — install from your distro |
|
|
69
|
+
|
|
70
|
+
Each backend ships with a **capability contract** (`caps` module) and a
|
|
71
|
+
generated support matrix ([`docs/backend_support.md`](docs/backend_support.md))
|
|
72
|
+
so documented features are never hand-written claims. Every backend is
|
|
73
|
+
covered by the shared conformance suite (`conformance/run.py`) against live
|
|
74
|
+
fixtures.
|
|
75
|
+
|
|
76
|
+
## Quick start
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# settings.py
|
|
80
|
+
PITCHFORK_CONNECTIONS = {
|
|
81
|
+
"default": {
|
|
82
|
+
"ENGINE": "django_pitchfork.backends.pitchfork.PitchforkEngine",
|
|
83
|
+
"URL": "http://127.0.0.1:8117/", # the pitchfork-server fixture
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# myapp/search_indexes.py
|
|
90
|
+
from django_pitchfork import indexes
|
|
91
|
+
|
|
92
|
+
class NoteIndex(indexes.SearchIndex, indexes.Indexable):
|
|
93
|
+
text = indexes.CharField(document=True)
|
|
94
|
+
title = indexes.CharField(model_attr="title")
|
|
95
|
+
|
|
96
|
+
def get_model(self):
|
|
97
|
+
return Note
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
python manage.py rebuild_index
|
|
102
|
+
python manage.py haystack_info
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from django_pitchfork.query import SearchQuerySet
|
|
107
|
+
|
|
108
|
+
results = SearchQuerySet().filter(content="haystack").order_by("-pub_date")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Install:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
python -m pip install "django-pitchfork" # core (engine included)
|
|
115
|
+
python -m pip install "django-pitchfork[whoosh]" # Whoosh
|
|
116
|
+
python -m pip install "django-pitchfork[solr]" # pysolr
|
|
117
|
+
python -m pip install "django-pitchfork[elasticsearch]" # ES 7.x client
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Xapian: no extra — the python-xapian bindings are not on PyPI. Install them
|
|
121
|
+
from your distro (`libxapian-dev` + `python3-xapian`) and the backend picks
|
|
122
|
+
them up (or use the xapian-capable image in `docker/xa/`).
|
|
123
|
+
|
|
124
|
+
The engine repo has the full quick start for the server and embedded
|
|
125
|
+
transports: <https://gitlab.com/co-plus-plus/pitchfork-engine/>
|
|
126
|
+
|
|
127
|
+
## Migrating from django-haystack
|
|
128
|
+
|
|
129
|
+
Mechanical import-sed (`haystack.` → `django_pitchfork.`) is the primary
|
|
130
|
+
path — every public haystack module has a same-named counterpart, and the
|
|
131
|
+
legacy `HAYSTACK_*` setting names keep working. See
|
|
132
|
+
[`MIGRATION.md`](MIGRATION.md).
|
|
133
|
+
|
|
134
|
+
For in-progress migrations an explicit-use compat shim mirrors haystack's
|
|
135
|
+
import surface (parity-tested against a frozen list of haystack's public
|
|
136
|
+
names, with live parity enforced when haystack is installed as a dev dep):
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
import django_pitchfork.compat.haystack as haystack
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The shim is never installed as `haystack` — that name collides with the
|
|
143
|
+
real distribution.
|
|
144
|
+
|
|
145
|
+
## Docs & benchmarks
|
|
146
|
+
|
|
147
|
+
- Sphinx docs: `docs/` (`sphinx-build -W -b html docs/ docs/_build/`).
|
|
148
|
+
- Conformance-suite baselines per backend: [`BENCHMARK.md`](BENCHMARK.md).
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
BSD-3-Clause. The haystack API surface it replaces is also BSD-3-Clause
|
|
153
|
+
(django-haystack).
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# django-pitchfork
|
|
2
|
+
|
|
3
|
+
**Full-text search for Django, powered by Rust.** `django-pitchfork` is a
|
|
4
|
+
drop-in replacement for [django-haystack](https://github.com/django-haystack/django-haystack):
|
|
5
|
+
the same public API your templates, views and management commands already
|
|
6
|
+
speak — `SearchQuerySet`, `SearchIndex`, fields, inputs, views, forms,
|
|
7
|
+
templatetags, management commands — backed by a fast Rust/Tantivy engine
|
|
8
|
+
instead of Python-only indexes.
|
|
9
|
+
|
|
10
|
+
It is the Django-facing search layer on top of
|
|
11
|
+
[pitchfork-engine](https://gitlab.com/co-plus-plus/pitchfork-engine) (the
|
|
12
|
+
search engine itself: a standalone Tantivy-backed HTTP server, a PyO3
|
|
13
|
+
in-process extension, and the haystack backend machinery).
|
|
14
|
+
|
|
15
|
+
## Why
|
|
16
|
+
|
|
17
|
+
- **Same API, faster core.** Your code keeps using the haystack API; queries
|
|
18
|
+
and indexing run against Rust's Tantivy (GIL released, native query AST).
|
|
19
|
+
- **Escape hatch from unmaintained backends.** Upstream haystack's
|
|
20
|
+
Elasticsearch support tracks ES 7 only; here every backend is tested
|
|
21
|
+
against a live fixture in CI, and gaps are documented, not silent.
|
|
22
|
+
- **One-way dependency.** `django_pitchfork` imports `pitchfork` (the
|
|
23
|
+
engine); the engine never imports the replacement.
|
|
24
|
+
|
|
25
|
+
## Backends
|
|
26
|
+
|
|
27
|
+
| Backend | Transport | Notes |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| **pitchfork** (native) | HTTP server or in-process PyO3 | The perf baseline; Rust/Tantivy |
|
|
30
|
+
| Whoosh | file-based index | No service needed |
|
|
31
|
+
| Solr | HTTP (pysolr) | Solr 6 fixture semantics; managed-schema generation |
|
|
32
|
+
| Elasticsearch | HTTP (elasticsearch-py) | 7.x tested; 8.x is a documented gap |
|
|
33
|
+
| Xapian | in-process bindings | Bindings not on PyPI — install from your distro |
|
|
34
|
+
|
|
35
|
+
Each backend ships with a **capability contract** (`caps` module) and a
|
|
36
|
+
generated support matrix ([`docs/backend_support.md`](docs/backend_support.md))
|
|
37
|
+
so documented features are never hand-written claims. Every backend is
|
|
38
|
+
covered by the shared conformance suite (`conformance/run.py`) against live
|
|
39
|
+
fixtures.
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# settings.py
|
|
45
|
+
PITCHFORK_CONNECTIONS = {
|
|
46
|
+
"default": {
|
|
47
|
+
"ENGINE": "django_pitchfork.backends.pitchfork.PitchforkEngine",
|
|
48
|
+
"URL": "http://127.0.0.1:8117/", # the pitchfork-server fixture
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# myapp/search_indexes.py
|
|
55
|
+
from django_pitchfork import indexes
|
|
56
|
+
|
|
57
|
+
class NoteIndex(indexes.SearchIndex, indexes.Indexable):
|
|
58
|
+
text = indexes.CharField(document=True)
|
|
59
|
+
title = indexes.CharField(model_attr="title")
|
|
60
|
+
|
|
61
|
+
def get_model(self):
|
|
62
|
+
return Note
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python manage.py rebuild_index
|
|
67
|
+
python manage.py haystack_info
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from django_pitchfork.query import SearchQuerySet
|
|
72
|
+
|
|
73
|
+
results = SearchQuerySet().filter(content="haystack").order_by("-pub_date")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Install:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python -m pip install "django-pitchfork" # core (engine included)
|
|
80
|
+
python -m pip install "django-pitchfork[whoosh]" # Whoosh
|
|
81
|
+
python -m pip install "django-pitchfork[solr]" # pysolr
|
|
82
|
+
python -m pip install "django-pitchfork[elasticsearch]" # ES 7.x client
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Xapian: no extra — the python-xapian bindings are not on PyPI. Install them
|
|
86
|
+
from your distro (`libxapian-dev` + `python3-xapian`) and the backend picks
|
|
87
|
+
them up (or use the xapian-capable image in `docker/xa/`).
|
|
88
|
+
|
|
89
|
+
The engine repo has the full quick start for the server and embedded
|
|
90
|
+
transports: <https://gitlab.com/co-plus-plus/pitchfork-engine/>
|
|
91
|
+
|
|
92
|
+
## Migrating from django-haystack
|
|
93
|
+
|
|
94
|
+
Mechanical import-sed (`haystack.` → `django_pitchfork.`) is the primary
|
|
95
|
+
path — every public haystack module has a same-named counterpart, and the
|
|
96
|
+
legacy `HAYSTACK_*` setting names keep working. See
|
|
97
|
+
[`MIGRATION.md`](MIGRATION.md).
|
|
98
|
+
|
|
99
|
+
For in-progress migrations an explicit-use compat shim mirrors haystack's
|
|
100
|
+
import surface (parity-tested against a frozen list of haystack's public
|
|
101
|
+
names, with live parity enforced when haystack is installed as a dev dep):
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
import django_pitchfork.compat.haystack as haystack
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The shim is never installed as `haystack` — that name collides with the
|
|
108
|
+
real distribution.
|
|
109
|
+
|
|
110
|
+
## Docs & benchmarks
|
|
111
|
+
|
|
112
|
+
- Sphinx docs: `docs/` (`sphinx-build -W -b html docs/ docs/_build/`).
|
|
113
|
+
- Conformance-suite baselines per backend: [`BENCHMARK.md`](BENCHMARK.md).
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
BSD-3-Clause. The haystack API surface it replaces is also BSD-3-Clause
|
|
118
|
+
(django-haystack).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""django_pitchfork — the public haystack replacement layer on pitchfork-engine.
|
|
2
|
+
|
|
3
|
+
One-way import rule: this package imports
|
|
4
|
+
``pitchfork`` (the engine); the engine never imports this package.
|
|
5
|
+
|
|
6
|
+
The user-facing ``connections`` facade reads ``PITCHFORK_CONNECTIONS`` first
|
|
7
|
+
and falls back to the legacy ``HAYSTACK_CONNECTIONS``, resolving lazily so
|
|
8
|
+
``import django_pitchfork`` never touches Django settings at import time.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from django.conf import settings
|
|
12
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
13
|
+
|
|
14
|
+
from .routers import DefaultRouter # noqa: F401 (public surface)
|
|
15
|
+
from .utils.loading import ConnectionFacade, ConnectionRouter
|
|
16
|
+
|
|
17
|
+
__version__ = "0.0.1"
|
|
18
|
+
|
|
19
|
+
DEFAULT_ALIAS = "default"
|
|
20
|
+
|
|
21
|
+
connections = ConnectionFacade()
|
|
22
|
+
connection_router = ConnectionRouter()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Legacy-setting guards (haystack parity: fail loudly, don't silently ignore).
|
|
26
|
+
if settings.configured:
|
|
27
|
+
if hasattr(settings, "HAYSTACK_SITECONF"):
|
|
28
|
+
raise ImproperlyConfigured(
|
|
29
|
+
"The HAYSTACK_SITECONF setting is no longer used & can be removed."
|
|
30
|
+
)
|
|
31
|
+
if hasattr(settings, "HAYSTACK_SEARCH_ENGINE"):
|
|
32
|
+
raise ImproperlyConfigured(
|
|
33
|
+
"The HAYSTACK_SEARCH_ENGINE setting has been replaced with "
|
|
34
|
+
"HAYSTACK_CONNECTIONS (or PITCHFORK_CONNECTIONS)."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def reset_search_queries(**kwargs):
|
|
39
|
+
for conn in connections.all():
|
|
40
|
+
if conn:
|
|
41
|
+
conn.reset_queries()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if settings.configured and settings.DEBUG:
|
|
45
|
+
from django.core import signals as django_signals
|
|
46
|
+
|
|
47
|
+
django_signals.request_started.connect(reset_search_queries)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from django import VERSION as django_version
|
|
2
|
+
from django.contrib.admin.options import ModelAdmin, csrf_protect_m
|
|
3
|
+
from django.contrib.admin.views.main import SEARCH_VAR, ChangeList
|
|
4
|
+
from django.core.exceptions import PermissionDenied
|
|
5
|
+
from django.core.paginator import InvalidPage, Paginator
|
|
6
|
+
from django.shortcuts import render
|
|
7
|
+
from django.utils.encoding import force_str
|
|
8
|
+
from django.utils.translation import ngettext
|
|
9
|
+
|
|
10
|
+
from django_pitchfork import connections
|
|
11
|
+
from django_pitchfork.constants import DEFAULT_ALIAS
|
|
12
|
+
from django_pitchfork.query import SearchQuerySet
|
|
13
|
+
from django_pitchfork.utils import get_model_ct_tuple
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SearchChangeList(ChangeList):
|
|
17
|
+
def __init__(self, **kwargs):
|
|
18
|
+
self.haystack_connection = kwargs.pop("haystack_connection", DEFAULT_ALIAS)
|
|
19
|
+
super_kwargs = kwargs
|
|
20
|
+
if django_version[0] >= 4:
|
|
21
|
+
super_kwargs["search_help_text"] = "Search..."
|
|
22
|
+
super().__init__(**super_kwargs)
|
|
23
|
+
|
|
24
|
+
def get_results(self, request):
|
|
25
|
+
if SEARCH_VAR not in request.GET:
|
|
26
|
+
return super().get_results(request)
|
|
27
|
+
|
|
28
|
+
# Note that pagination is 0-based, not 1-based.
|
|
29
|
+
sqs = (
|
|
30
|
+
SearchQuerySet(self.haystack_connection)
|
|
31
|
+
.models(self.model)
|
|
32
|
+
.auto_query(request.GET[SEARCH_VAR])
|
|
33
|
+
.load_all()
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
paginator = Paginator(sqs, self.list_per_page)
|
|
37
|
+
# Get the number of objects, with admin filters applied.
|
|
38
|
+
result_count = paginator.count
|
|
39
|
+
full_result_count = (
|
|
40
|
+
SearchQuerySet(self.haystack_connection).models(self.model).all().count()
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
can_show_all = result_count <= self.list_max_show_all
|
|
44
|
+
multi_page = result_count > self.list_per_page
|
|
45
|
+
|
|
46
|
+
# Get the list of objects to display on this page.
|
|
47
|
+
try:
|
|
48
|
+
result_list = paginator.page(self.page_num).object_list
|
|
49
|
+
# Grab just the Django models, since that's what everything else is
|
|
50
|
+
# expecting.
|
|
51
|
+
result_list = [result.object for result in result_list]
|
|
52
|
+
except InvalidPage:
|
|
53
|
+
result_list = ()
|
|
54
|
+
|
|
55
|
+
self.result_count = result_count
|
|
56
|
+
self.full_result_count = full_result_count
|
|
57
|
+
self.result_list = result_list
|
|
58
|
+
self.can_show_all = can_show_all
|
|
59
|
+
self.multi_page = multi_page
|
|
60
|
+
self.paginator = paginator
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class SearchModelAdminMixin:
|
|
64
|
+
# haystack connection to use for searching
|
|
65
|
+
haystack_connection = DEFAULT_ALIAS
|
|
66
|
+
|
|
67
|
+
@csrf_protect_m
|
|
68
|
+
def changelist_view(self, request, extra_context=None):
|
|
69
|
+
if not self.has_change_permission(request, None):
|
|
70
|
+
raise PermissionDenied
|
|
71
|
+
|
|
72
|
+
if SEARCH_VAR not in request.GET:
|
|
73
|
+
# Do the usual song and dance.
|
|
74
|
+
return super().changelist_view(request, extra_context)
|
|
75
|
+
|
|
76
|
+
# Do a search of just this model and populate a Changelist with the
|
|
77
|
+
# returned bits.
|
|
78
|
+
indexed_models = (
|
|
79
|
+
connections[self.haystack_connection]
|
|
80
|
+
.get_unified_index()
|
|
81
|
+
.get_indexed_models()
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if self.model not in indexed_models:
|
|
85
|
+
# Oops. That model isn't being indexed. Return the usual
|
|
86
|
+
# behavior instead.
|
|
87
|
+
return super().changelist_view(request, extra_context)
|
|
88
|
+
|
|
89
|
+
# So. Much. Boilerplate.
|
|
90
|
+
# Why copy-paste a few lines when you can copy-paste TONS of lines?
|
|
91
|
+
list_display = list(self.list_display)
|
|
92
|
+
|
|
93
|
+
kwargs = {
|
|
94
|
+
"haystack_connection": self.haystack_connection,
|
|
95
|
+
"request": request,
|
|
96
|
+
"model": self.model,
|
|
97
|
+
"list_display": list_display,
|
|
98
|
+
"list_display_links": self.list_display_links,
|
|
99
|
+
"list_filter": self.list_filter,
|
|
100
|
+
"date_hierarchy": self.date_hierarchy,
|
|
101
|
+
"search_fields": self.search_fields,
|
|
102
|
+
"list_select_related": self.list_select_related,
|
|
103
|
+
"list_per_page": self.list_per_page,
|
|
104
|
+
"list_editable": self.list_editable,
|
|
105
|
+
"list_max_show_all": self.list_max_show_all,
|
|
106
|
+
"model_admin": self,
|
|
107
|
+
}
|
|
108
|
+
if hasattr(self, "get_sortable_by"): # Django 2.1+
|
|
109
|
+
kwargs["sortable_by"] = self.get_sortable_by(request)
|
|
110
|
+
changelist = SearchChangeList(**kwargs)
|
|
111
|
+
changelist.formset = None
|
|
112
|
+
media = self.media
|
|
113
|
+
|
|
114
|
+
# Build the action form and populate it with available actions.
|
|
115
|
+
# Check actions to see if any are available on this changelist
|
|
116
|
+
actions = self.get_actions(request)
|
|
117
|
+
if actions:
|
|
118
|
+
action_form = self.action_form(auto_id=None)
|
|
119
|
+
action_form.fields["action"].choices = self.get_action_choices(request)
|
|
120
|
+
else:
|
|
121
|
+
action_form = None
|
|
122
|
+
|
|
123
|
+
selection_note = ngettext(
|
|
124
|
+
"0 of %(count)d selected",
|
|
125
|
+
"of %(count)d selected",
|
|
126
|
+
len(changelist.result_list),
|
|
127
|
+
)
|
|
128
|
+
selection_note_all = ngettext(
|
|
129
|
+
"%(total_count)s selected",
|
|
130
|
+
"All %(total_count)s selected",
|
|
131
|
+
changelist.result_count,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
context = {
|
|
135
|
+
"module_name": force_str(self.model._meta.verbose_name_plural),
|
|
136
|
+
"selection_note": selection_note % {"count": len(changelist.result_list)},
|
|
137
|
+
"selection_note_all": selection_note_all
|
|
138
|
+
% {"total_count": changelist.result_count},
|
|
139
|
+
"title": changelist.title,
|
|
140
|
+
"is_popup": changelist.is_popup,
|
|
141
|
+
"cl": changelist,
|
|
142
|
+
"media": media,
|
|
143
|
+
"has_add_permission": self.has_add_permission(request),
|
|
144
|
+
"opts": changelist.opts,
|
|
145
|
+
"app_label": self.model._meta.app_label,
|
|
146
|
+
"action_form": action_form,
|
|
147
|
+
"actions_on_top": self.actions_on_top,
|
|
148
|
+
"actions_on_bottom": self.actions_on_bottom,
|
|
149
|
+
"actions_selection_counter": getattr(self, "actions_selection_counter", 0),
|
|
150
|
+
}
|
|
151
|
+
context.update(extra_context or {})
|
|
152
|
+
request.current_app = self.admin_site.name
|
|
153
|
+
app_name, model_name = get_model_ct_tuple(self.model)
|
|
154
|
+
return render(
|
|
155
|
+
request,
|
|
156
|
+
self.change_list_template
|
|
157
|
+
or [
|
|
158
|
+
"admin/%s/%s/change_list.html" % (app_name, model_name),
|
|
159
|
+
"admin/%s/change_list.html" % app_name,
|
|
160
|
+
"admin/change_list.html",
|
|
161
|
+
],
|
|
162
|
+
context,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class SearchModelAdmin(SearchModelAdminMixin, ModelAdmin):
|
|
167
|
+
pass
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""App config for django_pitchfork."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from django.apps import AppConfig
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
|
|
8
|
+
from . import connection_router, connections
|
|
9
|
+
|
|
10
|
+
SIGNAL_PROCESSOR_PATH_DEFAULT = "pitchfork.signals.BaseSignalProcessor"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PitchforkConfig(AppConfig):
|
|
14
|
+
name = "django_pitchfork"
|
|
15
|
+
verbose_name = "django_pitchfork"
|
|
16
|
+
signal_processor = None
|
|
17
|
+
stream = None
|
|
18
|
+
|
|
19
|
+
def ready(self):
|
|
20
|
+
# Setup default logging (engine-owned facade).
|
|
21
|
+
log = logging.getLogger("pitchfork")
|
|
22
|
+
self.stream = logging.StreamHandler()
|
|
23
|
+
self.stream.setLevel(logging.INFO)
|
|
24
|
+
log.addHandler(self.stream)
|
|
25
|
+
|
|
26
|
+
# Setup the signal processor.
|
|
27
|
+
if not self.signal_processor:
|
|
28
|
+
signal_processor_path = getattr(
|
|
29
|
+
settings,
|
|
30
|
+
"PITCHFORK_SIGNAL_PROCESSOR",
|
|
31
|
+
getattr(settings, "HAYSTACK_SIGNAL_PROCESSOR", SIGNAL_PROCESSOR_PATH_DEFAULT),
|
|
32
|
+
)
|
|
33
|
+
from django_pitchfork.utils.loading import import_class
|
|
34
|
+
|
|
35
|
+
signal_processor_class = import_class(signal_processor_path)
|
|
36
|
+
self.signal_processor = signal_processor_class(
|
|
37
|
+
connections, connection_router
|
|
38
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Backends for django_pitchfork. Public ``ENGINE`` paths live under
|
|
2
|
+
``backends.{pitchfork,simple}.``; the base-class seam is ``backends.base``
|
|
3
|
+
(re-exports of the engine's machinery).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .base import ( # noqa: F401
|
|
7
|
+
VALID_GAPS,
|
|
8
|
+
BaseEngine,
|
|
9
|
+
BaseSearchBackend,
|
|
10
|
+
BaseSearchQuery,
|
|
11
|
+
EmptyResults,
|
|
12
|
+
SPELLING_SUGGESTION_HAS_NOT_RUN,
|
|
13
|
+
SearchBackend,
|
|
14
|
+
SearchNode,
|
|
15
|
+
SQ,
|
|
16
|
+
log_query,
|
|
17
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Base-class seam — re-exports the engine's machinery under the replacement
|
|
2
|
+
namespace (the public extension point for third-party backends)."""
|
|
3
|
+
|
|
4
|
+
from pitchfork.backends.machinery import ( # noqa: F401
|
|
5
|
+
VALID_GAPS,
|
|
6
|
+
BaseEngine,
|
|
7
|
+
BaseSearchBackend,
|
|
8
|
+
BaseSearchQuery,
|
|
9
|
+
EmptyResults,
|
|
10
|
+
SPELLING_SUGGESTION_HAS_NOT_RUN,
|
|
11
|
+
SearchBackend,
|
|
12
|
+
SearchNode,
|
|
13
|
+
SQ,
|
|
14
|
+
log_query,
|
|
15
|
+
)
|