【ElasticSearch】json查询语法和可用的客户端

【ElasticSearch】json查询语法

【一】Elasticsearch查询语法详解

【1】基本查询结构

java 复制代码
GET /index_name/_search
{
  "query": {
    // 查询条件
  },
  "aggs": {
    // 聚合条件
  },
  "sort": [
    // 排序条件
  ],
  "from": 0,
  "size": 10,
  "highlight": {
    // 高亮设置
  }
}

【2】关键字

(1)一般查询(Query)

​匹配查询(Match Query)​​:用于全文搜索。

词条查询(Term Query)​​:用于精确值匹配。

​范围查询(Range Query)​​:用于范围过滤。

​布尔查询(Bool Query)​​:组合多个查询条件(must, should, must_not, filter)。

​多匹配查询(Multi Match Query)​​:在多个字段上执行匹配查询。

​前缀查询(Prefix Query)​​:匹配以指定前缀开头的词条。

​通配符查询(Wildcard Query)​​:使用通配符匹配。

​正则表达式查询(Regexp Query)​​:使用正则表达式匹配。

​模糊查询(Fuzzy Query)​​:匹配与指定词条相似的词条。

​嵌套查询(Nested Query)​​:查询嵌套对象。

(2)指标聚合(Metric Aggregations)​​

avg:平均值。

sum:求和。

min:最小值。

max:最大值。

count:计数。

stats:包含count, min, max, avg, sum。

extended_stats:扩展的统计信息,包括方差、标准差等。

cardinality:基数统计(类似distinct count)

(3)桶聚合(Bucket Aggregations)​​

terms:按词条分组。

range:按范围分组。

date_range:日期范围分组。

histogram:直方图(数值间隔分组)。

date_histogram:日期直方图。

nested:嵌套聚合。

reverse_nested:从嵌套聚合返回父级。

filter:按过滤条件分组。

filters:多个过滤条件分组。

missing:处理缺失字段。

(4)管道聚合(Pipeline Aggregations)​​

对其它聚合的结果进行再聚合。

avg_bucket:计算多个桶的平均值。

sum_bucket:计算多个桶的总和。

min_bucket:计算多个桶的最小值。

max_bucket:计算多个桶的最大值。

【3】常用查询类型

(1)匹配查询-match

基本匹配查询(查找所有华为手机)

java 复制代码
GET /product_info/_search
{
  "query": {
    "match": {
      "product_name": "华为手机"
    }
  }
}

(2)多字段搜索-multi_match(在名称和描述中搜索)

java 复制代码
GET /product_info/_search
{
  "query": {
    "multi_match": {
      "query": "防水 智能",
      "fields": ["product_name", "description"],
      "type": "best_fields"
    }
  }
}

(3)精确过滤-must(查找中国产电子产品)

java 复制代码
GET /product_info/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {"category": "电子产品"}}
      ],
      "filter": [
        {"term": {"origin": "中国"}}
      ]
    }
  }
}

(4)范围查询-range(价格在1000-5000之间的商品)

java 复制代码
GET /product_info/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 1000,
        "lte": 5000
      }
    }
  }
}

(5)组合查询(查找库存大于100的服装)

java 复制代码
GET /product_info/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {"category": "服装"}}
      ],
      "filter": [
        {"range": {"stock": {"gt": 100}}}
      ]
    }
  }
}

(6)分页排序(按价格降序排列)

java 复制代码
GET /product_info/_search
{
  "query": {"match_all": {}},
  "sort": [
    {"price": {"order": "desc"}}
  ],
  "from": 0,
  "size": 10
}

(7)高亮显示(高亮搜索结果)

java 复制代码
GET /product_info/_search
{
  "query": {
    "match": {
      "description": "防水"
    }
  },
  "highlight": {
    "fields": {
      "description": {}
    },
    "pre_tags": ["<strong>"],
    "post_tags": ["</strong>"]
  }
}

(8)布尔查询(Bool)

