Skip to content

Commit cefd547

Browse files
committed
feat: 新增 elasticsearch 搜索 suggest_search 拼写纠正功能
1 parent b05a4c3 commit cefd547

File tree

4 files changed

+85
-11
lines changed

4 files changed

+85
-11
lines changed

blog/views.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
from django.views.decorators.csrf import csrf_exempt
1212
from django.views.generic.detail import DetailView
1313
from django.views.generic.list import ListView
14+
from haystack.views import SearchView
1415

15-
from blog.models import Article, Category, Tag, Links, LinkShowType
16+
from blog.models import Article, Category, LinkShowType, Links, Tag
1617
from comments.forms import CommentForm
17-
from djangoblog.utils import cache, get_sha256, get_blog_setting
18+
from djangoblog.utils import cache, get_blog_setting, get_sha256
1819

1920
logger = logging.getLogger(__name__)
2021

@@ -267,6 +268,23 @@ def get_queryset(self):
267268
return Links.objects.filter(is_enable=True)
268269

269270

271+
class EsSearchView(SearchView):
272+
def get_context(self):
273+
paginator, page = self.build_page()
274+
context = {
275+
"query": self.query,
276+
"form": self.form,
277+
"page": page,
278+
"paginator": paginator,
279+
"suggestion": None,
280+
}
281+
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
282+
context["suggestion"] = self.results.query.get_spelling_suggestion()
283+
context.update(self.extra_context())
284+
285+
return context
286+
287+
270288
@csrf_exempt
271289
def fileupload(request):
272290
"""

djangoblog/elasticsearch_backend.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.utils.encoding import force_str
22
from elasticsearch_dsl import Q
33
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
4+
from haystack.forms import ModelSearchForm
45
from haystack.models import SearchResult
56
from haystack.utils import log as logging
67

@@ -18,6 +19,7 @@ def __init__(self, connection_alias, **connection_options):
1819
connection_alias,
1920
**connection_options)
2021
self.manager = ArticleDocumentManager()
22+
self.include_spelling = True
2123

2224
def _get_models(self, iterable):
2325
models = iterable if iterable and iterable[0] else Article.objects.all()
@@ -51,15 +53,38 @@ def remove(self, obj_or_string):
5153
def clear(self, models=None, commit=True):
5254
self.remove(None)
5355

56+
@staticmethod
57+
def get_suggestion(body: str):
58+
"""获取建议 keyword """
59+
search = ArticleDocument.search() \
60+
.query("match", body=body) \
61+
.suggest('suggest_search', body, term={'field': 'body'}) \
62+
.execute()
63+
64+
keywords = []
65+
for suggest in search.suggest.suggest_search:
66+
if suggest["options"]:
67+
keywords.append(suggest["options"][0]["text"])
68+
69+
return ' '.join(keywords) if keywords else body
70+
5471
@log_query
5572
def search(self, query_string, **kwargs):
5673
logger.info('search query_string:' + query_string)
5774

5875
start_offset = kwargs.get('start_offset')
5976
end_offset = kwargs.get('end_offset')
6077

61-
q = Q('bool', should=[Q('match', body=query_string), Q(
62-
'match', title=query_string)], minimum_should_match="70%")
78+
# 搜索建议
79+
is_suggest = getattr(self, "is_suggest", None)
80+
if is_suggest is not False:
81+
suggestion = self.get_suggestion(query_string)
82+
else:
83+
suggestion = query_string
84+
85+
q = Q('bool',
86+
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
87+
minimum_should_match="70%")
6388

6489
search = ArticleDocument.search() \
6590
.query('bool', filter=[q]) \
@@ -85,7 +110,7 @@ def search(self, query_string, **kwargs):
85110
**additional_fields)
86111
raw_results.append(result)
87112
facets = {}
88-
spelling_suggestion = None
113+
spelling_suggestion = None if query_string == suggestion else suggestion
89114

90115
return {
91116
'results': raw_results,
@@ -134,6 +159,25 @@ def get_count(self):
134159
results = self.get_results()
135160
return len(results) if results else 0
136161

162+
def get_spelling_suggestion(self, preferred_query=None):
163+
return self._spelling_suggestion
164+
165+
def build_params(self, spelling_query=None):
166+
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
167+
return kwargs
168+
169+
170+
class ElasticSearchModelSearchForm(ModelSearchForm):
171+
172+
def search(self):
173+
# 是否建议搜索
174+
self.searchqueryset.query.backend.is_suggest = True
175+
if self.data.get("is_suggest") == "no":
176+
self.searchqueryset.query.backend.is_suggest = False
177+
178+
sqs = super().search()
179+
return sqs
180+
137181

138182
class ElasticSearchEngine(BaseEngine):
139183
backend = ElasticSearchBackend

djangoblog/urls.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@
1414
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
1515
"""
1616
from django.conf import settings
17-
from django.urls import include, re_path
1817
from django.conf.urls.static import static
1918
from django.contrib.sitemaps.views import sitemap
2019
from django.urls import include
20+
from django.urls import re_path
21+
from haystack.views import search_view_factory
2122

23+
from blog.views import EsSearchView
2224
from djangoblog.admin_site import admin_site
25+
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
2326
from djangoblog.feeds import DjangoBlogFeed
24-
from djangoblog.sitemap import StaticViewSitemap, ArticleSiteMap, CategorySiteMap, TagSiteMap, UserSiteMap
27+
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
2528

2629
sitemaps = {
2730

@@ -43,10 +46,11 @@
4346
re_path(r'', include('accounts.urls', namespace='account')),
4447
re_path(r'', include('oauth.urls', namespace='oauth')),
4548
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
46-
name='django.contrib.sitemaps.views.sitemap'),
49+
name='django.contrib.sitemaps.views.sitemap'),
4750
re_path(r'^feed/$', DjangoBlogFeed()),
4851
re_path(r'^rss/$', DjangoBlogFeed()),
49-
re_path(r'^search', include('haystack.urls'), name='search'),
52+
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
53+
name='search'),
5054
re_path(r'', include('servermanager.urls', namespace='servermanager')),
5155
re_path(r'', include('owntracks.urls', namespace='owntracks'))
5256
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

templates/search/search.html

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,16 @@
1515
<div id="content" role="main">
1616
{% if query %}
1717
<header class="archive-header">
18-
19-
<h2 class="archive-title"> 搜索:<span style="color: red">{{ query }}</span></h2>
18+
{% if suggestion %}
19+
<h2 class="archive-title">
20+
已显示<span style="color: red"> “{{ suggestion }}” </span>的搜索结果。&nbsp;&nbsp;
21+
仍然搜索:<a style="text-transform: none;" href="/search/?q={{ query }}&is_suggest=no">{{ query }}</a> <br>
22+
</h2>
23+
{% else %}
24+
<h2 class="archive-title">
25+
搜索:<span style="color: red">{{ query }} </span> &nbsp;&nbsp;
26+
</h2>
27+
{% endif %}
2028
</header><!-- .archive-header -->
2129
{% endif %}
2230
{% if query and page.object_list %}

0 commit comments

Comments
 (0)