分布式高级篇1 —— 全文检索
Elasticsearch
- Elasticsearch
- 简介
- 一、基本概念
- 1、index(索引)
- 2、Type(类型)
- 3、Document(文档)
- 4、倒排索引
- 二、Docker 安装 EL
- 1、拉取镜像
- 2、创建实例
- 三、初步探索
- 1、_cat
- 2、索引一个文档(保存)
- 3、查询文档
- 3、更新文档
- 4、删除文档&索引
- 5、_bulk 批量 AP
- 6、样本测试数据
- 四、进阶检索
- 1、 Search API
- 2、Query DSL
- (1)基本语法格式
- (2)返回部分字段
- (3)match 匹配查询
- (4)match_phrase 短语匹配
- (5)多字段匹配
- (6)bool 复合查询
- (7)filter 结果过滤
- (8)term
- 扩展:keyword
- (9) aggregations(执行聚合)
- 3、Mapping 映射
- (1)新版本改变
- (2)创建索引并指定映射
- (3)添加新字段映射
- (4)更新字段映射
- (5)数据迁移
- 4、分词
- (1)安装 ik 分词
- (2)自定义词库
- 五、Elasticsearch-Rest-Client
- 1、SpringBoot 整合 ES
- 2、使用 SpringBoot 测试 ElasticSearch
- (1)增加索引
- (2)更新索引
- (3)删除索引
- (4)复杂查询
- 六、拓展:Docker 安装 Nginx
视频来源: 【Java项目《谷粒商城》Java架构师 | 微服务 | 大型电商项目】
简介
Elasticsearch 是什么? | Elastic
全文搜索属于最常见的需求,开源的 Elasticsearch 是目前全文搜索引擎的首选。 它可以快速地储存、搜索和分析海量数据。维基百科、Stack Overflow、Github 都采用它
Elastic 的底层是开源库 Lucene。但是,你没法直接用 Lucene,必须自己写代码去调用它的 接口。Elastic 是 Lucene 的封装,提供了 REST API 的操作接口,开箱即用。 REST
官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
官方中文:https://www.elastic.co/guide/cn/elasticsearch/guide/current/foreword_id.html
社区中文: https://es.xiaoleilu.com/index.html http://doc.codingdict.com/elasticsearch/0/
一、基本概念
1、index(索引)
动词,相当于 mysql 中的 insert
名词,相当于 mysql 中的 database
2、Type(类型)
在 Index(索引)中,可以定义一个或多个类型。
类似于 MySQL 中的 Table;每一种类型的数据放在一起
3、Document(文档)
保存在某个索引(Index)下,某种类型(Type)的一个数据(Document),文档是 JSON 格 式的,Document 就像是 MySQL 中的某个 Table 里面的内容;
4、倒排索引
Elasticsearch 检索速度快是因为使用了一种倒排索引的数据结构
保存的记录:
1-红海行动
2-探索红海行动
3-红海特别行动
4-红海记录篇
5-特工红海特别探索
Elasticsearch 会基于这些保存的数据额外维护一张表,这些表会将保存的数据拆分成单词,并记录命中的记录【如图所示】。
比如:红海行动 会拆分成 红海、行动
, 探索红海行动 被拆分成 探索、红海、行动
当检索时:
1)、红海特工行动?
会将检索的内容也拆分成单词,红海、特工、行动
,并在表中查询每个单词所命中的记录。 并按照相关性得分排序,即每个文档跟查询的匹配程度
二、Docker 安装 EL
使用 su 切换到 root 用户登录
1、拉取镜像
docker pull elasticsearch:7.4.2 # 存储和检索数据 docker pull kibana:7.4.2 # 可视化检索数据
2、创建实例
创建 EL
1、创建与elasticsearch挂载的目录
mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data
mkdir -p /mydata/elasticsearch/plugins
2、修改权限,避免由于权限问题,elasticsearch报错
chmod -R 777 /mydata/elasticsearch/ 保证权限
3、0.0.0.0 是所有主机均可访问 elasticsearch 接口
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
4、创建实例
9200:9200 提供给别的服务访问的接口
9300:9300 elasticsearch内部数据的访问
discovery.type=single-node 单节点模式启动
ES_JAVA_OPTS=“-Xms64m -Xmx512m” 设置 elasticsearch 启动所占用的内存。如果不设置会占用你所有的内存。导致OOM
docker run --name elasticsearch --restart=always -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2
5、访问 9200 端口
创建 Kibana 实例
# 改成自己的 ip 地址
docker run --name kibana --restart=always -e ELASTICSEARCH_HOSTS=http://192.168.56.111:9200 -p 5601:5601 \
-d kibana:7.4.2
将 Kibana设置成中文
# 进入容器
docker exec -it Kibana容器id bash
cd config/
vi kibana.yml
# 在配置文件中增加
i18n.locale: "zh-CN"
#重启实例即可
docker restart 容器id
访问 IP地址:5601
三、初步探索
1、_cat
只需要发送对应的请求即可。
GET /_cat/nodes:查看所有节点 _
GET /_cat/health:查看 es 健康状况 _
GET /_cat/master:查看主节点 _
GET /_cat/indices:查看所有索引 == show databases;
2、索引一个文档(保存)
PUT 方式
保存一个数据,保存在哪个索引的哪个类型下,指定用哪个唯一标识
# 在 customer 索引下的 external 类型下保存 1 号数据
PUT customer/external/1
{ "name": "John Doe"
}
(1)使用 PUT 请求带 id,第一次发送请求时增加操作,版本号为1.、第二次发送请求就是更新操作,版本号为2
(2)PUT 方式必须携带ID,否则就会报错
POST 方式
POST 请求也可以新增数据
# 在 customer 索引下的 external 类型下保存 1 号数据
POST customer/external/1
{ "name": "John Doe"
}
(1)POST 请求携带 id ,当第一次发送POST请求是新增操作,当第二次发送POST请求是更新操作,并且版本号+1
第二次携带 id 发送POST 请求
(2)POST方式可以不携带 id ,el 会自动随机生成一个不重复的 id,也就是说,如果不携带ID发送 POST 请求,每一个都是新增
3、查询文档
请求
GET /customer/external/1
返回结果
{ "_index": "customer", //在哪个索引"_type": "external", //在哪个类型"_id": "1", //记录 id"_version": 2, //版本号"_seq_no": 1, //并发控制字段,每次更新就会+1,用来做乐观锁"_primary_term": 1, //同上,主分片重新分配,如重启,就会变化"found": true, "_source": { //真正的内容"name": "John Doe"}
}
使用 _seq_no + _primary_term 用来做乐观锁 :?if_seq_no=0&if_primary_term=1
假设请求A发送修改请求:
PUT http://192.168.56.111:9200/customer/external/1?if_seq_no=1&if_primary_term=1
返回结果:此时 _seq_no 被修改成了5
{"_index": "customer","_type": "external","_id": "1","_version": 3,"result": "updated","_shards": {"total": 2,"successful": 1,"failed": 0},"_seq_no": 5,"_primary_term": 1
}
请求B 同样发送修改请求
请求B认为没有别的请求修改数据,认为 _seq_no 仍然为 1
PUT http://192.168.56.111:9200/customer/external/1?if_seq_no=1&if__primary_term=1
返回结果:报错reason就是: 需要 seqNo [1] ,但是 seqNo [5]
{"error": {"root_cause": [{"type": "version_conflict_engine_exception","reason": "[1]: version conflict, required seqNo [1], primary term [1]. current document has seqNo [5] and primary term [1]","index_uuid": "LDc8rC6ERbm0KN5XfSLmuQ","shard": "0","index": "customer"}],"type": "version_conflict_engine_exception","reason": "[1]: version conflict, required seqNo [1], primary term [1]. current document has seqNo [5] and primary term [1]","index_uuid": "LDc8rC6ERbm0KN5XfSLmuQ","shard": "0","index": "customer"},"status": 409
}
3、更新文档
第一种方式: POST请求带_update
POST customer/external/1/_update
{ "doc":{ "name": "John Doew"}
}
第二种方式: POST请求不带_update
POST customer/external/1
{ "name": "John Doe2"
}
第三种方式: PUT请求不带_update
PUT customer/external/1
{ "name": "John Doe"
}
三者区别:
POST请求带_update 会对比源文档数据,如果一样,就不进行任何操作。 包括 version、 _seq_no 也不会变化
不带 _update ,每发送一次请求,version、 _seq_no 都会变化
{"_index": "customer","_type": "external","_id": "1","_version": 12,"result": "noop", // 结果是no operation,没有进行任何操作"_shards": {"total": 0,"successful": 0,"failed": 0},"_seq_no": 14,"_primary_term": 1
}
更新时增加属性:
POST customer/external/1/_update
{ "doc": { "name": "Jane Doe", "age": 20 }
}
POST/PUT 不带 _update 也可以
POST customer/external/1
{ "name": "Jane Doe", "age": 20
}PUT customer/external/1/
{ "name": "Jane Doe", "age": 20
}
4、删除文档&索引
删除文档
DELETE customer/external/1
返回结果
{"_index": "customer","_type": "external","_id": "1","_version": 13,"result": "deleted","_shards": {"total": 2,"successful": 1,"failed": 0},"_seq_no": 15,"_primary_term": 1
}
删除索引
DELETE customer
返回结果
{"acknowledged": true
}
5、_bulk 批量 AP
语法格式:
action: 动作,可以是 delete-删除,index-新增,update-修改
metadata :操作的数据,比如
- {“_id”:“1”} 操作 id 为1 的数据
- {“_index”: “user”, " _type": “man”} 操作 索引为 user,类型为 man 下的所有数据
request body :请求体
{ action: { metadata }}
{ request body }{ action: { metadata }}
{ request body }
案例演示1
// _bulk 表示批量操作
POST /customer/external/_bulk
// index 新增,id=1
{"index":{"_id":"1"}}
// 请求体,新增的内容
{"name": "John Doe"}{"index":{"_id":"2"}}
{"name": "Jane Doe"}
返回结果
bulk API 以此按顺序执行所有的 action(动作)。如果一个单个的动作因任何原因而失败, 它将继续处理它后面剩余的动作。当 bulk API 返回时,它将提供每个动作的状态(与发送 的顺序相同),所以可以检查是否一个指定的动作是不是失败了。
{"took" : 120, //花费时间"errors" : false, // 是否有异常"items" : [{"index" : {"_index" : "customer","_type" : "external","_id" : "1","_version" : 1,"result" : "created","_shards" : {"total" : 2,"successful" : 1,"failed" : 0},"_seq_no" : 0,"_primary_term" : 1,"status" : 201}},{"index" : {"_index" : "customer","_type" : "external","_id" : "2","_version" : 1,"result" : "created","_shards" : {"total" : 2,"successful" : 1,"failed" : 0},"_seq_no" : 1,"_primary_term" : 1,"status" : 201}}]
}
案例演示2
第一个删除操作,第二个新增操作,第三个新增操作,第四个修改操作
POST /_bulk
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "My first blog post" }
{ "index": { "_index": "website", "_type": "blog" }}
{ "title": "My second blog post" }
{ "update": { "_index": "website", "_type": "blog", "_id": "123"}}
{ "doc" : {"title" : "My updated blog post"} }
6、样本测试数据
准备了一份顾客银行账户信息的虚构的 JSON 文档样本。每个文档都有下列的 schema (模式):
{ "account_number": 0, "balance": 16623, "firstname": "Bradshaw", "lastname": "Mckenzie", "age": 29, "gender": "F", "address": "244 Columbus Place", "employer": "Euron", "email": "bradshawmckenzie@euron.com", "city": "Hobucken", "state": "CO"
}
测试数据:yangzhaoguang/ES-: ES测试数据 (github.com)
四、进阶检索
1、 Search API
ES 支持两种基本方式检索 :
-
一个是通过使用 REST request URI 发送搜索参数(uri+检索参数)
-
另一个是通过使用 REST request body 来发送它们(uri+请求体)
(1)uri + 检索参数
// * 查询所有,sort 排序
GET bank/_search?q=*&sort=account_number:asc
返回结果
took Elasticsearch 执行搜索的时间(毫秒)
time_out 告诉我们搜索是否超时
_shards 告诉我们多少个分片被搜索了,以及统计了成功/失败的搜索分片
hits 搜索结果 hits.total - 搜索结果
hits.hits - 实际的搜索结果数组(默认为前 10 的文档)
sort 结果的排序 key(键)(没有则按 score 排序)
score 和 max_score 相关性得分和最高得分(全文检索用)
(2)uri + 请求体
GET bank/_search
{"query": {"match_all": {}},"sort": [{"account_number": {"order": "desc"},"balance": {"order": "asc"}}]
}
在请求体中 封装查询的条件,使用 Query DSL 语句
2、Query DSL
(1)基本语法格式
Elasticsearch 提供了一个可以执行查询的 Json 风格的 DSL(domain-specific language 领域特 定语言)。这个被称为 Query DSL。
结构语法:
{QUERY_NAME: {ARGUMENT: VALUE, ARGUMENT: VALUE,... }
}
query 定义如何查询, match_all 查询类型【代表查询所有的所有】
sort 排序,多字段排序,会在前序字段相等时后续字段内部排序,否则以前序为准
“from”: 10,
“size”: 5
从第十条记录开始,查询五条数据。使用 from + size 可以达到分页效果。
GET bank/_search
{"query": {"match_all": {}},"sort": [{"balance": {"order": "desc"}}],"from": 10,"size": 5}
(2)返回部分字段
_source 可指定返回的字段
GET bank/_search
{"query": {"match_all": {}},"sort": [{"balance": {"order": "desc"}}],"from": 10,"size": 5,"_source": ["account_number","balance"]
}
(3)match 匹配查询
match 精确匹配
如果字段值是数字类型的,就是精确匹配。匹配 account_number = 2 的记录
# match 精确匹配
GET bank/_search
{"query": {"match": {"account_number": 2}}
}
match 模糊匹配
字段值是 字符串类型的,就是模糊匹配。值得注意的是,El 会将 Laurel Avenue 进行分词,将所有包含 Laurel 或者 Avenue 或者 Laurel Avenue 都查出来。
# match 模糊匹配
GET bank/_search
{"query": {"match": {"address": "Laurel Avenue"}}
}
(4)match_phrase 短语匹配
不会将查询条件分词 ,查询 address 包含 Laurel Avenue 短语 的记录
# match_phrase 短语匹配,不会分词
GET bank/_search
{"query": {"match_phrase": {"address": "Laurel Avenue"}}
}
(5)多字段匹配
query : 查询条件
fields 查询字段
multi_match 也会分词查询,address 和 city 里只要包含 Laurel、Belvoir、Laurel Belvoir 都能查询出来。
# multi_match 多字段匹配
GET bank/_search
{"query": {"multi_match": {"query": "Laurel Belvoir","fields": ["address","city"]}}
}
(6)bool 复合查询
bool 用来做复合查询 :
复合语句 可以合并 任何 其它 查询语句,包括复合语句,了解这一点是很重要的。这就意味 着,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑 。
must: 必须符合的条件
查询的数据必须符合:gender为F,age为35 的记录
# bool 复合查询
GET bank/_search
{"query": {"bool": {"must": [{"match": {"gender": "F"}},{"match": {"age": "35"}}]}}
}
must_not 必须不是指定的条件
查询的数据必须符合:gender为F,age为35 并且 city 不为 Ironton 的记录
# bool 复合查询
GET bank/_search
{"query": {"bool": {"must": [{"match": {"gender": "F"}},{"match": {"age": "35"}}],"must_not": [{"match": {"city": "Ironton"}}]}}
}
should 查询的数据当中应当包含此条件,如果包含此条件会提高相关性得分,不包含也是可以的。
# bool 复合查询
GET bank/_search
{"query": {"bool": {"must": [{"match": {"gender": "F"}},{"match": {"age": "35"}}],"must_not": [{"match": {"city": "Ironton"}}],"should": [{"match": {"city": "Darrtown"}}]}}
}
(7)filter 结果过滤
过滤掉不符合条件的数据,和 must一样,但是 filter 不会计算相关性得分
查询出符合 10<=age<=30 的记录
# filter 不会计算相关性得分
GET bank/_search
{"query": {"bool": {"filter": {"range": {"age": {"gte": 10,"lte": 30}}}}}
}
(8)term
和 match 一样。匹配某个属性的值。全文检索字段用 match,其他非 text 字段匹配用 term。
使用 match
GET bank/_search
{"query": {"match": {"address": "880 Holmes Lane"}}
}
返回结果 一共有 16 条数据。
使用 term
GET bank/_search
{"query": {"term": {"address": "880 Holmes Lane"}}
}
返回结果 0 条数据
{"took" : 0,"timed_out" : false,"_shards" : {"total" : 1,"successful" : 1,"skipped" : 0,"failed" : 0},"hits" : {"total" : {"value" : 0,"relation" : "eq"},"max_score" : null,"hits" : [ ]}
}
官方是这样说的:
避免使用 term 对文本字段查询
默认情况下,Elasticsearch会作为分析的一部分更改文本字段的值。这会使查找文本字段值的精确匹配变得困难。
对于查询文本字段,使用 match
使用 term 查找非文本字段:
# 使用 term 查询非文本字段(精确查找)
GET bank/_search
{"query": {"term": {"age": "28"}}
}
因此以后规定使用 term 查询非文本字段,使用 match 查询文本字段。
扩展:keyword
每一个字段都有 keyword 属性,用来实现精确查找。
查询 address 为 880 Holmes Lane 的记录
# keywords
GET bank/_search
{"query": {"match": {"address.keyword": "880 Holmes Lane"}}
}
他 与 match_phrase 的区别?
match_phrase 指明查询条件是一个短语,是一个包含关系。而 keyword 指明查询条件是一个 equals 关系。
# keywords
GET bank/_search
{"query": {"match": {"address.keyword": "880 Holmes"}}
}GET bank/_search
{"query": {"match_phrase": {"address.keyword": "880 Holmes"}}
}
使用 keyword : 会查询出 address 与 880 Holmes 相等的记录
使用 match_phrase: 会查询出 address 包含 880 Holmes 短语的记录
(9) aggregations(执行聚合)
聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于 SQL GROUP BY 和 SQL 聚合函数。在 Elasticsearch 中,您有执行搜索返回 hits(命中结果),并且同时返 回聚合结果,把一个响应中的所有 hits(命中结果)分隔开的能力。这是非常强大且有效的, 您可以执行查询和多个聚合,并且在一次使用中得到各自的(任何一个的)返回结果,使用 一次简洁和简化的 API 来避免网络往返。
语法格式:
文档:Aggregations | Elasticsearch Guide 7.4] | Elastic
"aggregations" : {"<aggregation_name>" : {"<aggregation_type>" : {<aggregation_body>}[,"meta" : { [<meta_data_body>] } ]?[,"aggregations" : { [<sub_aggregation>]+ } ]?}[,"<aggregation_name_2>" : { ... } ]*
}
aggregation_name :为本次聚合设置一个名字(自定义)
aggregation_type : 聚合的类型(官方提供的聚合类型)
aggregation_body : 实现聚合
aggregation_name_2 : 可以设置多个聚合
案例一:搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄,但不显示这些人的详情。
GET bank/_search
{"query": {"match": {"address": "mill"}},"aggs": {"aggAge": {"terms": {"field": "age","size": 10}},"ageAvg": {"avg": {"field": "age"}}},"size": 0
}
第一个聚合 aggAge 统计 age 的分布情况,并显示前10条记录
第二个聚合 ageAvg 统计 age 的平均年龄
"size": 0 不显示查询记录的详情
返回结果
案例二:按照年龄聚合,并且请求这些年龄段的这些人的平均薪资
在 aggAge 聚合里面嵌套 balanceAgg 聚合:先查看年龄分步情况,基于以上的结果,计算平均薪资
# **案例二**:按照年龄聚合,并且请求这些年龄段的这些人的平均薪资
GET bank/_search
{"query": {"match_all": {}},"aggs": {"aggAge": {"terms": {"field": "age","size": 10},"aggs": {"balanceAgg": {"avg": {"field": "balance"}}}}},"size": 0
}
案例三:查出所有年龄分布,并且这些年龄段中 M 的平均薪资和 F 的平均薪资以及这个年龄 段的总体平均薪资
# 查出所有年龄分布,并且这些年龄段中 M 的平均薪资和 F 的平均薪资以及这个年龄 段的总体平均薪资
GET bank/_search
{"query": {"match_all": {}},"aggs": {"ageAgg": {"terms": {"field": "age","size": 100},"aggs": {"genderAgg": {"terms": {"field": "gender.keyword"},"aggs": {"balanceAgg": {"avg": {"field": "balance"}}}}}}},"size": 0
}
3、Mapping 映射
Mapping 是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和 索引的。
比如:
- 哪些字段是 text类型,哪些字段是 boolean 类型
ES 中的映射类型有:
(1)新版本改变
Es7 及以上移除了 type 的概念。
关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用, 但 ES 中不是这样的。elasticsearch 是基于 Lucene 开发的搜索引擎,而 ES 中不同 type 下名称相同的 filed 最终在 Lucene 中的处理方式是一样的。
- 两个不同 type 下的两个 user_name,在 ES 同一个索引下其实被认为是同一个 filed, 你必须在两个不同的 type 中定义相同的 filed 映射。否则,不同 type 中的相同字段 名称就会在处理中出现冲突的情况,导致 Lucene 处理效率下降。
- 去掉 type 就是为了提高 ES 处理数据的效率。
(2)创建索引并指定映射
# 创建索引并指定映射
PUT /my_index
{"mappings": {"properties": {"age": {"type": "integer"},"address": {"type": "text"}}}
}
(3)添加新字段映射
“index”: false 表示不会被索引到
# 增加新的映射关系
PUT /my_index/_mapping
{"properties": {"email": {"type": "keyword","index": false}}
}
(4)更新字段映射
对于已经存在的映射是无法更新的。否则就会报错
{"error": {"root_cause": [{"type": "resource_already_exists_exception","reason": "index [my_index/c7ZZpqz9RzGdKJS_FTE3qA] already exists","index_uuid": "c7ZZpqz9RzGdKJS_FTE3qA","index": "my_index"}],"type": "resource_already_exists_exception","reason": "index [my_index/c7ZZpqz9RzGdKJS_FTE3qA] already exists","index_uuid": "c7ZZpqz9RzGdKJS_FTE3qA","index": "my_index"},"status": 400
}
想要更新映射,唯一的方法就是创建新的映射关系,然后将数据迁移过去。
除了支持的映射参数外,您不能更改现有字段的映射或字段类型。更改现有字段可能会使已编入索引的数据失效。如果需要更改字段的映射,请使用正确的映射创建一个新索引,然后将数据重新索引到该索引中。
(5)数据迁移
基本语法:
POST _reindex
{
"source": { "index": "twitter"},
"dest": {"index": "new_twitter"}
}
如果是 ES6之前指定了 Type
POST _reindex
{
"source": { "index": "twitter","type" : "type"},
"dest": {"index": "new_twitter"}
}
1、创建新的映射
# 创建新的映射
PUT /new_bank
{"mappings": {"properties": {"address": {"type": "text"},"age": {"type": "integer"},"balance": {"type": "long"},"city": {"type": "keyword"},"email": {"type": "keyword"},"employer": {"type": "keyword"},"firstname": {"type": "keyword"},"gender": {"type": "keyword"},"lastname": {"type": "keyword"},"state": {"type": "keyword"}}}
}
2、迁移数据
# 数据迁移
POST _reindex
{"source": {"index": "bank","type": "account"},"dest": {"index": "new_bank"}
}
4、分词
一个 tokenizer(分词器)接收一个字符流,将之分割为独立的 tokens(词元,通常是独立 的单词),然后输出 tokens 流。
例如,whitespace tokenizer 遇到空白字符时分割文本。它会将文本 “Quick brown fox!” 分割 为 [Quick, brown,fox!]
ES 提供的分词器都是只支持英文分词,如果是中文就有些不尽人意了。
# 使用分词器
POST _analyze
{"analyzer": "standard","text": "我爱中国"
}
分词结果:按照我们中国人的逻辑 正常是 [我 , 爱 ,中国]
(1)安装 ik 分词
查看对应版本安装: Releases · medcl/elasticsearch-analysis-ik (github.com)
由于我们在启动 ES 时,将 plugins 与我们主机目录进行了挂载,只需要通过 XFTP 将压缩包 传入 主机的 /mydata/elasticsearch/plugins 目录进行解压即可。
使用命令解压缩:
unzip 压缩包名称
我们可以进入到 ES 容器内部查看插件是否安装:
docker exec -it 容器id /bin/bash
# 切换到 bin 目录下
# 查看安装的 插件
elasticsearch-plugin list
安装好后,重启 ES 容器
docker restart 容器id
使用 ik 分词器:
# 使用 ik 分词器
POST _analyze
{"analyzer": "ik_smart","text": "我爱中国"
}
POST _analyze
{"analyzer": "ik_max_word","text": "我爱中国"
}
(2)自定义词库
虽然使用了 ik 分词器,但是一些词语的分词还是达不到我们想要的效果,这是因为 ES 中的默认词库中没有我们想要的单词,因此我们需要自定义一个词库,每次分词都先在自定义词库中查询。
比如:期待的分词效果就是 [尚硅谷, 电商, 项目]
POST _analyze
{"analyzer": "ik_smart","text": "尚硅谷电商项目"
}
但是分词结果:
我们可以利用 Nginx 做转发,将分词的查询请求发送给 Nginx
1、首先在 /mydata/nginx/html/es
目录下创建 fenci.txt 用来做我们的词库,我写的词语:
2、Nginx 默认是从 html 目录下访问 index.html 页面,我们就可以指定词库的路径 192.168.56.111/es/fenci.txt
,从页面上也是可以访问到的
3、修改 /mydata/elasticsearch/plugins/ik/config
下的 IKAnalyzer.cfg.xml
文件,配置我们自定义的词库访问地址
4、重启 ES 容器
5、测试效果
POST _analyze
{"analyzer": "ik_smart","text": "尚硅谷电商项目"
}
分析达到我们想要的效果了
五、Elasticsearch-Rest-Client
1、9300:TCP
- spring-data-elasticsearch:transport-api.jar;
- springboot 版本不同, transport-api.jar 不同,不能适配 es 版本
- 7.x 已经不建议使用,8 以后就要废弃
2、9200:HTTP
- JestClient:非官方,更新慢
- RestTemplate:模拟发 HTTP 请求,ES 很多操作需要自己封装,麻烦
- HttpClient:同上
3、Elasticsearch-Rest-Client:官方 RestClient,封装了 ES 操作,API 层次分明,上手简单
最终选择 Elasticsearch-Rest-Client(elasticsearch-rest-high-level-client) https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html
1、SpringBoot 整合 ES
(1)导入依赖
要在 properties 指明 elasticSearch的全局版本号。因为在 SpringBoot 中的默认版本号 是 6.4.3
<properties><java.version>8</java.version><elasticsearch.version>7.4.2</elasticsearch.version></properties><!--elasticSearch--><dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId><version>${elasticsearch.version}</version></dependency>
(2)编写配置
@Configuration
public class GulimallElasticSearchConfig {@Beanpublic RestHighLevelClient esClient() {RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(new HttpHost("192.168.56.111", 9200, "http")));return client;}
}
2、使用 SpringBoot 测试 ElasticSearch
ElasticSearch 的所有 API 使用方法都能在 官方文档中找到:Update API | Java REST Client 7.4] | Elastic
(1)增加索引
(1)创建共享的请求部分 放在 配置类中
public static final RequestOptions COMMON_OPTIONS;static {RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();// builder.addHeader("Authorization", "Bearer " + TOKEN);// builder.setHttpAsyncResponseConsumerFactory(// new HttpAsyncResponseConsumerFactory// .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024));COMMON_OPTIONS = builder.build();}
(2)测试新增索引
@Testpublic void addIndex() throws IOException {// 参数为索引名IndexRequest request = new IndexRequest("users");// 设置idrequest.id("1");// 第一种封装数据方式// request.source("name","张三","age","17","address","河北");// 第二种封装数据方式User user = new User();user.setName("张三");user.setAge(22);user.setAddress("二仙桥");// 将对象转换为 jsonString toJSONString = JSON.toJSONString(user);request.source(toJSONString, XContentType.JSON);// 发送请求新增索引IndexResponse response = restHighLevelClient.index(request, GulimallElasticSearchConfig.COMMON_OPTIONS);System.out.println(response);}@Dataclass User {private String name;private Integer age;private String address;}
(3)查看结果
(2)更新索引
/** 更新索引* */@Testpublic void test() throws IOException {// index,idUpdateRequest updateRequest = new UpdateRequest("users","1");User user = new User();user.setName("李四");String jsonString = JSON.toJSONString(user);updateRequest.doc(jsonString,XContentType.JSON);UpdateResponse updateResponse = restHighLevelClient.update(updateRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);System.out.println(updateResponse);}
(3)删除索引
@Testpublic void delIndex() throws IOException {DeleteRequest deleteRequest = new DeleteRequest("users","1");DeleteResponse deleteResponse = restHighLevelClient.delete(deleteRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);System.out.println(deleteRequest);}
(4)复杂查询
1、构建普通的查询条件
// 创建查询请求
SearchRequest searchRequest = new SearchRequest();
// 构建DSL检索条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
searchRequest.source(searchSourceBuilder);
2、聚合条件
“构建聚合”提供了所有可用聚合及其对应的AggregationBuilder对象和AggregationBuilders帮助方法的列表。
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();TermsAggregationBuilder aggregation = AggregationBuilders.terms("by_company").field("company.keyword");
aggregation.subAggregation(AggregationBuilders.avg("average_age").field("age"));searchSourceBuilder.aggregation(aggregation);
3、返回信息
通过执行搜索返回的SearchResponse提供了有关搜索执行本身的详细信息以及对返回文档的访问。首先,有关于请求执行本身的有用信息,如HTTP状态代码、执行时间或请求是否提前终止或超时:
RestStatus status = searchResponse.status();
TimeValue took = searchResponse.getTook();
Boolean terminatedEarly = searchResponse.isTerminatedEarly();
boolean timedOut = searchResponse.isTimedOut();
获取分片信息
int totalShards = searchResponse.getTotalShards();
int successfulShards = searchResponse.getSuccessfulShards();
int failedShards = searchResponse.getFailedShards();
for (ShardSearchFailure failure : searchResponse.getShardFailures()) {// failures should be handled here
}
对应图中:
要访问返回的文档,我们需要首先获取响应中包含的SearchHits:
SearchHits hits = searchResponse.getHits();
SearchHits提供所有点击的全局信息,如点击总数或最高得分:
TotalHits totalHits = hits.getTotalHits();
// the total number of hits, must be interpreted in the context of totalHits.relation
long numHits = totalHits.value;
TotalHits.Relation relation = totalHits.relation;
float maxScore = hits.getMaxScore();
对应图中:
获取查询出来的文档记录
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {// do something with the SearchHit
}
返回的聚合信息:
Aggregations aggregations = searchResponse.getAggregations();
// 通过聚合名获取聚合信息
Terms byCompanyAggregation = aggregations.get("ageAgg");
// 获取key
Bucket elasticBucket = byCompanyAggregation.getBucketByKey("key"); Avg averageAge = elasticBucket.getAggregations().get("BalanceAgg");
// 获取 value
double avg = averageAge.getValue();
案例一: 在 bank 索引中,按照年龄聚合,并且请求这些年龄段的这些人的平均薪资
@Testpublic void search() throws IOException {// 案例: 按照年龄聚合,并且请求这些年龄段的这些人的平均薪资SearchRequest searchRequest = new SearchRequest("bank");// 1、sourceBuilder 构建DSL检索条件SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();// query中的match// sourceBuilder.query(QueryBuilders.matchQuery("address","mill"));// query 中的 match_phrase// sourceBuilder.query(QueryBuilders.matchPhraseQuery("address","Holmes Lane "));// 2、sourceBuilder 同样可以构建分页信息// sourceBuilder.from(10);// sourceBuilder.size(20);// query中的match_allsourceBuilder.query(QueryBuilders.matchAllQuery());// 3、sourceBuilder封装聚合条件// subAggregation 可嵌套聚合TermsAggregationBuilder aggregation = AggregationBuilders.terms("ageAgg").field("age").size(10).subAggregation(AggregationBuilders.avg("BalanceAgg").field("balance"));sourceBuilder.aggregation(aggregation);searchRequest.source(sourceBuilder);System.out.println("查询条件: " + sourceBuilder);// 执行检索SearchResponse response = restHighLevelClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);System.out.println("响应结果: " +response);// 4、分析结果SearchHits hits = response.getHits();SearchHit[] searchHits = hits.getHits();for (SearchHit searchHit : searchHits) {// 查询结果String sourceAsString = searchHit.getSourceAsString();// 通过JSON转换工具,将json——》实体类AccountBean accountBean = JSON.parseObject(sourceAsString, AccountBean.class);System.out.println("Account对象: " + accountBean);}// 5、查看聚合信息Aggregations aggregations = response.getAggregations();// 获取指定的聚合Terms ageAgg = aggregations.get("ageAgg");for (Terms.Bucket bucket : ageAgg.getBuckets()) {System.out.println("年龄: " + bucket.getKeyAsString());System.out.println("人数: " + bucket.getDocCount());// 获取嵌套的聚合Avg balanceAgg = bucket.getAggregations().get("BalanceAgg");System.out.println("平均薪资: " + balanceAgg.getValue());}}
案例二 :搜索 bank 索引中包含 mill 的所有人的年龄分布以及平均年龄,但不显示这些人的详情。
/** 案例* */@Testpublic void caseTwo() throws IOException {// 搜索 bank 中包含 mill 的所有人的年龄分布以及平均年龄,但不显示这些人的详情。SearchRequest searchRequest = new SearchRequest("bank");// 构建DSL 查询条件SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();sourceBuilder.query(QueryBuilders.matchQuery("address","mill"));sourceBuilder.size(0);// 构建聚合TermsAggregationBuilder aggregation1 = AggregationBuilders.terms("ageAgg").field("age");AvgAggregationBuilder aggregation2 = AggregationBuilders.avg("avgAgg").field("age");sourceBuilder.aggregation(aggregation1);sourceBuilder.aggregation(aggregation2);searchRequest.source(sourceBuilder);System.out.println("条件: " + sourceBuilder);// 发送请求SearchResponse searchResponse = restHighLevelClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);// 获取聚合结果Terms ageAgg = searchResponse.getAggregations().get("ageAgg");for (Terms.Bucket bucket : ageAgg.getBuckets()) {System.out.println("年龄: " + bucket.getKey());System.out.println("人数: " + bucket.getDocCount());}Avg avgAgg = searchResponse.getAggregations().get("avgAgg");System.out.println("平均年龄: " + avgAgg.getValue());}
六、拓展:Docker 安装 Nginx
1、先随便启动一个 Nginx 复制出里面的配置文件
docker run -p 80:80 --name nginx -d nginx:1.10
2、创建 /mydata/nginx/conf 目录, 并切换到此目录下,将 Nginx 配置文件都拷贝文件夹里
docker container cp nginx:/etc/nginx .
3、删除容器,并重新启动 Nginx 容器
docker run -p 80:80 --name nginx --restart=always \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-v /mydata/nginx/conf:/etc/nginx \
-d nginx:1.10
4、访问 虚拟机 IP 地址即可访问 Nginx
相关文章:
分布式高级篇1 —— 全文检索
Elasticsearch Elasticsearch简介一、基本概念1、index(索引)2、Type(类型)3、Document(文档)4、倒排索引二、Docker 安装 EL1、拉取镜像2、创建实例三、初步探索1、_cat2、索引一个文档(保存)3、查询文档3、更新文档4、删除文档&索引5、_bulk 批量 AP6、样本测试数据四、进…...
集成电路开发及应用-模拟数字部分专栏目录
三角波发生器电路图分析_XMJYBY的博客-CSDN博客运算放大器正反馈负反馈判别法_如何理解运算放大器的反馈机制,分哪几种_XMJYBY的博客-CSDN博客运算放大器实现多路同向反向加减运算电路公式推导(一)_反向减法运算电路_XMJYBY的博客-CSDN博客运算放大器实现多路同向反向加减运算电…...
ios使用SARUnArchiveANY 解压rar文件(oc和swift版本)
SARUnArchiveANY简介 开源库网址: https://github.com/saru2020/SARUnArchiveANY 简介: 一个iOS的非常有用的库来解压zip,.rar,7z文件。 他是以下库的简单集成: UnrarKitSSZipArchiveLzmaSDKObjC (7z) 需要注意的是…...
【Python学习笔记】21.Python3 函数(2)
前言 本章介绍调用函数时可使用的正式参数。 参数 以下是调用函数时可使用的正式参数类型: 必需参数关键字参数默认参数不定长参数 必需参数 必需参数须以正确的顺序传入函数。调用时的数量必须和声明时的一样。 调用 printme() 函数,你必须传入一…...
day57回文子串_最长回文子序列
力扣647.回文子串 题目链接:https://leetcode.cn/problems/palindromic-substrings/ 思路 dp数组含义 dp[i][j]:以s[i]为开头,s[j]为结尾的子串是否是回文子串 递推公式 子串范围为[i,j],当s[i]s[j]时,有三种情况࿱…...
Element UI框架学习篇(二)
Element UI框架学习篇(二) 1 整体布局 1.1 前提说明 el-container标签里面的标签默认是从左往右排列,若想要从上往下排列,只需要写el-header或者el-footer就行了 <el-container>:外层容器 <el-header>:顶栏容器。 <el-aside>&#…...
【C++】类与对象(上)
文章目录一、面向过程和面向对象初步认识二、类的引入三、类的定义四、类的访问限定符及封装①访问限定符②封装五、类的作用域六、类的实例化七、类对象模型①如何计算类对象大小②类对象的存储方式③结构体中内存对齐规则八、this指针①this指针的引出②this指针的特性一、面…...
Leetcode.1797 设计一个验证系统
题目链接 Leetcode.1797 设计一个验证系统 Rating : 1534 题目描述 你需要设计一个包含验证码的验证系统。每一次验证中,用户会收到一个新的验证码,这个验证码在 currentTime时刻之后 timeToLive秒过期。如果验证码被更新了,那么它会在 curr…...
Kaldi - 数据文件准备
文章目录数据文件准备wav.scputt2spkspk2utttext相关代码根据文件生成 utt2spk 和 wav.scputt2spk -- spk2utt 转换数据文件准备 在训练/解码中: 有三个文件是必要的: wav.scp 语音编号 – 路径信息utt2spk 语音编号 – 说话人编号spk2utt 说话人编号 …...
91.【SpringBoot-03】
SpringBoot-03(十四)、任务1.异步任务2.邮件任务(1).简单邮箱发送(2).复杂邮箱发送3.定时任务(1).cron表达式(2).特殊表达式(3).定时任务测试(4).常用cron表达式(十五)、Dubbo和Zookeeper集成1.分布式原理(1).Dubbo文档2.什么是RPC?3.Dubbo的概念和介绍(1).Dubbo是什么(2). Du…...
【本地项目】上传到【GitLab】流程详解
文章目录1、安装Git2、创建GitLab项目文件夹3、创建密钥4、向GitLab上传项目注意:本篇文章中提到的上传流程所需要的命令,几乎在GitLab的Command line instructions中都有所记载 1、安装Git 具体安装流程这里不做过多说明,安装流程可以参考…...
初阶指针C
🚀🚀🚀大家觉不错的话,就恳求大家点点关注,点点小爱心,指点指点🚀🚀🚀 目录 🐰指针是什么 🐰指针和指针类型 🌸指针-整数 &#x…...
云原生安全2.X 进化论系列|揭秘云原生安全2.X的五大特征
随着云计算技术的蓬勃发展,传统上云实践中的应用升级缓慢、架构臃肿、无法快速迭代等“痛点”日益明显。能够有效解决这些“痛点”的云原生技术正蓬勃发展,成为赋能业务创新的重要推动力,并已经应用到企业核心业务。然而,云原生技…...
json文件在faster_rcnn中从测试到训练 可行性
1.确认任务 经过mydataset文件处理后 - > 在train_res50_fpn文件内应用 # load train data set # VOCdevkit -> VOC2012 -> ImageSets -> Main -> train.txt train_dataset VOCDataSet(VOC_root, "2012", data_transform["train"], &…...
golang 1.20正式发布,更好更易更强
预期中的Go 2不会有了,1.20也算是一个小gap,从中可以一窥Go未来的发展之路。对于Go来说,未来保持1.x持续演进和兼容性之外,重点就是让Go性能更优,同时保持大道至简原则,使用尽可能容易,从这两个…...
图片显示一半怎么回事?
不知道小伙伴是否遇到过,刚刚上传的一个文件夹,有一多半的图片突然就变成了无法显示该图片或者是图片显示一半,而另外一半就显示灰色蓝色粉色条状。而且还把原文件删除了。面对这种情况,有什么解决方法呢?下面让我们一起来来看看…...
102-并发编程详解(中篇)
这里续写上一章博客 Phaser新特性 : 特性1:动态调整线程个数 CyclicBarrier 所要同步的线程个数是在构造方法中指定的,之后不能更改,而 Phaser 可以在运行期间动态地 调整要同步的线程个数,Phaser 提供了下面这些方…...
jsp羽毛球场馆管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目
一、源码特点 jsp 羽毛球场馆管理系统 是一套完善的web设计系统,对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,Myeclipse8.5开发,数据库为Mysql,…...
CacheLib 原理说明
CacheLib 介绍 CacheLib 是 facebook 开源的一个用于访问和管理缓存数据的 C 库。它是一个线程安全的 API,使开发人员能够构建和自定义可扩展的并发缓存。 主要功能: 实现了针对 DRAM 和 NVM 的混合缓存,可以将从 DRAM 驱逐的缓存数据持久…...
【dapr】服务调用(Service Invokation) - app id的解析
逻辑图解 上图来自Dapr官网教程,其中Checkout是一个服务,负责生成订单号, Order Processor是另一个服务,负责处理订单。Checkout服务需要调用Order Processor的API, 让Order Processor获取到其生成的订单号并进行处理。…...
Odoo丨5步轻松实现在Odoo中打开企微会话框
Odoo丨5步轻松实现在Odoo中打开企微会话框 在Odoo中开启企微会话框 企业微信作为一个很好的企业级应用发布平台,尤其是提供的数据和接口,极大地为很多企业级应用提供便利,在日常中应用广泛! 最近在项目中就遇到一个与企业微信相…...
python读取.stl文件
目录 .1 文本方式读取 1.2 stl解析 1.3 stl创建 .2 把点转换为.stl .1 文本方式读取 代码如下 stl_path/home/pxing/codes/point_improve/data/003_cracker_box/0.stlpoints[] f open(stl_path) lines f.readlines() prefixvertex num3 for line in lines:#print (l…...
vue2.0项目第一部分
论坛项目后端管理系统服务器地址:http://172.16.11.18:9090swagger地址:http://172.16.11.18:9090/doc.html前端h5地址:http://172.16.11.18:9099/h5/#/前端管理系统地址:http://172.16.11.18:9099/admin/#/搭建项目vue create . …...
锁与原子操作
锁与原子操作 锁 以自增操作为例子: void *func(void *arg) {int *pcount (int *)arg;int i 0;//while (i < 100000) {(*pcount) ; // 并不会到达100000usleep(1);} }int main(){int i 0;for (i 0;i < THREAD_COUNT;i ) {pthread_create(&thid…...
Prometheus Pushgetway讲解与实战操作
目录 一、概述 1、Pushgateway优点: 2、Pushgateway缺点: 二、Pushgateway 架构 三、实战操作演示...
常见字符串函数的使用,你确定不进来看看吗?
👦个人主页:Weraphael ✍🏻作者简介:目前是C语言学习者 ✈️专栏:C语言航路 🐋 希望大家多多支持,咱一起进步!😁 如果文章对你有帮助的话 欢迎 评论💬 点赞&a…...
Elasticsearch:在搜索中使用衰减函数(Gauss)
在我之前的文章 “Elasticsearch:使用 function_score 及 script_score 定制搜索结果的分数” 我有讲到 Decay 函数在搜索中的使用。在那里,我有一个例子讲述在规定的时间里,分数不进行衰减。同一的函数也可以适用于地理位置的搜索。位置搜索…...
微信小程序 Springboot英语在线学习助手系统 uniapp
四六级助手系统用户端是基于微信小程序端,管理员端是基于web端,本系统是基于java编程语言,mysql数据库,idea开发工具, 系统分为用户和管理员两个角色,其中用户可以注册登陆小程序,查看英语四六级…...
LeetCode算法题解——双指针2
LeetCode算法题解——双指针2第五题思路代码第六题思路代码第七题思路代码这里介绍双指针在数组中的第二类题型:两端夹击。 第五题 977. 有序数组的平方 题目描述: 给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的…...
线性杂双功能peg化试剂——HS-PEG-COOH,Thiol-PEG-Acid
英文名称:HS-PEG-COOH,Thiol-PEG-Acid 中文名称:巯基-聚乙二醇-羧基 HS-PEG-COOH是一种含有硫醇和羧酸的线性杂双功能聚乙二醇化试剂。它是一种有用的带有PEG间隔基的交联或生物结合试剂。巯基或SH、巯基或巯基选择性地与马来酰亚胺、OPSS、…...
南通动态网站建设/2023近期舆情热点事件
新职业层出不穷,老职业越老越吃香。人才市场的竞争永远激烈,其间有多少是最受职场人士关注、三千宠爱集一身的职业?未来几年的金牌职业有哪些?我们的金牌职业、俗称“金饭碗”具有以下特征:含金量高、收入多、发展前景广阔、相对稳定、身上…...
旅游wordpress/网站是否含有seo收录功能
一、简介 EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点。ehcache官网:http://www.ehcache.org/ 可以下载文档看看,里面关于EhCache缓存写的非常清楚。 二、特点 主要的特性有: 1. 快速 2. 简单 3. 多种缓存策略 …...
wordpress底部CSS/app推广平台
据统计,全球范围内被投递的钓鱼邮件每天约达到1亿封,经常会遇到一些邮件发送方,被spammer利用于伪造各种钓鱼/诈骗邮件,如:伪造银行、保险等金融企业,支付宝、Paypal等支付商,知名网站、政府网站…...
8x8x域名解析ip地址查询 1080p/北京seo做排名
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。 基本思路:建立一个包含K个元素的大顶堆。 注:python中貌似只能直接建小顶堆。 # -*- coding:utf-8 -*- class Solution:def …...
抚宁网站建设/推广普通话宣传语100字
已结贴√问题点数:10 回复次数:21关于田忌赛马问题.。。帮忙看下。。谢谢了。。题目描述Here is a famous story in Chinese history."That was about 2300 years ago. General Tian Ji was a high official in the country Qi. He likes to play h…...
广德县建设协会网站/企业模板建站
html中form表示一个表单,用来把一系列的控件包围起来,然后再统一发送这些数据到目标,比如最常见的注册,你说需要填写的资料,都是被封装在form里的,填写完毕后,提交form内的内容,如果不再form内则不会提交而table则是用来布局的 当你填写资料的时候 你有没有发现页面所提供的文本…...