diff --git a/blog/forms.py b/blog/forms.py index 715be76..891c56c 100644 --- a/blog/forms.py +++ b/blog/forms.py @@ -1,7 +1,7 @@ import logging from django import forms -from haystack.forms import SearchForm + logger = logging.getLogger(__name__) diff --git a/blog/search_indexes.py b/blog/search_indexes.py index 7f1dfac..6be3e74 100644 --- a/blog/search_indexes.py +++ b/blog/search_indexes.py @@ -1,4 +1,4 @@ -from haystack import indexes + from blog.models import Article diff --git a/blog/views.py b/blog/views.py index d5dc7ec..77c2c09 100644 --- a/blog/views.py +++ b/blog/views.py @@ -13,7 +13,8 @@ from django.utils.translation import gettext_lazy as _ from django.views.decorators.csrf import csrf_exempt from django.views.generic.detail import DetailView from django.views.generic.list import ListView -from haystack.views import SearchView +from django.db.models import Q + from blog.models import Article, Category, LinkShowType, Links, Tag from comments.forms import CommentForm @@ -283,21 +284,66 @@ class LinkListView(ListView): return Links.objects.filter(is_enable=True) -class EsSearchView(SearchView): - def get_context(self): - paginator, page = self.build_page() - context = { - "query": self.query, - "form": self.form, - "page": page, - "paginator": paginator, - "suggestion": None, - } - if hasattr(self.results, "query") and self.results.query.backend.include_spelling: - context["suggestion"] = self.results.query.get_spelling_suggestion() - context.update(self.extra_context()) - - return context +#class EsSearchView(SearchView): +# def get_context(self): +# paginator, page = self.build_page() +# context = { +# "query": self.query, +# "form": self.form, +# "page": page, +# "paginator": paginator, +# "suggestion": None, +# } +# if hasattr(self.results, "query") and self.results.query.backend.include_spelling: +# context["suggestion"] = self.results.query.get_spelling_suggestion() +# context.update(self.extra_context()) +# +# return context +# 移除原来的 EsSearchView 类,替换为原生搜索视图 +def search_view(request): + """ + Django原生搜索视图 + 替换原来的haystack搜索功能 + """ + query = request.GET.get('q', '').strip() + results = Article.objects.none() + results_count = 0 + + if query: + # 多字段搜索:标题、内容、摘要 + search_conditions = Q( + Q(title__icontains=query) | + Q(content__icontains=query) | + Q(summary__icontains=query) + ) + results = Article.objects.filter( + search_conditions, + status='p', # 只搜索已发布的文章 + type='a' # 只搜索文章类型 + ).distinct().order_by('-created_time') + + results_count = results.count() + + # 分页设置 + paginator = Paginator(results, settings.PAGINATE_BY) + page_number = request.GET.get('page') + + try: + page_obj = paginator.page(page_number) + except PageNotAnInteger: + page_obj = paginator.page(1) + except EmptyPage: + page_obj = paginator.page(paginator.num_pages) + + context = { + 'results': page_obj, + 'query': query, + 'results_count': results_count, + 'page_obj': page_obj, + 'page_type': '搜索结果', + } + + return render(request, 'search/search.html', context) @csrf_exempt diff --git a/comments/models.py b/comments/models.py index 7c3bbc8..91d79ef 100644 --- a/comments/models.py +++ b/comments/models.py @@ -2,6 +2,7 @@ from django.conf import settings from django.db import models from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ +from django.utils import timezone from blog.models import Article diff --git a/djangoblog/__init__.py b/djangoblog/__init__.py index 1e205f4..2a075c2 100644 --- a/djangoblog/__init__.py +++ b/djangoblog/__init__.py @@ -1 +1,3 @@ default_app_config = 'djangoblog.apps.DjangoblogAppConfig' +import pymysql +pymysql.install_as_MySQLdb() \ No newline at end of file diff --git a/djangoblog/elasticsearch_backend.py b/djangoblog/elasticsearch_backend.py deleted file mode 100644 index 4afe498..0000000 --- a/djangoblog/elasticsearch_backend.py +++ /dev/null @@ -1,183 +0,0 @@ -from django.utils.encoding import force_str -from elasticsearch_dsl import Q -from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query -from haystack.forms import ModelSearchForm -from haystack.models import SearchResult -from haystack.utils import log as logging - -from blog.documents import ArticleDocument, ArticleDocumentManager -from blog.models import Article - -logger = logging.getLogger(__name__) - - -class ElasticSearchBackend(BaseSearchBackend): - def __init__(self, connection_alias, **connection_options): - super( - ElasticSearchBackend, - self).__init__( - connection_alias, - **connection_options) - self.manager = ArticleDocumentManager() - self.include_spelling = True - - def _get_models(self, iterable): - models = iterable if iterable and iterable[0] else Article.objects.all() - docs = self.manager.convert_to_doc(models) - return docs - - def _create(self, models): - self.manager.create_index() - docs = self._get_models(models) - self.manager.rebuild(docs) - - def _delete(self, models): - for m in models: - m.delete() - return True - - def _rebuild(self, models): - models = models if models else Article.objects.all() - docs = self.manager.convert_to_doc(models) - self.manager.update_docs(docs) - - def update(self, index, iterable, commit=True): - - models = self._get_models(iterable) - self.manager.update_docs(models) - - def remove(self, obj_or_string): - models = self._get_models([obj_or_string]) - self._delete(models) - - def clear(self, models=None, commit=True): - self.remove(None) - - @staticmethod - def get_suggestion(query: str) -> str: - """获取推荐词, 如果没有找到添加原搜索词""" - - search = ArticleDocument.search() \ - .query("match", body=query) \ - .suggest('suggest_search', query, term={'field': 'body'}) \ - .execute() - - keywords = [] - for suggest in search.suggest.suggest_search: - if suggest["options"]: - keywords.append(suggest["options"][0]["text"]) - else: - keywords.append(suggest["text"]) - - return ' '.join(keywords) - - @log_query - def search(self, query_string, **kwargs): - logger.info('search query_string:' + query_string) - - start_offset = kwargs.get('start_offset') - end_offset = kwargs.get('end_offset') - - # 推荐词搜索 - if getattr(self, "is_suggest", None): - suggestion = self.get_suggestion(query_string) - else: - suggestion = query_string - - q = Q('bool', - should=[Q('match', body=suggestion), Q('match', title=suggestion)], - minimum_should_match="70%") - - search = ArticleDocument.search() \ - .query('bool', filter=[q]) \ - .filter('term', status='p') \ - .filter('term', type='a') \ - .source(False)[start_offset: end_offset] - - results = search.execute() - hits = results['hits'].total - raw_results = [] - for raw_result in results['hits']['hits']: - app_label = 'blog' - model_name = 'Article' - additional_fields = {} - - result_class = SearchResult - - result = result_class( - app_label, - model_name, - raw_result['_id'], - raw_result['_score'], - **additional_fields) - raw_results.append(result) - facets = {} - spelling_suggestion = None if query_string == suggestion else suggestion - - return { - 'results': raw_results, - 'hits': hits, - 'facets': facets, - 'spelling_suggestion': spelling_suggestion, - } - - -class ElasticSearchQuery(BaseSearchQuery): - def _convert_datetime(self, date): - if hasattr(date, 'hour'): - return force_str(date.strftime('%Y%m%d%H%M%S')) - else: - return force_str(date.strftime('%Y%m%d000000')) - - def clean(self, query_fragment): - """ - Provides a mechanism for sanitizing user input before presenting the - value to the backend. - - Whoosh 1.X differs here in that you can no longer use a backslash - to escape reserved characters. Instead, the whole word should be - quoted. - """ - words = query_fragment.split() - cleaned_words = [] - - for word in words: - if word in self.backend.RESERVED_WORDS: - word = word.replace(word, word.lower()) - - for char in self.backend.RESERVED_CHARACTERS: - if char in word: - word = "'%s'" % word - break - - cleaned_words.append(word) - - return ' '.join(cleaned_words) - - def build_query_fragment(self, field, filter_type, value): - return value.query_string - - def get_count(self): - results = self.get_results() - return len(results) if results else 0 - - def get_spelling_suggestion(self, preferred_query=None): - return self._spelling_suggestion - - def build_params(self, spelling_query=None): - kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query) - return kwargs - - -class ElasticSearchModelSearchForm(ModelSearchForm): - - def search(self): - # 是否建议搜索 - self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no" - sqs = super().search() - return sqs - - -class ElasticSearchEngine(BaseEngine): - backend = ElasticSearchBackend - query = ElasticSearchQuery diff --git a/djangoblog/settings.py b/djangoblog/settings.py index d076bb6..901a914 100644 --- a/djangoblog/settings.py +++ b/djangoblog/settings.py @@ -53,7 +53,7 @@ INSTALLED_APPS = [ 'django.contrib.sites', 'django.contrib.sitemaps', 'mdeditor', - 'haystack', + 'blog', 'accounts', 'comments', @@ -109,14 +109,14 @@ WSGI_APPLICATION = 'djangoblog.wsgi.application' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', - 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', - 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', - 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root', - 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', - 'PORT': int( - os.environ.get('DJANGO_MYSQL_PORT') or 3306), - 'OPTIONS': { - 'charset': 'utf8mb4'}, + 'NAME': 'djangoblog', + 'USER': 'root', # 通常是 root + 'PASSWORD': '050322yyd*', # MySQL安装时设置的密码 + 'HOST': 'localhost', + 'PORT': '3306', + 'OPTIONS':{ + 'charset': 'utf8mb4', + } }} # Password validation @@ -160,14 +160,7 @@ USE_TZ = False # https://docs.djangoproject.com/en/1.10/howto/static-files/ -HAYSTACK_CONNECTIONS = { - 'default': { - 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', - 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), - }, -} -# Automatically update searching index -HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' + # Allow user login with username and password AUTHENTICATION_BACKENDS = [ 'accounts.user_login_backend.EmailOrUsernameModelBackend'] @@ -326,11 +319,7 @@ if os.environ.get('DJANGO_ELASTICSEARCH_HOST'): 'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') }, } - HAYSTACK_CONNECTIONS = { - 'default': { - 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', - }, - } + # Plugin System PLUGINS_DIR = BASE_DIR / 'plugins' diff --git a/djangoblog/urls.py b/djangoblog/urls.py index 4aae58a..0e65d76 100644 --- a/djangoblog/urls.py +++ b/djangoblog/urls.py @@ -19,11 +19,10 @@ from django.conf.urls.static import static from django.contrib.sitemaps.views import sitemap from django.urls import path, include from django.urls import re_path -from haystack.views import search_view_factory -from blog.views import EsSearchView +from blog.views import search_view from djangoblog.admin_site import admin_site -from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm + from djangoblog.feeds import DjangoBlogFeed from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap @@ -41,7 +40,8 @@ handler500 = 'blog.views.server_error_view' handle403 = 'blog.views.permission_denied_view' urlpatterns = [ - path('i18n/', include('django.conf.urls.i18n')), + #path('i18n/', include('django.conf.urls.i18n')), + path('search/', search_view, name='search'), ] urlpatterns += i18n_patterns( re_path(r'^admin/', admin_site.urls), @@ -54,8 +54,8 @@ urlpatterns += i18n_patterns( name='django.contrib.sitemaps.views.sitemap'), re_path(r'^feed/$', DjangoBlogFeed()), re_path(r'^rss/$', DjangoBlogFeed()), - re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), - name='search'), + #path('search/', search_view, name='search'), + re_path(r'', include('servermanager.urls', namespace='servermanager')), re_path(r'', include('owntracks.urls', namespace='owntracks')) , prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/djangoblog/whoosh_cn_backend.py b/djangoblog/whoosh_cn_backend.py index 04e3f7f..c540b6a 100644 --- a/djangoblog/whoosh_cn_backend.py +++ b/djangoblog/whoosh_cn_backend.py @@ -14,14 +14,7 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured from datetime import datetime from django.utils.encoding import force_str -from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query -from haystack.constants import DJANGO_CT, DJANGO_ID, ID -from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument -from haystack.inputs import Clean, Exact, PythonData, Raw -from haystack.models import SearchResult -from haystack.utils import get_identifier, get_model_ct -from haystack.utils import log as logging -from haystack.utils.app_loading import haystack_get_model + from jieba.analyse import ChineseAnalyzer from whoosh import index from whoosh.analysis import StemmingAnalyzer @@ -101,13 +94,12 @@ class WhooshSearchBackend(BaseSearchBackend): "You must specify a 'PATH' in your settings for connection '%s'." % connection_alias) - self.log = logging.getLogger('haystack') - + def setup(self): """ Defers loading until needed. """ - from haystack import connections + new_index = False # Make sure the index is there. @@ -150,7 +142,7 @@ class WhooshSearchBackend(BaseSearchBackend): DJANGO_CT: WHOOSH_ID(stored=True), DJANGO_ID: WHOOSH_ID(stored=True), } - # Grab the number of keys that are hard-coded into Haystack. + # We'll use this to (possibly) fail slightly more gracefully later. initial_key_count = len(schema_fields) content_field_name = '' @@ -438,9 +430,7 @@ class WhooshSearchBackend(BaseSearchBackend): narrowed_results = None self.index = self.index.refresh() - if limit_to_registered_models is None: - limit_to_registered_models = getattr( - settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) + if models and len(models): model_choices = sorted(get_model_ct(model) for model in models) @@ -582,10 +572,7 @@ class WhooshSearchBackend(BaseSearchBackend): narrowed_results = None self.index = self.index.refresh() - if limit_to_registered_models is None: - limit_to_registered_models = getattr( - settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) - + if models and len(models): model_choices = sorted(get_model_ct(model) for model in models) elif limit_to_registered_models: @@ -682,7 +669,7 @@ class WhooshSearchBackend(BaseSearchBackend): query_string='', spelling_query=None, result_class=None): - from haystack import connections + results = [] # It's important to grab the hits first before slicing. Otherwise, this @@ -701,7 +688,7 @@ class WhooshSearchBackend(BaseSearchBackend): score = raw_page.score(doc_offset) or 0 app_label, model_name = raw_result[DJANGO_CT].split('.') additional_fields = {} - model = haystack_get_model(app_label, model_name) + if model and model in indexed_models: for key, value in raw_result.items(): @@ -903,7 +890,7 @@ class WhooshSearchQuery(BaseSearchQuery): return ' '.join(cleaned_words) def build_query_fragment(self, field, filter_type, value): - from haystack import connections + query_frag = '' is_datetime = False diff --git a/haystack/__init__.py b/haystack/__init__.py new file mode 100644 index 0000000..a60141c --- /dev/null +++ b/haystack/__init__.py @@ -0,0 +1 @@ +"" diff --git a/requirements.txt b/requirements.txt index 9dc5c93..fb769af 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/servermanager/api/blogapi.py b/servermanager/api/blogapi.py index 8a4d6ac..d2d8afa 100644 --- a/servermanager/api/blogapi.py +++ b/servermanager/api/blogapi.py @@ -1,27 +1,43 @@ -from haystack.query import SearchQuerySet +import logging +from django.http import JsonResponse +from django.db.models import Q from blog.models import Article, Category - +logger = logging.getLogger(__name__) class BlogApi: def __init__(self): - self.searchqueryset = SearchQuerySet() - self.searchqueryset.auto_query('') - self.__max_takecount__ = 8 + self.searchqueryset = Article.objects.all() def search_articles(self, query): - sqs = self.searchqueryset.auto_query(query) - sqs = sqs.load_all() - return sqs[:self.__max_takecount__] + if query: + # 使用 Q 对象进行多字段搜索 + results = Article.objects.filter( + Q(title__icontains=query) | + Q(body__icontains=query) | + Q(category__name__icontains=query) | + Q(tags__name__icontains=query) + ).filter(status='p') # 只搜索已发布的文章 + return results.distinct()[:self.__max_takecount__] + return Article.objects.none() def get_category_lists(self): return Category.objects.all() def get_category_articles(self, categoryname): - articles = Article.objects.filter(category__name=categoryname) + articles = Article.objects.filter(category__name=categoryname,status='p') + if articles: return articles[:self.__max_takecount__] return None def get_recent_articles(self): - return Article.objects.all()[:self.__max_takecount__] + return Article.objects.filter( + status='p' + ).order_by('-pub_time')[:self.__max_takecount__] + + def get_popular_articles(self): + """获取热门文章(按浏览量排序)""" + return Article.objects.filter( + status='p' + ).order_by('-views')[:self.__max_takecount__] \ No newline at end of file diff --git a/templates/search/search.html b/templates/search/search.html index 1404c60..19a153d 100644 --- a/templates/search/search.html +++ b/templates/search/search.html @@ -1,66 +1,233 @@ {% extends 'share_layout/base.html' %} {% load blog_tags %} +{% load static %} + {% block header %} -