在Amazon OpenSearch Serverless中实现带空值检查的嵌套标签过滤
我正在使用Amazon OpenSearch Serverless开展项目,需基于标签存储和搜索内容。过滤标签与内容标签均可为多个,部分文档可能不含tags字段。需求为:仅对比过滤标签与内容标签中group_id相同的条目,若group_id相同则id和src_code必须一致,以此筛选文档。
示例结构
内容文档的tags字段示例
"_source": { "tags": [ { "group_id": 5, "id": 1, "src_code": "2324" }, { "group_id": 6, "id": 2, "src_code": "91" }, { "group_id": 7, "id": 3, "src_code": "3124" }, { "group_id": 8, "id": 4, "src_code": "5543" } ] }
过滤标签示例
{ "filter_tags": [ {"id": 123, "group_id": 1, "src_code": "2"}, {"id": 124, "group_id": 2, "src_code": "23"}, {"id": 1, "group_id": 5, "src_code": "2324"}, {"id": 4, "group_id": 6, "src_code": "2325"}, ] }
目标:返回所有符合条件的文档,即过滤标签中与内容标签group_id相同的条目,其id和src_code均需一致;过滤标签中group_id不存在于内容的条目无需对比。以上示例中,仅需对比第3、4个过滤标签,因第4个标签id和src_code不匹配,故该文档不应被返回。
已尝试方案及问题
- 未检查tags字段存在性的查询执行报错,错误提示无法访问
doc['tags']。 - 添加tags字段存在性检查后,所有文档(含tags字段的)均返回0.8分,不符合预期。
1. 正确的空值检查
在Painless脚本中,针对嵌套类型的tags字段,正确的存在性和非空性检查需注意:
- 嵌套字段无法通过
doc对象直接以数组形式访问,需通过ctx._source获取原始数据; - 需覆盖三种情况:文档无tags字段、tags字段为null、tags字段为空数组。
正确的检查代码:
// 检查tags字段是否存在且非空 if (!ctx._source.containsKey('tags') || ctx._source.tags == null || ctx._source.tags.isEmpty()) { // 无tags的文档,根据需求处理:允许通过则返回1.0,否则返回0.0 return 1.0; }
2. 正确的过滤逻辑实现
核心思路:先将内容标签按group_id构建映射,快速匹配过滤标签中存在对应group_id的条目,只要有一个匹配项的id或src_code不一致,直接排除文档;所有需检查的过滤标签都匹配时,返回有效分数。
完整查询脚本:
POST /bedrock-knowledge-base-default-index/_search { "query": { "function_score": { "query": { "match_all": {} }, "functions": [ { "script_score": { "script": { "source": """ // 处理无tags的文档,可根据需求调整返回值 if (!ctx._source.containsKey('tags') || ctx._source.tags == null || ctx._source.tags.isEmpty()) { return 1.0; } // 将内容标签按group_id构建映射,便于快速查找 Map<Integer, Map<String, Object>> contentTagMap = new HashMap(); for (def contentTag : ctx._source.tags) { Integer groupId = contentTag.group_id; contentTagMap.put(groupId, contentTag); } // 遍历过滤标签,仅检查内容中存在对应group_id的条目 for (def filterTag : params.filter_tags) { Integer filterGroupId = filterTag.group_id; if (contentTagMap.containsKey(filterGroupId)) { Map<String, Object> matchedContentTag = contentTagMap.get(filterGroupId); // 对比id和src_code,不匹配则返回0排除文档 if (matchedContentTag.id != filterTag.id || !matchedContentTag.src_code.equals(filterTag.src_code)) { return 0.0; } } } // 所有需检查的标签都匹配,返回1.0 return 1.0; """, "params": { "filter_tags": [ {"id": 1, "group_id": 5, "src_code": "2324"}, {"id": 2, "group_id": 6, "src_code": "91"} ] } } } } ], "boost_mode": "replace", "min_score": 1.0 // 只返回分数≥1.0的符合条件的文档 } }, "_source": ["tags"] }
关键说明
- 不再使用
nested查询,直接操作整个文档的tags数组; - 通过
min_score过滤不符合条件的文档; - 无tags文档的处理逻辑可按需调整:若需排除,将返回值改为0.0即可。
3. 文档结构优化建议
若当前查询复杂度或性能不达标,可调整文档结构简化查询:
方案1:扁平化标签结构
将tags数组转换为以group_id为键、id_src_code为值的对象,示例:
"_source": { "tags_map": { "5": "1_2324", "6": "2_91", "7": "3_3124", "8": "4_5543" } }
查询时直接用term匹配对应group_id的键值对,无需脚本遍历,性能更优。例如检查group_id=5的匹配情况,只需查询tags_map.5: "1_2324"。
方案2:嵌套字段+原生查询
若需保留数组结构,确保tags字段设为nested类型,并为group_id、id、src_code单独建索引,通过布尔嵌套查询组合实现过滤:
POST /bedrock-knowledge-base-default-index/_search { "query": { "bool": { "must_not": [ // 排除存在group_id匹配但id或src_code不匹配的文档 { "nested": { "path": "tags", "query": { "bool": { "should": [ { "bool": { "must": [ {"term": {"tags.group_id": 5}}, {"bool": {"should": [{"term": {"tags.id": {"value": 1, "boost": 0}}}, {"term": {"tags.src_code": {"value": "2324", "boost": 0}}}]}} ] } }, { "bool": { "must": [ {"term": {"tags.group_id": 6}}, {"bool": {"should": [{"term": {"tags.id": {"value": 2, "boost": 0}}}, {"term": {"tags.src_code": {"value": "91", "boost": 0}}}]}} ] } } ] } } } } ], // 可选:包含无tags的文档 "should": [ {"bool": {"must_not": {"exists": {"field": "tags"}}}} ], "minimum_should_match": 0 } } }
该方式利用OpenSearch原生查询能力,避免脚本执行的性能开销,但需根据过滤标签动态生成查询条件。
内容的提问来源于stack exchange,提问作者Shanazar