java 复制代码
{
  "query": {
    "bool": {
      "must": [
        { "match": { "field1": "value1" } }
      ],
      "should": [
        { "match": { "field2": "value2" } }
      ],
      "must_not": [
        { "term": { "field3": "value3" } }
      ],
      "filter": [
        { "range": { "price": { "gte": 100 } } }
      ]
    }
  }
}
java 复制代码
GET /product_info/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "product_name": "手机" } }
      ],
      "filter": [
        { "term": { "status": "active" } },
        { "range": { "price": { "gte": 1000, "lte": 5000 } } }
      ]
    }
  }
}

(9)地理位置搜索

java 复制代码
GET /stores/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "咖啡" } }
      ],
      "filter": {
        "geo_distance": {
          "distance": "2km",
          "location": {
            "lat": 31.2304,
            "lon": 121.4737
          }
        }
      }
    }
  },
  "sort": [
    {
      "_geo_distance": {
        "location": {
          "lat": 31.2304,
          "lon": 121.4737
        },
        "order": "asc",
        "unit": "km"
      }
    }
  ]
}

【4】聚合语法

(1)基本词项聚合-Terms(按商品分类分组)

java 复制代码
GET /product_info/_search
{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category",
        "size": 5
      }
    }
  }
}
java 复制代码
GET /product_info/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "product_name": "智能手机" } }
      ],
      "filter": [
        { "term": { "status": "active" } },
        { "range": { "price": { "gte": 1000, "lte": 5000 } } }
      ]
    }
  },
  "sort": [
    { "price": { "order": "asc" } }
  ],
  "from": 0,
  "size": 10,
  "highlight": {
    "fields": {
      "product_name": {},
      "description": {}
    }
  }
}

(2)指标聚合-Metrics(计算平均价格)

java 复制代码
GET /product_info/_search
{
  "size": 0,
  "aggs": {
    "avg_price": {
      "avg": {
        "field": "price"
      }
    }
  }
}
java 复制代码
GET /product_info/_search
{
  "query": {
    "multi_match": {
      "query": "防水 智能",
      "fields": ["product_name", "description"],
      "type": "best_fields"
    }
  },
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category",
        "size": 5
      },
      "aggs": {
        "avg_price": {
          "avg": { "field": "price" }
        }
      }
    },
    "price_stats": {
      "stats": { "field": "price" }
    }
  },
  "size": 0
}

(3)多级聚合-Metrics(按分类分组后计算平均价格)

java 复制代码
GET /product_info/_search
{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category",
        "size": 5
      },
      "aggs": {
        "avg_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}
java 复制代码
{
  "aggs": {
    "avg_price": {
      "avg": { "field": "price" }
    },
    "max_price": {
      "max": { "field": "price" }
    },
    "min_price": {
      "min": { "field": "price" }
    },
    "sum_price": {
      "sum": { "field": "price" }
    }
  }
}

(4)范围聚合-Range(按价格区间分组)

java 复制代码
GET /product_info/_search
{
  "size": 0,
  "aggs": {
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          {"to": 1000},
          {"from": 1000, "to": 5000},
          {"from": 5000}
        ]
      }
    }
  }
}
java 复制代码
GET /product_info/_search
{
  "query": {
    "range": {
      "stock": {
        "gt": 0
      }
    }
  },
  "aggs": {
    "by_category_origin": {
      "terms": {
        "field": "category",
        "size": 10
      },
      "aggs": {
        "by_origin": {
          "terms": {
            "field": "origin",
            "size": 5,
            "order": { "avg_price": "asc" }
          },
          "aggs": {
            "avg_price": {
              "avg": { "field": "price" }
            },
            "price_ranges": {
              "range": {
                "field": "price",
                "ranges": [
                  { "to": 1000 },
                  { "from": 1000, "to": 3000 },
                  { "from": 3000 }
                ]
              }
            }
          }
        }
      }
    },
    "global_price_stats": {
      "extended_stats": { "field": "price" }
    }
  },
  "size": 0
}

(5)复杂多级聚合(按产地分组后按分类分组)

java 复制代码
GET /product_info/_search
{
  "size": 0,
  "aggs": {
    "by_origin": {
      "terms": {
        "field": "origin",
        "size": 5
      },
      "aggs": {
        "by_category": {
          "terms": {
            "field": "category",
            "size": 3
          },
          "aggs": {
            "avg_price": {
              "avg": {
                "field": "price"
              }
            }
          }
        }
      }
    }
  }
}

(6)排序聚合-Order(按平均价格升序排列)

java 复制代码
GET /product_info/_search
{
  "size": 0,
  "aggs": {
    "by_origin": {
      "terms": {
        "field": "origin",
        "size": 5,
        "order": {
          "avg_price": "asc"
        }
      },
      "aggs": {
        "avg_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}

(7)嵌套聚合-Nested

java 复制代码
{
  "aggs": {
    "nested_agg": {
      "nested": {
        "path": "specifications"
      },
      "aggs": {
        "by_spec": {
          "terms": {
            "field": "specifications.name"
          }
        }
      }
    }
  }
}

(8)统计聚合-Stats

java 复制代码
{
  "aggs": {
    "price_stats": {
      "stats": { "field": "price" }
    }
  }
}

(9)扩展统计聚合-Extended Stats

java 复制代码
{
  "aggs": {
    "price_extended_stats": {
      "extended_stats": { "field": "price" }
    }
  }
}

【二】完整查询与聚合组合案例

【1】聚合案例

(1)案例1:带过滤条件的聚合分析

java 复制代码
GET /product_info/_search
{
  "query": {
    "bool": {
      "filter": [
        {"term": {"status": "active"}},
        {"range": {"stock": {"gt": 0}}}
      ]
    }
  },
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category",
        "size": 5
      },
      "aggs": {
        "by_origin": {
          "terms": {
            "field": "origin",
            "size": 3,
            "order": {
              "avg_price": "asc"
            }
          },
          "aggs": {
            "avg_price": {
              "avg": {
                "field": "price"
              }
            },
            "min_price": {
              "min": {
                "field": "price"
              }
            },
            "max_price": {
              "max": {
                "field": "price"
              }
            }
          }
        }
      }
    },
    "global_stats": {
      "stats": {
        "field": "price"
      }
    }
  }
}

(2)案例2:高亮搜索与聚合结合

java 复制代码
GET /product_info/_search
{
  "query": {
    "match": {
      "description": "智能"
    }
  },
  "highlight": {
    "fields": {
      "description": {}
    }
  },
  "aggs": {
    "price_stats": {
      "stats": {
        "field": "price"
      }
    }
  },
  "size": 5
}

(3)案例3:多条件过滤聚合(中国产电子产品价格分析)

java 复制代码
GET /product_info/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {"category": "电子产品"}}
      ],
      "filter": [
        {"term": {"origin": "中国"}},
        {"range": {"price": {"gte": 1000}}}
      ]
    }
  },
  "size": 0,
  "aggs": {
    "by_brand": {
      "terms": {
        "field": "product_name.keyword",
        "size": 5
      },
      "aggs": {
        "avg_price": {
          "avg": {
            "field": "price"
          }
        },
        "price_distribution": {
          "histogram": {
            "field": "price",
            "interval": 1000
          }
        }
      }
    }
  }
}

(4)案例4:日期范围聚合(按创建月份分组)

java 复制代码
GET /product_info/_search
{
  "size": 0,
  "aggs": {
    "by_month": {
      "date_histogram": {
        "field": "created_at",
        "calendar_interval": "month",
        "format": "yyyy-MM"
      },
      "aggs": {
        "total_sales": {
          "sum": {
            "field": "sales"
          }
        }
      }
    }
  }
}

(5)案例5:指标聚合(Metric Aggregations)​​

计算商品的平均价格、最高价格、最低价格:

java 复制代码
GET /product_info/_search
{
  "size": 0,
  "aggs": {
    "avg_price": { "avg": { "field": "price" } },
    "max_price": { "max": { "field": "price" } },
    "min_price": { "min": { "field": "price" } }
  }
}

(6)案例6:桶聚合(Bucket Aggregations)​​

按商品类别分组,并计算每组的平均价格:

java 复制代码
GET /product_info/_search
{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": { "field": "category" },
      "aggs": {
        "avg_price": { "avg": { "field": "price" } }
      }
    }
  }
}

(7)案例7:范围聚合(Range Aggregation)​​

按价格范围分组:

java 复制代码
GET /product_info/_search
{
  "size": 0,
  "aggs": {
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "to": 1000 },
          { "from": 1000, "to": 5000 },
          { "from": 5000 }
        ]
      },
      "aggs": {
        "avg_rating": { "avg": { "field": "rating" } }
      }
    }
  }
}

(8)案例8:嵌套聚合(Nested Aggregation)​​

对嵌套的规格属性进行聚合,统计内存大小的分布:

java 复制代码
GET /product_info/_search
{
  "size": 0,
  "aggs": {
    "specs": {
      "nested": { "path": "specifications" },
      "aggs": {
        "memory_sizes": {
          "filter": { "term": { "specifications.name": "内存" } },
          "aggs": {
            "sizes": { "terms": { "field": "specifications.value" } }
          }
        }
      }
    }
  }
}

(9)​案例9:管道聚合(Pipeline Aggregation)​​

按类别分组后,再计算每个类别的平均价格,然后按平均价格排序:

java 复制代码
GET /product_info/_search
{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category",
        "order": { "avg_price": "desc" }  // 按子聚合avg_price降序排序
      },
      "aggs": {
        "avg_price": { "avg": { "field": "price" } }
      }
    }
  }
}

(10)案例10:组合查询和聚合

查询价格在1000到5000之间的活跃商品,按类别分组,并计算每组的平均价格和商品数量,并按平均价格降序排序:

java 复制代码
GET /product_info/_search
{
  "size": 0,
  "query": {
    "bool": {
      "filter": [
        { "term": { "status": "active" } },
        { "range": { "price": { "gte": 1000, "lte": 5000 } } }
      ]
    }
  },
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category",
        "size": 10,
        "order": { "avg_price": "desc" }
      },
      "aggs": {
        "avg_price": { "avg": { "field": "price" } },
        "product_count": { "value_count": { "field": "id" } }
      }
    }
  }
}

【2】实践技巧

(1)性能优化

java 复制代码
GET /product_info/_search
{
  "query": {
    "bool": {
      "filter": [
        {"term": {"category": "电子产品"}}
      ]
    }
  },
  "size": 0,
  "aggs": {
    "by_origin": {
      "terms": {
        "field": "origin",
        "execution_hint": "map",  -- 使用map执行模式
        "size": 5
      }
    }
  }
}

(2)复合聚合(处理大数据集)

java 复制代码
GET /product_info/_search
{
  "size": 0,
  "aggs": {
    "by_category_origin": {
      "composite": {
        "sources": [
          {"category": {"terms": {"field": "category"}}},
          {"origin": {"terms": {"field": "origin"}}}
        ],
        "size": 100
      },
      "aggs": {
        "avg_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}

(3)使用搜索模板

java 复制代码
POST /_scripts/product_search_template
{
  "script": {
    "lang": "mustache",
    "source": {
      "query": {
        "bool": {
          "must": [
            {"match": {"category": "{{category}}"}}
          ],
          "filter": [
            {"term": {"origin": "{{origin}}"}}
          ]
        }
      },
      "aggs": {
        "price_stats": {
          "stats": {
            "field": "price"
          }
        }
      },
      "size": "{{size}}"
    }
  }
}

-- 调用模板
GET /product_info/_search/template
{
  "id": "product_search_template",
  "params": {
    "category": "电子产品",
    "origin": "中国",
    "size": 10
  }
}

(4)使用索引别名

java 复制代码
-- 创建别名
POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "product_info",
        "alias": "current_products",
        "filter": {
          "term": {"status": "active"}
        }
      }
    }
  ]
}

-- 使用别名查询
GET /current_products/_search
{
  "query": {
    "match_all": {}
  }
}

【3】常见问题

(1)查询性能慢

​优化方案​:添加路由和过滤条件

java 复制代码
GET /product_info/_search?routing=category
{
  "query": {
    "bool": {
      "filter": [
        {"term": {"category": "电子产品"}}
      ]
    }
  }
}

(2)聚合桶数量不足

​解决方案​:增加size参数

java 复制代码
GET /product_info/_search
{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category",
        "size": 20  -- 默认是10
      }
    }
  }
}

(3)处理空值聚合

​解决方案​:使用missing参数

java 复制代码
GET /product_info/_search
{
  "size": 0,
  "aggs": {
    "by_origin": {
      "terms": {
        "field": "origin",
        "missing": "未知产地"
      }
    }
  }
}

【三】springboot整合es的工具

【1】可选工具和客户端

(1)ElasticsearchRepository​

这是Spring Data Elasticsearch提供的一个接口,类似于Spring Data JPA的Repository。它提供了基本的CRUD操作和简单的查询方法(通过方法名或注解定义查询)。适用于简单的CRUD操作和查询,能够快速开发。

(2)​ElasticsearchRestTemplate​(或旧版的ElasticsearchTemplate):

(1)ElasticsearchTemplate是Spring Data Elasticsearch早期版本中的主要类,基于TransportClient(已弃用)。

(2)ElasticsearchRestTemplate是Spring Data Elasticsearch 3.2.x及以上版本推荐的类,基于High Level REST Client。它提供了更底层的操作,可以执行复杂的查询和聚合,适用于需要高度自定义查询的场景。

(3)High Level REST Client​

Elasticsearch官方提供的Java高级 REST 客户端,用于与Elasticsearch集群通信。它提供了所有Elasticsearch操作的方法,但使用起来相对繁琐,需要手动构建请求和解析响应。在Spring Data Elasticsearch中,通常不需要直接使用,因为ElasticsearchRestTemplate已经对其进行了封装。

(4)​Java API Client​

Elasticsearch 7.15及以上版本引入了新的Java API客户端,这是一个基于Jackson的强类型客户端,提供了更好的类型安全和性能。但是,Spring Data Elasticsearch目前(截至3.2.x)还没有完全整合这个新客户端。

【2】如何选择

(1)如果只需要基本的CRUD和简单查询,推荐使用ElasticsearchRepository,因为它使用简单,代码量少。

(2)如果需要执行复杂的查询、聚合、或者需要更灵活地控制查询过程,那么应该使用ElasticsearchRestTemplate。

(3)如果Spring Data Elasticsearch提供的功能无法满足需求(例如,使用一些非常新的Elasticsearch特性),可以考虑直接使用Elasticsearch的Java API Client,但这样会失去Spring Data的便利性。

【3】如何使用

(1)ElasticsearchRepository 使用

(1)创建Repository接口

java 复制代码
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
    // 自定义查询方法
    List<Product> findByName(String name);
    
    List<Product> findByPriceBetween(Double minPrice, Double maxPrice);
    
    Page<Product> findByCategory(String category, Pageable pageable);
    
    // 使用@Query注解自定义DSL
    @Query("{\"match\": {\"name\": \"?0\"}}")
    Page<Product> findByNameCustom(String name, Pageable pageable);
}

(2)使用示例

java 复制代码
@Service
@RequiredArgsConstructor
public class ProductService {
    private final ProductRepository productRepository;
    
    public Page<Product> searchProducts(String keyword, int page, int size) {
        return productRepository.findByNameCustom(
            keyword, 
            PageRequest.of(page, size, Sort.by("price").descending())
        );
    }
    
    public List<Product> findProductsByPriceRange(double min, double max) {
        return productRepository.findByPriceBetween(min, max);
    }
}

(2)ElasticsearchRestTemplate 使用

(1)配置类

java 复制代码
@Configuration
public class ElasticsearchConfig {
    
    @Bean
    public ElasticsearchRestTemplate elasticsearchRestTemplate(
            ElasticsearchRestClient elasticsearchRestClient) {
        return new ElasticsearchRestTemplate(elasticsearchRestClient);
    }
}

(2)复杂查询实现

java 复制代码
@Service
@RequiredArgsConstructor
public class ProductSearchService {
    private final ElasticsearchRestTemplate elasticsearchRestTemplate;
    
    public SearchPage<Product> complexSearch(String keyword, String category, 
                                           Double minPrice, Double maxPrice, 
                                           int page, int size) {
        // 构建布尔查询
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        
        if (StringUtils.hasText(keyword)) {
            boolQuery.must(QueryBuilders.matchQuery("name", keyword).boost(2.0f));
        }
        
        if (StringUtils.hasText(category)) {
            boolQuery.must(QueryBuilders.termQuery("category", category));
        }
        
        if (minPrice != null || maxPrice != null) {
            RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
            if (minPrice != null) rangeQuery.gte(minPrice);
            if (maxPrice != null) rangeQuery.lte(maxPrice);
            boolQuery.must(rangeQuery);
        }
        
        // 构建分页和排序
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(boolQuery)
                .withPageable(PageRequest.of(page, size))
                .withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC))
                .build();
        
        SearchHits<Product> searchHits = elasticsearchRestTemplate.search(searchQuery, Product.class);
        
        return SearchHitSupport.searchPageFor(searchHits, searchQuery.getPageable());
    }
    
    // 聚合查询示例
    public Map<String, Long> getCategoryStats() {
        TermsAggregationBuilder aggregation = AggregationBuilders
                .terms("category_agg")
                .field("category")
                .size(10);
        
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .addAggregation(aggregation)
                .build();
        
        SearchHits<Product> searchHits = elasticsearchRestTemplate.search(searchQuery, Product.class);
        
        Terms terms = searchHits.getAggregations().get("category_agg");
        return terms.getBuckets().stream()
                .collect(Collectors.toMap(
                    Terms.Bucket::getKeyAsString, 
                    Terms.Bucket::getDocCount
                ));
    }
}

(3)新的Java API Client使用(Elasticsearch 7.17+)

(1)添加依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-elasticsearch</artifactId>
    <version>5.1.0</version>
</dependency>
<dependency>
    <groupId>co.elastic.clients</groupId>
    <artifactId>elasticsearch-java</artifactId>
    <version>8.11.4</version>
</dependency>

(2)配置客户端

java 复制代码
@Configuration
public class ElasticsearchClientConfig {
    
    @Value("${spring.elasticsearch.uris}")
    private String[] elasticsearchUris;
    
    @Bean
    public ElasticsearchClient elasticsearchClient() {
        // 创建低级客户端
        RestClient restClient = RestClient
                .builder(HttpHost.create(elasticsearchUris[0]))
                .build();
        
        // 创建传输层
        ElasticsearchTransport transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper());
        
        // 创建API客户端
        return new ElasticsearchClient(transport);
    }
}

(3)使用Java API Client

java 复制代码
@Service
@RequiredArgsConstructor
public class ProductJavaClientService {
    private final ElasticsearchClient elasticsearchClient;
    
    public void createProduct(Product product) throws IOException {
        elasticsearchClient.index(i -> i
                .index("products")
                .id(product.getId())
                .document(product));
    }
    
    public List<Product> searchProducts(String keyword) throws IOException {
        SearchResponse<Product> response = elasticsearchClient.search(s -> s
                .index("products")
                .query(q -> q
                    .match(m -> m
                        .field("name")
                        .query(keyword)
                    )
                ),
            Product.class);
        
        return response.hits().hits().stream()
                .map(Hit::source)
                .collect(Collectors.toList());
    }
}