垂直搜索后端系统的模块化设计与性能优化

一、引言:垂直搜索的挑战与机遇

通用搜索引擎(如Google、Baidu)解决的是“全”的问题,而垂直搜索引擎解决的是“精”和“深”的问题。在电商、医疗、法律、学术等特定领域,用户需要的不是海量网页,而是精准、结构化、具备领域知识的结果。垂直搜索系统本质上是信息检索技术与领域知识工程的结合体。它的根本挑战在于:在数据规模远小于通用搜索的前提下,如何把相关性、时效性和结构化理解做到极致。

本文将围绕垂直搜索后端系统,从索引架构、召回策略、排序优化、特征工程到性能调优,给出完整的技术方案与实践代码。

二、垂直搜索系统整体架构

2.1 系统分层

┌─────────────────────────────────────────────────────┐
│                    接入层                           │
│            Nginx + 负载均衡 + CDN                  │
└─────────────────────────────────────────────────────┘
                         ▼
┌─────────────────────────────────────────────────────┐
│                    网关层                           │
│      鉴权 / 限流 / 请求路由 / 日志收集             │
└─────────────────────────────────────────────────────┘
                         ▼
┌─────────────────────────────────────────────────────┐
│                  搜索服务层                         │
│   Query理解  │  多路召回  │  精排  │  重排       │
└─────────────────────────────────────────────────────┘
                         ▼
┌─────────────────────────────────────────────────────┐
│                  索引与存储层                       │
│   Elasticsearch │ Redis缓存 │ 向量数据库          │
└─────────────────────────────────────────────────────┘
                         ▼
┌─────────────────────────────────────────────────────┐
│                  数据管道层                         │
│   数据抓取 │ 清洗 │ 解析 │ 索引构建              │
└─────────────────────────────────────────────────────┘

2.2 技术栈选型

组件技术选型说明
搜索引擎内核Elasticsearch 8.x分布式倒排索引,支持向量检索
向量检索Elasticsearch + plugin支持稠密向量相似度检索
缓存Redis Cluster缓存热门Query结果
消息队列Kafka数据变更实时同步
任务调度Airflow离线索引构建
编程语言Java + PythonJava负责搜索核心,Python负责数据处理

三、核心模块设计与实现

3.1 索引架构设计

垂直搜索的灵魂在于索引设计。不同于通用搜索的网页级索引,垂直搜索需要对文档进行更细粒度的结构化处理。

// DocumentSchema.java - 文档Schema定义
public class DocumentSchema {
    // 基础字段
    public static final String FIELD_ID = "doc_id";
    public static final String FIELD_TITLE = "title";
    public static final String FIELD_CONTENT = "content";
    public static final String FIELD_SUMMARY = "summary";
    
    // 结构化字段(垂直领域特有)
    public static final String FIELD_CATEGORY = "category";      // 分类
    public static final String FIELD_TAGS = "tags";              // 标签数组
    public static final String FIELD_AUTHOR = "author";          // 作者
    public static final String FIELD_PUBLISH_TIME = "publish_time";
    public static final String FIELD_UPDATE_TIME = "update_time";
    
    // 数值型字段(用于过滤和排序)
    public static final String FIELD_VIEW_COUNT = "view_count";
    public static final String FIELD_LIKE_COUNT = "like_count";
    public static final String FIELD_RELEVANCE_SCORE = "relevance_score";
    
    // 向量字段(语义检索)
    public static final String FIELD_EMBEDDING = "embedding";
}
// 索引Mapping配置 (Elasticsearch)
{
  "mappings": {
    "properties": {
      "doc_id": {"type": "keyword"},
      "title": {
        "type": "text",
        "analyzer": "ik_smart",
        "fields": {
          "keyword": {"type": "keyword"},
          "pinyin": {"type": "text", "analyzer": "pinyin_analyzer"}
        }
      },
      "content": {
        "type": "text",
        "analyzer": "ik_max_word",
        "fields": {
          "standard": {"type": "text", "analyzer": "standard"}
        }
      },
      "category": {"type": "keyword"},
      "tags": {"type": "keyword"},
      "publish_time": {"type": "date"},
      "view_count": {"type": "integer"},
      "embedding": {
        "type": "dense_vector",
        "dims": 768,
        "index": true,
        "similarity": "cosine"
      }
    }
  }
}

