ElasticSearch 零基础入门(1):基本概念、集群模式、查询语法和聚合语法

目录

说明

Elasticsearch 是一个准实时的分布式搜索分析引擎,提供索引、搜索和分析功能。 Logstash、xxBeats 是配套的数据导入工具,Kibana 提供了可视化页面。

学习资料:

基本概念:Document、Index 和 Mapping

Document:一份序列化成 json 格式的文档数据,它由多个 filed 组成,每个 field 是一个 key-value。

Index: es 中的 Document 的组织单位,每个 Document 都隶属一个 Index。

Mapping: index 中记录 Document 的每个 filed 的数值类型。

与关系型数据库类比:

                   关系型数据库                     ES  
----------------------------------------------------------------------------
管理的数据单元     包含多个列的关系型行记录        包含多个k-v filed 的json 字符串
数据的组织单位     数据表                          Index 索引
数据类型描述       建表语句                        Index 的 Mapping

Document 的每个 field 可以使用不同的数据类型,es 根据 field 的类型使用不同的索引方式,例如:

  1. text fields 存储在倒排索引中
  2. numeric 和 geo 存储在 BKD tree 中

Mapping 可以手动创建,或者启用 dynamic mapping, 让 es 自动添加并推断 filed 的数据类型。

ES 的特点:

  1. 文档数据(Document)写入后会在 1s 之内完成索引、可被搜索
  2. 为 text 类型的 filed 建立「倒排索引/ inverted index」,支持快速的全文搜索

快速上手

Es 检查运行状态:

curl http://127.0.0.1:9200

创建 Index

创建 Index 并设置 Mapping:

PUT /my-index-000001?pretty
{
  "mappings": {
    "properties": {
      "age":    { "type": "integer" },  
      "email":  { "type": "keyword"  }, 
      "name":   { "type": "text"  }     
    }
  }
}

PUT 上传的内容就是 Mapping,其中 age、email、name 是 Document 中的 filed 名称,type 是 filed 的数值类型。。

查看 Index

查看 es 中所有 index:

GET /_cat/indices?v

health status index                          uuid                   pri rep docs.count docs.deleted store.size pri.store.size
green  open   .kibana-event-log-7.9.2-000001 BXulL2qmTTq2FYYOYdNqbQ   1   0          1            0      5.5kb          5.5kb
green  open   .apm-custom-link               CS8LkfBvRKGdSXjI0ky6tw   1   0          0            0       208b           208b
green  open   .kibana_task_manager_1         569Jhi3NS8etY8pYF4jxvg   1   0          6          267    136.2kb        136.2kb
green  open   kibana_sample_data_ecommerce   _6pu_ggDQCSLwL9jvrA-Fg   1   0       4675            0      4.7mb          4.7mb
green  open   .apm-agent-configuration       t9t6VR2KQi-YDewFQjI0Fw   1   0          0            0       208b           208b
green  open   .kibana_1                      pEI_2VXrTRagNq_fyWd5eg   1   0         75            1     12.6mb         12.6mb
yellow open   my-index-000001                6mmUoVrASWSTBWQjKDicqg   1   1          0            0       208b           208b

查看 Index my-index-000001 的 Mapping:

GET  /my-index-000001/_mapping

{
  "my-index-000001" : {
    "mappings" : {
      "properties" : {
        "age" : {
          "type" : "integer"
        },
        "email" : {
          "type" : "keyword"
        },
        "name" : {
          "type" : "text"
        }
      }
    }
  }
}

写入 Document

_doc 是 ES 定义的接口路径,1 是要写入的 Document 的 id,Body 是 Document 内容:

PUT /my-index-000001/_doc/1
{
      "age":    11,  
      "email":  "[email protected]", 
      "name":   "小王"   
}

查询 Document:通过 ID 查询

查询 ID 为 1 的 Document:

GET  /my-index-000001/_doc/1

{
  "_index" : "my-index-000001",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "age" : 11,
    "email" : "[email protected]",
    "name" : "小王"
  }
}

返回的结果中带 _ 前缀 filed 是 Document 的 Metadata fileds,ES 支持的 Metadata fields 列表:

查询 Document:通过 field 数值查找

查找年龄为 11 的 Document,使用 term 语句查询:

GET /my-index-000001/_search
{
  "query": {
    "term": {
      "age": 11
    }
  }
}

通过匹配 field 数值查询得到的内容是一个列表,内容多于通过 id 查询返回的内容,hits.hits 是命中查询条件的文档列表,其它字段是对查询结果的说明:

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my-index-000001",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "age" : 11,
          "email" : "[email protected]",
          "name" : "小王"
        }
      }
    ]
  }
}

删除 Document

DELETE /my-index-000001/_doc/1
{
  "_index" : "my-index-000001",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 6,
  "result" : "deleted",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 5,
  "_primary_term" : 1
}

数据已经删除:

GET /my-index-000001/_doc/1
{
  "_index" : "my-index-000001",
  "_type" : "_doc",
  "_id" : "1",
  "found" : false
}

集群模式

ES 支持集群模式部署 Set up a cluster for high availability,支持数据多副本备份。

节点角色

ES 支持的功能较多(譬如机器学习),为了更好的支持这些功能,es 定义了多种节点角色,一个节点可以同时担任多种角色,默认具有以下角色:

  1. master
  2. data
  3. data_content
  4. data_hot
  5. data_warm
  6. data_cold
  7. data_frozen
  8. ingest
  9. ml
  10. remote_cluster_client
  11. transform

ES 集群中的节点大致可以分为管理节点数据节点任务节点

管理节点

  1. 角色配置为 master,能够参与 leader 投票、可以被选为 leader 的节点
  2. 被选为 leader 的节点负责集群的管理工作
  3. 如果同时配置了 voting_only 角色,该节点只参与选举投票,不做候选人

数据节点

  1. 角色配置为 data,负责存储数据和提供数据查询、聚合处理的节点
  2. 数据节点可以进一步细分:data_content,data_hot,data_warm,data_cold,data_frozen
  3. data_content: 存放 document 数据
  4. data_hot: 存放刚写入的时间序列数据(热点数据),要能够快速读写
  5. data_warm:不再经常更新、低频查询的数据
  6. data_cold:极少访问的只读数据
  7. data_frozen:缓存从快照中查询出的数据,如果不存在从快照读取后缓存,

任务节点 类型较多,每个角色负责专门的任务:

  1. []: 角色为空,coordinating node,没有任何角色,只负责将收到的请求转发到其它节点
  2. ingest:承担 pipeline 处理任务的节点
  3. remote_cluster_client:跨集群操作时,与其它机器进行通信的节点
  4. ml:执行机器学习任务和处理机器学习 api 的节点,通常建议同时配置角色 remote_cluster_client
  5. transform:数据处理节点,对文档数据进行再次加工,通常建议同时配置角色 remote_cluster_client

数据存储方式

Scalability and resilience: clusters, nodes, and shards

文档的组织单位是 index,每个 index 对应有多个 shard(数据分片),每个 shard 是一个小的 index。

Index 中的文档被分散到一组 shard 上存储,这些 shard 分布在不同的机器上。

Shard 分为 primariy shardreplica shard

primariy shards : 和其它 primariy shard 合并承担 index 中全量文档的存储,每个文档只会被 primariy shard 存储一次。primariy shard 目的是用水平扩展方式提高数据存储能力。

replica shards: 每个 primariy shard 对应的备份。replica shard 目的是对数据进行冗余备份,防丢失。

primariy shards 数量在 index 创建时固定,replica shards 数量可以随时更改。

shards 数量建议

primariy shards 的数量如果太多:

  1. 每个 shard 上存储的数据越少,管理开销会增加
  2. 单个 shard 的查询虽然加快,但是用户的一次查询可能要被分发到更多 shard 上执行,整体变慢

primariy shards 的数量如果太少:

  1. 单个 shard 过大,es 进行数据迁移的时间增加

推荐做法:

  1. 单个 shard 的大小控制在 几GB~几十GB,如果是时间序列数据,单个 Shard 建议 20GB~40GB

更多做法参考:testing with your own data and queries

跨集群备份

ES 支持跨集群备份,即创建另外一个集群做为备集群,主集群负责写操作,副集群平时只读,在主集群故障接替主集群,见 Cross-cluster replication

容灾部分方案参考:Designing for resilience

正确性与一致性等级

Reading and Writing documents 介绍了 ES 的读写行为。

ES 的数据备份使用的 primary-backup 模型:

  1. primary shard 承接写操作 ,并负责将改动同步到 replica shards
  2. primary shard 和 replica shards 承接读操作

primay-backup 模型参考PacificA: Replication in Log-Based Distributed Storage Systems

ES 的正确性没有保障,无法保证正确的场景:

  1. 读到未确认的数据:primary shard 完成本地写之后,数据就对外可见,此时这些数据被没有同步到 replia shard
  2. 读到脏数据:primary shard 被“孤立/isolated”,完成了本地写,但无法同步到 replia shard,这时发送到该 primary shard 的读请求可能读到脏数据。

ES的一致性等级应该是 会话单调写一致,等级较低,只能保证每个节点上的数据写入顺序相同。

写操作过程

写操作

  1. coordinating stage:根据文档 ID 找到对应的 primary shard,将请求转发过去,
  2. primary stage:primary shard 完成本地操作后,将操作同步到 master 节点维护的 in-sync 队列中的 replica shards, 所有 replica shards 操作完成后 ,primary shard 向客户端返回成功
  3. replica stage:replica shards 完成本地操作产生数据副本的过程

coordinating stage、primary stage、replica stage,三个阶段串行执行,每个阶段都要下个阶段完成后,才返回完成。

异常处理

  1. 如果 primary shard 故障,当前写操作等待 master 节点选出新的 primary shard (默认等待 1 分钟),然后将请求转发给新选出的 primary shard
  2. 如果 primary shard 同步操作时没有得到 replica shard 的回应,primary shard 通知 master 将响应的 replica shard 从 in-sync 队列移除。得到 master 的回应后 ,primary shard 结束操作回应客户端。与此同时,master 新建一个 replica shard 进行数据同步
  3. 如果在执行期间 primary shard 失去了 primary 身份而不自知(断网隔离/长GC导致),它向 replica shard 发送请求后,会被 replica shard 拒绝,然后 primary shard 访问 mater 获知最新的 primary shard,将请求转发给新的 primary shard

读操作过程

读操作

读操作有两类,一类是通过 ID 直接读,一类是条件查询。

  1. 收到读请求的节点为 coordinating node,es 的所有节点都具备 coordinating 能力
  2. coordinating node 解析请求内容,找到该请求需要覆盖的 replica group
  3. coordinating node 从相关的 replica group 中选出一个 shard,可能是 primary shar 也可能是 replica shard
  4. coordinating node 将请求分拆后发送给选出的 shard
  5. coordinating node 收集所有 shard 返回的结果,整理后返回客户端。

第 3 步 coordinating node 选 shard 的时候,使用轮询法或者 Adaptive replica selection

异常处理

  1. 如果 shard 未返回查询响应 coordinating node 选择下一个 shard,直到成功或 shard 用尽

如果一次读请求涉及多个 shard,部分成功部分失败时,SearchMulti SearchBulkMulti Get 会返回成功部分的数据,并且返回码是 200 ok,失败 shard 的情况要从返回的响应头中获取。即可能返回不完整的数据

更多操作

Index 的自动创建

ES 支持在写入的 document 的时候自动创建 index,不需要提前创建 index:

PUT /new-index-000001/_doc/1
{
      "age":    11,  
      "email":  "[email protected]", 
      "name":   "小王"   
}

查看:

GET /new-index-000001/

批量写入Document

wget https://raw.githubusercontent.com/elastic/elasticsearch/master/docs/src/test/resources/accounts.json
curl -H "Content-Type: application/json" -XPOST "localhost:9200/banke/_bulk?pretty&refresh" --data-binary "@accounts.json"

field 数值类型汇总

