分享这篇文章
🔍 搜索系统概览
✨ 核心功能特性
1. 智能搜索引擎
- 多模型搜索:支持多个内容模型同时搜索
- 智能分词:支持搜索引擎式拆词(空格、引号)
- 字段配置:可配置每个模型的搜索字段
- 结果优化:智能排序和分页
2. 搜索历史管理
- 自动记录:自动记录用户搜索词
- 独立成文:每个搜索词生成独立页面
- 点击统计:记录搜索页面访问量
- 状态管理:支持完整的发布工作流
3. 敏感词过滤
- 实时检测:搜索时自动检测敏感词
- 智能屏蔽:发现敏感词时返回特定结果
- 可配置开关:支持开启/关闭过滤功能
- 灵活管理:可自定义敏感词库
4. 权限与安全
- API限流:搜索接口支持限流保护
- 并发控制:原子操作更新点击量
- 输入验证:严格的参数验证
- 防攻击:防止SQL注入和暴力搜索
5. 模板系统
- 主题继承:支持多主题模板
- 结果模板:专门的搜索结果页面
- 历史模板:搜索历史详情页面
- 可扩展:支持自定义模板
6. 统计与分析
- 搜索热度:记录每个搜索词的点击量
- 使用频率:统计搜索历史使用次数
- 时间分析:分析搜索时间分布
- 结果分析:统计搜索结果数量
7. API接口
- 搜索接口:统一的搜索入口
- 统计接口:点击量统计接口
- 管理接口:搜索历史管理接口
- RESTful:符合RESTful设计
8. 用户体验
- 实时搜索:快速返回搜索结果
- 分页支持:支持查看更多结果
- 多维度搜索:可按模型路径搜索
- 智能提示:可扩展搜索建议
🎯 后台使用指南
1. 管理搜索历史
ID | 标题(搜索词) | 状态标签 | 创建时间 | 更新时间 | 扩展功能colors = {
0: '#666', # 已入库 - 深灰
1: '#999', # 草稿 - 灰色
2: '#ffc107', # 已提交 - 黄色
3: '#17a2b8', # 审核中 - 青色
4: '#28a745', # 已通过 - 绿色
5: '#dc3545', # 已驳回 - 红色
6: '#6c757d', # 已归档 - 灰暗
99: '#007bff' # 已发布 - 蓝色
}1. 浏览:查看搜索历史页面
2. 移除静态:清除该页面的静态缓存基础信息:
- 标题:搜索词
- 标题拼音:自动生成
- 状态:选择发布状态
内容管理:
- 内容:富文本编辑器,可编写详细内容
发布设置:
- 允许评论:控制是否允许评论
- 点击量:自动统计,只读
- 个性模板:选择自定义模板
- 更新时间/创建时间:自动记录,只读
2. 搜索配置管理
# 基本配置
updateswitch: 是否记录搜索历史
searchengine: 是否启用搜索引擎模式
内容模型搜索
contentmodels: 启用搜索的内容模型列表
contentfields: 内容模型的搜索字段
用户空间搜索
userspacesearch: 用户自定义模型搜索配置
# 配置内容模型搜索
contentmodels = ['articles.article', 'downloads.download']
contentfields = ['title', 'content', 'meta_description']
配置用户空间搜索
userspacesearch = {
'articles.article': {
'searchfields': ['title', 'content', 'author'],
'displayfields': ['title', 'author', 'createtime'],
'limit': 10
}
}
3. 敏感词配置
filterswitch: 是否开启敏感词过滤
其他敏感词配置项
1. 用户发起搜索请求
- 系统检查敏感词过滤开关
- 如果开启,检测搜索词是否包含敏感词
- 包含敏感词:返回特定结果,不执行搜索
- 不包含敏感词:正常执行搜索
🔧 搜索功能详解
1. 搜索流程
GET /search/?key=搜索词&model_path=articles.article1. 获取搜索词和模型路径参数
敏感词检测(如果开启)
记录搜索历史(如果开启)
构建动态查询条件
执行多模型搜索
返回格式化结果
更新搜索历史点击量
2. 智能分词算法
def split_query(self, query):
"""搜索引擎式拆词逻辑"""
1. 按空格拆分基础词
base_words = re.split(r'\s+', query.strip())
2. 提取带引号的短语
phrases = re.findall(r'\"(.+?)\"', query)
3. 合并结果并去重
terms = list(set(base_words + phrases))
4. 过滤空词
return [term for term in terms if term]
def builddynamicquery(self, fields, query, searchengine=False):
"""构建动态查询条件"""
qobjects = Q()
if searchengine:
# 搜索引擎模式:使用AND逻辑连接各分词
terms = self.splitquery(query)
for term in terms:
termq = Q()
for field in fields:
termq |= (Q({f"{field}_icontains": term})
& Q({f"{field}isnull": False}))
qobjects &= termq
else:
# 普通模式:使用OR逻辑连接各字段
for field in fields:
qobjects |= (Q(*{f"{field}icontains": query})
& Q(*{f"{field}_isnull": False}))
return qobjects3. 多模型搜索实现
# 搜索指定模型
/search/?key=关键词&model_path=articles.article
只搜索文章模型
在指定模型的配置字段中搜索
# 搜索所有配置的模型
/search/?key=关键词
在所有配置的内容模型中搜索
支持分模型显示结果
# 搜索用户自定义的模型
需要预先在SearchConfig中配置
userspacesearch = {
'app.model': {
'searchfields': [...], # 搜索字段
'displayfields': [...], # 显示字段
'limit': 10 # 结果限制
}
}
4. 搜索历史管理
def createuniquesearchhistory(self, title):
"""创建唯一的搜索历史记录"""
try:
# 使用getorcreate确保唯一性
obj, created = SearchHistory.objects.getorcreate(
title=title,
defaults={
'pytitle': '', # 可自动生成拼音
'content': '', # 默认空内容
'status': SearchHistory.StatusChoices.REVIEW
})
return (created, obj)
except IntegrityError:
# 处理并发冲突
existing = SearchHistory.objects.filter(title=title).first()
return (False, existing) if existing else (False, None)@requireGET
def incrementsearchhits(request):
"""原子操作更新点击量"""
try:
objid = request.GET.get('id')
record = SearchHistory.objects.get(pk=objid)
# 原子操作,避免并发问题
record.hits = models.F('hits') + 1
record.save(updatefields=['hits'])
record.refreshfromdb() # 重新加载最新值
return JsonResponse({'id': record.id, 'hits': record.hits})
except SearchHistory.DoesNotExist:
return JsonResponse({'error': 'Record not found'}, status=404)📊 字段说明速查
SearchHistory模型字段(继承CreateCommon)
字段分组 | 字段名 | 说明 | 必填 | 默认值 |
|---|---|---|---|---|
基础信息 | title | 搜索词/标题 | 是 | - |
pytitle | 标题拼音 | 自动 | 空 | |
status | 审核状态 | 是 | 99(已发布) | |
内容管理 | content | 详细内容 | 否 | 空 |
发布设置 | allowcomment | 允许评论 | 否 | True |
hits | 点击量 | 自动 | 0 | |
template | 个性模板 | 否 | 空 | |
导航关联 | previousobj | 上一篇 | 否 | null |
nextobj | 下一篇 | 否 | null | |
权限控制 | staffonly | 仅员工可见 | 否 | False |
allowedgroups | 允许用户组 | 否 | 空 | |
时间信息 | createtime | 创建时间 | 自动 | 当前时间 |
updatetime | 更新时间 | 自动 | 当前时间 | |
其他字段 | thumbimage | 缩略图 | 否 | 空 |
istop | 是否推荐 | 否 | False |
SearchConfig配置字段
字段名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
updateswitch | 是否记录搜索历史 | Boolean | False |
searchengine | 是否启用搜索引擎模式 | Boolean | False |
contentmodels | 内容模型列表 | JSON | [] |
contentfields | 内容模型搜索字段 | JSON | ['title'] |
userspacesearch | 用户空间搜索配置 | JSON | {} |
🎨 前端使用指南
1. 搜索表单示例
<form action="/search/" method="get" class="search-form">
<input type="text" name="key" placeholder="输入搜索关键词..."
value="{{ query|default:'' }}" class="search-input">
<button type="submit" class="search-button">搜索</button>
</form><form action="/search/" method="get" class="advanced-search">
<input type="text" name="key" placeholder="搜索关键词..."
value="{{ query|default:'' }}">
<!-- 模型选择 -->
<select name="model_path">
<option value="">全站搜索</option>
<option value="articles.article">文章</option>
<option value="downloads.download">下载</option>
<option value="wikis.wiki">百科</option>
</select>
<!-- 搜索模式 -->
<label>
<input type="checkbox" name="exact" value="1"> 精确匹配
</label>
<button type="submit">搜索</button>
</form>
2. 搜索结果展示
{# 搜索结果页面 #}
{% extends "base.html" %}
{% block content %}
<div class="search-results">
<h1>搜索"{{ query }}"的结果</h1>
{% if sensitive %}
<div class="alert alert-warning">
搜索词包含敏感内容,已屏蔽搜索结果。
</div>
{% else %}
{% if not has_results %}
<div class="no-results">
没有找到相关结果。
</div>
{% endif %}
{# 按模型分组显示结果 #}
{% for modelpath, data in contentresults.items %}
<div class="model-group">
<h2>{{ data.verbose_name }} ({{ data.results|length }})</h2>
{% for result in data.results %}
<div class="result-item">
<h3>
<a href="{{ result.getabsoluteurl }}">
{{ result.title }}
</a>
</h3>
{% if result.metadescription %}
<p>{{ result.metadescription|truncatechars:200 }}</p>
{% endif %}
<div class="result-meta">
<span class="update-time">
{{ result.update_time|date:"Y-m-d" }}
</span>
</div>
</div>
{% endfor %}
{% if data.more %}
<div class="more-results">
<a href="/search/?key={{ query|urlencode }}&modelpath={{ modelpath }}">
查看更多{{ data.verbose_name }}结果
</a>
</div>
{% endif %}
</div>
{% endfor %}
{# 用户空间搜索结果 #}
{% for modelpath, data in userresults.items %}
<div class="user-space-results">
<h2>{{ data.verbose_name }}</h2>
<table>
<thead>
<tr>
{% for field in data.fields %}
<th>{{ field|title }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for result in data.results %}
<tr>
{% for field in data.fields %}
<td>{{ result|getattr:field }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
{% endif %}
</div>
{% endblock %}
3. 搜索历史页面
{# 搜索历史独立页面 #}
{% extends "base.html" %}
{% block title %}{{ search_obj.title }} - 搜索历史{% endblock %}
{% block content %}
<article class="search-history-detail">
<header>
<h1>{{ search_obj.title }}</h1>
{% if searchobj.subtitle %}
<h2 class="subtitle">{{ searchobj.subtitle }}</h2>
{% endif %}
<div class="meta">
<span>搜索次数: {{ searchobj.hits }}</span>
<span>更新时间: {{ searchobj.update_time|date:"Y-m-d H:i" }}</span>
</div>
</header>
{% if searchobj.content %}
<div class="content">
{{ searchobj.content|safe }}
</div>
{% endif %}
{# 相关搜索结果 #}
<div class="related-results">
<h3>相关搜索结果</h3>
{% include "search/resultspartial.html" %}
</div>
{% if searchobj.allowcomment %}
<div class="comments">
{% include "comments/comments.html" %}
</div>
{% endif %}
</article>
{% endblock %}
4. 实时点击量更新
// 在搜索历史详情页加载时更新点击量
document.addEventListener('DOMContentLoaded', function() {
const searchId = '{{ searchobj.id }}';
// 发送点击量更新请求
fetch(/api/search/increment-hits/?id=${searchId}, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
}
})
.then(response => response.json())
.then(data => {
if (data.hits) {
// 可更新页面上的点击量显示
const hitsElement = document.querySelector('.hits-count');
if (hitsElement) {
hitsElement.textContent = data.hits;
}
}
})
.catch(error => console.error('Error updating hits:', error));
});
💡 最佳实践建议
1. 搜索性能优化
# 为搜索字段添加索引
class Article(models.Model):
title = models.CharField(maxlength=200, dbindex=True)
content = models.TextField()
class Meta:
indexes = [
models.Index(fields=['title'], name='idx_article_title'),
# 可考虑全文索引
]</code></pre></div><div class="hyc-code-scrollbar__track" style="bottom:4px;height:7px;left:4px;position:absolute;right:4px;"><div class="hyc-code-scrollbar__thumb" style="display:block;height:100%;position:relative;width:0px;"> </div></div><div style="border-radius:3px;bottom:2px;position:absolute;right:2px;top:2px;width:6px;"><div style="background-color:rgba(0, 0, 0, 0.2);border-radius:inherit;cursor:pointer;display:block;height:0px;position:relative;width:100%;"> </div></div></div></div><div class="ybc-p"><strong>查询优化</strong>:</div><div class="hyc-common-markdown__code"><div class="hyc-common-markdown__code__hd"> </div><div class="hyc-code-scrollbar" style="height:100%;overflow:hidden;position:relative;width:100%;"><div class="hyc-code-scrollbar__view" style="inset:0px;margin-bottom:-13px;margin-right:-13px;overflow:scroll;position:relative;"><pre><code class="language-python"># 只查询需要的字段
results = model.objects.filter(qobjects).only(
'title', 'metadescription', 'update_time', 'pk'
)
限制结果数量
results = results[:11] # 10+1用于判断是否有更多
# 缓存热门搜索结果
from django.core.cache import cache
def getsearchresults(query, modelpath=None):
cachekey = f'search:{query}:{modelpath}'
results = cache.get(cachekey)
if not results:
# 执行搜索
results = performsearch(query, modelpath)
# 缓存5分钟
cache.set(cache_key, results, 300)
return results
2. 搜索体验优化
// 实时搜索建议
const searchInput = document.querySelector('.search-input');
const suggestions = document.querySelector('.search-suggestions');
searchInput.addEventListener('input', function() {
const query = this.value.trim();
if (query.length >= 2) {
fetch(/api/search/suggestions/?q=${encodeURIComponent(query)})
.then(response => response.json())
.then(data => {
suggestions.innerHTML = '';
data.suggestions.forEach(suggestion => {
const li = document.createElement('li');
li.textContent = suggestion;
li.addEventListener('click', () => {
searchInput.value = suggestion;
suggestions.innerHTML = '';
});
suggestions.appendChild(li);
});
});
} else {
suggestions.innerHTML = '';
}
});
# 搜索结果分页
from django.core.paginator import Paginator
def search_view(request):
query = request.GET.get('key', '')
page = request.GET.get('page', 1)
results = perform_search(query)
paginator = Paginator(results, 10) # 每页10条
try:
pageobj = paginator.page(page)
except PageNotAnInteger:
pageobj = paginator.page(1)
except EmptyPage:
pageobj = paginator.page(paginator.numpages)
return render(request, 'search/results.html', {
'query': query,
'pageobj': pageobj,
'results': pageobj.objectlist,
})
3. 安全与防护
# 验证搜索词长度
def cleansearchquery(query):
query = query.strip()
# 长度限制
if len(query) > 100:
raise ValidationError("搜索词过长")
# 字符限制
if not re.match(r'^[\w\s"\'-]+$', query):
raise ValidationError("包含非法字符")
return query
# 使用Django Ratelimit
from django_ratelimit.decorators import ratelimit
@ratelimit(key='ip', rate='10/m', method='GET')
def searchview(request):
# 搜索逻辑
pass
# 检测恶意搜索模式
def ismalicious_search(query):
# 检测重复字符
if re.match(r'^(.)\1+$', query):
return True
# 检测过长无意义词
if len(query) > 50 and not re.search(r'[\u4e00-\u9fa5]', query):
return True
# 检测SQL注入特征
sqlkeywords = ['SELECT', 'INSERT', 'DELETE', 'UPDATE', 'DROP', 'UNION']
for keyword in sqlkeywords:
if keyword in query.upper():
return True
return False
4. 数据分析与监控
# 记录搜索统计
class SearchStats(models.Model):
query = models.CharField(maxlength=200)
count = models.IntegerField(default=1)
lastsearched = models.DateTimeField(autonow=True)
hasresults = models.BooleanField(default=True)
@classmethod
def recordsearch(cls, query, hasresults=True):
stats, created = cls.objects.getorcreate(
query=query,
defaults={'hasresults': hasresults}
)
if not created:
stats.count += 1
stats.hasresults = hasresults
stats.save(updatefields=['count', 'hasresults', 'lastsearched'])
# 获取热门搜索词
def gethot_searches(limit=10, days=7):
from django.utils import timezone
from django.db.models import Count
cutoff_date = timezone.now() - timezone.timedelta(days=days)
return SearchStats.objects.filter(
lastsearchedgte=cutoffdate
).orderby('-count')[:limit]
# 分析无结果的搜索词
def getnoresultsearches(limit=20):
return SearchStats.objects.filter(
hasresults=False
).orderby('-count', '-last_searched')[:limit]