Elasticsearch 完全指南

相关文档:PostgreSQL完全指南(/postgresql-wan-quan-zhi-nan/) MongoDB完全指南(/mongodb-wan-quan-zhi-nan/) Redis完全指南(/redis-wan-quan-zhi-nan/) Elasticsearch 是基于 Apache Lucene 的分布式全文搜索引擎,RESTful API 驱动,支持近实时搜索和分析。 Mapping 定义字段类型,一旦设置不可更改(只能新增字段)。 聚合类似 SQL 的 GROUP BY + 统计函数。 Mapping 提前定义,不依赖动态映射:E

分享

官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
适用版本:Elasticsearch 8.x(2026-05-07 核实)

相关文档:PostgreSQL完全指南 MongoDB完全指南 Redis完全指南


1. 基础概念

Elasticsearch 是什么

Elasticsearch 是基于 Apache Lucene 的分布式全文搜索引擎,RESTful API 驱动,支持近实时搜索和分析。

概念 说明 类比(关系型数据库)
Index 存储文档的逻辑命名空间 数据库
Document JSON 格式的数据单元
Field 文档的键值对
Mapping 字段类型定义 Schema
Shard Index 的物理分片 分区
Replica Shard 的副本(高可用) 主从复制

核心特性

  • 倒排索引:term → [doc_id, position] 映射,全文检索极快
  • 近实时(NRT):默认 1 秒刷新,写入后约 1 秒可搜索
  • 分布式:数据自动分片,水平扩展
  • 丰富查询 DSL:bool/match/range/geo/aggregation 等

安装

# Docker 单节点(开发用)
docker run -d \
  --name elasticsearch \
  -p 9200:9200 \
  -e "discovery.type=single-node" \
  -e "xpack.security.enabled=false" \
  docker.elastic.co/elasticsearch/elasticsearch:8.12.0

# Python 客户端
pip install elasticsearch[async]   # 异步客户端(推荐)

2. 基础 CRUD

from elasticsearch import AsyncElasticsearch

es = AsyncElasticsearch("http://localhost:9200")

# 创建 / 更新文档
await es.index(
    index="articles",
    id="1",
    document={
        "title": "Elasticsearch 入门",
        "content": "Elasticsearch 是分布式搜索引擎...",
        "author": "Alice",
        "created_at": "2026-01-01T00:00:00",
        "tags": ["search", "database"],
        "views": 100,
    }
)

# 获取文档
doc = await es.get(index="articles", id="1")
print(doc["_source"])

# 更新文档(部分更新)
await es.update(
    index="articles",
    id="1",
    doc={"views": 101},
)

# 删除文档
await es.delete(index="articles", id="1")

# 关闭连接
await es.close()

3. Mapping 定义

Mapping 定义字段类型,一旦设置不可更改(只能新增字段)。

# 创建索引并定义 Mapping
await es.indices.create(
    index="articles",
    body={
        "settings": {
            "number_of_shards": 1,
            "number_of_replicas": 0,
            "analysis": {
                "analyzer": {
                    "ik_smart_analyzer": {
                        "type": "custom",
                        "tokenizer": "ik_smart",  # 需安装 IK 分词插件
                    }
                }
            }
        },
        "mappings": {
            "properties": {
                "title": {
                    "type": "text",
                    "analyzer": "ik_smart",       # 索引时分词
                    "search_analyzer": "ik_smart", # 搜索时分词
                    "fields": {
                        "keyword": {              # 同时保留 keyword 类型(精确匹配/排序)
                            "type": "keyword",
                            "ignore_above": 256,
                        }
                    }
                },
                "content": {"type": "text", "analyzer": "ik_smart"},
                "author": {"type": "keyword"},
                "created_at": {"type": "date"},
                "tags": {"type": "keyword"},
                "views": {"type": "integer"},
                "location": {"type": "geo_point"},
            }
        }
    }
)

常用字段类型

类型 说明 用途
text 分词后建立倒排索引 全文搜索
keyword 不分词,精确值 过滤、排序、聚合
integer/long/float 数值 范围查询、聚合
date 日期(ISO 8601 或时间戳) 时间范围查询
boolean 布尔值 过滤
nested 嵌套对象数组(保留对象关系) 复杂嵌套查询
geo_point 经纬度坐标 地理位置查询