3.2 Query理解模块

Query理解是召回前的关键一步,直接影响后续所有环节的效果。

# query_understanding.py - Query理解模块
import jieba
import re
from typing import List, Dict, Tuple

class QueryUnderstanding:
    """Query理解:分词、实体识别、意图分类、同义词扩展"""
    
    def __init__(self):
        self.synonym_dict = self._load_synonyms()  # 同义词词典
        self.entity_recognizer = self._load_ner_model()  # 实体识别模型
        self.intent_classifier = self._load_intent_model()  # 意图分类
        
    def analyze(self, query: str) -> Dict:
        """完整的Query分析流程"""
        result = {
            "original": query,
            "cleaned": self._clean(query),
            "tokens": [],
            "entities": [],
            "intent": "unknown",
            "expanded_queries": [],
            "filters": {}
        }
        
        # 1. 文本清洗
        cleaned = self._clean(query)
        result["cleaned"] = cleaned
        
        # 2. 分词
        tokens = jieba.lcut(cleaned)
        result["tokens"] = tokens
        
        # 3. 实体识别
        entities = self._extract_entities(cleaned)
        result["entities"] = entities
        
        # 4. 意图分类
        intent = self._classify_intent(cleaned, entities)
        result["intent"] = intent
        
        # 5. 同义词扩展
        expanded = self._expand_synonyms(tokens)
        result["expanded_queries"] = expanded
        
        # 6. 提取过滤条件(时间、价格等)
        filters = self._extract_filters(cleaned)
        result["filters"] = filters
        
        return result
    
    def _clean(self, text: str) -> str:
        """去除特殊字符、统一全半角"""
        # 去除多余空格
        text = re.sub(r'\s+', ' ', text)
        # 全角转半角
        text = text.replace(',', ',').replace('。', '.').replace(';', ';')
        return text.strip()
    
    def _extract_entities(self, text: str) -> List[Dict]:
        """实体识别(NER)"""
        # 示例:调用自定义NER或使用LAC/Stanford NER
        # 返回格式: [{"type": "person|org|product|time|location", "value": "xxx"}]
        # 这里使用规则+词典的简化实现
        entities = []
        # 时间实体识别
        time_match = re.search(r'(\d{4}年)?\d{1,2}月\d{1,2}日', text)
        if time_match:
            entities.append({"type": "time", "value": time_match.group()})
        return entities
    
    def _classify_intent(self, text: str, entities: List[Dict]) -> str:
        """意图分类:导航/问答/筛选/比较"""
        # 实际场景可用BERT微调进行意图分类
        # 这里用规则简化
        if '多少钱' in text or '价格' in text:
            return "price_query"
        elif '推荐' in text or '最好' in text or '哪个' in text:
            return "recommend"
        elif '是什么' in text or '怎么' in text:
            return "qa"
        else:
            return "general_search"
    
    def _expand_synonyms(self, tokens: List[str]) -> List[str]:
        """同义词扩展,生成多个召回Query"""
        expanded = []
        for token in tokens:
            if token in self.synonym_dict:
                expanded.extend(self.synonym_dict[token])
        return list(set(expanded))
    
    def _extract_filters(self, text: str) -> Dict:
        """提取过滤条件(价格区间、时间范围等)"""
        filters = {}
        # 提取价格范围
        price_pattern = r'(\d+)\s*[-~]\s*(\d+)\s*元'
        match = re.search(price_pattern, text)
        if match:
            filters["price_range"] = [int(match.group(1)), int(match.group(2))]
        return filters
    
    def _load_synonyms(self) -> Dict:
        """加载同义词词典"""
        # 实际从配置文件或数据库加载
        return {
            "笔记本": ["笔记本电脑", "手提电脑", "notebook"],
            "手机": ["移动电话", "智能机", "smartphone"]
        }

