2020-03-13|2020-03-13 使用curl 查询 elasticsearch

1. curl查询的语法 比如,要对 qbank 索引中文档的 queston 属性按“题目含有关于圆周率的数学公式”进行模糊查询:

curl -H "Content-Type: application/json" -XPOST http://localhost:9200/qbank/_search -d '{ "query": { "match": { "question": "题目含有关于圆周率的数学公式" } } }'

其效果,类似在浏览器中使用:
http://172.17.0.1:9200/question/_search?q=question=题目含有关于圆周率的数学公式

来查,但前者可以使用完整的查询语法,实现很多复杂的条件组合;后者只能支持其中的 match 查询。
另外,之前曾经用:
curl -XGET http://localhost:9200/qbank/_search -d '{ "query": { "match": { "question": "题目含有关于圆周率的数学公式" } } }'

来查,结果报错:
{"error":"Content-Type header [application/x-www-form-urlencoded] is not supported","status":406}

网上很多帖子都说这种查询是可以的,实际操作结果是不行,或者我的 7.6.1 版本不再支持这种写法了。
2. ES支持的几种常用查询模式的例子 本章节以 sql 为标的,列出要达成相似查询效果所需的 dsl 查询语句和 以spring-data-elasticsearch 的 ElasticSearchRestTemplate来查询的Java代码。例子全部在 elasticsearch@7.6.1 环境实际测试通过。
假设我的数据模型对应的表名为 t_question,其ElasticResearch 模型如下:
/** * 题目对象 * @author xxx * 2020年3月11日 下午9:56:02 */ @Document(indexName = "question") public class Question implements Serializable {@Id @Field(type= FieldType.Keyword) private String id; /** * 题号 */ @Field(type=FieldType.Keyword) private String questionNo; /** * 题目类型 */ @Field(type=FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") private String category; /** * 题目 */ @Field(type=FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") private String question; /** * 从题目中提出取来的公式列表 */ @Field(type = FieldType.Keyword) private List formulas; }

系统自动产生的mapping 映射如下:
{ "mapping": { "question": { "properties": { "category": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "formulas": { "type": "keyword" }, "id": { "type": "keyword" }, "question": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "questionNo": { "type": "keyword" } } } } }

2.1 select * from t_question; 显然sql select * from t_question 是要列出所有的题目。
温馨提示:实际操作中千万不要执行类似代码。在一个大数据库中,这个操作可以直接让服务器OOM。
DSL查询语句如下:
{ "query": { "match_all": {} } }

Java查询代码如下:
@Test void testList() { QueryBuilder qb = QueryBuilders.matchAllQuery(); SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(qb).build(); List results = this.esRestTemplate.queryForList(searchQuery, Question.class); assertNotNull(results); assertFalse(results.isEmpty()); }

2.2. 按 keyword 精确查询 在 v7.6.1 中,keyword 就是以前的 no_analysed 的text类型字段。keyword 字段在文档插入时,以文档的对应字段的整个词条内容为一个完整的字符串进行索引,不会先执行分词过程。
在 v7.6.1 版本中,一个text类型的字段,会自动创建 keyword 的映射。也就是它会同时拥有 text 类型的映射 和 keyword 类型的映射。只是如果要引用其 keyword 类型的值,必须使用 字段名.keyword = VALUE 的语法。
这一点,可以通过查看对应索引的mapping数据来验证。
2.2.1. 单个条件值的查询
SQL查询语句如下:
select t.* from t_question t where t.questionNo = '2';

DSL查询语句如下:
{ "query": { "term": { "questionNo.keyword": "2" } } }

term 条件,允许指定一个 keyword 类型的值。
Java查询代码如下:
@Test void testList() { QueryBuilder qb = QueryBuilders.termQuery("questionNo.keyword", "2"); SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(qb).build(); List results = this.esRestTemplate.queryForList(searchQuery, Question.class); assertNotNull(results); assertEquals(1, results.size()); }

2.2.2. 单个条件多个值的查询
SQL查询语句如下:
select t.* from t_question t where t.questionNo in ("2", "3", "4")

或者:
select t.* from t_question t where t.questionNo = "2" or t.questionNo = "3" or t.questionNo = "4";