4. 查询 DSL

全文搜索

# match — 单字段全文搜索(分词后搜索)
response = await es.search(
    index="articles",
    body={
        "query": {
            "match": {
                "title": "Elasticsearch 搜索引擎"
            }
        }
    }
)

# multi_match — 多字段全文搜索
response = await es.search(
    index="articles",
    body={
        "query": {
            "multi_match": {
                "query": "搜索引擎",
                "fields": ["title^2", "content"],  # title 权重加倍
                "type": "best_fields",
            }
        }
    }
)

# match_phrase — 短语搜索(词序和位置都匹配)
response = await es.search(
    index="articles",
    body={
        "query": {
            "match_phrase": {
                "content": "分布式搜索引擎"
            }
        }
    }
)

bool 组合查询

response = await es.search(
    index="articles",
    body={
        "query": {
            "bool": {
                "must": [                         # 必须匹配(影响评分)
                    {"match": {"title": "Elasticsearch"}}
                ],
                "filter": [                       # 必须匹配(不影响评分,有缓存)
                    {"term": {"author": "Alice"}},
                    {"range": {"created_at": {"gte": "2026-01-01"}}},
                    {"terms": {"tags": ["search", "database"]}},
                ],
                "should": [                       # 可选(匹配则提升评分)
                    {"match": {"content": "倒排索引"}},
                ],
                "must_not": [                     # 不能匹配
                    {"term": {"author": "Bob"}},
                ],
            }
        }
    }
)

term / range / exists

# term — 精确匹配(keyword 类型)
{"term": {"author": "Alice"}}
{"terms": {"author": ["Alice", "Bob"]}}

# range — 范围查询
{"range": {"views": {"gte": 100, "lte": 1000}}}
{"range": {"created_at": {"gte": "2026-01-01", "lt": "2027-01-01"}}}

# exists — 字段存在
{"exists": {"field": "tags"}}

# wildcard — 通配符(性能较差,慎用)
{"wildcard": {"title.keyword": "Elastic*"}}

# prefix — 前缀匹配
{"prefix": {"title.keyword": "Elastic"}}

5. 聚合(Aggregation)

聚合类似 SQL 的 GROUP BY + 统计函数。

response = await es.search(
    index="articles",
    body={
        "size": 0,  # 不返回文档,只返回聚合结果
        "aggs": {
            # Bucket 聚合 — 分组
            "by_author": {
                "terms": {
                    "field": "author",
                    "size": 10,
                    "order": {"avg_views": "desc"},
                },
                "aggs": {
                    # 子聚合
                    "avg_views": {"avg": {"field": "views"}},
                    "total_views": {"sum": {"field": "views"}},
                }
            },
            # Metric 聚合 — 统计
            "views_stats": {
                "stats": {"field": "views"}
                # 返回:count/min/max/avg/sum
            },
            # Date Histogram — 按时间分组
            "by_month": {
                "date_histogram": {
                    "field": "created_at",
                    "calendar_interval": "month",
                    "format": "yyyy-MM",
                },
                "aggs": {
                    "article_count": {"value_count": {"field": "_id"}}
                }
            },
            # Range 聚合
            "views_range": {
                "range": {
                    "field": "views",
                    "ranges": [
                        {"to": 100},
                        {"from": 100, "to": 1000},
                        {"from": 1000},
                    ]
                }
            }
        }
    }
)

# 获取聚合结果
buckets = response["aggregations"]["by_author"]["buckets"]
for bucket in buckets:
    print(bucket["key"], bucket["avg_views"]["value"])

6. 批量操作

from elasticsearch.helpers import async_bulk

# bulk 写入(高效批量索引)
async def bulk_index(documents: list[dict]):
    actions = [
        {
            "_index": "articles",
            "_id": doc["id"],
            "_source": doc,
        }
        for doc in documents
    ]
    success, failed = await async_bulk(es, actions, chunk_size=500)
    return success, failed

# 批量查询
response = await es.mget(
    index="articles",
    body={"ids": ["1", "2", "3"]},
)

7. 分页

