Skip to content

Commit c5f0810

Browse files
authored
Merge pull request #599 from liangliangyy/dev
feat: 新增 elasticsearch 搜索 suggest_search 拼写纠正功能
2 parents 38a2d42 + 3ec5ab6 commit c5f0810

File tree

5 files changed

+86
-13
lines changed

5 files changed

+86
-13
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: 46 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,40 @@ 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(query: str) -> str:
58+
"""获取推荐词, 如果没有找到添加原搜索词"""
59+
60+
search = ArticleDocument.search() \
61+
.query("match", body=query) \
62+
.suggest('suggest_search', query, term={'field': 'body'}) \
63+
.execute()
64+
65+
keywords = []
66+
for suggest in search.suggest.suggest_search:
67+
if suggest["options"]:
68+
keywords.append(suggest["options"][0]["text"])
69+
else:
70+
keywords.append(suggest["text"])
71+
72+
return ' '.join(keywords)
73+
5474
@log_query
5575
def search(self, query_string, **kwargs):
5676
logger.info('search query_string:' + query_string)
5777

5878
start_offset = kwargs.get('start_offset')
5979
end_offset = kwargs.get('end_offset')
6080

61-
q = Q('bool', should=[Q('match', body=query_string), Q(
62-
'match', title=query_string)], minimum_should_match="70%")
81+
# 推荐词搜索
82+
if getattr(self, "is_suggest", None):
83+
suggestion = self.get_suggestion(query_string)
84+
else:
85+
suggestion = query_string
86+
87+
q = Q('bool',
88+
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
89+
minimum_should_match="70%")
6390

6491
search = ArticleDocument.search() \
6592
.query('bool', filter=[q]) \
@@ -85,7 +112,7 @@ def search(self, query_string, **kwargs):
85112
**additional_fields)
86113
raw_results.append(result)
87114
facets = {}
88-
spelling_suggestion = None
115+
spelling_suggestion = None if query_string == suggestion else suggestion
89116

90117
return {
91118
'results': raw_results,
@@ -134,6 +161,22 @@ def get_count(self):
134161
results = self.get_results()
135162
return len(results) if results else 0
136163

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

138181
class ElasticSearchEngine(BaseEngine):
139182
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)

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
coverage==6.4
22
bleach==5.0.0
3-
Django==4.0.7
4-
django-compressor==4.0
3+
Django==4.1
4+
django-compressor==4.1
55
django-haystack==3.2.1
66
django-ipware==4.0.2
77
django-mdeditor==0.1.20

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)