这两天参与公司的Elasticsearch服务接口升级,发现搜索团队的同事在使用Elasticsearch-Dsl库,发现特别好用,所以在这里记录一下相关用法。
背景
此前的做法中,我是直接根据ES的查询语法,使用PHP数组拼装出一个完整的查询请求。
在开发中,为了将一些共性的部分抽象出来,我封装过一些类似TermQuery的对象,传入参数就可以返回一个term过滤子句的PHP数组结构。
后来接触到Elasticsearch-Dsl库,才发现原来已经有人把ES几乎所有的语法全部封装成了对象,通过代码拼装这些语法对象的树形结构,最后由DSL库可以生成一个查询JSON。
其实像JAVA等强类型语言,本身的Elasticsearch官方客户端就是这种对象风格,只是PHP官方客户端并没有实现这一块而已。
下面将模拟一个场景,演示Elasticsearch-Dsl库是如何简化PHP ES开发工作的。
使用
相关代码在github上可以看到:https://github.com/owenliang/elasticsearch-dsl-usage。
测试数据
启动一个本地Elasticsearch,然后运行index.php来生成2条测试数据,它们分别代表2篇文章:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<?php require_once(__DIR__ . "/vendor/autoload.php"); // 1, 创建客户端 $client = \Elasticsearch\ClientBuilder::create()->build(); $request = [ 'index' => 'article', 'type' => 'doc', 'body' => [ 'article_title' => '只卖88元的高级西装', 'publish_time' => time(), 'article_type' => '西装', 'is_anonymous' => 1, ] ]; $client->index($request); $request = [ 'index' => 'article', 'type' => 'doc', 'body' => [ 'article_title' => '只卖2000元的辣鸡西装', 'publish_time' => time(), 'article_type' => '高级西装', 'is_anonymous' => 0, ] ]; $client->index($request); |
- article_title:标题
- publish_time:发布时间
- article_type:文章类型
- is_anonymous:是否为匿名用户
DSL查询
首先,创建一个ES官方客户端:
1 2 |
// 1, 创建客户端 $client = \Elasticsearch\ClientBuilder::create()->build(); |
然后,创建一个搜索请求:
1 2 |
// 2, 创建搜索体(body) $search = new \ONGR\ElasticsearchDSL\Search(); |
创建一个布尔查询:
1 2 |
// 3, 布尔子查询 $boolQuery = new \ONGR\ElasticsearchDSL\Query\Compound\BoolQuery(); |
在布尔查询里添加must、must_not、filter条件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 4, 增加bool -> must子句 $boolQuery->add( new \ONGR\ElasticsearchDSL\Query\FullText\MatchPhraseQuery("article_title", "西装"), \ONGR\ElasticsearchDSL\Query\Compound\BoolQuery::MUST ); // 5, 增加bool -> filter子句 $boolQuery->add( new \ONGR\ElasticsearchDSL\Query\TermLevel\RangeQuery("publish_time", [\ONGR\ElasticsearchDSL\Query\TermLevel\RangeQuery::LTE => time()]), \ONGR\ElasticsearchDSL\Query\Compound\BoolQuery::FILTER ); // 6, 增加bool -> must_not子句 $boolQuery->add( new \ONGR\ElasticsearchDSL\Query\TermLevel\TermsQuery('article_type', ['食品', '家居']), \ONGR\ElasticsearchDSL\Query\Compound\BoolQuery::MUST_NOT ); |
希望返回的文章,标题必须包含”西装”,发布时间必须小于当前时间,文章类型不允许是”食品”和”家居”。
现在把这个布尔查询添加到请求里:
1 2 |
// 7, 布尔查询添加到search $search->addQuery($boolQuery); |
为请求设置翻页:
1 2 3 |
// 8, 设置翻页 $search->setFrom(0); $search->setSize(10); |
按照发布时间倒排:
1 2 |
// 9, 增加一个排序规则 $search->addSort(new \ONGR\ElasticsearchDSL\Sort\FieldSort('publish_time', 'desc', ['missing' => 0])); |
接下来,我希望对上述查询结果顺便做一个聚合+统计。
聚合方法是使用bucket agg,它的类型是filters agg。
它将查询结果分成2个桶,一个桶内是所有匿名发布的文章,另外一个桶内是实名发布的文章,代码如下:
1 2 3 4 5 6 7 |
// 10, 创建一个bucket filter agg(生成2个桶,匿名发布的文章anony_articles和实名发布的文章no_anony_articles) $filterAgg = new \ONGR\ElasticsearchDSL\Aggregation\Bucketing\FiltersAggregation('anonymous_bucketing', [ 'anony_articles' => new \ONGR\ElasticsearchDSL\Query\TermLevel\TermQuery('is_anonymous', 1) ]); $filterAgg->setParameters([ 'other_bucket_key' => 'no_anony_articles'] ); |
这个agg的规则名(自己定义)叫做anonymous_bucketing,然后is_anonymous=1的文章被划入anony_articles桶。
额外的给这个agg设置一个参数叫做other_bucket_key,它的意思是没有被划入任何桶的文章放入no_anony_articles这个桶。
文章被分入2个桶后,现在希望对每个桶再做统计,希望每个桶内只保留前10篇文章,排序规则是按照发布时间倒排。
1 2 3 4 5 6 7 |
// 11, 对每个桶执行top hits metrics agg $metricsAgg = new ONGR\ElasticsearchDSL\Aggregation\Metric\TopHitsAggregation('latest_articles', 10, 0, new ONGR\ElasticsearchDSL\Sort\FieldSort('publish_time', 'desc')); // top his agg返回的文档只包含article_title字段 $metricsAgg->addParameter('_source', ['includes' => ['article_title']]); // 12, metrics agg添加到bucket agg下面 $filterAgg->addAggregation($metricsAgg); |
这里创建了一个metrics agg,它的类型是top hits agg。
它对桶内文章按照publish_time倒排,然后保留从偏移量0开始的10篇文章在桶内;额外的,希望桶内返回的10篇文档只包含article_title字段,因为对其他字段不感兴趣。
因为metrics agg是对桶内进行的统计操作,所以嵌入到bucekt agg内部,也就是先分桶后统计。
最后将search对象序列化一个查询体,提交到ES即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 14, 生成请求体body $body = $search->toArray(); // 15, 生成完整请求 $request = [ 'index' => 'article', 'type' => 'doc', 'body' => $body, ]; $response = $client->search($request); echo json_encode($request) . PHP_EOL; echo json_encode($response) . PHP_EOL; |
查询request的结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
{ "index": "article", "type": "doc", "body": { "query": { "bool": { "must": [{ "match_phrase": { "article_title": { "query": "西装" } } }], "filter": [{ "range": { "publish_time": { "lte": 1524023261 } } }], "must_not": [{ "terms": { "article_type": ["食品", "家居"] } }] } }, "sort": [{ "publish_time": { "missing": 0, "order": "desc" } }], "aggregations": { "anonymous_bucketing": { "filters": { "filters": { "anony_articles": { "term": { "is_anonymous": 1 } } }, "other_bucket_key": "no_anony_articles" }, "aggregations": { "latest_articles": { "top_hits": { "sort": { "publish_time": { "order": "desc" } }, "size": 10, "from": 0, "_source": { "includes": ["article_title"] } } } } } }, "from": 0, "size": 10 } } |
每一部分都可以很清晰的与代码对应,就不展开说明了。
应答response结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
{ "took": 17, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 3, "max_score": null, "hits": [{ "_index": "article", "_type": "doc", "_id": "AWLWsbE4HDPWzba-qL2o", "_score": null, "_source": { "article_title": "只卖88元的高级西装", "publish_time": 1524020392, "article_type": "西装", "is_anonymous": 1 }, "sort": [1524020392] }, { "_index": "article", "_type": "doc", "_id": "AWLWsbFVHDPWzba-qL2p", "_score": null, "_source": { "article_title": "只卖2000元的辣鸡西装", "publish_time": 1524020392, "article_type": "高级西装", "is_anonymous": 0 }, "sort": [1524020392] }, { "_index": "article", "_type": "doc", "_id": "AWLWr8icHDPWzba-qL2n", "_score": null, "_source": { "article_title": "只卖88元的高级西装", "publish_time": 1524020266, "article_type": "西装", "is_anonymous": 1 }, "sort": [1524020266] }] }, "aggregations": { "anonymous_bucketing": { "buckets": { "anony_articles": { "doc_count": 2, "latest_articles": { "hits": { "total": 2, "max_score": null, "hits": [{ "_index": "article", "_type": "doc", "_id": "AWLWsbE4HDPWzba-qL2o", "_score": null, "_source": { "article_title": "只卖88元的高级西装" }, "sort": [1524020392] }, { "_index": "article", "_type": "doc", "_id": "AWLWr8icHDPWzba-qL2n", "_score": null, "_source": { "article_title": "只卖88元的高级西装" }, "sort": [1524020266] }] } } }, "no_anony_articles": { "doc_count": 1, "latest_articles": { "hits": { "total": 1, "max_score": null, "hits": [{ "_index": "article", "_type": "doc", "_id": "AWLWsbFVHDPWzba-qL2p", "_score": null, "_source": { "article_title": "只卖2000元的辣鸡西装" }, "sort": [1524020392] }] } } } } } } } |
应答中hits对应请求中query部分的查询结果,aggregations对应请求中aggregations部分的聚合统计结果。
可以看出通过filters bucket agg聚合出了2个桶分别叫做anony_articles和no_anony_articles,每个桶内通过top hits metrics agg保留了最近发布的最多10篇文章(我数据库里一共就2条记录),并且只有article_title字段被保留。
总结
Elasticsearch-Dsl库非常好用,也非常利于提升对ES语法体系结构的理解。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

对的, 强类型语言, 比如java, 进行sql查询的时候, 参考 hibernate 这个ORM , 其中有一种用法就是:
所有的查询条件都是Criteria对象, 对象有 where , order by , 各种 and , or 对象/属性
666
对