# 普通分页(from + size,深度分页性能差,建议 from < 10000)
response = await es.search(
    index="articles",
    body={
        "from": 0,
        "size": 20,
        "query": {"match_all": {}},
        "sort": [{"created_at": "desc"}, {"_score": "desc"}],
    }
)

# Search After — 游标分页(深度分页推荐)
response = await es.search(
    index="articles",
    body={
        "size": 20,
        "query": {"match_all": {}},
        "sort": [{"created_at": "desc"}, {"_id": "asc"}],
        "search_after": ["2026-01-15T10:00:00", "abc123"],  # 上一页最后一条的 sort 值
    }
)

# Scroll API — 全量导出(不适合实时搜索)
response = await es.search(
    index="articles",
    scroll="2m",      # scroll 上下文保留 2 分钟
    size=1000,
    body={"query": {"match_all": {}}}
)
scroll_id = response["_scroll_id"]

while True:
    hits = response["hits"]["hits"]
    if not hits:
        break
    process(hits)
    response = await es.scroll(scroll_id=scroll_id, scroll="2m")

await es.clear_scroll(scroll_id=scroll_id)

8. 高亮显示

response = await es.search(
    index="articles",
    body={
        "query": {"match": {"content": "搜索引擎"}},
        "highlight": {
            "fields": {
                "content": {
                    "fragment_size": 150,         # 每个片段的字符数
                    "number_of_fragments": 3,      # 最多返回几个片段
                    "pre_tags": ["<em>"],
                    "post_tags": ["</em>"],
                }
            }
        }
    }
)

for hit in response["hits"]["hits"]:
    print(hit["highlight"]["content"])  # ['...关键词<em>搜索引擎</em>...']

9. 与 FastAPI 集成

# src/core/elasticsearch.py
from contextlib import asynccontextmanager
from elasticsearch import AsyncElasticsearch
from fastapi import FastAPI

es: AsyncElasticsearch = None

@asynccontextmanager
async def lifespan(app: FastAPI):
    global es
    es = AsyncElasticsearch("http://localhost:9200")
    yield
    await es.close()

app = FastAPI(lifespan=lifespan)

# 搜索 API
from fastapi import Query

@app.get("/search")
async def search_articles(
    q: str = Query(..., description="搜索关键词"),
    page: int = Query(1, ge=1),
    size: int = Query(20, ge=1, le=100),
    author: str | None = None,
):
    filters = []
    if author:
        filters.append({"term": {"author": author}})

    response = await es.search(
        index="articles",
        body={
            "from": (page - 1) * size,
            "size": size,
            "query": {
                "bool": {
                    "must": [{"multi_match": {"query": q, "fields": ["title^2", "content"]}}],
                    "filter": filters,
                }
            },
            "highlight": {
                "fields": {"title": {}, "content": {"fragment_size": 200}}
            },
        }
    )

    hits = response["hits"]["hits"]
    return {
        "total": response["hits"]["total"]["value"],
        "results": [
            {**h["_source"], "highlight": h.get("highlight", {})}
            for h in hits
        ]
    }

10. 常用代码段

检查索引是否存在

exists = await es.indices.exists(index="articles")
if not exists:
    await create_index()

更新 Mapping(新增字段)

await es.indices.put_mapping(
    index="articles",
    body={
        "properties": {
            "new_field": {"type": "keyword"}
        }
    }
)

删除并重建索引(Reindex)

# 重建索引(修改不可变的 mapping 时使用)
await es.reindex(body={
    "source": {"index": "articles"},
    "dest": {"index": "articles_v2"},
})
await es.indices.put_alias(index="articles_v2", name="articles_alias")

统计文档数

count = await es.count(index="articles", body={"query": {"match_all": {}}})
print(count["count"])

11. 最佳实践

Mapping 设计原则

  • 对需要全文搜索的字段用 text,同时用 .keyword 子字段支持排序和聚合
  • 对不需要搜索、只需过滤的字段用 keyword(如 ID、状态)
  • index: false 关闭不需要搜索的字段的索引,减少存储
"fields": {
    "status": {"type": "keyword"},
    "raw_content": {"type": "text", "index": False},  # 只存储不搜索
}

filter vs must

  • 过滤条件(不影响评分)用 filter,ES 会缓存 filter 结果,性能更好
  • 只有需要影响相关性评分的条件才放入 must

