ES的优势
相比其他的存储引擎,ES的优势主要在于:
全文搜索:ES使用倒排索引和实时索引更新,使得搜索性能非常的好,远比数据库的全表索引要好
指标分析:ES的存储结构是json,天生就适合从数据中提取指标,进行指标搜索和聚合,擅长排序、分页、聚合等操作
ES一个非常经典的应用就是ELK(ES+logstash+kibana)日志框架:
ES:承担数据仓储和检索能力
logstash:承担日志搬运、分析能力
kibana:承担前端接口,使ES的数据结果具备可读性
ES的数据存储结构
逻辑结构
ES的逻辑存储结构是json,一切数据最终都被序列化为json对象,称之为“文档(Document)”
每个文档包括两部分:元数据、数据
元数据(Metadata)描述文档自身的信息,通常以“_”开头,包括:
_index:文档所属的索引,类似于数据库的“库”或“表”。_id:文档的唯一标识符,可以手动指定或由系统自动生成。_version:文档的版本号,用于并发控制。_source:存储原始的 JSON 数据本身。
数据(Field)以键值对的形式存在,有不同的数据类型,被存储在"_source"字段的value中:
文本Text:简单的字符串类型
关键字Keyword:用于精确匹配、过滤、排序、聚合的字段,例如
"tags": ["elasticsearch", "database"]数字Numeric:简单的数字类型
日期Date:支持多种格式的日期字符串或时间戳,例如
"join_date": "2024/05/01"布尔Boolean:true或false
对象Object:JSON内嵌对象,也是以json形式组织
地理位置Geo:用于存储地理位置或形状,例如
"location": {"lat": 40.71, "lon": -74.01}
一个简单的文档如下:
{
"_index": "my-first-index",
"_id": "1",
"_version": 1,
"_source": { // _source 里存放的就是原始的 JSON 数据
"email": "john@example.com",
"first_name": "John",
"last_name": "Smith",
"info": {
"bio": "Eco-warrior",
"age": 25,
"interests": ["dolphins", "whales"]
},
"join_date": "2024/05/01"
}
}物理结构
ES的物理存储结构主要是三个点:分片、倒排索引、段
分片Shard:在物理上被划分为多个分片。每个分片本质上是一个完整的 Lucene 索引,它是 Elasticsearch 实现分布式和并行处理的基础。当你写入一个文档时,它会被路由到某个特定的主分片中存储。
倒排索引Inverted Index:这是 Elasticsearch 能够实现快速全文搜索的核心数据结构。它不是记录“文档包含哪些词”,而是记录“每个词出现在哪些文档中”
词典:所有不重复的词项列表
倒排列表:包含这些词项的文档ID列表。比如,为“Elasticsearch”这个词建立的倒排索引,会记录它存在于文档1、文档5、文档10中。这样,当你搜索“Elasticsearch”时,系统能立刻定位到这些文档,而不是逐个文档去扫描。
段Segment:分片内部的数据还会进一步被组织成多个“段”。段本身是一个小型的、不可变的倒排索引。新文档首先写入内存,然后定期刷写到磁盘上,形成一个段。后台还有一个合并过程,会将多个小段合并成一个大段,以清理数据并优化查询性能。
分词
因为ES是基于文本构建倒排索引,就必须知道这个文本中有哪些词,因此ES存储时必须进行分词
分词仅对TEXT类型操作,用于全文匹配搜索,而keyword类型不会分词,将整个字符串作为词项存入索引,用于精确匹配
正常情况下,ES会对存入的数据进行自动映射,但是实际业务场景,可能一个字段既需要分词匹配,又需要精确查询,这时,我们可以主动映射,创建索引,再上传数据就可以了,参考后面
ES的api
ES的查询方法
全文查询:match, match_phrase, match_phrase_prefix, multi_match, query_string, simple_query_string
精确查询:term, terms, range, exists, missing, wildcard, regexp, fuzzy, prefix, ids
复合查询:bool, boosting, constant_score, dis_max, function_score
地理查询:geo_distance, geo_bounding_box, geo_polygon, geo_shape
连接查询:nested, has_child, has_parent
特殊查询:more_like_this, script, percolate
match和term
在请求体中传入query参数,标识查询方法和查询内容,例如:
{
"query": {
"term": {
"title": "Elasticsearch Guide"
}
}
}即以term查询方法,查询title为Elasticsearch Guide的内容
term和match是最常用的两个查询方法
term:精确匹配
机制:不会对查询词进行分析,直接将输入当作一个整体去倒排索引中查找完全相等的词项。
适用字段:通常用于
keyword类型(或未经过分析的字段),例如枚举值、ID、状态标签等。特点:
区分大小写(必须完全一致)。
不进行分词,搜索词必须与索引中的词项完全匹配。
如果用于
text字段,通常无法匹配到预期结果(因为text字段在索引时已被分词,存储的是多个词项,而 term 查询的是整个短语)。
案例:文档有text类型{"title": "Elasticsearch Guide"},且 title 是 text 类型,索引时被分词为 ["elasticsearch", "guide"],则以
{ "query": { "term": { "title": "Elasticsearch Guide" } } }查询时,是查不到的
match:全文搜索
机制:先对查询词进行分析(如同索引时的分析器),将输入拆分为多个词项,然后查找包含任意(或全部)这些词项的文档,并计算相关性得分。
适用字段:主要用于
text类型,适合全文检索场景,如文章内容、商品描述等。特点:
不区分大小写(取决于分析器,通常转小写)。
支持分词,可以匹配包含部分关键词的文档。
默认行为是
OR(任一词项匹配即可),可通过operator改为AND。
案例:还是上面的text类型{"title": "Elasticsearch Guide"},因为这里会分词,记录成Elasticsearch和Guide两个,使用
{ "query": { "match": { "title": "Elasticsearch Guide" } } }查询时,会对查询内容页进行分词,分成Elasticsearch和guide,然后去匹配,发现or匹配是可以匹配上的,因此能查到结果。当然,使用AND匹配也能匹配上,即{"query":{"match":{"title":{"query":"elasticsearch guide","operator":"and"}}}}
全文查询
全文查询:match, match_phrase, match_phrase_prefix, multi_match, query_string, simple_query_string
对输入文本进行分词,然后去倒排索引中匹配,例如:
match_phrase:短语匹配,要求分词后的词项不仅出现,而且顺序一致,
{"match_phrase": {"title": "Elasticsearch guide"}}
精确值查询
精确查询:term, terms, range, exists, missing, wildcard, regexp, fuzzy, prefix, ids
这类查询不会对输入进行分析,直接查找倒排索引中的精确词项,适用于 keyword、数值、日期等字段,例如:
range:范围查询,支持数值、日期、ip等,
{"range": {"price": {"gte": 10, "lte": 100}}}exists:查询存在性,
{"exists": {"field": "tags"}}prefix:前缀匹配,
{"prefix": {"code": "abc"}}
复合查询
复合查询:bool, boosting, constant_score, dis_max, function_score
用于组合多个查询,控制逻辑关系。
地理查询
地理查询:geo_distance, geo_bounding_box, geo_polygon, geo_shape
针对地理坐标和形状的查询。
连接查询
连接查询:nested, has_child, has_parent
用于在父子文档或嵌套对象之间建立关联。
特殊查询
特殊查询:more_like_this, script, percolate
Python对接ES
查询
首先需要安装elasticsearch库
pip install elasticsearch一个基础查询案例,使用match_all方法无差别匹配:
from elasticsearch import Elasticsearch
# 1. 连接到 Elasticsearch(默认地址:http://localhost:9200)
es = Elasticsearch("http://localhost:9200")
# 检查连接是否成功
if not es.ping():
raise ValueError("连接失败,请确认 Elasticsearch 是否运行在 http://localhost:9200")
# 2. 定义索引名和查询
index_name = "my_index" # 替换为你的索引名
query_body = {
"query": {
"match_all": {} # 查询所有文档(可根据需要修改)
},
"size": 10 # 返回前10条结果
}
# 3. 执行查询
try:
response = es.search(index=index_name, body=query_body)
except Exception as e:
print(f"查询出错: {e}")
exit(1)
# 4. 处理结果
hits = response["hits"]["hits"] # 获取匹配的文档列表
total = response["hits"]["total"]["value"] # 总匹配数
print(f"总共找到 {total} 条文档,返回 {len(hits)} 条:")
for doc in hits:
print(f"ID: {doc['_id']}, 得分: {doc['_score']}, 内容: {doc['_source']}")主动映射索引
from elasticsearch import Elasticsearch
# 连接
es = Elasticsearch("http://localhost:9200")
if not es.ping():
raise ConnectionError("无法连接到 Elasticsearch")
index_name = "products"
# mappings 定义
mapping = {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"product_id": {"type": "keyword"},
"name": {
"type": "text",
# 多字段:name 用于搜索,name.keyword 用于排序/聚合
# ignore_above 256指的是超过该长度的字符串不索引为 keyword
"fields": {"keyword": {"type": "keyword", "ignore_above": 256}}
},
"price": {"type": "float"},
"created_at": {"type": "date", "format": "yyyy-MM-dd HH:mm:ss"},
"tags": {"type": "keyword"},
"description": {"type": "text", "analyzer": "standard"}
}
}
}
# 创建索引(如果已存在则忽略)
if not es.indices.exists(index=index_name):
response = es.indices.create(index=index_name, body=mapping)
print("索引创建成功")
else:
print("索引已存在,如需更新请先删除或使用别名迁移")
# 验证
print(es.indices.get_mapping(index=index_name))可以通过python字典和es.indices.create 方法进行索引的主动映射,name字段指定type为text,即创建了一个name索引,然后加了一个fields,指的是额外创建了一个name.keyword索引,用于排序、聚合
然后可以使用es.index方法指定对应的索引进行数据上传
from elasticsearch import Elasticsearch
es = Elasticsearch("http://localhost:9200")
doc = {
"product_id": "P1001",
"name": "Elasticsearch 实战",
"price": 59.9,
"created_at": "2025-03-15 10:30:00",
"tags": ["搜索引擎", "大数据"],
"description": "这是一本关于Elasticsearch的权威指南,涵盖从入门到精通。"
}
# 上传文档,自动生成 ID
resp = es.index(index="products", body=doc)
print(resp)
# 或者指定文档 ID
# resp = es.index(index="products", id="1", body=doc)也可以使用helpers.bulk批量上传
from elasticsearch import Elasticsearch, helpers
es = Elasticsearch("http://localhost:9200")
actions = [
{
"_index": "products",
"_id": i, # 可选,不指定则自动生成
"_source": {
"product_id": f"P{i:04d}",
"name": f"产品 {i}",
"price": 10.0 + i,
"created_at": "2025-03-15 12:00:00",
"tags": ["tag1", "tag2"],
"description": f"这是第 {i} 个产品的描述。"
}
}
for i in range(1, 101) # 生成100条数据
]
success, failed = helpers.bulk(es, actions, stats_only=True)
print(f"成功: {success}, 失败: {failed}")注意:
字段名必须一致:文档中的字段名必须与映射中定义的完全一致(大小写敏感),否则如果动态映射开启(默认),新字段会被自动添加映射,可能导致类型不符合预期。
动态映射行为:默认情况下(
"dynamic": true),如果上传的文档包含映射中未定义的字段,ES 会自动推断类型并添加映射。如果你希望严格禁止未定义字段,可以在创建索引时设置"dynamic": "strict",此时上传包含未知字段的文档会报错。日期格式:日期字段必须符合映射中定义的格式,否则会引发解析错误。
数组处理:任何字段都可以接受数组值,ES 会自动处理(数组中元素类型需一致)。
基于term和match匹配
基于上面创建的索引和存储的数据,使用match和term两种方法匹配
# 测试 text 字段分词搜索
resp = es.search(index="products", body={
"query": {"match": {"name": "实战"}}
})
print(resp["hits"]["total"]["value"]) # 应返回包含"实战"的文档
# 测试 keyword 字段精确匹配
resp = es.search(index="products", body={
"query": {"term": {"product_id": "P1001"}}
})
print(resp["hits"]["total"]["value"]) # 应返回1组合查询使用示例
query = {
"bool": {
"must": [
{"match": {"title": "python book"}}
],
"filter": [
{"range": {"price": {"lte": 50}}},
{"term": {"status": "published"}}
],
"should": [
{"match": {"description": "beginner"}}
],
"minimum_should_match": 1 # 至少满足一个 should 条件
}
}
response = es.search(index="products", body={"query": query})可以关注一下bool类型,即对不同的条件进行组合,must代表AND操作,should代表OR操作
复杂一些,一般不太用到
Kibana与ES的协作
在ELK架构中,Kibana承担了前端/用户界面的能力,他的职责是将KQL解析成ES请求体,然后传给ES获取结果
常见的KQL与ES查询DSL映射如下:
评论区