内容简介: elasticsearch是一个近实时分布式搜索和分析引擎,它用于全文搜索、结构化搜索、分析以及将这三者混合使用,使用java编写,基于Lucene 实现建议安装Elasticsearch+Kibana,在Kibana的操作界面对es进行操作Elasticsearch提供了RESTful接口可以对Elasticsearch进行操作
elasticsearch是一个近实时分布式搜索和分析引擎,它用于全文搜索、结构化搜索、分析以及将这三者混合使用,使用 java 编写,基于Lucene 实现
优势:
-
分布式的实时文件存储,每个字段都被索引并可被搜索
-
实时分析的分布式搜索引擎
-
横向可扩展:支持上百台服务节点的扩展,集群增加机器简单,支持处理PB级数据
-
分片机制:
允许水平分割扩展数据,允许分片和并行操作从而提高性能和吞吐量
提供高性能:同一个索引可以分多个主分片(
primary shard
),每个主分片拥有自己的副本分片(replica shard
),每个副本分片都可以提供服务,提升系统搜索请求的吞吐量和性能提供高可用性:同一个索引可以分多个主分片,每个主分片拥有零个或者多个副本,如果主分片挂了,可以从副本分片中选择一个作为主分片继续提供服务
-
隐藏复杂实现:Elasticsearch 内部隐藏了分布式系统的复杂性,我们不用去关心它是如何做到高可用,可扩展,高性能的
-
易用开源:不需要额外配置,就可以运行一个Elasticsearch服务,开源
基本概念
-
Cluster
:集群一个集群包含多个节点,对外提供服务,每个节点属于哪个集群通过配置文件中的集群名称决定
-
Node
:节点集群中的一个节点,每个节点也有一个名称,默认是随机分配,也可以自己指定,在es集群中通过节点名称进行管理和通信
-
Index
:索引索引是具有相同结构的文档集合,作用相当于 mysql 中的库
-
Type
:类型一个索引可以对应一个或者多个类型,类型可以当做是索引的逻辑分区,作用相当于mysql中的表
-
Document
:文档存储在es中的一个
JSON
格式的字符串,每一个文档有一个文档ID,如果没有自己指定ID,系统会自动生成一个ID,文档的index/type/id必须是唯一的,作用相当于mysql中的行 -
field
:字段一个文档会包含多个字段,每个字段都对应一个字段类型,类似于mysql中的列
-
shard
:分片es中分为
primary shard
主分片和replica shard
副本分片主分片:当存一个文档的时候会先存储在主分片中,然后复制到不同的副本分片中,默认一个索引会有5个主分片,当然可以自己指定分片数量,当分片一旦建立,分片数量不能改变
副本分片:每一个主分片会有零个或者多个副本,副本主要是主分片的复制,通过副本分片可以提供高可用性,当一个主分片挂了,可以从副本分片中选择一个作为主分片,还可以提高性能,所以主分片不能和副本分片部署在相同的节点上
-
replica
:复制复制是为了防止单点问题,可以做到对故障进行转移,保证系统的高可用
-
映射
描述数据在每个字段内如何存储,是定义存储和索引的文档类型及字段的过程,索引中的每一个文档都有一个类型,每种类型都有它自己的映射,一个映射定义了文档结构内每个字段的数据类型
使用
GET /index/_mapping/type
获取对应的/index/type
的映射信息
基本操作
建议安装Elasticsearch+Kibana,在Kibana的操作界面对es进行操作
Elasticsearch提供了RESTful接口可以对Elasticsearch进行操作
Kibana操作页面
在Kibana的Dev Tools界面可以对es进行操作,在console界面敲命令,点执行,会在右面输出结果
验证Elasticsearch是否安装成功
{ "name" : "UzOujcc", //节点名称 "cluster_name" : "mx", //集群名称,我自己设置的 "cluster_uuid" : "d2K1M95DRzG9XOPDOR_DEQ", "version" : { "number" : "6.2.4", //集群版本 "build_hash" : "ccec39f", "build_date" : "2018-04-12T20:37:28.497551Z", "build_snapshot" : false, "lucene_version" : "7.2.1", "minimum_wire_compatibility_version" : "5.6.0", "minimum_index_compatibility_version" : "5.0.0" }, "tagline" : "You Know, for Search" }
es提供了一套api,叫做cat api,可以查看es中的信息数据
查看集群健康状况
命令: GET /_cat/health?v
status
代表着集群的健康程度
green yellow red
索引操作
查看索引信息
命令: GET _cat/indices?v
有五个索引,都是的测试数据
新建索引
命令: PUT /myindex
{ "acknowledged": true, "shards_acknowledged": true, "index": "myindex" }
删除索引
命令: DELETE myindex
{ "acknowledged": true }
文档操作
添加文档
添加文档是向索引中添加一条文档,让其能够搜索,文档格式是json串,如果es中有相同id的文档存在则更新这个文档
当创建文档的时候,如果索引不存在,则会自动创建该索引,而且es默认会对document每个field都建立倒排索引,让其可以被搜索
命令:
PUT /index/type/id { "json数据结构体 " }
例:
PUT /school/student/1 { "name":"张三", "age":21, "class":2, "gender":"男" }
返回:
{ "_index": "school", //索引 "_type": "student", //类型 "_id": "1", //id,如果不指定则会系统生成一个20位的id,文档被分到那个分片上就是根据id的散劣值进行控制 "_version": 1, //文档版本号,通过这个进行并发控制 "result": "created", //操作类型 "_shards": { //分片信息 "total": 2, //文档被创建时在多少个分片进行了操作(包括主分片和副本分片) "successful": 1, //添加成功的索引分片数量 "failed": 0 //失败的索引分片数量 }, "_seq_no": 0, "_primary_term": 1 }
修改文档
方式1:使用put方式更新文档
PUT /school/student/1 { "name":"吕布", "age":21, "class":2, "gender":"男" }
返回:
{ "_index": "school", "_type": "student", "_id": "1", "_version": 2, //版本号+1 "result": "updated", // 修改 "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 1, "_primary_term": 1 }
这种方式替换需要带上所有的field,才能进行信息的修改,操作类似于覆盖
方式2:post更新文档
POST /school/student/1/_update { "doc": { "name":"吕布1" } }
使用post更新文档,可以只更新部分字段
查询文档
查询单条文档
命令: GET /school/student/1
返回:
{ "_index": "school", "_type": "student", "_id": "1", "_version": 3, "found": true, "_source": { "name": "吕布1", "age": 21, "class": 2, "gender": "男" } }
删除文档
命令: DELETE school/student/1
返回:
{ "_index": "school", "_type": "student", "_id": "1", "_version": 4, "result": "deleted", //删除 "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 3, "_primary_term": 1 }
这时在查询就会显示 "found": false
映射与分析
Elasticsearch 中的数据可以概括的分为两类:精确值和全文
- 精确值:精确值是确定的值,比如用户ID,字符串也可以表示精确值,例如用户名或邮箱地址。对于精确值来讲,
Foo
和foo
是不同的,精确值的查询简单,要么匹配查询,要么不匹配 - 全文:全文是指文本数据(通常以人类容易识别的语言书写),例如一个推文的内容或一封邮件的内容,全文的查询较为复杂,他需要的是匹配查询的程度有多大
在es中使用 倒排索引来进行快速的全文搜索。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表, 倒排索引 具体内容请戳: https://www.elastic.co/guide/cn/elasticsearch/guide/current/inverted-index.html
映射
es中的映射( mapping
)用来描述数据在每个字段内如何存储,是定义存储和索引的文档类型及字段的过程,索引中的每一个文档都有一个类型,每种类型都有它自己的映射,一个映射定义了文档结构内每个字段的数据类型,作用相当于 mysql
中的 DDL
语句
查询索引类型的映射
GET /ad/_mapping/phone
{ "school": { "mappings": { "student": { "properties": { "age": { "type": "long" }, "class": { "type": "long" }, "gender": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "name": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } } } } }
动态映射
动态映射不需要事先定义映射,文档在写入es的时候,会根据文档字段自动识别类型
映射规则:
Json 数据类型 | es 数据类型 |
---|---|
null | 没有字段添加 |
true,false | boolean |
Integer | long |
object | object |
array | 依赖于数组中首个非空值 |
string | text和keyword |
日期 | date或text |
静态映射
静态映射需要事先定义好映射,包含文档的各个字段及其类型
PUT books { "mappings": { "book":{ "properties": { "id":{"type": "long"}, "bookName":{"type": "text"}, "ad":{"type": "text"} } } } }
es中的字符串类型分为 keyword
和 text
keyword text
有时候一个字段同时拥有全文类型(text)和关键字类型(keyword)是有用的:一个用于全文搜索,另一个用于聚合和排序。这可以通过多字段类型来实现(动态映射是字符串的默认映射类型)
多种查询
url参数搜索
这种方式就是类似于get请求,将请求参数拼接到链接上,例 GET /school/student/_search?参数
,多个参数用&分开
查询所有
命令: GET /school/student/_search
返回:
{ "took": 7, //查询耗时,毫秒 "timed_out": false, //是否超时,timeout 不是停止执行查询,它仅仅是告知正在协调的节点返回到目前为止收集的结果并且关闭连接 "_shards": { "total": 5, //请求的分片数量,索引拆成了5个分片,所以对于搜索请求,会打到所有的primary shard "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 2, //符合条件的总条数,这里查的是所有 "max_score": 1, //匹配分数 "hits": [ //数据 { "_index": "school", "_type": "student", "_id": "2", "_score": 1, "_source": { "name": "houyi", "age": 23, "class": 2, "gender": "男" } }, { "_index": "school", "_type": "student", "_id": "1", "_score": 1, "_source": { "name": "吕布", "age": 21, "class": 2, "gender": "男" } } ] } }
多索引,多type搜索
在URL中指定特殊的索引和类型进行多索引,多type搜索
-
/_search
:在所有的索引中搜索所有的类型 -
/school/_search
:在school
索引中搜索所有的类型 -
/school,ad/_search
:在school
和ad
索引中搜索所有的类型 -
/s*,a*/_search
:在所有以g
和a
开头的索引中所有所有的类型 -
/school/student/_search
:在school
索引中搜索student
类型 -
/school,ad/student,phone/_search
:在school
和ad
索引上搜索student
和phone
类型 -
/_all/student,phone/_search
:在所有的索引中搜索student
和phone
类型
按条件查询
命令: GET /school/student/_search?q=name:houyi
查询name是houyi的记录
更多搜索参数:
查询DSL
elasticsearch提供了基于JSON的完整查询DSL来定义查询,DSL拥有一套查询组件,这些组件可以以无限组合的方式进行搭配,构建各种复杂的查询
-
叶子语句:就像match语句,被用于将查询的字符串与一个字段或多个字段进行对比(单个条件)
比如:
GET /ad/phone/_search { "query": { "match": { "name": "phone" } } }
-
复合查询:用户合并其他查询语句,比如一个
bool
语句,允许你在需要的时候组合其他语句,包括must
,must_not
,should
和filter
语句(多条件组合查询)比如:
GET /ad/phone/_search { "query": { "bool": { "must": [ {"match": { "name": "phone" }} ] , "must_not": [ {"match": { "color": "red" }} ] , "should": [ {"match": { "price": 5000 }} ] , "filter": { "term": { "label": "phone" } } } } }
must
:表示文档一定要包含查询的内容must_not
:表示文档一定不要包含查询的内容should
:表示如果文档匹配上可以增加文档相关性得分
事实上我们可以使用两种结构化语句: 结构化查询 query DSL
和结构化过滤 Filter DSL
-
结构化查询
query DSL
用于检查内容与条件是否匹配,内容查询中使用的bool和match字句,用于计算每个文档的匹配得分,元字段_score表示匹配度,查询的结构中以query参数开始来执行内容查询
-
结构化过滤
Filter DSL
只是简单的决定文档是否匹配,内容过滤中使用的term和range字句,会过滤 调不匹配的文档,并且不影响计算文档匹配得分
使用过滤查询会被es自动缓存用来提高效率
原则上来说,使用查询语句做全文本搜索或其他需要进行相关性评分的时候,剩下的全部用过滤语句
新建一个稍微复杂的索引,添加三条文档
PUT /ad/phone/1 { "name":"phone 8", "price": 6000, "color":"white", "ad":"this is a white phone", "label":["white","nice"] } PUT /ad/phone/2 { "name":"xiaomi 8", "price": 4000, "color":"red", "ad":"this is a red phone", "label":["white","xiaomi"] } PUT /ad/phone/3 { "name":"huawei p30", "price": 5000, "color":"white", "ad":"this is a white phone", "label":["white","huawei"] }
查询示例:
-
获取所有
GET /ad/phone/_search { "query": { "match_all": {} } }
match_all
匹配所有数据,返回的结果中元字段_score
得分为1 -
分页查询,从第二条开始,查两条(不要使用
from
,size
进行深度分页,会有性能问题)GET /ad/phone/_search { "query": { "match_all": {} }, "from": 1, "size": 2 }
这种分页方式如果进行深度分页,比如到100页,每页十条数据,它会从每个分片都查询出100*10条数据,假设有五个分片,就是5000条数据,然后在内存中进行排序,然后返回拍过序之后的集合中的第1000-1010条数据
-
指定查询出来的数据返回的字段
GET /ad/phone/_search { "query": { "match_all": {} }, "_source": ["name","price"] }
返回的数据中只返回
name
和price
字段 -
ad字段中包含单词white
GET /ad/phone/_search { "query": { "match": { "ad": "white" } } }
返回的结果中元字段
_score
有评分,说明使用query
会计算评分 -
ad字段中包含单词white,并按照价格升序排列
GET /ad/phone/_search { "query": { "match": { "ad": "white" } }, "sort": [ { "price": { "order": "asc" } } ] }
-
价格字段大于5000
GET /ad/phone/_search { "query": { "bool": { "filter": { "range": { "price": { "gt": 5000 } } } } } }
返回的结果中元字段
_score
字段等于0,没评分,说明使用filter
不会计算评分 -
ad字段中包含单词white,价格字段大于5000
GET /ad/phone/_search { "query": { "bool": { "must": [ { "match": { "ad": "white" } } ], "filter": { "range": { "price": { "gt": 5000 } } } } } }
-
查询
name
字段包含单词phone
的文档的数量GET /ad/phone/_count { "query": { "match": { "name": "phone" } } }
搜索示例
-
match_all
查询查询简单的匹配所有文档
GET /ad/phone/_search { "query": { "match_all": {} } }
-
match
查询支持全文搜索和精确查询,取决于字段是否支持全文检索
全文检索:
GET /ad/phone/_search { "query": { "match": { "ad": "a red" } } }
全文检索会将查询的字符串先进行分词,
a red
会分成为a
和red
,然后在倒排索引中进行匹配,所以这条语句会将三条文档都查出来精确查询:
GET /ad/phone/_search { "query": { "match": { "price": "6000" } } }
对于精确值的查询,可以使用 filter 语句来取代 query,因为 filter 将会被缓存
operator
操作:match
查询还可以接受operator
操作符作为输入参数,默认情况下该操作符是or
。我们可以将它修改成and
让所有指定词项都必须匹配GET /ad/phone/_search { "query": { "match": { "ad": { "query": "a red", "operator": "and" } } } }
精确度匹配:
match
查询支持minimum_should_match
最小匹配参数, 可以指定必须匹配的词项数用来表示一个文档是否相关。我们可以将其设置为某个具体数字(指需要匹配倒排索引的词的数量),更常用的做法是将其设置为一个百分数,因为我们无法控制用户搜索时输入的单词数量GET /ad/phone/_search { "query": { "match": { "ad": { "query": "a red", "minimum_should_match": "2" } } } }
只会返回匹配上
a
和red
两个词的文档返回,如果minimum_should_match
是1,则只要匹配上其中一个词,文档就会返回 -
multi_match
查询多字段查询,比如查询
color
和ad
字段包含单词red
的文档GET /ad/phone/_search { "query": { "multi_match": { "query": "red", "fields": ["color","ad"] } } }
-
range
查询范围查询,查询价格大于4000小于6000的文档
GET /ad/phone/_search { "query": { "range": { "price": { "gt": 4000, "lt": 6000 } } } }
范围查询操作符:
gt
(大于),gte
(大于等于),lt
(小于),lte
(小于等于); -
term
查询精确值查询
查询
price
字段等于6000的文档GET /ad/phone/_search { "query": { "term": { "price": { "value": "6000" } } } }
查询
name
字段等于phone 8
的文档GET /ad/phone/_search { "query": { "term": { "name": { "value": "phone 8" } } } }
返回值如下,没有查询到名称为
phone 8
的文档{ "took": 5, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 0, "max_score": null, "hits": [] } }
为什么没有查到
phone 8
的这个文档那,这里需要介绍一下term
的查询原理
term
查询会去倒排索引中寻找确切的term
,它并不会走分词器,只会去配倒排索引 ,而name
字段的type
类型是text
,会进行分词,将phone 8
分为phone
和8
,我们使用term
查询phone 8
时倒排索引中没有phone 8
,所以没有查询到匹配的文档term
查询与match
查询的区别-
term
查询时,不会分词,直接匹配倒排索引 -
match
查询时会进行分词,查询phone 8
时,会先分词成phone
和8
,然后去匹配倒排索引,所以结果会将phone 8
和xiaomi 8
两个文档都查出来
还有一点需要注意,因为
term
查询不会走分词器,但是回去匹配倒排索引,所以查询的结构就跟分词器如何分词有关系,比如新增一个/ad/phone
类型下的文档,name
字段赋值为Oppo
,这时使用term
查询Oppo
不会查询出文档,这时因为es默认是用的standard
分词器,它在分词后会将单词转成小写输出,所以使用oppo
查不出文档,使用小写oppo
可以查出来GET /ad/phone/_search { "query": { "term": { "name": { "value": "Oppo" //改成oppo可以查出新添加的文档 } } } }
这里说的并不是想让你了解
standard
分词器,而是要get到所有像term
这类的查询结果跟选择的分词器有关系,了解选择的分词器分词方式有助于我们编写查询语句 -
-
terms
查询terms
查询与term
查询一样,但它允许你指定多直进行匹配,如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件GET /ad/phone/_search { "query": { "terms": { "ad": ["red","blue"] } } }
-
exists
查询和missing
查询用于查找那些指定字段中有值 (
exists
) 或无值 (missing
) 的文档指定
name
字段有值:GET /ad/phone/_search { "query": { "bool": { "filter": { "exists": { "field": "name" } } } } }
指定
name
字段无值:GET /ad/phone/_search { "query": { "bool": { "filter": { "missing": { "field": "name" } } } } }
-
match_phrase
查询短语查询,精确匹配,查询
a red
会匹配ad
字段包含a red
短语的,而不会进行分词查询,也不会查询出包含a 其他词 red
这样的文档GET /ad/phone/_search { "query": { "match_phrase": { "ad": "a red" } } }
-
scroll
查询类似于分页查询,不支持跳页查询,只能一页一页往下查询,
scroll
查询不是针对实时用户请求,而是针对处理大量数据,例如为了将一个索引的内容重新索引到具有不同配置的新索引中POST /ad/phone/_search?scroll=1m { "query": { "match_all": {} }, "size": 1, "from": 0 }
返回值包含一个
"_scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAAQFlV6T3VqY2NaVDBLRG5uZXdiZ0hFYUEAAAAAAAAAERZVek91amNjWlQwS0RubmV3YmdIRWFBAAAAAAAAABIWVXpPdWpjY1pUMEtEbm5ld2JnSEVhQQAAAAAAAAATFlV6T3VqY2NaVDBLRG5uZXdiZ0hFYUEAAAAAAAAAFBZVek91amNjWlQwS0RubmV3YmdIRWFB"
下次查询的时候使用
_scroll_id
就可以查询下一页的文档POST /_search/scroll { "scroll" : "1m", "scroll_id" : "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAAYFlV6T3VqY2NaVDBLRG5uZXdiZ0hFYUEAAAAAAAAAGRZVek91amNjWlQwS0RubmV3YmdIRWFBAAAAAAAAABYWVXpPdWpjY1pUMEtEbm5ld2JnSEVhQQAAAAAAAAAXFlV6T3VqY2NaVDBLRG5uZXdiZ0hFYUEAAAAAAAAAFRZVek91amNjWlQwS0RubmV3YmdIRWFB" }
-
multi get
查询允许基于索引,类型(可选)和id(以及可能的路由)获取多个文档,如果某个文档获取失败则将错误信息包含在响应中
GET /ad/phone/_mget { "ids": ["1","8"] }
-
bulk
批量操作bulk
批量操作可以在单次API调用中实现多个文档的create
、index
、update
或delete
。这可以大大提高索引速度bulk
请求体如下{ action: { metadata }}\n { request body }\n { action: { metadata }}\n { request body }\n ...
行为(
action
)必须是以下几种:| 行为 | 解释 |
| ——– | —————————————————— |
|
create
| 当文档不存在时创建之。详见《创建文档》 ||
index
| 创建新文档或替换已有文档。见《索引文档》和《更新文档》 ||
update
| 局部更新文档。见《局部更新》 ||
delete
| 删除一个文档。见《删除文档》 |在索引、创建、更新或删除时必须指定文档的
_index
、_type
、_id
这些元数据(metadata
)例:
PUT _bulk { "create" : { "_index" : "ad", "_type" : "phone", "_id" : "6" }} { "doc" : {"name" : "bulk"}} { "index" : { "_index" : "ad", "_type" : "phone", "_id" : "6" }} { "doc" : {"name" : "bulk"}} { "delete":{ "_index" : "ad", "_type" : "phone", "_id" : "1"}} { "update":{ "_index" : "ad", "_type" : "phone", "_id" : "3"}} { "doc" : {"name" : "huawei p20"}}
返回:
{ "took": 137, "errors": true, //如果任意一个文档出错,这里返回true, "items": [ //items数组,它罗列了每一个请求的结果,结果的顺序与我们请求的顺序相同 { //create这个文档已经存在,所以异常 "create": { "_index": "ad", "_type": "phone", "_id": "6", "status": 409, "error": { "type": "version_conflict_engine_exception", "reason": "[phone][6]: version conflict, document already exists (current version [2])", "index_uuid": "9F5FHqgISYOra_P09HReVQ", "shard": "2", "index": "ad" } } }, { //index这个文档已经存在,会覆盖 "index": { "_index": "ad", "_type": "phone", "_id": "6", "_version": 3, "result": "updated", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 6, "_primary_term": 5, "status": 200 } }, { //删除 "delete": { "_index": "ad", "_type": "phone", "_id": "1", "_version": 1, "result": "not_found", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 4, "_primary_term": 5, "status": 404 } }, { //修改 "update": { "_index": "ad", "_type": "phone", "_id": "3", "_version": 3, "result": "noop", "_shards": { "total": 2, "successful": 1, "failed": 0 }, "status": 200 } } ] }
bulk
请求不是原子操作,它们不能实现事务。每个请求操作时分开的,所以每个请求的成功与否不干扰其它操作
以上所述就是小编给大家介绍的《Elasticsearch介绍,基本查询详解》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Retrofit使用详解-注解介绍
- Keepalived高可用介绍与配置详解
- 抓包技术详解及抓包软件介绍
- 新手入门之spring boot介绍及使用详解
- 开源字节码插装工具 HiBeaver 介绍与原理详解
- nginx在linux系统应用详解之一基础介绍和全局配置
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Machine Learning
Kevin Murphy / The MIT Press / 2012-9-18 / USD 90.00
Today's Web-enabled deluge of electronic data calls for automated methods of data analysis. Machine learning provides these, developing methods that can automatically detect patterns in data and then ......一起来看看 《Machine Learning》 这本书的介绍吧!