批量写入

  • 单条写入延迟高,批量用 async_bulk,每批 500~1000 条
  • 写入时关闭 refresh_interval(设为 -1),完成后手动触发 refresh
await es.indices.put_settings(index="articles", body={"refresh_interval": "-1"})
# ... 批量写入 ...
await es.indices.refresh(index="articles")
await es.indices.put_settings(index="articles", body={"refresh_interval": "1s"})

最佳实践

Mapping 提前定义,不依赖动态映射:ES 的动态映射会猜测字段类型,把数字字符串(如 "123")映射为 textlong,后续难以修改。生产环境必须在创建索引时显式定义所有字段的 Mapping。

text 字段同时配置 .keyword 子字段text 用于全文搜索,keyword 用于精确匹配、聚合、排序。两者通常同时需要。

"title": {
  "type": "text",
  "analyzer": "ik_max_word",
  "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } }
}

批量写入关闭自动刷新以提升吞吐:默认每 1 秒 refresh 一次(数据可见),大批量导入时关闭可提升写入速度 2–5 倍,完成后手动 refresh。

# 关闭自动 refresh
await es.indices.put_settings(index="articles", body={"refresh_interval": "-1"})
# 批量写入...
await es.helpers.async_bulk(es, actions)
# 恢复
await es.indices.refresh(index="articles")
await es.indices.put_settings(index="articles", body={"refresh_interval": "1s"})

bool 查询中将过滤条件放入 filter,不放 mustfilter 子句不参与相关性评分且结果可被缓存;must 参与评分,每次都要计算 TF-IDF/BM25。能用 filter 的条件(状态、时间范围)一律放 filter

分片数量在创建时确定,不可修改:主分片数在索引创建后不可更改。索引创建前根据数据量估算:每个分片 20–50GB 为宜,不要追求过多分片(增加集群协调开销)。

使用 search_after 替代 from+size 实现深度分页from + size > 10000 时 ES 默认拒绝,且即使放开限制,深度分页需要协调所有分片的 top-N,开销随深度线性增长。


常见陷阱

陷阱:中文搜索按单字切分,结果不准确

现象: 搜索"机器学习"只能搜到包含"机"、"器"、"学"、"习"单字的文档,不能按词搜索。

原因: ES 内置分析器(standard/whitespace)对中文按 Unicode 码点切分,不做词法分析。

解决: 安装 IK 分词插件,Mapping 中指定 "analyzer": "ik_max_word"(建库分词)和 "search_analyzer": "ik_smart"(搜索分词)。

./bin/elasticsearch-plugin install analysis-ik
"content": {
  "type": "text",
  "analyzer": "ik_max_word",
  "search_analyzer": "ik_smart"
}

陷阱:字段类型一旦建立无法修改

现象: 需要把 pricekeyword 改为 float,执行 PUT /_mapping 报错 mapper cannot be changed from type [keyword] to [float]

原因: ES 的倒排索引结构决定了已有字段的类型在索引建立后不可修改(修改需要重建倒排索引)。

解决: 新建索引,用 _reindex API 将数据从旧索引迁移到新索引,更新别名后切换流量。

# 1. 新建索引(新 Mapping)
await es.indices.create(index="articles_v2", body={...})

# 2. 重索引
await es.reindex(body={
    "source": {"index": "articles_v1"},
    "dest": {"index": "articles_v2"}
})

# 3. 切换别名
await es.indices.update_aliases(body={
    "actions": [
        {"remove": {"index": "articles_v1", "alias": "articles"}},
        {"add": {"index": "articles_v2", "alias": "articles"}},
    ]
})

陷阱:text 字段直接排序或聚合报错

现象: {"sort": [{"title": "asc"}]} 报错 Text fields are not optimised for operations that require per-document field data

原因: text 字段已经分词,每个词条都是独立的 token,无法对原始字符串排序或聚合。

解决: 使用 .keyword 子字段进行排序和聚合(需在 Mapping 中提前定义 keyword 子字段)。

# 错误
"sort": [{"title": "asc"}]

# 正确:使用 keyword 子字段
"sort": [{"title.keyword": "asc"}]

# 聚合也一样
"aggs": {"by_title": {"terms": {"field": "title.keyword"}}}

参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog