Laravel 中直接使用原生 Elasticsearch 客户端比 Scout 更可控易调试,尤其在多条件组合、高亮、分面聚合等场景;bool 查询需严格区分 must(影响评分)、filter(不评分可缓存)和 should(需设 minimum_should_match);须白名单校验参数防注入,category_path 用 prefix 支持类目树模糊匹配,高亮需手动映射字段并判空处理。

直接说结论:Laravel 里用原生 elasticsearch/elasticsearch 客户端写搜索逻辑,比用 Scout + 第三方驱动更可控、更易调试,尤其在需要多条件组合、高亮、分面聚合(facets)或动态字段过滤时——Scout 抽象层会掩盖底层 query 结构,出问题反而更难定位。
怎么构造一个带 filter 和 must 的 bool 查询
商品搜索最常踩的坑是把 term 和 match 混用,或者把 filter 写进 must 里导致相关度打分异常。Elasticsearch 9.x 要求 bool 查询必须显式区分 must(影响评分)、filter(不评分、可缓存)和 should(可选匹配)。
-
filter里放上架状态、类目 ID、价格区间等确定性条件,比如:['term' => ['sale' => true]]或['range' => ['price' => ['gte' => 10, 'lte' => 500]]] -
must里放全文检索项,比如multi_match,字段加权重如'title^3';注意:如果只搜关键词且不需要评分,用query_string+default_field更简洁 - 别漏掉
minimum_should_match: 1(当用了should时),否则可能返回空结果
Laravel 中怎么安全传参避免注入风险
Elasticsearch 不像 SQL 有预编译,但用户输入直接拼进 query body 仍可能触发恶意 DSL 注入(比如通过 script 或 regexp 查询)。实际项目中必须做白名单校验。
- 关键词搜索前先 trim 并 strip_tags,再用
mb_substr($kw, 0, 100)截断长度,防止超长查询拖垮集群 - 排序字段必须限定在白名单数组里,比如
['price', 'created_at', 'sales_count'],禁止用户传_script或_doc - 分页参数
from/size做整型校验并限制上限(size不超过 100,from + size不超过 10000),避免 deep pagination 导致 OOM
为什么 category_path 用 prefix 而不用 term
电商类目通常是树形结构(一级→二级→三级),category_path 字段存的是类似 "1-2-5-" 这样的路径字符串。这时候用 prefix 是为了支持“查所有三级类目下的商品”,而 term 只能精确匹配完整路径。
- 如果类目层级是动态的(比如用户从一级类目跳转),
prefix能天然支持向上兼容;term必须提前知道完整 path -
prefix查询性能不如term,但配合keyword类型和合理分词器,延迟差异在毫秒级,可接受 - 别忘了在 mapping 里把
category_path设为"type": "keyword",否则prefix会失效
高亮返回时字段名和原始字段对不上怎么办
调用 highlight 时返回的 highlight 对象 key 默认是字段名,但前端渲染常需要映射回中文字段或别名(比如 title → “商品标题”)。ES 不提供字段别名高亮,得靠后端处理。
- 在 query body 里显式指定
highlight.fields,例如:['title' => new \stdClass(), 'desc' => new \stdClass()],避免自动推导出没权限的字段 - 返回结果时,把
highlight数组和_source合并成新结构,比如:['title' => $hit['_source']['title'], 'title_highlight' => $hit['highlight']['title'][0] ?? $hit['_source']['title']] - 注意:如果字段内容为空或 null,
highlight不会返回该 key,别直接取$hit['highlight']['title'][0],要先 isset
真正麻烦的不是写对一条 query,而是让不同筛选条件之间不互相干扰——比如价格区间 filter 和关键词 must 共存时,还要支持点击某个 tag 后保持其他条件不变。这种状态管理很容易在 Laravel 的 request 参数里丢字段,建议把搜索上下文抽成独立对象(如 GoodsSearchContext),而不是堆砌一堆 if-else。


















