1.概述
在前一篇文章中Elasticsearch实现文档标签管理介绍了如何在elasticsearch中对文档的标签进行增删改查,包括标签的复杂查询。 但这里有个问题,如果所有标签都存储在同一个字段(比如tags字段)中,如何区分不同的标签呢? 本文将通过不同的方案实现有多个不同类型标签的复杂检索,从如下几个方面进行介绍:
- 需求描述
- 需求分析
- 方案设计
- 方案对比
- 方案选择
2.需求描述
需求描述:
- 一个标签有多个值,如果同时查询多个标签,多个标签之间是and的关系,如果一个标签选择多个值,那么一个标签的多个值是or的关系
- 将数据保存到elasticsearch并且支持标签和内容的复杂查询
需求描述举例:多个标签之间是逻辑与(AND),比如同时有标签A和标签B。而每个标签自身可能有多个值,这些值之间是逻辑或(OR)。例如,标签"颜色"可能有值"红色"或"蓝色",同时标签"尺寸"可能有值"大"或"中",希望查询同时满足颜色为红或蓝,且尺寸为大或中的文档。
标签和文档的表结构如下:
3.需求分析
使用下面的sql可以实现标签查询需求,根据多个 tag_id 和 tag_value 查询关联的 doc_id 列表的交集
sql
SELECT doc_id
FROM doc_tag
WHERE (tag_id, tag_value) IN (('color', '红色'), ('size', '中'))
GROUP BY doc_id
HAVING COUNT(DISTINCT tag_id) = 2;
如果需要支持更多的标签组合,只需在 IN 子句中添加更多的组合,并相应地调整 HAVING 子句中的计数值,比如有3个标签需要查询,使用的sql如下:
sql
SELECT doc_id
FROM doc_tag
WHERE (tag_id, tag_value) IN (('color', '红色'), ('size', '中'), ('category', '服饰'))
GROUP BY doc_id
HAVING COUNT(DISTINCT tag_id) = 3;
文档数据示例如下
json
{
"doc_id": 1,
"title": "衣服",
"tags": [
{
"tag_id": "color",
"tag_name": "颜色",
"tag_value": "红色",
"value_type": "string"
},
{
"tag_id": "size",
"tag_name": "尺寸",
"tag_value": "中",
"value_type": "string"
},
{
"tag_id": "category",
"tag_name": "类别",
"tag_value": "服饰",
"value_type": "string"
}
]
}
在elasticsearch中支持 nested type , 可以使用嵌套对象或者多字段来存储标签。例如,每个文档的标签是一个对象数组,每个对象有key和value字段。这样,可以通过 nested query
来处理每个标签的键值对。
这种情况下,可以使用Elasticsearch的nested类型来存储tags字段,然后进行嵌套查询。例如,查询颜色为红或蓝,且尺寸为大或中的文档,查询结构可能如下:
json
{
"query": {
"bool": {
"must": [
{
"nested": {
"path": "tags",
"query": {
"bool": {
"must": [
{
"term": {
"tags.key": "颜色"
}
},
{
"terms": {
"tags.value": [
"红",
"蓝"
]
}
}
]
}
}
}
},
{
"nested": {
"path": "tags",
"query": {
"bool": {
"must": [
{
"term": {
"tags.key": "尺寸"
}
},
{
"terms": {
"tags.value": [
"大",
"中"
]
}
}
]
}
}
}
}
]
}
}
}
这样,每个nested查询对应一个标签键的条件,其中该标签的值需要满足OR关系,而不同标签键之间是AND关系。这符合用户的需求:多个标签键之间AND,同一个标签键的多个值OR。
假设用户是将所有标签放在同一个字段中,比如tags字段,每个标签是一个独立的词条,例如"颜色:红"、"颜色:蓝"、"尺寸:大"等。此时,要查询同时满足多个标签条件,每个标签可能有多个值,就需要构造每个标签的OR条件,然后将这些OR条件用AND连接。
这种情况下,查询结构可能如下:
json
{
"query": {
"bool": {
"must": [
{
"bool": {
"should": [
{
"term": {
"tags": "颜色:红"
}
},
{
"term": {
"tags": "颜色:蓝"
}
}
]
}
},
{
"bool": {
"should": [
{
"term": {
"tags": "尺寸:大"
}
},
{
"term": {
"tags": "尺寸:中"
}
}
]
}
}
]
}
}
}
这确实满足了同一标签多个值OR,不同标签之间AND的需求。但这种方法依赖于标签命名的约定,可能不够灵活,但适用于简单的场景。
总结来说,解决方案有两种:
-
使用嵌套类型(nested)存储标签的键值对,这样可以针对每个键进行OR查询,不同键之间AND。
-
使用扁平化数据结构,但通过标签命名规则(如key:value),然后在查询时构造相应的条件。
方案设计
针对 Elasticsearch 中实现复杂标签查询的需求(多个标签之间使用 AND 逻辑,单个标签的多个值使用 OR 逻辑),以下是分步解决方案:
步骤 1:设计合理的数据结构
根据查询需求选择合适的数据存储方式,这里提供两种方案:
方案一:键值对嵌套结构
使用 nested
类型存储标签的键值对,便于精准控制标签逻辑关系:
python
# 修改映射创建方法
def _create_index(self):
if not self.es.indices.exists(index=self.index_name):
mapping = {
"mappings": {
"properties": {
"title": {"type": "text"},
"content": {"type": "text"},
"tags": {
"type": "nested", # 使用嵌套类型
"properties": {
"key": {"type": "keyword"},
"value": {"type": "keyword"}
}
},
"created_at": {"type": "date"}
}
}
}
self.es.indices.create(index=self.index_name, body=mapping)
方案二:扁平化字符串标签
使用 keyword 类型存储标签的键值对:
python
def _create_index(self):
"""创建索引并定义映射"""
if not self.es.indices.exists(index=self.index_name):
mapping = {
"mappings": {
"properties": {
"title": {"type": "text"},
"content": {"type": "text"},
"tags": {
"type": "keyword", # 使用keyword类型便于精确匹配和聚合
"null_value": [] # 处理空值情况
},
"created_at": {"type": "date"}
}
}
}
self.es.indices.create(index=self.index_name, body=mapping)
使用命名约定合并键值(如 key:value
):
python
# 文档示例
{
"title": "示例文档",
"tags": ["color:red", "color:blue", "size:large"]
}
步骤 2:实现复杂查询逻辑
根据数据结构选择对应的查询构建方式:
方案一:嵌套结构查询实现
python
def search_with_nested_tags(self, tags_conditions):
"""
tags_conditions 结构示例:
{
"color": ["red", "blue"],
"size": ["large", "medium"]
}
表示:color必须为red或blue,且size必须为large或medium
"""
query = {"bool": {"must": []}}
for key, values in tags_conditions.items():
nested_query = {
"nested": {
"path": "tags",
"query": {
"bool": {
"must": [
{"term": {"tags.key": key}},
{"terms": {"tags.value": values}}
]
}
}
}
}
query["bool"]["must"].append(nested_query)
return self.es.search(index=self.index_name, body={"query": query})
方案二:扁平化标签查询实现
python
def search_with_flat_tags(self, tags_conditions):
"""
tags_conditions 结构示例:
{
"color": ["red", "blue"],
"size": ["large", "medium"]
}
转换为实际查询标签:["color:red", "color:blue"] 和 ["size:large", "size:medium"]
"""
query = {"bool": {"must": []}}
for key, values in tags_conditions.items():
# 生成类似 ["color:red", "color:blue"]
combined_tags = [f"{key}:{value}" for value in values]
tag_query = {"terms": {"tags": combined_tags}}
query["bool"]["must"].append(tag_query)
return self.es.search(index=self.index_name, body={"query": query})
步骤 3:完整类实现示例
基于嵌套结构
python
from elasticsearch import Elasticsearch
from elasticsearch.helpers import bulk
class TagSearchEngine:
def __init__(self, host='localhost', port=9200):
self.es = Elasticsearch([{'host': host, 'port': port}])
self.index_name = "tagged_documents"
self._create_index()
def _create_index(self):
"""创建支持嵌套标签的索引"""
if not self.es.indices.exists(index=self.index_name):
mapping = {
"mappings": {
"properties": {
"title": {"type": "text"},
"content": {"type": "text"},
"tags": {
"type": "nested",
"properties": {
"key": {"type": "keyword"},
"value": {"type": "keyword"}
}
},
"created_at": {"type": "date"}
}
}
}
self.es.indices.create(index=self.index_name, body=mapping)
def add_document(self, title, content, tags):
"""添加新文档"""
doc = {
"title": title,
"content": content,
"tags": [{"key": k, "value": v} for k, v in tags.items()],
"created_at": "now"
}
return self.es.index(index=self.index_name, body=doc)
def complex_tag_search(self, tag_conditions, size=10):
"""
执行复杂标签查询
:param tag_conditions: 字典格式 {tag_key: [values], ...}
:return: 匹配的文档结果
"""
query = {"bool": {"must": []}}
for key, values in tag_conditions.items():
nested_query = {
"nested": {
"path": "tags",
"query": {
"bool": {
"must": [
{"term": {"tags.key": key}},
{"terms": {"tags.value": values}}
]
}
}
}
}
query["bool"]["must"].append(nested_query)
return self.es.search(
index=self.index_name,
body={"query": query, "size": size}
)
# 使用示例
if __name__ == "__main__":
engine = TagSearchEngine()
# 添加测试数据
engine.add_document(
title="夏季服装指南",
content="夏季服装选择技巧...",
tags={"color": ["red", "blue"], "size": ["large"]}
)
# 执行复杂查询:查找color=red或blue,且size=large或medium的文档
results = engine.complex_tag_search(
tag_conditions={
"color": ["red", "blue"],
"size": ["large", "medium"]
}
)
print("查询结果:", results["hits"]["hits"])
基于扁平化结构完整代码实现
python
from datetime import datetime
from elasticsearch import Elasticsearch
class TagSearchEngine:
def __init__(self, host='localhost', port=9200):
self.es = Elasticsearch(
host=[{'host': host, 'port': port}],
http_auth=('username', 'password'))
self.index_name = "search_text"
self._create_index()
def _create_index(self):
"""创建索引并定义映射"""
if not self.es.indices.exists(index=self.index_name):
body = {
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings": {
"_doc": {
"properties": {
"doc_id": {
"type": "keyword"
},
"title": {
"type": "text"
},
"content": {
"type": "text"
},
"tags": {
"type": "keyword", # 使用keyword类型便于精确匹配和聚合
"null_value": [] # 处理空值情况
},
"gmt_create": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
}
}
}
}
}
self.es.indices.create(index=self.index_name, body=body)
def add_document(self, doc_id, title, content, tags: dict[str, list]):
"""
添加新文档
tags 结构示例:
{
"color": ["red", "blue"],
"size": ["large", "medium"]
}
转换为实际查询标签:["color:red", "color:blue"] 和 ["size:large", "size:medium"]
"""
doc = {
"doc_id": doc_id,
"title": title,
"content": content,
"tags": [f'{k}:{t}' for k, v in tags.items() for t in v],
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
return self.es.index(
index=self.index_name,
doc_type="_doc",
body=doc,
id=doc_id
)
def complex_tag_search(self, tag_conditions: dict[str, list]):
"""
tag_conditions 结构示例:
{
"color": ["red", "blue"],
"size": ["large", "medium"]
}
转换为实际查询标签:["color:red", "color:blue"] 和 ["size:large", "size:medium"]
"""
query = {"bool": {"must": []}}
for key, values in tag_conditions.items():
# 生成类似 ["color:red", "color:blue"]
# 生成标签键值组合列表
combined_tags = [f"{key}:{value}" for value in values]
# 每个标签的OR条件
tag_query = {"terms": {"tags": combined_tags}}
# 多个标签的AND条件
query["bool"]["must"].append(tag_query)
return self.es.search(index=self.index_name, body={"query": query})
# 使用示例
if __name__ == "__main__":
engine = TagSearchEngine()
# 添加测试数据
engine.add_document(
doc_id="1",
title="夏季服装指南",
content="夏季服装选择技巧...",
tags={"color": ["red", "blue"], "size": ["large"]}
)
# 执行复杂查询:查找color=red或blue,且size=large或medium的文档
results = engine.complex_tag_search(
tag_conditions={
"color": ["red", "blue"],
"size": ["large", "medium"]
}
)
print("查询结果:", results["hits"]["hits"])
方案对比
以下是两种标签查询方案的详细对比分析,帮助开发者根据实际场景选择最适合的实现方式:
方案一:嵌套结构查询实现(Nested Documents)
📌 数据结构示例
json
{
"title": "夏季服装",
"tags": [
{"key": "color", "value": "red"},
{"key": "color", "value": "blue"},
{"key": "size", "value": "large"}
]
}
✅ 优点
-
精准的键值关联
- 严格保证每个值的归属关系(如
color:red
不会与size:red
混淆) - 支持同一标签键的多值存储(如同时存储
color:red
和color:blue
)
- 严格保证每个值的归属关系(如
-
灵活的查询能力
-
原生支持键值组合查询:
sqlWHERE tags.key='color' AND tags.value IN ('red','blue')
-
可扩展范围查询、权重控制等高级功能
-
-
数据结构清晰
- 适合需要严格分类的标签系统(如商品属性、用户画像)
- 便于动态添加新标签类型
-
避免命名冲突
- 天然解决标签命名歧义问题(如
color:dark
和size:dark
明确区分)
- 天然解决标签命名歧义问题(如
⚠️ 缺点
-
查询性能开销
- 嵌套查询需要访问子文档,比普通查询慢约 5-10 倍(ES官方数据)
- 每次嵌套查询相当于执行 mini-join 操作
-
存储成本增加
- 嵌套文档存储为独立Lucene文档,索引体积增加约 30-50%
- 影响写入吞吐量(需维护父子文档关系)
-
开发复杂度高
-
需要处理嵌套查询的DSL语法
-
更新操作需使用特定API:
pythonself.es.update( index=index, id=doc_id, body={"script": "ctx._source.tags.add(params.new_tag)"}, params={"new_tag": {"key": "material", "value": "cotton"}} )
-
🎯 适用场景
- 需要严格区分标签键值的系统(如电商商品属性)
- 标签需要附加元信息的场景(如带权重的标签)
- 要求精确匹配的业务场景(如医疗数据标注)
方案二:扁平化标签查询实现(Flattened Tags)
📌 数据结构示例
json
{
"title": "夏季服装",
"tags": ["color:red", "color:blue", "size:large"]
}
✅ 优点
-
极简查询性能
- 使用
terms
查询速度比嵌套查询快 5 倍以上 - 适合高并发查询场景(每秒数千次请求)
- 使用
-
存储效率优化
- 相同数据量下索引体积减少约 40%
- 写入速度提升约 30%(无嵌套文档维护开销)
-
开发简单直接
-
使用标准DSL语法即可完成查询:
python{"terms": {"tags": ["color:red", "color:blue"]}}
-
无需处理嵌套文档的更新逻辑
-
-
兼容现有系统
- 可平滑迁移传统标签系统的数据格式
- 方便与第三方系统对接(如导入/导出CSV)
⚠️ 缺点
-
键值管理风险
- 需严格约定命名规则(如
key:value
分隔符) - 错误示例:
"tags": ["red:color"]
(键值颠倒导致查询失效)
- 需严格约定命名规则(如
-
扩展能力受限
- 无法直接实现值范围查询(如
price>100
) - 添加标签元信息需要重构数据结构
- 无法直接实现值范围查询(如
-
多值查询冗余
-
查询
color=red|blue
需要构造多个标签:python["color:red", "color:blue"]
-
🎯 适用场景
- 标签系统结构简单的应用(如博客分类)
- 需要极致查询性能的场景
- 已有扁平化标签数据的迁移项目
方案选择
决策树:如何选择方案?
参考文档