引言
搜索是现代应用的核心功能之一。无论是电商商品搜索、日志分析还是全文检索,Elasticsearch和OpenSearch都是最常用的解决方案。本文将深入讲解搜索引擎的架构设计与性能优化实战。
Elasticsearch核心概念
Elasticsearch核心概念:
┌─────────────────────────────────────────┐
│ 集群(Cluster) │
│ 由一个或多个节点组成 │
│ 共同持有整个数据 │
│ │
│ 节点(Node) │
│ 集群中的单个服务器 │
│ 存储数据并参与集群的索引和搜索能力 │
│ │
│ 索引(Index) │
│ 具有相似特征的文档集合 │
│ 类似于关系数据库中的"数据库" │
│ │
│ 分片(Shard) │
│ 索引的水平分割单元 │
│ 每个分片是一个独立的Lucene索引 │
│ │
│ 副本(Replica) │
│ 主分片的拷贝 │
│ 提供高可用和读取扩展 │
│ │
│ 文档(Document) │
│ 可被索引的基本信息单元 │
│ 以JSON格式表示 │
└─────────────────────────────────────────┘
索引设计与Mapping
索引命名规范
索引命名规范:
┌─────────────────────────────────────────┐
│ 格式:<业务>-<类型>-<环境>-<版本> │
│ │
│ 示例: │
│ ecommerce-products-prod-v1 │
│ user-logs-dev-2026.08 │
│ order-events-staging-v2 │
│ │
│ 时间序列索引(日志、指标): │
│ logs-nginx-2026.08.12 │
│ metrics-app-2026.08.12 │
│ │
│ 别名(Alias): │
│ logs-nginx-current → logs-nginx-2026.08.12
│ products-latest → ecommerce-products-prod-v2
└─────────────────────────────────────────┘
Mapping设计
// 电商商品索引Mapping
PUT /ecommerce-products-prod-v1
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "5s",
"analysis": {
"analyzer": {
"ik_smart_analyzer": {
"type": "custom",
"tokenizer": "ik_smart"
},
"product_analyzer": {
"type": "custom",
"tokenizer": "ik_max_word",
"filter": ["lowercase", "product_synonym"]
}
},
"filter": {
"product_synonym": {
"type": "synonym",
"synonyms_path": "synonyms.txt"
}
}
}
},
"mappings": {
"properties": {
"product_id": {
"type": "keyword"
},
"title": {
"type": "text",
"analyzer": "product_analyzer",
"search_analyzer": "ik_smart_analyzer",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"description": {
"type": "text",
"analyzer": "ik_smart_analyzer"
},
"category_ids": {
"type": "keyword"
},
"brand_id": {
"type": "keyword"
},
"price": {
"type": "scaled_float",
"scaling_factor": 100
},
"original_price": {
"type": "scaled_float",
"scaling_factor": 100
},
"stock": {
"type": "integer"
},
"sales_count": {
"type": "integer"
},
"rating": {
"type": "float"
},
"tags": {
"type": "keyword"
},
"attributes": {
"type": "nested",
"properties": {
"key": {
"type": "keyword"
},
"value": {
"type": "keyword"
}
}
},
"images": {
"type": "keyword",
"index": false
},
"status": {
"type": "keyword"
},
"created_at": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||epoch_millis"
},
"updated_at": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||epoch_millis"
},
"location": {
"type": "geo_point"
},
"suggest": {
"type": "completion",
"analyzer": "ik_smart_analyzer"
}
}
}
}
字段类型选择指南
字段类型选择:
┌─────────────────────────────────────────┐
│ keyword: │
│ - 精确匹配(term、terms) │
│ - 聚合、排序 │
│ - 适合:ID、状态、标签、枚举值 │
│ │
│ text: │
│ - 全文搜索(match、multi_match) │
│ - 分词处理 │
│ - 适合:标题、描述、内容 │
│ │
│ integer/long: │
│ - 数值范围查询 │
│ - 聚合统计 │
│ - 适合:数量、计数、年龄 │
│ │
│ scaled_float: │
│ - 精确小数(货币) │
│ - 避免浮点精度问题 │
│ - 适合:价格、金额 │
│ │
│ date: │
│ - 时间范围查询 │
│ - 时间聚合 │
│ - 适合:创建时间、更新时间 │
│ │
│ nested: │
│ - 对象数组的独立查询 │
│ - 避免对象扁平化问题 │
│ - 适合:SKU属性、标签键值对 │
│ │
│ geo_point: │
│ - 地理位置查询 │
│ - 距离排序 │
│ - 适合:门店位置、配送范围 │
│ │
│ completion: │
│ - 自动补全 │
│ - 快速前缀匹配 │
│ - 适合:搜索建议、自动完成 │
└─────────────────────────────────────────┘
查询DSL实战
电商搜索查询
// 综合搜索(相关性+销量+价格)
GET /ecommerce-products-prod-v1/_search
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "iPhone 15 Pro Max",
"fields": ["title^3", "description^1", "tags^2"],
"type": "best_fields",
"fuzziness": "AUTO"
}
}
],
"filter": [
{
"term": {
"status": "on_sale"
}
},
{
"range": {
"stock": {
"gt": 0
}
}
},
{
"range": {
"price": {
"gte": 5000,
"lte": 15000
}
}
}
]
}
},
"sort": [
{
"_score": {
"order": "desc"
}
},
{
"sales_count": {
"order": "desc"
}
}
],
"from": 0,
"size": 20,
"highlight": {
"fields": {
"title": {},
"description": {}
},
"pre_tags": ["<em>"],
"post_tags": ["</em>"]
},
"aggs": {
"categories": {
"terms": {
"field": "category_ids",
"size": 10
}
},
"brands": {
"terms": {
"field": "brand_id",
"size": 10
}
},
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{ "to": 1000 },
{ "from": 1000, "to": 5000 },
{ "from": 5000, "to": 10000 },
{ "from": 10000 }
]
}
}
}
}
自动补全查询
// 搜索建议
GET /ecommerce-products-prod-v1/_search
{
"suggest": {
"product-suggest": {
"prefix": "iPh",
"completion": {
"field": "suggest",
"size": 10,
"skip_duplicates": true
}
}
}
}
地理位置查询
// 附近门店搜索
GET /stores-index/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"status": "open"
}
}
],
"filter": [
{
"geo_distance": {
"distance": "5km",
"location": {
"lat": 39.9042,
"lon": 116.4074
}
}
}
]
}
},
"sort": [
{
"_geo_distance": {
"location": {
"lat": 39.9042,
"lon": 116.4074
},
"order": "asc",
"unit": "km"
}
}
]
}
Go客户端集成
Elasticsearch Go客户端
package search
import (
"bytes"
"context"
"encoding/json"
"log"
"github.com/elastic/go-elasticsearch/v8"
"github.com/elastic/go-elasticsearch/v8/esapi"
)
type SearchService struct {
client *elasticsearch.Client
index string
}
func NewSearchService(addresses []string, index string) (*SearchService, error) {
cfg := elasticsearch.Config{
Addresses: addresses,
}
client, err := elasticsearch.NewClient(cfg)
if err != nil {
return nil, err
}
return &SearchService{
client: client,
index: index,
}, nil
}
type SearchRequest struct {
Query string
CategoryIDs []string
BrandIDs []string
MinPrice float64
MaxPrice float64
Sort string
Page int
PageSize int
}
type SearchResult struct {
Total int64 `json:"total"`
Products []Product `json:"products"`
Aggregations map[string]interface{} `json:"aggregations"`
}
func (s *SearchService) Search(ctx context.Context, req SearchRequest) (*SearchResult, error) {
// 构建查询
query := map[string]interface{}{
"query": map[string]interface{}{
"bool": map[string]interface{}{
"must": []interface{}{
map[string]interface{}{
"multi_match": map[string]interface{}{
"query": req.Query,
"fields": []string{"title^3", "description^1", "tags^2"},
"type": "best_fields",
"fuzziness": "AUTO",
},
},
},
"filter": buildFilters(req),
},
},
"sort": buildSort(req.Sort),
"from": (req.Page - 1) * req.PageSize,
"size": req.PageSize,
"highlight": map[string]interface{}{
"fields": map[string]interface{}{
"title": map[string]interface{}{},
"description": map[string]interface{}{},
},
},
"aggs": map[string]interface{}{
"categories": map[string]interface{}{
"terms": map[string]interface{}{
"field": "category_ids",
"size": 10,
},
},
"price_ranges": map[string]interface{}{
"range": map[string]interface{}{
"field": "price",
"ranges": []map[string]interface{}{
{"to": 1000},
{"from": 1000, "to": 5000},
{"from": 5000, "to": 10000},
{"from": 10000},
},
},
},
},
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(query); err != nil {
return nil, err
}
res, err := s.client.Search(
s.client.Search.WithContext(ctx),
s.client.Search.WithIndex(s.index),
s.client.Search.WithBody(&buf),
s.client.Search.WithTrackTotalHits(true),
)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.IsError() {
return nil, fmt.Errorf("search error: %s", res.String())
}
var result map[string]interface{}
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
return nil, err
}
return parseSearchResult(result)
}
func buildFilters(req SearchRequest) []interface{} {
var filters []interface{}
// 状态过滤
filters = append(filters, map[string]interface{}{
"term": map[string]interface{}{
"status": "on_sale",
},
})
// 库存过滤
filters = append(filters, map[string]interface{}{
"range": map[string]interface{}{
"stock": map[string]interface{}{
"gt": 0,
},
},
})
// 分类过滤
if len(req.CategoryIDs) > 0 {
filters = append(filters, map[string]interface{}{
"terms": map[string]interface{}{
"category_ids": req.CategoryIDs,
},
})
}
// 价格范围
if req.MinPrice > 0 || req.MaxPrice > 0 {
priceRange := map[string]interface{}{}
if req.MinPrice > 0 {
priceRange["gte"] = req.MinPrice
}
if req.MaxPrice > 0 {
priceRange["lte"] = req.MaxPrice
}
filters = append(filters, map[string]interface{}{
"range": map[string]interface{}{
"price": priceRange,
},
})
}
return filters
}
func buildSort(sort string) []interface{} {
switch sort {
case "price_asc":
return []interface{}{
map[string]interface{}{"price": map[string]interface{}{"order": "asc"}},
}
case "price_desc":
return []interface{}{
map[string]interface{}{"price": map[string]interface{}{"order": "desc"}},
}
case "sales":
return []interface{}{
map[string]interface{}{"sales_count": map[string]interface{}{"order": "desc"}},
}
case "newest":
return []interface{}{
map[string]interface{}{"created_at": map[string]interface{}{"order": "desc"}},
}
default:
return []interface{}{
map[string]interface{}{"_score": map[string]interface{}{"order": "desc"}},
map[string]interface{}{"sales_count": map[string]interface{}{"order": "desc"}},
}
}
}
性能优化
索引优化
// 索引优化配置
PUT /my-index
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
// 刷新间隔(默认1s,写入密集场景可调大)
"refresh_interval": "5s",
// 合并策略
"merge": {
"scheduler": {
"max_thread_count": 2
}
},
// 转储设置
"translog": {
"durability": "async",
"sync_interval": "5s",
"flush_threshold_size": "1gb"
},
// 缓存设置
"queries": {
"cache": {
"enabled": true
}
},
// 字段数据缓存
"fielddata": {
"cache": "node"
}
}
}
查询优化
查询优化技巧:
┌─────────────────────────────────────────┐
│ 1. 使用filter代替query │
│ - filter不计算相关性分数,更快 │
│ - filter结果可缓存 │
│ │
│ 2. 避免深度分页 │
│ - 使用search_after代替from/size │
│ - 或使用scroll API │
│ │
│ 3. 只返回需要的字段 │
│ - 使用_source过滤 │
│ - 减少网络传输 │
│ │
│ 4. 使用routing │
│ - 将相关数据路由到同一分片 │
│ - 减少跨分片查询 │
│ │
│ 5. 预热filesystem cache │
│ - 使用index.store.preload │
│ - 启动时加载常用索引 │
│ │
│ 6. 使用doc_values │
│ - 用于排序和聚合 │
│ - 磁盘存储,内存友好 │
│ │
│ 7. 避免wildcard查询 │
│ - 使用前缀查询代替 │
│ - 或使用n-gram分词 │
└─────────────────────────────────────────┘
批量操作优化
// 批量索引文档
func (s *SearchService) BulkIndex(ctx context.Context, products []Product) error {
var buf bytes.Buffer
for _, product := range products {
// 元数据行
meta := map[string]interface{}{
"index": map[string]interface{}{
"_index": s.index,
"_id": product.ID,
},
}
json.NewEncoder(&buf).Encode(meta)
// 数据行
json.NewEncoder(&buf).Encode(product)
}
res, err := s.client.Bulk(
s.client.Bulk.WithContext(ctx),
s.client.Bulk.WithBody(&buf),
s.client.Bulk.WithRefresh("wait_for"),
)
if err != nil {
return err
}
defer res.Body.Close()
if res.IsError() {
return fmt.Errorf("bulk error: %s", res.String())
}
return nil
}
集群管理
集群健康检查
# 集群健康状态
GET /_cluster/health
# 返回示例:
{
"cluster_name": "production",
"status": "green",
"number_of_nodes": 5,
"number_of_data_nodes": 3,
"active_primary_shards": 45,
"active_shards": 90,
"unassigned_shards": 0
}
# 状态说明:
# green: 所有分片正常分配
# yellow: 主分片正常,部分副本未分配
# red: 部分主分片未分配
索引生命周期管理(ILM)
// 定义ILM策略
PUT /_ilm/policy/logs-policy
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_size": "50gb",
"max_age": "1d"
},
"set_priority": {
"priority": 100
}
}
},
"warm": {
"min_age": "7d",
"actions": {
"shrink": {
"number_of_shards": 1
},
"forcemerge": {
"max_num_segments": 1
},
"set_priority": {
"priority": 50
}
}
},
"cold": {
"min_age": "30d",
"actions": {
"freeze": {}
}
},
"delete": {
"min_age": "90d",
"actions": {
"delete": {}
}
}
}
}
}
总结
Elasticsearch最佳实践
| 场景 | 推荐配置 | 原因 |
|---|---|---|
| 电商搜索 | 3主分片+1副本,IK分词 | 平衡性能与扩展性 |
| 日志分析 | 按天索引+ILM策略 | 便于管理和清理 |
| 实时搜索 | refresh_interval=1s | 近实时可见 |
| 批量导入 | refresh_interval=-1 | 提升写入性能 |
| 高并发读取 | 增加副本数 | 分散读取压力 |
关键原则
- 合理设计Mapping:选择正确的字段类型
- 优化查询DSL:使用filter、避免深度分页
- 批量操作:使用Bulk API提升性能
- 监控集群:关注健康状态和资源使用
- 生命周期管理:自动管理索引的冷热数据
- 定期优化:forcemerge、reindex等操作
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。