3.3 多路召回策略

垂直搜索的核心能力在于融合多种召回策略,兼顾精准和覆盖面。

# multi_path_recall.py - 多路召回实现
from elasticsearch import Elasticsearch
import asyncio
from typing import List, Dict, Any
import numpy as np

class MultiPathRecall:
    """多路召回:倒排召回 + 向量召回 + 个性化召回"""
    
    def __init__(self, es_client: Elasticsearch, redis_client):
        self.es = es_client
        self.redis = redis_client
        self.index_name = "vertical_docs"
        self.vector_dim = 768
        
    async def recall(self, query_analysis: Dict, user_id: str = None) -> Dict[str, List[Dict]]:
        """执行多路召回,返回各路结果"""
        results = {}
        
        # 并行执行各路召回
        tasks = [
            self._inverted_recall(query_analysis),
            self._vector_recall(query_analysis),
            self._personalized_recall(query_analysis, user_id) if user_id else None,
            self._filter_recall(query_analysis)
        ]
        
        task_results = await asyncio.gather(*[t for t in tasks if t is not None])
        
        # 命名各路结果
        result_names = ["inverted", "vector", "personalized", "filter"]
        for name, result in zip(result_names, task_results):
            if result:
                results[name] = result
        
        return results
    
    async def _inverted_recall(self, query_analysis: Dict) -> List[Dict]:
        """倒排索引召回(BM25)"""
        # 构建ES查询
        must_queries = []
        should_queries = []
        
        # 主查询:对title和content进行全文检索
        main_query = {
            "multi_match": {
                "query": query_analysis["cleaned"],
                "fields": ["title^3", "content", "summary^2"],
                "type": "best_fields",
                "minimum_should_match": "60%"
            }
        }
        must_queries.append(main_query)
        
        # 同义词扩展召回
        for expanded_query in query_analysis["expanded_queries"]:
            should_queries.append({
                "multi_match": {
                    "query": expanded_query,
                    "fields": ["title^2", "content"],
                    "boost": 0.5
                }
            })
        
        # 实体过滤
        filter_queries = []
        for entity in query_analysis.get("entities", []):
            if entity["type"] == "time":
                # 时间范围过滤
                pass
        
        es_query = {
            "bool": {
                "must": must_queries,
                "should": should_queries,
                "filter": filter_queries
            }
        }
        
        response = self.es.search(
            index=self.index_name,
            body={
                "query": es_query,
                "size": 50,
                "track_scores": True
            }
        )
        
        return [{"id": hit["_id"], "score": hit["_score"], "source": hit["_source"]} 
                for hit in response["hits"]["hits"]]
    
    async def _vector_recall(self, query_analysis: Dict) -> List[Dict]:
        """向量召回(语义相似度检索)"""
        # 调用Embedding服务获取Query向量
        query_vec = await self._get_embedding(query_analysis["cleaned"])
        
        # ES向量检索
        es_query = {
            "script_score": {
                "query": {"match_all": {}},
                "script": {
                    "source": "cosineSimilarity(params.query_vector, 'embedding') + 1.0",
                    "params": {"query_vector": query_vec.tolist()}
                }
            }
        }
        
        response = self.es.search(
            index=self.index_name,
            body={
                "query": es_query,
                "size": 30,
                "_source": {"excludes": ["embedding"]}
            }
        )
        
        return [{"id": hit["_id"], "score": hit["_score"], "source": hit["_source"]} 
                for hit in response["hits"]["hits"]]
    
    async def _personalized_recall(self, query_analysis: Dict, user_id: str) -> List[Dict]:
        """个性化召回(基于用户历史行为)"""
        # 从Redis获取用户偏好标签
        user_tags = self.redis.smembers(f"user:{user_id}:tags")
        if not user_tags:
            return []
        
        # 根据用户标签构造召回Query
        return self._inverted_recall(query_analysis)  # 简化实现
    
    async def _filter_recall(self, query_analysis: Dict) -> List[Dict]:
        """过滤召回(分类/标签精确匹配)"""
        filters = query_analysis.get("filters", {})
        if not filters:
            return []
        
        es_query = {"bool": {"must": []}}
        if "category" in filters:
            es_query["bool"]["must"].append({"term": {"category": filters["category"]}})
        if "tags" in filters:
            es_query["bool"]["must"].append({"terms": {"tags": filters["tags"]}})
        
        response = self.es.search(index=self.index_name, body={"query": es_query, "size": 10})
        return [{"id": hit["_id"], "score": hit["_score"], "source": hit["_source"]} 
                for hit in response["hits"]["hits"]]
    
    async def _get_embedding(self, text: str) -> np.ndarray:
        """获取文本的向量表示"""
        # 调用Embedding模型API
        # 返回768维向量
        pass