field 可以使用以下数据类型, 不同版本的 es 支持情况可能不同,详情见 Field data types

常规类型:

  1. binary
  2. boolean
  3. keywords
  4. numbers
  5. date/date_nanos
  6. alias

对象类型:

  1. object
  2. flattened
  3. nested
  4. join

结构化类型:

  1. range
  2. ip
  3. version
  4. murmur3

聚合数据类型:

  1. aggregate_metric_double
  2. histogram

搜索文本类型

  1. text fields
  2. annotated-text
  3. completion
  4. search_as_you_type
  5. token_count

文档排名类型

  1. dense_vector
  2. sparse_evctor
  3. rank_feature
  4. rank_features

地理位置类型

  1. geo_point
  2. geo_shape
  3. point
  4. shape

其它类型

  1. percolator

数组类型不需要专门定义,每个 filed 的数值都可以是一个数组列表

一个 filed 可以被设置多种类型,以用于不同目的,这个特性叫做 multi-fields

如下所示,city field 有两个类型 text 和 keyword:

PUT my-index-000001
{
  "mappings": {
    "properties": {
      "city": {
        "type": "text",           <-- 第一个类型
        "fields": {               <-- 在 fileds 中继续设置其它类型
          "raw": { 
            "type":  "keyword"   
          }
        }
      }
    }
  }
}

field 的配置项汇总

Mapping 中的 field 不止有 type 这一个配置项,还有很多其它功能的配置,譬如下的 index

为已有的 Index 增加属性设置,_mapping

PUT /my-index-000001/_mapping
{
  "properties": {
    "employee-id": {
      "type": "keyword",
      "index": false         <-- false,不为 employee-id 创建索引
    }
  }
}

下面是 field 可用的配置项, 不同版本的 es 支持情况可能不同 Mapping Parameters

analyzer
boost
coerce
copy_to
doc_values
dynamic
eager_global_ordinals
enabled
fielddata
fields
format
ignore_above
ignore_malformed
index_options
index_phrases
index_prefixes
index
meta
normalizer
norms
null_value
position_increment_gap
properties
search_analyzer
similarity
store
term_vector

Document 的 Metadata fields

查询的 Document 的时候会发现,返回了很多带有 _ 前缀的 field:

GET  /my-index-000001/_doc/1

{
  "_index" : "my-index-000001",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "age" : 11,
    "email" : "[email protected]",
    "name" : "小王"
  }
}

ES 支持的 Metadata fields

_field_names field
_ignored field
_id field
_index field
_meta field
_routing field
_source field
_type field

查询语法:Query

下面示例中使用的是 kibana 中的 Dev Tools 提供的 http 接口。ES query 语法见 ES Query DSL

查询文档时,es 根据查询语句为已有的 Document 做出相关性评分,查询结果中的_score 就是评分情况。查询结果按照评分从高到低排列。

{
  "took" : 254,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my-index-000001",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "age" : 11,
          "email" : "[email protected]",
          "name" : "小王"
        }
      },
      {
        "_index" : "my-index-000001",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "age" : 11,
          "email" : "[email protected]",
          "name" : "小明"
        }
      }
    ]
  }
}

查询语句结构

查询语句分为叶子语句(Leaf query clauses)和组合语句(Compound query clauses)。

叶子语句:针对单个 field 查询,查询条件为指定 field 的数值满足xx条件,例如 match/term/range 等。

GET /banke/_search
{
  "query": { "match": { "address": "mill lane" } }
}

组合语句:是叶子语句/组合语句的组合,譬如多个叶子语句的条件组,例如 bool 查询语句。

POST _search
{
  "query": {
    "bool" : {
      "must" : {
        "term" : { "user.id" : "kimchy" }
      },
      "filter": {
        "term" : { "tags" : "production" }
      },
      "must_not" : {
        "range" : {
          "age" : { "gte" : 10, "lte" : 20 }
        }
      },
      "should" : [
        { "term" : { "tags" : "env1" } },
        { "term" : { "tags" : "deployed" } }
      ],
      "minimum_should_match" : 1,
      "boost" : 1.0
    }
  }
}

查询语句语义

ES 查询语句有 query context 和 filter context 两个使用场景,这两个上下文中的语句的语义不同。

query 中的查询语句位于 query context 中:

GET /banke/_search
{
  "query": { "match": { "address": "mill lane" } }    <-- 位于 query context
}

query 中的 filter 中的查询语句位于 filter context 中,例如 bool 语句中的 filter :

GET /_search
{
  "query": { 
    "bool": { 
      "must": [
        { "match": { "title":   "Search"        }},    <--  位于 query context
        { "match": { "content": "Elasticsearch" }}
      ],
      "filter": [   
        { "term":  { "status": "published" }},         <--  位于 filter context
        { "range": { "publish_date": { "gte": "2015-01-01" }}}
      ]
    }
  }
}

query context 中语句效果:按照这些查询语句对 es 中的文档进行相关性评分,选出相关性高的文档。

filter context 中语句效果:从 query 的查询结果中过滤掉匹配这些条件的文档。

全部查询:match_all / match_none

match_all 最简单的查询语句,没有查询条件,把 es 中所有文档都查出来:

GET /_search
{
    "query": {
        "match_all": {}
    }
}

match_all 默认所有文档的相关性得分都是默认值 1.0, 如果要修改默认值用 boost 设置:

GET /_search
{
  "query": {
    "match_all": { "boost" : 1.2 }  <--  所有文档的相关性得分都设置为 1.2
  }
}

match_none 是 match_all 的取反,返回空结果:

GET /_search
{
  "query": {
    "match_none": {}
  }
}

精确查询:Term-level Queries

term-level-queries 用于对结构化数据进行精确查询,主要有以下语句:

  1. exists
  2. fuzzy
  3. ids
  4. prefix
  5. range
  6. regexp
  7. term
  8. terms
  9. terms_set
  10. type
  11. wildcard

语句:exists

精确匹配文档中的 field,ES 提供了多个 term level 级别的查询: ES term level query

## 是否存在
GET /_search
{
  "query": {
    "exists": {
      "field": "user"
    }
  }
}

语句:fuzzy

## 近似值
GET /_search
{
  "query": {
    "fuzzy": {
      "user.id": {
        "value": "ki"
      }
    }
  }
}

语句:ids

## 按 id 查询
GET /_search
{
  "query": {
    "ids" : {
      "values" : ["1", "4", "100"]
    }
  }
}

语句:prefix

## 前缀匹配
GET /_search
{
  "query": {
    "prefix": {
      "user.id": {
        "value": "ki"
      }
    }
  }
}

语句:range

## 范围查找
GET /_search
{
  "query": {
    "range": {
      "age": {
        "gte": 10,
        "lte": 20,
        "boost": 2.0
      }
    }
  }
}

语句:regexp

## 正则匹配
GET /_search
{
  "query": {
    "regexp": {
      "user.id": {
        "value": "k.*y",
        "flags": "ALL",
        "max_determinized_states": 10000,
        "rewrite": "constant_score"
      }
    }
  }
}

语句:term

## field 值匹配
GET /_search
{
  "query": {
    "term": {
      "user.id": {
        "value": "kimchy",
        "boost": 1.0
      }
    }
  }
}

语句:terms

## field 值匹配
GET /_search
{
  "query": {
    "terms": {
      "user.id": [ "kimchy", "elkbee" ],
      "boost": 1.0
    }
  }
}

语句:terms_set

GET /job-candidates/_search
{
  "query": {
    "terms_set": {
      "programming_languages": {
        "terms": [ "c++", "java", "php" ],
        "minimum_should_match_field": "required_matches"
      }
    }
  }
}

语句:type

## 类型查询
GET /_search
{
  "query": {
    "type": {
      "value": "_doc"
    }
  }
}

语句:wildcard

## 通配符
GET /_search
{
  "query": {
    "wildcard": {
      "user.id": {
        "value": "ki*y",
        "boost": 1.0,
        "rewrite": "constant_score"
      }
    }
  }
}

全文检索:full text queries

