parent
76918f2c7f
commit
f496e4f042
@ -1 +1,3 @@
|
||||
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
|
||||
import pymysql
|
||||
pymysql.install_as_MySQLdb()
|
||||
@ -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
|
||||
@ -0,0 +1 @@
|
||||
""
|
||||
Binary file not shown.
@ -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__]
|
||||
@ -1,66 +1,233 @@
|
||||
{% extends 'share_layout/base.html' %}
|
||||
{% load blog_tags %}
|
||||
{% load static %}
|
||||
|
||||
{% block header %}
|
||||
<title>{{ SITE_NAME }} | {{ SITE_DESCRIPTION }}</title>
|
||||
<meta name="description" content="{{ SITE_SEO_DESCRIPTION }}"/>
|
||||
<meta name="keywords" content="{{ SITE_KEYWORDS }}"/>
|
||||
<title>{% if query %}搜索 "{{ query }}" - {% endif %}{{ SITE_NAME }}</title>
|
||||
<meta name="description" content="搜索{{ SITE_NAME }}的文章内容"/>
|
||||
<meta name="keywords" content="搜索,{{ SITE_KEYWORDS }}"/>
|
||||
<meta property="og:type" content="blog"/>
|
||||
<meta property="og:title" content="{{ SITE_NAME }}"/>
|
||||
<meta property="og:description" content="{{ SITE_DESCRIPTION }}"/>
|
||||
<meta property="og:url" content="{{ SITE_BASE_URL }}"/>
|
||||
<meta property="og:title" content="搜索 - {{ SITE_NAME }}"/>
|
||||
<meta property="og:description" content="搜索{{ SITE_DESCRIPTION }}"/>
|
||||
<meta property="og:url" content="{{ SITE_BASE_URL }}/search/"/>
|
||||
<meta property="og:site_name" content="{{ SITE_NAME }}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="primary" class="site-content">
|
||||
<div id="content" role="main">
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-box-container" style="margin-bottom: 30px;">
|
||||
<form method="get" action="{% url 'search' %}" class="search-form">
|
||||
<div class="search-input-group">
|
||||
<input type="text" name="q" class="search-input"
|
||||
placeholder="输入关键词搜索文章..."
|
||||
value="{{ query|default:'' }}"
|
||||
aria-label="搜索">
|
||||
<button type="submit" class="search-button">
|
||||
<i class="fas fa-search"></i> 搜索
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if query %}
|
||||
<header class="archive-header">
|
||||
{% if suggestion %}
|
||||
<h2 class="archive-title">
|
||||
已显示<span style="color: red"> “{{ suggestion }}” </span>的搜索结果。
|
||||
仍然搜索:<a style="text-transform: none;" href="/search/?q={{ query }}&is_suggest=no">{{ query }}</a> <br>
|
||||
</h2>
|
||||
{% else %}
|
||||
<h2 class="archive-title">
|
||||
搜索:<span style="color: red">{{ query }} </span>
|
||||
</h2>
|
||||
{% endif %}
|
||||
<h2 class="archive-title">
|
||||
搜索:<span style="color: red">"{{ query }}"</span>
|
||||
{% if results_count %}
|
||||
<span style="font-size: 0.8em; color: #666; margin-left: 15px;">
|
||||
找到 {{ results_count }} 条结果
|
||||
</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
</header><!-- .archive-header -->
|
||||
{% endif %}
|
||||
{% if query and page.object_list %}
|
||||
{% for article in page.object_list %}
|
||||
{% load_article_detail article.object True user %}
|
||||
|
||||
{% if query and results %}
|
||||
<!-- 显示搜索结果 -->
|
||||
{% for article in results %}
|
||||
{% load_article_detail article True user %}
|
||||
{% endfor %}
|
||||
{% if page.has_previous or page.has_next %}
|
||||
|
||||
<!-- 分页导航 -->
|
||||
{% if page_obj.has_previous or page_obj.has_next %}
|
||||
<nav id="nav-below" class="navigation" role="navigation">
|
||||
<h3 class="assistive-text">文章导航</h3>
|
||||
{% if page.has_previous %}
|
||||
<div class="nav-previous"><a
|
||||
href="?q={{ query }}&page={{ page.previous_page_number }}"><span
|
||||
class="meta-nav">←</span> 早期文章</a></div>
|
||||
{% if page_obj.has_previous %}
|
||||
<div class="nav-previous">
|
||||
<a href="?q={{ query }}&page={{ page_obj.previous_page_number }}">
|
||||
<span class="meta-nav">←</span> 上一页
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if page.has_next %}
|
||||
<div class="nav-next"><a href="?q={{ query }}&page={{ page.next_page_number }}">较新文章
|
||||
<span
|
||||
class="meta-nav">→</span></a>
|
||||
|
||||
<span class="page-numbers" style="margin: 0 15px;">
|
||||
第 {{ page_obj.number }} 页 / 共 {{ page_obj.paginator.num_pages }} 页
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<div class="nav-next">
|
||||
<a href="?q={{ query }}&page={{ page_obj.next_page_number }}">
|
||||
下一页 <span class="meta-nav">→</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</nav><!-- .navigation -->
|
||||
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<header class="archive-header">
|
||||
|
||||
<h1 class="archive-title">哎呀,关键字:<span>{{ query }}</span>没有找到结果,要不换个词再试试?</h1>
|
||||
</header><!-- .archive-header -->
|
||||
{% else %}
|
||||
<!-- 无结果时的显示 -->
|
||||
{% if query %}
|
||||
<header class="archive-header">
|
||||
<h1 class="archive-title">
|
||||
哎呀,关键字:<span style="color: red">{{ query }}</span>没有找到结果,要不换个词再试试?
|
||||
</h1>
|
||||
<div class="search-suggestions" style="margin-top: 20px;">
|
||||
<p>搜索建议:</p>
|
||||
<div class="suggestion-tags">
|
||||
<a href="?q=Django" class="tag">Django</a>
|
||||
<a href="?q=Python" class="tag">Python</a>
|
||||
<a href="?q=Web开发" class="tag">Web开发</a>
|
||||
<a href="?q=数据库" class="tag">数据库</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{% else %}
|
||||
<header class="archive-header">
|
||||
<h1 class="archive-title">欢迎使用搜索功能</h1>
|
||||
<p>请在搜索框中输入关键词来查找您感兴趣的文章。</p>
|
||||
</header>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div><!-- #content -->
|
||||
</div><!-- #primary -->
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block sidebar %}
|
||||
{% load_sidebar request.user 'i' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.search-box-container {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-input-group {
|
||||
display: flex;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #007bff;
|
||||
border-right: none;
|
||||
border-radius: 4px 0 0 4px;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
padding: 12px 20px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: 2px solid #007bff;
|
||||
border-radius: 0 4px 4px 0;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.search-button:hover {
|
||||
background: #0056b3;
|
||||
border-color: #0056b3;
|
||||
}
|
||||
|
||||
.search-suggestions {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.suggestion-tags {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
padding: 8px 15px;
|
||||
background: #e9ecef;
|
||||
color: #495057;
|
||||
text-decoration: none;
|
||||
border-radius: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tag:hover {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 30px 0;
|
||||
padding: 20px 0;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.nav-previous, .nav-next {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.nav-previous a, .nav-next a {
|
||||
padding: 10px 20px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
color: #495057;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-previous a:hover, .nav-next a:hover {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.page-numbers {
|
||||
color: #6c757d;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 搜索框获得焦点时的事件
|
||||
const searchInput = document.querySelector('.search-input');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('focus', function() {
|
||||
this.parentElement.style.boxShadow = '0 0 10px rgba(0, 123, 255, 0.3)';
|
||||
});
|
||||
|
||||
searchInput.addEventListener('blur', function() {
|
||||
this.parentElement.style.boxShadow = 'none';
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
Reference in new issue