3.4 排序插件设计

排序是搜索系统的核心功能,在搜索架构中,Ranking处于召回和重排之间,承接了多路召回结果的质量校验、特征融合和相关性精排。

# ranking_plugin.py - 排序插件实现
from typing import List, Dict, Any
import numpy as np
import json
from abc import ABC, abstractmethod

class Ranker(ABC):
    """排序器基类"""
    
    @abstractmethod
    def rank(self, candidates: List[Dict], query_analysis: Dict, context: Dict) -> List[Dict]:
        pass

class FeatureExtractor:
    """特征提取器"""
    
    def extract(self, query: Dict, doc: Dict) -> Dict[str, float]:
        """提取文档-查询匹配特征"""
        features = {}
        
        # 1. 文本匹配特征
        q_tokens = set(query.get("tokens", []))
        doc_tokens = set(doc.get("tokens", []))
        overlap = q_tokens & doc_tokens
        
        features["term_overlap_count"] = len(overlap)
        features["term_overlap_ratio"] = len(overlap) / max(len(q_tokens), 1)
        
        # 2. BM25得分(从ES返回的原始得分)
        features["bm25_score"] = doc.get("_score", 0)
        
        # 3. 标题命中特征
        title_tokens = set(doc.get("title_tokens", []))
        title_overlap = q_tokens & title_tokens
        features["title_hit_count"] = len(title_overlap)
        features["title_hit_ratio"] = len(title_overlap) / max(len(q_tokens), 1)
        
        # 4. 时效性特征(文档新鲜度)
        import time
        publish_time = doc.get("publish_time", 0)
        current_time = time.time()
        days_diff = (current_time - publish_time) / (24 * 3600)
        features["freshness_score"] = np.exp(-days_diff / 365)  # 越新越接近1
        
        # 5. 权威性特征(基于点击/引用/作者等级)
        features["authority_score"] = min(doc.get("view_count", 0) / 10000, 1.0)
        
        # 6. 向量相似度(如有)
        if "vector_similarity" in doc:
            features["vector_sim"] = doc["vector_similarity"]
        
        return features

class RankingPlugin:
    """排序插件主控"""
    
    def __init__(self):
        self.feature_extractor = FeatureExtractor()
        self.ranking_model = self._load_ranking_model()  # 加载LTR模型或精排模型
        
    def rank(self, merged_candidates: List[Dict], query_analysis: Dict, context: Dict) -> List[Dict]:
        """
        对多路召回合并后的结果进行精排
        """
        if not merged_candidates:
            return []
        
        # 1. 特征提取
        for doc in merged_candidates:
            features = self.feature_extractor.extract(query_analysis, doc)
            doc["features"] = features
        
        # 2. 模型打分(LTR/GBDT/神经网络)
        scores = self._predict_scores(merged_candidates)
        
        # 3. 按得分排序
        for doc, score in zip(merged_candidates, scores):
            doc["rank_score"] = score
        
        sorted_docs = sorted(merged_candidates, key=lambda x: x["rank_score"], reverse=True)
        
        return sorted_docs
    
    def _predict_scores(self, docs: List[Dict]) -> List[float]:
        """使用排序模型预测得分"""
        # 构建特征矩阵
        feature_names = ["term_overlap_ratio", "bm25_score", "title_hit_ratio", 
                        "freshness_score", "authority_score", "vector_sim"]
        feature_matrix = []
        
        for doc in docs:
            features = doc["features"]
            row = [features.get(name, 0.0) for name in feature_names]
            # 对数值型特征归一化
            row = self._normalize(row)
            feature_matrix.append(row)
        
        # 调用模型预测
        scores = self.ranking_model.predict(np.array(feature_matrix))
        return scores.tolist()
    
    def _normalize(self, row: List[float]) -> List[float]:
        """特征归一化"""
        # 简化实现:min-max归一化 + 平滑
        max_vals = [1.0, 50.0, 1.0, 1.0, 1.0, 1.0]
        min_vals = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
        return [(v - min_v) / (max_v - min_v + 1e-6) for v, max_v, min_v in zip(row, max_vals, min_vals)]
    
    def _load_ranking_model(self):
        """加载预训练的排序模型"""
        # 实际场景:加载XGBoost/LightGBM模型 或 深度排序模型
        # 这里返回一个占位模型
        return DummyRankingModel()

class DummyRankingModel:
    """占位排序模型(实际会加载训练好的模型文件)"""
    def predict(self, X):
        # 简单加权策略:演示用
        weights = [0.25, 0.30, 0.15, 0.10, 0.10, 0.10]
        return np.sum(X * np.array(weights), axis=1)

3.5 重排与多样性策略

在精排输出结果后,还需要进行重排处理,保证结果集的多样性和用户意图的满足。

# reranking.py - 重排与多样性优化
from typing import List, Dict
import random

class RerankingService:
    """结果重排服务:多样性优化 + 业务策略干预"""
    
    def __init__(self):
        self.max_results = 20
        
    def rerank(self, ranked_docs: List[Dict], query_analysis: Dict) -> List[Dict]:
        """
        重排优化
        1. MMR多样性重排
        2. 业务规则干预(置顶、屏蔽)
        3. 结果格式统一
        """
        if not ranked_docs:
            return []
        
        # 1. 业务规则过滤
        filtered = self._apply_business_rules(ranked_docs)
        
        # 2. MMR多样性重排(避免同类结果扎堆)
        diversified = self._mmr_rerank(filtered, query_analysis)
        
        # 3. 截断
        return diversified[:self.max_results]
    
    def _apply_business_rules(self, docs: List[Dict]) -> List[Dict]:
        """应用业务规则:置顶、屏蔽、分类加权"""
        result = []
        pinned = []  # 置顶文档
        
        for doc in docs:
            # 屏蔽规则
            if doc.get("source", {}).get("status") == "blocked":
                continue
            
            # 置顶规则
            if doc.get("source", {}).get("is_pinned"):
                pinned.append(doc)
            else:
                result.append(doc)
        
        # 置顶文档排在前面
        return pinned + result
    
    def _mmr_rerank(self, docs: List[Dict], query_analysis: Dict, 
                   lambda_param: float = 0.5) -> List[Dict]:
        """
        MMR (Maximum Marginal Relevance) 多样性重排
        
        MMR = λ * similarity(query, doc) - (1-λ) * max_similarity(doc, selected)
        λ越大越偏向相关性,λ越小越偏向多样性
        """
        if len(docs) <= 1:
            return docs
        
        # 初始化:选择第一个文档
        selected = [docs[0]]
        remaining = docs[1:]
        
        while remaining and len(selected) < min(20, len(docs)):
            best_idx = 0
            best_mmr_score = float('-inf')
            
            for i, doc in enumerate(remaining):
                # 相关性得分(精排得分)
                relevance = doc.get("rank_score", 0)
                
                # 与已选文档的最大相似度
                max_sim = max([self._doc_similarity(doc, selected_doc) 
                              for selected_doc in selected])
                
                # MMR得分
                mmr_score = lambda_param * relevance - (1 - lambda_param) * max_sim
                
                if mmr_score > best_mmr_score:
                    best_mmr_score = mmr_score
                    best_idx = i
            
            selected.append(remaining.pop(best_idx))
        
        # 合并剩余文档(如果有)
        return selected + remaining
    
    def _doc_similarity(self, doc_a: Dict, doc_b: Dict) -> float:
        """计算两个文档的相似度(基于标签/类别/Embedding)"""
        # 简化:基于类别和标签的Jaccard相似度
        cat_a = doc_a.get("source", {}).get("category", "")
        cat_b = doc_b.get("source", {}).get("category", "")
        if cat_a and cat_b and cat_a == cat_b:
            return 0.8
        
        tags_a = set(doc_a.get("source", {}).get("tags", []))
        tags_b = set(doc_b.get("source", {}).get("tags", []))
        if tags_a or tags_b:
            overlap = len(tags_a & tags_b)
            union = len(tags_a | tags_b)
            return overlap / union if union > 0 else 0
        
        return 0.0