针对 text 类型的 field,使用full text queries ,支持以下语句:

  1. intervals
  2. match
  3. match_bool_prefix
  4. match_phrase
  5. match_phrase_prefix
  6. multi_match
  7. combined_fields
  8. query_string
  9. simple_query_string

语句:match

指定多个 field 和期待的 value,然后通过 operator 等参数控制行为,ES Match query

GET /_search
{
  "query": {
    "match": {
      "message": {
        "query": "this is a test",
        "operator": "and"
      }
    }
  }
}

组合查询:Compound queries

查询条件不止一个时,用 Compound queries 进行组合,支持以下语句:

  1. bool
  2. boosting
  3. constant_score
  4. dis_max
  5. function_score

语句:bool

组合多个子查询语句,子查询语句的组合条件可以是 must、filter、should、must_not。

ES Boolean query

POST _search
{
  "query": {
    "bool" : {
      "must" : {
        "term" : { "user.id" : "kimchy" }
      },
      "filter": {
        "term" : { "tags" : "production" }
      },
      "must_not" : {
        "range" : {
          "age" : { "gte" : 10, "lte" : 20 }
        }
      },
      "should" : [
        { "term" : { "tags" : "env1" } },
        { "term" : { "tags" : "deployed" } }
      ],
      "minimum_should_match" : 1,
      "boost" : 1.0
    }
  }
}

语句:boosting

降低命中特定条件的文档的评分,ES boosting query

GET /_search
{
  "query": {
    "boosting": {
      "positive": {
        "term": {
          "text": "apple"
        }
      },
      "negative": {
        "term": {
          "text": "pie tart fruit crumble tree"
        }
      },
      "negative_boost": 0.5
    }
  }
}

语句:constant_score

为匹配的文档设置固定的评分,ES constant score

GET /_search
{
  "query": {
    "constant_score": {
      "filter": {
        "term": { "user.id": "kimchy" }
      },
      "boost": 1.2
    }
  }
}

语句:dis_max

ES Disjunction max query

语句:function_score

ES Function score query

连接查询:Join

es 的 Joining queries 语句提供了一定的连接查询能力:

  1. nested
  2. has child
  3. has parent
  4. parent id

地理查询:Geo

es 支持地理数据查询,Geo queries

  1. geo-bounding box
  2. geo-distance
  3. geo-polygon
  4. geoshape

几何查询:Shape

es 支持存放二维的几何图形,Shape queries

  1. shape

无法归类的特殊查询

还有一些特殊查询无法归类, Specialized queries

  1. distance feature
  2. more like this
  3. percolate
  4. rank feature
  5. script
  6. script score
  7. wrapper
  8. pinned query

聚合语法:Aggregation

如果关心的不是单个文档,而是文档的中数值的分布等数学特征,使用 aggs 语句。

GET /bank/_search
{
  "size": 0,
  "aggs": {
    "group_by_state": {
      "terms": {
        "field": "state.keyword"
      }
    }
  }
}
GET /bank/_search
{
  "size": 0,
  "aggs": {
    "group_by_state": {
      "terms": {
        "field": "state.keyword"
      },
      "aggs": {
        "average_balance": {
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  }
}
GET /bank/_search
{
  "size": 0,
  "aggs": {
    "group_by_state": {
      "terms": {
        "field": "state.keyword",
        "order": {
          "average_balance": "desc"
        }
      },
      "aggs": {
        "average_balance": {
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  }
}

SQL 查询

SQL查询

参考

  1. 李佶澳的博客
  2. es官网文档
  3. Install Kibana With Docker
  4. Install Elasticsearch With Docker
  5. Elastic Stack Doc
  6. Running the Elastic Stack On Docker

推荐阅读

赞助商广告

Copyright @2011-2019 All rights reserved. 转载请添加原文连接,合作请加微信lijiaocn或者发送邮件: [email protected],备注网站合作

友情链接:  李佶澳的博客  小鸟笔记  软件手册  编程手册  运营手册  爱马影视  网络课程  奇技淫巧  课程文档  精选文章  发现知识星球  百度搜索 谷歌搜索