DSL查询语句如下:
{ "query": { "terms": { "questionNo.keyword": ["2", "3", "4"] } } }

terms 条件,允许列出多个 keyword 类型的值,只要有一个值匹配,则视为条件匹配。相当于sql 的 in
或者:
{ "query": { "bool": { "should": [ { "term": { "questionNo.keyword": "2" } }, { "term": { "questionNo.keyword": "3" } }, { "term": { "questionNo.keyword": "4" } } ] } } }

should 条件,相当于sql的 or。只要有一个条件满足,就视为满足条件。
Java查询代码:
@Test void testList() { QueryBuilder qb = QueryBuilders.termsQuery("questionNo.keyword", Arrays.asList("2", "3", "4")); SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(qb).build(); List results = this.esRestTemplate.queryForList(searchQuery, Question.class); assertNotNull(results); assertEquals(3, results.size()); }


@Test void testList() { BoolQueryBuilder qb = QueryBuilders .boolQuery() .should(QueryBuilders.termQuery("questionNo.keyword", "2")) .should(QueryBuilders.termQuery("questionNo.keyword", "3")) .should(QueryBuilders.termQuery("questionNo.keyword", "4")); SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(qb).build(); List results = this.esRestTemplate.queryForList(searchQuery, Question.class); assertNotNull(results); assertEquals(3, results.size()); }

2.3 单个字段的全文本查询 ElasticSearch 的单个字段的全文本查询,其效果大致可以用 SQL 的 like ‘%条件值% 来类比。但你要是真的在 SQL 中执行这样的模糊查询,又碰巧了这个表数据量很大的话,基本上你会被老板炒掉了;因为关系数据库中执行这样的查询,会导致性能极度低下的全表扫描查询;系统轻则卡死,重则宕机。这也是当初 lucene 全文本框架被设计出来的原因。
ElasticSearch 对全文本字段的查询,包含两个过程:
  • 保存文档时:ES 先分析对应字段的值,用分词器将字段文本拆分为一系列的短语,然后再根据短语创建从短语到文档的倒排索引。
  • 查询文档时:分 match_phrase 查询和 match 查询。
    • match_phrase:将查询条件视为一个完整的字符串,并不用分词器进行拆分,直接去匹配索引,搜索结果。
    • match:先用分词器将查询条件拆分为多个短语,再用这些短语去匹配索引,结果加入计分,按计分高低排序返回结果。计分越高表明越匹配。
2.3.1. 查询题目包含文本“方程”的题目
SQL脚本如下:
select t.* from t_question t where t.question like '%方程%';

DSL脚本为:
{ "query": { "match_phrase": { "question": "题目" } } }

Java代码是:
@Test void testMatchPhraseWithDefaultAnalynizer() { QueryBuilder qb = QueryBuilders.matchPhraseQuery("question", "题目"); SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(qb).build(); List results = this.esRestTemplate.queryForList(searchQuery, Question.class); assertNotNull(results); assertEquals(3, results.size()); }

检索时,不分析条件文本,直接作为一个短语去索引中检索。使用kibana分析DSL的结果如下:

2020-03-13|2020-03-13 使用curl 查询 elasticsearch
文章图片
不分析条件文本的检索分析
可以看到,其检索消耗大约时0.6ms。如果是SQL检索,如果有50万行数据的表,可能会慢的让你怀疑人生。这就是lucene在全文本搜索方面的强大能力体现。
搜索出来的结果如下:
"hits" : { "total" : { "value" : 1, "relation" : "eq" }, "max_score" : 1.2688576, "hits" : [ { "_index" : "question", "_type" : "question", "_id" : "7e840a47-a04c-402b-9413-77dd3f1a9cf5", "_score" : 1.2688576, "_source" : { "id" : "7e840a47-a04c-402b-9413-77dd3f1a9cf5", "questionNo" : "13", "category" : "测试题目", "question" : """这道题目是为了测试有两个公式的题目\(x^4 - 16 = 0\)和\(x^3 = -1\) 是否能正常检索""", "formulas" : [ "x^4 - 16 = 0", "x^3 = -1" ] } } ] }

2.3.2. 按匹配度查询和文本“求解二项方程式”类似的题目
这个条件,SQL没有办法做到,这就是lucene最擅长处理的场景之一。如果实在要SQL进行模拟,比如搜索和那么需要按如下步骤进行再额外需要大约500行纯粹的Java代码,而且很可能效果还不好:
  1. 先将“求解二项方程式”拆分为“求”、“解”、“二项”、“方程式”
  2. 执行如下SQL:
select t.* from t_question t where t.question like '%求%' union select t.* from t_question t where t.question like '%解%' union select t.* from t_question t where t.question like '%二项%' union select t.* from t_question t where t.question like '%方程式%'

  1. 想办法处理2的查询结果,从中挑选比较匹配的。
而 lucene 处理起来,非常的得心应手:
DSL脚本如下:
{ "query": { "match": { "question": "求解二项方程式" } } }

Java代码如下:
@Test void testMatchPhraseWithDefaultAnalynizer() { QueryBuilder qb = QueryBuilders.matchQuery("question", "求解二项方程式"); SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(qb).build(); List results = this.esRestTemplate.queryForList(searchQuery, Question.class); assertNotNull(results); assertEquals(3, results.size()); }

需要注意的是:这个DSL的效果,跟采用的分词器有关。因为 question 这个列,是 text 类型的,在添加document时,会先用分词器分析得到短语列表,再建立短语和document的索引。搜索时,match 条件,会将搜索条件先用分词器处理,再去匹配。
分词器的使用场景,分为添加文档时维护索引的分析器,和检索结果时分析查询文本时使用的分析器。两个分析器使用场景不同,需要分别设置;如果不设置,则会使用默认分析器,就是将每个汉字拆分为一个独立的短语。关于如何指定两种分析器可以看这里。
  • 使用默认分词器,那么会将“求解二项方程式”中的每个字都处理为一个短语,也就是“求”、“解”、“二”、“项”、“方”、“程”和“式”一共7个短语。因此,搜索的结果,会是分别用上述7个短语在索引中检索后再综合评分的结果。使用kibana对上面的DSL进行分析的结果如下图:

    2020-03-13|2020-03-13 使用curl 查询 elasticsearch
    文章图片
    默认分词器的检索分析
  • 使用中文分词器,那么会认得“求解二项方程式”是由如下三个有意义的短语构成:“求解”、“二项”、“方程式”,而不会拆成没有含义的7个汉字。
    DSL分析如下:

    2020-03-13|2020-03-13 使用curl 查询 elasticsearch
    文章图片
    使用IK分析器作为查询条件分析器的DSL分析
得到的检索结果如下:
"hits" : { "total" : { "value" : 1, "relation" : "eq" }, "max_score" : 1.156497, "hits" : [ { "_index" : "question", "_type" : "question", "_id" : "ba43b4df-2494-43a6-a85c-5413eb65d1b1", "_score" : 1.156497, "_source" : { "id" : "ba43b4df-2494-43a6-a85c-5413eb65d1b1", "questionNo" : "10", "category" : "测试题目", "question" : """求解方程式:\(\left\{\begin{array}{**lr**}x=\dfrac{3\pi}{2}(1+2t)\cos(\dfrac{3\pi}{2}(1+2t))&\\y=s&\\z=\dfrac{3\pi}{2}(1+2t)\sin(\dfrac{3\pi}{2}(1+2t))\end{array}\right.\)""", "formulas" : [ "\\left\\{\\begin{array}{**lr**}x=\\dfrac{3\\pi}{2}(1+2t)\\cos(\\dfrac{3\\pi}{2}(1+2t))&\\\\y=s&\\\\z=\\dfrac{3\\pi}{2}(1+2t)\\sin(\\dfrac{3\\pi}{2}(1+2t))\\end{array}\\right." ] } } ] }

2.3. 多个条件的组合查询 在SQL中,可以通过 and or 的组合,提供多个条件的查询。DSL中也可以,但它是通过一个叫做 bool 的操作进行的。
2.3.1 查询含有公式x^3 = -1且含有文本方程的题目
SQL脚本如下:
select t.* from t_question t where t.formulas = 'x^3 = -1' and t.question like '%方程%'

实际上SQL是没有办法通过一个表来实现要求的查询的。因为同一个题目的公式,可能有两个:\(x^3 = -1\) 和 \(x^4 = 1\)。而表的一个字段,职能放一个公式。如果要放两个公式,要么将多个公式通过字符串存储起来,要么另外创建一个表存放公式,然后通过外键关联。但为了简化问题,我们先假设题目只有一个公式。
DSL脚本如下:
{ "query": { "bool": { "must": [{ "match": { "question": "方程" } }, { "term": { "formulas": "x^3 = -1" } }] } } }

Java代码如下:
@Test void testMustWith2Conditons() { BoolQueryBuilder bqb = QueryBuilders.boolQuery() .must(QueryBuilders.matchQuery("question", "方程")) .must(QueryBuilders.termQuery("formulas", "x^3 = -1")); SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(bqb).build(); List results = this.esRestTemplate.queryForList(searchQuery, Question.class); assertNotNull(results); assertEquals(1, results.size()); }

这个DSL的分析结果如下:

2020-03-13|2020-03-13 使用curl 查询 elasticsearch
文章图片
使用IK的ik_max_word作为查询文本分析器的分析结果
可以看到,由于在索引映射中指定采用了 ik_max_word 作为查询字符串的分析器,它分析出“方程”整体是一个词条,没有进一步拆分。
查询结果如下:
"hits" : { "total" : { "value" : 1, "relation" : "eq" }, "max_score" : 1.1848769, "hits" : [ { "_index" : "question", "_type" : "question", "_id" : "79edc184-c5a5-4cd3-8c53-a0cfce258c53", "_score" : 1.1848769, "_source" : { "id" : "79edc184-c5a5-4cd3-8c53-a0cfce258c53", "questionNo" : "3", "category" : "测试题目", "question" : """方程\(x^3 = -1\) 的根是""", "formulas" : [ "x^3 = -1" ] } } ] }

2.3.2 查询含有公式x^3 = -1或含有文本方程的题目
SQL脚本如下:
select t.* from t_question t where t.formulas = 'x^3 = -1' or t.question like '%方程%'

DSL脚本如下:
{ "query": { "bool": { "should": [{ "match": { "question": "方程" } }, { "term": { "formulas": "x^3 = -1" } }] } } }

Java代码如下:
@Test void testShouldWith2Conditons() { BoolQueryBuilder bqb = QueryBuilders.boolQuery() .should(QueryBuilders.matchQuery("question", "方程")) .should(QueryBuilders.termQuery("formulas", "x^3 = -1")); SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(bqb).build(); List results = this.esRestTemplate.queryForList(searchQuery, Question.class); assertNotNull(results); assertEquals(1, results.size()); }

这个DSL的分析结果如下:

2020-03-13|2020-03-13 使用curl 查询 elasticsearch
文章图片
使用IK的ik_max_word作为查询文本分析器的分析结果
对比一下 2.3.1. 的and关系,会发现 and 关系上有个 + 号,or 关系上是没有这个 + 号。
查询结果如下:
"hits" : { "total" : { "value" : 13, "relation" : "eq" }, "max_score" : 1.1848769, "hits" : [ { "_index" : "question", "_type" : "question", "_id" : "79edc184-c5a5-4cd3-8c53-a0cfce258c53", "_score" : 1.1848769, "_source" : { "id" : "79edc184-c5a5-4cd3-8c53-a0cfce258c53", "questionNo" : "3", "category" : "测试题目", "question" : """方程\(x^3 = -1\) 的根是""", "formulas" : [ "x^3 = -1" ] } }, { "_index" : "question", "_type" : "question", "_id" : "7e840a47-a04c-402b-9413-77dd3f1a9cf5", "_score" : 0.7549127, "_source" : { "id" : "7e840a47-a04c-402b-9413-77dd3f1a9cf5", "questionNo" : "13", "category" : "测试题目", "question" : """这道题目是为了测试有两个公式的题目\(x^4 - 16 = 0\)和\(x^3 = -1\) 是否能正常检索""", "formulas" : [ "x^4 - 16 = 0", "x^3 = -1" ] } }, ...... }

这个 or 关系的查询一出来,结果就多了,一共13条。这里只贴出前2条。
2.3.2 查询同时含有公式x^3 = -1x^4 - 16 = 0,或含有文本实数的题目
这个要求要用SQL写就复杂了。
  • 如果分为两个表,一个放题目,一个放公式,通过外键关联,那么其脚本如下:
select q.* from t_question q where q.question like '%实数%' or ( exists( select 1 from t_formula f where q.id = f.ques_id and f.formula = 'x^3 = -1' ) and exists( select 1 from t_formula f where q.id = f.ques_id and f.formula = 'x^4 - 16 = 0' ) )

  • 如果是在 t_question 表中,用一个文本字段来存储所有公式,那么SQL语句如下:
select t.* from t_question t where t.question like '%实数%' or ( t.formulas like 'x^3 = -1' and t.formulas like 'x^4 - 16 = 0' )

无论是上面那个语句,其性能都极其感人。
而对应效果的DSL脚本如下:
{ "query": { "bool": { "should": [ { "match": { "question": "实数" } }, { "bool": { "must": [ { "term": { "formulas": "x^3 = -1" } }, { "term": { "formulas": "x^4 - 16 = 0" } } ] } } ] } } }

Java代码如下:
@Test void testMixMustAndShould() { BoolQueryBuilder sqb = QueryBuilders.boolQuery() .must(QueryBuilders.termQuery("formulas", "x^4 - 16 = 0")) .must(QueryBuilders.termQuery("formulas", "x^3 = -1")); BoolQueryBuilder bqb = QueryBuilders.boolQuery() .should(QueryBuilders.matchQuery("question", "实数")) .should(sqb); SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(bqb).build(); List results = this.esRestTemplate.queryForList(searchQuery, Question.class); assertNotNull(results); assertEquals(1, results.size()); }

这个DSL的分析结果如下:

2020-03-13|2020-03-13 使用curl 查询 elasticsearch
文章图片
使用IK的ik_max_word作为查询文本分析器的分析结果 查询结果如下:
"hits" : { "total" : { "value" : 4, "relation" : "eq" }, "max_score" : 2.427258, "hits" : [ { "_index" : "question", "_type" : "question", "_id" : "16f00905-d2a9-4576-b4f9-0dbd1d63516d", "_score" : 2.427258, "_source" : { "id" : "16f00905-d2a9-4576-b4f9-0dbd1d63516d", "questionNo" : "13", "category" : "测试题目", "question" : """这道题目是为了测试有两个公式的题目\(x^4 - 16 = 0\)和\(x^3 = -1\) 是否能正常检索""", "formulas" : [ "x^4 - 16 = 0", "x^3 = -1" ] } }, { "_index" : "question", "_type" : "question", "_id" : "65317f92-fc9a-4219-ac35-3be55126fa44", "_score" : 1.3862942, "_source" : { "id" : "65317f92-fc9a-4219-ac35-3be55126fa44", "questionNo" : "6", "category" : "测试题目", "question" : """已知二项方程 \(3x^4 + m = 0 \)没有实数根,则m的取值范围是""", "formulas" : [ "3x^4 + m = 0 " ] } }, { "_index" : "question", "_type" : "question", "_id" : "d5112bcc-3436-4114-a1c2-9fb20cebad75", "_score" : 0.7098105, "_source" : { "id" : "d5112bcc-3436-4114-a1c2-9fb20cebad75", "questionNo" : "8", "category" : "测试题目", "question" : """对于二项方程 \(ax^n + b = 0(a \neq 0, b \neq 0)\),当n为偶数时,已知方程有两个实数根。那么 ab""", "formulas" : [ "ax^n + b = 0(a \\neq 0, b \\neq 0)" ] } }, { "_index" : "question", "_type" : "question", "_id" : "72a926a4-bbcb-46e2-92fe-6c2c76c191d2", "_score" : 0.6125949, "_source" : { "id" : "72a926a4-bbcb-46e2-92fe-6c2c76c191d2", "questionNo" : "9", "category" : "测试题目", "question" : """已知方程组\(\left\{\begin{array}{**lr**}y^2=2x&\\y=kx+1&\end{array}\right.\)有两个不相等的实数解,求k的取值范围""", "formulas" : [ "\\left\\{\\begin{array}{**lr**}y^2=2x&\\\\y=kx+1&\\end{array}\\right." ] } } ] }

【2020-03-13|2020-03-13 使用curl 查询 elasticsearch】`

    推荐阅读