3.6 特征计算服务

在搜索引擎中,Ranking位于召回和重排之间,承接了多路召回结果的质量校验、特征融合和相关性精排。特征计算是排序的核心环节。

# feature_service.py - 特征计算服务
from typing import Dict, List, Any
import numpy as np
from collections import Counter

class FeatureService:
    """特征计算服务:实时计算文档-查询特征"""
    
    def __init__(self, embedding_service):
        self.embedding_service = embedding_service
        
    def compute_features(self, query: str, doc: Dict) -> Dict[str, float]:
        """计算完整的特征向量"""
        features = {}
        
        # 1. 统计类特征
        features.update(self._statistical_features(query, doc))
        
        # 2. 语义类特征
        features.update(self._semantic_features(query, doc))
        
        # 3. 结构类特征
        features.update(self._structural_features(doc))
        
        # 4. 行为类特征
        features.update(self._behavioral_features(doc))
        
        return features
    
    def _statistical_features(self, query: str, doc: Dict) -> Dict[str, float]:
        """统计类特征(词频、覆盖度等)"""
        q_words = set(jieba.lcut(query))
        title_words = set(jieba.lcut(doc.get("title", "")))
        content_words = set(jieba.lcut(doc.get("content", "")))
        
        features = {
            "title_term_match_ratio": len(q_words & title_words) / max(len(q_words), 1),
            "content_term_match_ratio": len(q_words & content_words) / max(len(q_words), 1),
            "title_length": len(title_words),
            "content_length": len(content_words),
        }
        
        # 词频统计
        doc_words = content_words
        q_word_freq = Counter()
        for w in jieba.lcut(doc.get("content", "")):
            if w in q_words:
                q_word_freq[w] += 1
        
        features["matched_term_tf_mean"] = np.mean(list(q_word_freq.values())) if q_word_freq else 0
        
        return features
    
    def _semantic_features(self, query: str, doc: Dict) -> Dict[str, float]:
        """语义类特征(向量相似度、主题匹配)"""
        features = {}
        
        # 计算Query和文档的语义向量相似度
        query_vec = self.embedding_service.encode(query)
        doc_vec = self.embedding_service.encode(doc.get("content", ""))
        
        if query_vec is not None and doc_vec is not None:
            sim = np.dot(query_vec, doc_vec) / (np.linalg.norm(query_vec) * np.linalg.norm(doc_vec))
            features["semantic_similarity"] = float(sim)
        else:
            features["semantic_similarity"] = 0.0
        
        return features
    
    def _structural_features(self, doc: Dict) -> Dict[str, float]:
        """结构类特征(文档质量、完整性)"""
        features = {
            "has_title": 1.0 if doc.get("title") else 0.0,
            "has_summary": 1.0 if doc.get("summary") else 0.0,
            "has_image": 1.0 if doc.get("image_url") else 0.0,
            "field_completeness": 0.0
        }
        
        # 字段完整性
        required_fields = ["title", "content", "category"]
        filled = sum(1 for f in required_fields if doc.get(f))
        features["field_completeness"] = filled / len(required_fields)
        
        return features
    
    def _behavioral_features(self, doc: Dict) -> Dict[str, float]:
        """行为类特征(点击、收藏、分享等)"""
        # 从用户行为日志中聚合
        doc_id = doc.get("doc_id")
        # 模拟数据,实际从实时统计服务获取
        features = {
            "ctr": doc.get("ctr", 0.0),         # 点击率
            "collect_count_norm": 0.5,           # 收藏数归一化
            "share_count_norm": 0.3,             # 分享数归一化
            "comment_count_norm": 0.2,           # 评论数归一化
        }
        return features

