搜索功能实现:模糊查询与全文检索

📂 所属阶段:第四阶段 — 实战演练(道满博客)
🔗 相关章节:SQLAlchemy ORM · 文章发布与 Markdown 支持


1. 基础搜索:SQLAlchemy 模糊查询

1.1 文章搜索

# app/articles/routes.py
from sqlalchemy import or_

@articles_bp.route("/search")
def search():
    query = request.args.get("q", "").strip()
    page = request.args.get("page", 1, type=int)

    if not query:
        return render_template("articles/search.html", results=[], query="")

    # 模糊搜索:标题、摘要、正文
    search_pattern = f"%{query}%"
    pagination = Post.query.filter(
        Post.is_published == True,
        or_(
            Post.title.ilike(search_pattern),
            Post.summary.ilike(search_pattern),
            Post.content.ilike(search_pattern),
        )
    ).order_by(Post.created_at.desc())\
     .paginate(page=page, per_page=20, error_out=False)

    return render_template("articles/search.html",
                           results=pagination.items,
                           query=query,
                           pagination=pagination,
                           total=pagination.total)

1.2 高亮搜索词

# app/utils/search.py
import re

def highlight(text, query, max_length=300):
    """在文本中高亮搜索词"""
    if not text or not query:
        return text

    # 截取包含搜索词的部分
    pattern = re.compile(re.escape(query), re.IGNORECASE)
    match = pattern.search(text)

    if match:
        start = max(0, match.start() - 50)
        end = min(len(text), match.end() + 50)
        excerpt = text[start:end]
        if start > 0:
            excerpt = "..." + excerpt
        if end < len(text):
            excerpt = excerpt + "..."
    else:
        excerpt = text[:max_length]

    # 高亮
    highlighted = pattern.sub(f'<mark>{match.group()}</mark>', excerpt)
    return highlighted
<!-- templates/articles/search.html -->
<h2>搜索结果: "{{ query }}" ({{ total }} 个结果)</h2>

{% for post in results %}
<article class="search-result">
    <h3>
        <a href="{{ url_for('articles.detail', post_id=post.id) }}">
            {{ post.title }}
        </a>
    </h3>
    <p>{{ post.summary or (post.content[:200] + "...") }}</p>
</article>
{% else %}
<p>未找到相关文章。</p>
{% endfor %}

2. 高级搜索:分类 + 时间过滤

@articles_bp.route("/search")
def search():
    query = request.args.get("q", "").strip()
    category_id = request.args.get("category", type=int)
    date_from = request.args.get("from")
    page = request.args.get("page", 1, type=int)

    base_query = Post.query.filter(Post.is_published == True)

    if query:
        search_pattern = f"%{query}%"
        base_query = base_query.filter(
            or_(
                Post.title.ilike(search_pattern),
                Post.summary.ilike(search_pattern),
                Post.content.ilike(search_pattern),
            )
        )

    if category_id:
        base_query = base_query.filter_by(category_id=category_id)

    if date_from:
        from datetime import datetime
        base_query = base_query.filter(
            Post.created_at >= datetime.fromisoformat(date_from)
        )

    pagination = base_query.order_by(Post.created_at.desc())\
        .paginate(page=page, per_page=20, error_out=False)

    return render_template("articles/search.html",
                           pagination=pagination, query=query)

3. 全文检索(PostgreSQL)

# PostgreSQL 全文检索
from sqlalchemy import func, to_tsvector, to_tsquery

@articles_bp.route("/search")
def search():
    query = request.args.get("q", "").strip()
    page = request.args.get("page", 1, type=int)

    if query:
        # PostgreSQL 全文检索
        search_vector = func.to_tsvector("chinese", Post.title) | \
                       func.to_tsvector("chinese", Post.content)

        # 简单分词(实际应使用 jieba 等中文分词)
        ts_query = func.plainto_tsquery("chinese", query)

        pagination = Post.query\
            .filter(
                Post.is_published == True,
                search_vector.op("@@")(ts_query)
            )\
            .order_by(
                func.ts_rank(search_vector, ts_query).desc()
            )\
            .paginate(page=page, per_page=20)

    return render_template("articles/search.html",
                           pagination=pagination, query=query)

4. 小结

# 搜索速查

# 模糊搜索
Post.title.ilike(f"%{query}%")

# 多字段搜索
or_(Post.title.ilike(...), Post.content.ilike(...))

# PostgreSQL 全文检索
to_tsvector("chinese", Post.title).op("@@")(ts_query)
ts_rank(vector, query)  # 排序

💡 生产建议:小型博客用 LIKE 模糊搜索足够。中大型网站推荐 PostgreSQL + 全文索引 + 权重排序。真正大规模搜索用 Elasticsearch。


🔗 扩展阅读