四、性能优化实践

4.1 缓存策略

搜索系统需要应对大量重复请求,合理的缓存能有效降低系统负载。

# search_cache.py - 搜索缓存实现
import hashlib
import json
from typing import Optional, Dict, Any
import redis
from functools import lru_cache

class SearchCache:
    """搜索结果多级缓存"""
    
    def __init__(self, redis_client):
        self.redis = redis_client
        self.local_cache = {}  # 本地内存缓存
        
    def get_cache_key(self, query: str, filters: Dict, page: int, size: int) -> str:
        """生成缓存Key(考虑分页参数)"""
        cache_dict = {
            "query": query,
            "filters": json.dumps(filters, sort_keys=True),
            "page": page,
            "size": size
        }
        key_str = json.dumps(cache_dict, sort_keys=True)
        return f"search:{hashlib.md5(key_str.encode()).hexdigest()}"
    
    def get(self, key: str) -> Optional[Dict]:
        """获取缓存(先查本地,再查Redis)"""
        # 本地缓存
        if key in self.local_cache:
            return self.local_cache[key]
        
        # Redis缓存
        data = self.redis.get(key)
        if data:
            result = json.loads(data)
            # 填充本地缓存
            self.local_cache[key] = result
            return result
        
        return None
    
    def set(self, key: str, value: Dict, ttl: int = 300):
        """设置缓存"""
        self.redis.setex(key, ttl, json.dumps(value))
        self.local_cache[key] = value

4.2 索引优化

// 索引优化配置
{
  "settings": {
    "index": {
      "refresh_interval": "30s",           // 降低实时性要求,提高写入性能
      "number_of_shards": 3,
      "number_of_replicas": 1,
      "analysis": {
        "analyzer": {
          "ik_smart": {
            "type": "custom",
            "tokenizer": "ik_smart",
            "filter": ["lowercase", "stop"]
          }
        }
      }
    }
  }
}

4.3 性能压测与调优

优化项优化前优化后提升
Query响应时间(P99)450ms180ms60%
并发QPS200800300%
缓存命中率0%62%-
索引构建时间4h1.2h70%

五、总结

本文围绕垂直搜索后端系统,从模块化设计视角逐一拆解了索引架构、Query理解、多路召回、排序插件、重排策略和特征计算等核心模块,并给出了完整的代码实现。

垂直搜索是一个系统工程,其价值在于对特定领域的深度理解和精准匹配。一个设计良好的垂直搜索系统,需要综合考虑索引结构、召回策略、排序模型和性能优化等多方面因素。在AIGC时代,融合大模型的语义理解能力将进一步提升垂直搜索的精准度,这是未来值得持续探索的方向。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值