• 【ElasticSearch】使用学习


    【ElasticSearch】使用学习和许可证

    ===================================================

    0、许可证

    1、安装

    2、基本概念

    3、常用操作

    4、

    5、

    6、更新 fielddata

    7、翻页检索

    8、字符类型 使用 text 或 keyword 类型设置

    ===================================================

    0、许可证

    查看有效期

    curl -XGET http://localhost:9200/_xpack/license?pretty

    注册许可证

     https://register.elastic.co/

    更新许可证

    curl -XPUT -u elastic 'http://localhost:9200/_xpack/license?acknowledge=true' -H "Content-Type: application/json" -d @license.json

     

    1、安装

    2、基本概念

    基本请求格式

    一个 Elasticsearch 请求和任何 HTTP 请求一样由若干相同的部件组成:
    curl -X<VERB> '<PROTOCOL>://<HOST>:<PORT>/<PATH>?<QUERY_STRING>' -d '<BODY>'

    VERB
    适当的 HTTP 方法 或 谓词 : GET、 POST、 PUT、 HEAD 或者 DELETE。

    PROTOCOL
    http 或者 https(如果你在 Elasticsearch 前面有一个 https 代理)

    HOST
    Elasticsearch 集群中任意节点的主机名,或者用 localhost 代表本地机器上的节点。

    PORT
    运行 Elasticsearch HTTP 服务的端口号,默认是 9200 。

    PATH
    API 的终端路径(例如 _count 将返回集群中文档数量)。Path 可能包含多个组件,例如:_cluster/stats 和 _nodes/stats/jvm 。

    QUERY_STRING
    任意可选的查询字符串参数 (例如 ?pretty 将格式化地输出 JSON 返回值,使其更容易阅读)

    BODY
    一个 JSON 格式的请求体 (如果请求需要的话)

    文档

    Elasticsearch 使用 JavaScript Object Notation(或者 JSON)作为文档的序列化格式。JSON 序列化为大多数编程语言所支持,并且已经成为 NoSQL 领域的标准格式。 它简单、简洁、易于阅读。

    一个 Elasticsearch 集群可以 包含多个 索引 ,相应的每个索引可以包含多个 类型 。 这些不同的类型存储着多个 文档 ,每个文档又有 多个 属性

    全文搜索 match

    GET /myindex/mytype/_search
    {
      "query": {
        "match": {
          "about": "rock climbing"
        }
      }
    }

    Elasticsearch 默认按照相关性得分排序,即每个文档跟查询的匹配程度

    短语搜索 match_phrase

    找出一个属性中的独立单词是没有问题的,但有时候想要精确匹配一系列单词或者_短语_ 。
    比如, 我们想执行这样一个查询,仅匹配同时包含 “rock” 和 “climbing” ,并且 二者以短语 “rock climbing” 的形式紧挨着的雇员记录。

    GET /myindex/mytype/_search
    {
      "query": {
        "match_phrase": {
          "about": "rock climbing"
        }
      }
    }

    高亮搜索 highlight

    GET /myindex/mytype/_search
    {
      "query": {
        "match_phrase": {
          "about": "rock climbing"
        }
      },
      "highlight": {
        "fields": {
          "about": {}
        }
      }
    }

    返回结果多了一个叫做 highlight 的部分
    这个部分包含了 about 属性匹配的文本片段,并以 HTML 标签 <em></em> 封装:

    分析聚合 aggregations

    Elasticsearch 有一个功能叫聚合(aggregations),允许我们基于数据生成一些精细的分析结果。
    聚合与 SQL 中的 GROUP BY 类似但更强大。

    GET /myindex/mytype/_search
    {
      "aggs": {
        "all_interests": {
          "terms": { "field": "interests" }
        }
      }
    }

    报fielddata错误时修改

    PUT myindex/_mapping/mytype
    {
      "properties": {
        "interests": { 
          "type":     "text",
          "fielddata": true
        }
      }
    }

    姓 Smith 的员工中最受欢迎的兴趣爱好

    GET /myindex/mytype/_search
    {
      "query": {
        "match": {
          "last_name": "smith"
        }
      },
      "aggs": {
        "all_interests": {
          "terms": {
            "field": "interests"
          }
        }
      }
    }

    查询特定兴趣爱好员工的平均年龄

    GET /myindex/mytype/_search
    {
      "aggs": {
        "all_interests": {
          "terms": {
            "field": "interests"
          },
          "aggs": {
            "avg_age": {
              "avg": {
                "field": "age"
              }
            }
          }
        }
      }
    }

     两层聚合

    {
      "aggs": {
        "all_interests": {
          "terms": {
            "field": "interests"
          },
          "aggs": {
            "xingshi": {
              "terms": {
                "field": "last_name",
                "order": {
                  "avg_age": "asc"
                }
              },
              "aggs": {
                "avg_age": {
                  "avg": {
                    "field": "age"
                  }
                }
              }
            }
          }
        }
      }
    }

     集群健康

    GET /_cluster/health
    {
       "cluster_name":          "elasticsearch",
       "status":                "green", 
       "timed_out":             false,
       "number_of_nodes":       1,
       "number_of_data_nodes":  1,
       "active_primary_shards": 0,
       "active_shards":         0,
       "relocating_shards":     0,
       "initializing_shards":   0,
       "unassigned_shards":     0
    }

    status 字段指示着当前集群在总体上是否工作正常。它的三种颜色含义如下:
    green
    所有的主分片和副本分片都正常运行。
    yellow
    所有的主分片都正常运行,但不是所有的副本分片都正常运行。
    red
    有主分片没能正常运行。

    PUT /myindex
    {
       "settings" : {
          "number_of_shards" : 3,
          "number_of_replicas" : 1
       }
    }

    动态调整副本分片数

    PUT /myindex/_settings
    {
       "number_of_replicas" : 1
    }

    处理冲突

    在数据库领域中,有两种方法通常被用来确保并发更新时变更不会丢失:
    悲观并发控制
    这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。 一个典型的例子是读取一行数据之前先将其锁住,确保只有放置锁的线程能够对这行数据进行修改。
    乐观并发控制
    Elasticsearch 中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。 然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。 例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。

    文档的部分更新

    我们也介绍过文档是不可变的:他们不能被修改,只能被替换。 update API 必须遵循同样的规则。 从外部来看,我们在一个文档的某个位置进行部分更新。然而在内部, update API 简单使用与之前描述相同的 检索-修改-重建索引 的处理过程。 区别在于这个过程发生在分片内部,这样就避免了多次请求的网络开销。通过减少检索和重建索引步骤之间的时间,我们也减少了其他进程的变更带来冲突的可能性。

    POST /myindex/mytype/1/_update
    {
       "doc" : {
          "tags" : [ "testing" ],
          "views": 0
       }
    }

    doc是关键字

    更新和冲突
    这可以通过设置参数 retry_on_conflict 来自动完成, 这个参数规定了失败之前 update 应该重试的次数,它的默认值为 0

    取回多个文档

    mget API 要求有一个 docs 数组作为参数,每个元素包含需要检索文档的元数据, 包括 _index 、 _type 和 _id 。如果你想检索一个或者多个特定的字段,那么你可以通过 _source 参数来指定这些字段的名字:

    POST /_mget
    {
      "docs": [
        {
          "_index": "myindex",
          "_type": "mytype",
          "_id": 3
        },
        {
          "_index": "myindex",
          "_type": "mytype",
          "_id": 2,
          "_source": "age"
        },
        {
          "_index": "myindex",
          "_type": "mytype",
          "_id": 1,
          "_source": [
            "first_name",
            "last_name"
          ]
        }
      ]
    }

    该响应体也包含一个 docs 数组, 对于每一个在请求中指定的文档,这个数组中都包含有一个对应的响应,且顺序与请求中的顺序相同。

    如果想检索的数据都在相同的 _index 中(甚至相同的 _type 中),则可以在 URL 中指定默认的 /_index 或者默认的  /_index/_type 。

    POST /myindex/mytype/_mget
    {
      "docs": [
        {
          "_id": 2
        },
        {
          "_type": "mytype2",
          "_id": 1
        }
      ]
    }

    index和type都相同只传ID

    POST /myindex/mytype/_mget
    {
      "ids": ["1","2","3"]
    }

    代价较小的批量操作

    bulk 与其他的请求体格式稍有不同,如下所示
    { action: { metadata }}
    { request body }
    { action: { metadata }}
    { request body }
    每行一定要以换行符( )结尾, 包括最后一行
    action 必须是以下选项之一:
    create
    如果文档不存在,那么就创建它
    index
    创建一个新文档或者替换一个现有的文档
    update
    部分更新一个文档
    delete
    删除一个文档,没有请求体
    request body 行由文档的 _source 本身组成—​文档包含的字段和值。它是 index 和 create 操作所必需的,这是有道理的:你必须提供文档以索引

    {"delete":{"_index": "myindex", "_type": "mytype", "_id": "1"}}
    {"create":{"_index": "myindex", "_type": "mytype", "_id": "4"}}
    {"title":"My first blog post"}
    {"update":{"_index": "myindex", "_type": "mytype", "_id": "123", "_retry_on_conflict" : 3} }
    {"doc":{"title":"My updated blog post"} }
    {"index":{"_index": "website", "_type": "blog"}}
    {"title":"My second blog post"}

     映射(Mapping)描述数据在每个字段内如何存储

    分析(Analysis)全文是如何处理使之可以被搜索的
    领域特定查询语言(Query DSL)Elasticsearch 中强大灵活的查询语言
    hits
    返回结果中最重要的部分是 hits ,它包含 total 字段来表示匹配到的文档总数,并且一个 hits 数组包含所查询结果的前十个文档。
    在 hits 数组中每个结果包含文档的 _index 、 _type 、 _id ,加上 _source 字段。这意味着我们可以直接从返回的搜索结果中使用整个文档。这不像其他的搜索引擎,仅仅返回文档的ID,需要你单独去获取文档。
    每个结果还有一个 _score ,它衡量了文档与查询的匹配程度。默认情况下,首先返回最相关的文档结果,就是说,返回的文档是按照 _score 降序排列的。在这个例子中,我们没有指定任何查询,故所有的文档具有相同的相关性,因此对所有的结果而言 1 是中性的 _score 。
    max_score 值是与查询所匹配文档的 _score 的最大值。
    took 值告诉我们执行整个搜索请求耗费了多少毫秒。
    _shards 部分告诉我们在查询中参与分片的总数,以及这些分片成功了多少个失败了多少个。正常情况下我们不希望分片失败,但是分片失败是可能发生的。如果我们遭遇到一种灾难级别的故障,在这个故障中丢失了相同分片的原始数据和副本,那么对这个分片将没有可用副本来对搜索请求作出响应。假若这样,Elasticsearch 将报告这个分片是失败的,但是会继续返回剩余分片的结果。
    timed_out 值告诉我们查询是否超时。默认情况下,搜索请求不会超时。如果低响应时间比完成结果更重要,你可以指定 timeout 为 10 或者 10ms(10毫秒),或者 1s(1秒):

    多索引多类型
    /_search
    在所有的索引中搜索所有的类型
    /gb/_search
    在 gb 索引中搜索所有的类型
    /gb,us/_search
    在 gb 和 us 索引中搜索所有的文档
    /g*,u*/_search
    在任何以 g 或者 u 开头的索引中搜索所有的类型
    /gb/user/_search
    在 gb 索引中搜索 user 类型
    /gb,us/user,tweet/_search
    在 gb 和 us 索引中搜索 user 和 tweet 类型
    /_all/user,tweet/_search
    在所有的索引中搜索 user 和 tweet 类型

    分页
    size显示应该返回的结果数量,默认是 10
    from显示应该跳过的初始结果数量,默认是 0
    在分布式系统中深度分页
    理解为什么深度分页是有问题的,我们可以假设在一个有 5 个主分片的索引中搜索。 当我们请求结果的第一页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 50 个结果排序得到全部结果的前 10 个。
    现在假设我们请求第 1000 页—​结果从 10001 到 10010 。所有都以相同的方式工作除了每个分片不得不产生前10010个结果以外。 然后协调节点对全部 50050 个结果排序最后丢弃掉这些结果中的 50040 个结果。
    可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。这就是 web 搜索引擎对任何查询都不要返回超过 1000 个结果的原因。

    查看分词结果
    info/info/100001/_termvectors?fields=tree_id_position
    POST _analyze
    {
    "analyzer": "whitespace",
    "text": "The quick brown fox."
    }
    查看索引分词结果
    info/_analyze
    {
    "analyzer": "comma",
    "text": "1,b,c"
    }

    //静态设置
    index.number_of_shards //主分片数,默认为5.只能在创建索引时设置,不能修改
    index.shard.check_on_startup //是否应在索引打开前检查分片是否损坏,当检查到分片损坏将禁止分片被打开
    false //默认值
    checksum //检查物理损坏
    true //检查物理和逻辑损坏,这将消耗大量内存和CPU
    fix //检查物理和逻辑损坏。有损坏的分片将被集群自动删除,这可能导致数据丢失
    index.routing_partition_size //自定义路由值可以转发的目的分片数。默认为 1,只能在索引创建时设置。此值必须小于index.number_of_shards
    index.codec //默认使用LZ4压缩方式存储数据,也可以设置为 best_compression,它使用 DEFLATE 方式以牺牲字段存储性能为代价来获得更高的压缩比例。

    //动态设置
    index.number_of_replicas //每个主分片的副本数。默认为 1。
    index.auto_expand_replicas //基于可用节点的数量自动分配副本数量,默认为 false(即禁用此功能)
    index.refresh_interval //执行刷新操作的频率,这使得索引的最近更改可以被搜索。默认为 1s。可以设置为 -1 以禁用刷新。
    index.max_result_window //用于索引搜索的 from+size 的最大值。默认为 10000
    index.max_rescore_window // 在搜索此索引中 rescore 的 window_size 的最大值
    index.blocks.read_only //设置为 true 使索引和索引元数据为只读,false 为允许写入和元数据更改。
    index.blocks.read // 设置为 true 可禁用对索引的读取操作
    index.blocks.write //设置为 true 可禁用对索引的写入操作。
    index.blocks.metadata // 设置为 true 可禁用索引元数据的读取和写入。
    index.max_refresh_listeners //索引的每个分片上可用的最大刷新侦听器数

    查询

    查询领域特定语言(query domain-specific language) 或者 Query DSL 来写查询语句。

    GET myindex/mytype/_search
    {
      "match": {
        "last_name": "Smith"
      }
    }
    完整的查询
    GET myindex/mytype/_search
    {
      "query": {
        "match": {
          "last_name": "Smith"
        }
      }
    }

    合并查询语句

    叶子语句(Leaf clauses)像 match 被用于将查询字符串和一个字段(或者多个字段)对比
    复合(Compound) 语句 主要用于 合并其它查询语句一个 bool 语句 允许在你需要的时候组合其它语句,无论是 must 匹配、 must_not 匹配还是 should 匹配,同时它可以包含不评分的过滤器(filters):

    {
      "bool": {
        "must": {
          "match": {
            "email": "admin@126.com"
          }
        },
        "should": [
          {
            "match": {
              "starred": true
            }
          },
          {
            "bool": {
              "must": {
                "match": {
                  "folder": "inbox"
                }
              },
              "must_not": {
                "match": {
                  "spam": true
                }
              }
            }
          }
        ],
        "minimum_should_match": 1
      }
    }

    match 查询

    {
      "match": {
        "tweet": "About Search"
      }
    }

    multi_match 查询

    {
      "multi_match": {
        "query": "full text search",
        "fields": [
          "title",
          "body"
        ]
      }
    }

    range 查询

    gt大于,gte大于等于,lt小于,lte小于等于

    {
      "range": {
        "age": {
          "gte": 20,
          "lt": 30
        }
      }
    }

    term 查询

    被用于精确值匹配,这些精确值可能是数字、时间、布尔或者那些 not_analyzed 的字符串:

    { "term": { "age": 26 }}
    { "term": { "date": "2014-09-01" }}
    { "term": { "public": true }}
    { "term": { "tag": "full_text" }}

    terms 查询

    和 term 查询一样,但它允许你指定多值进行匹配。如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件
    { "terms": { "tag": [ "search", "full_text", "nosql" ] }}

    exists 查询和 missing 查询

    被用于查找那些指定字段中有值 (exists) 或无值 (missing) 的文档。这与SQL中的 IS_NULL (missing) 和 NOT IS_NULL (exists) 在本质上具有共性
    { "exists": { "field":"title" } }

    3、常用操作

    索引

    a. 添加索引

    PUT /myindex
    返回
    {
      "acknowledged": true,
      "shards_acknowledged": true,
      "index": "myindex"
    }

    指定setting

    PUT /myindex
    {
      "settings": {
        "number_of_shards": 1,
        "number_of_replicas": 1,
        "analysis": {
          "analyzer": {
            "comma": {
              "type": "pattern",
              "pattern": ","
            }
          }
        }
      }
    }

    指定setting 和 mapping

    {
      "settings": {
        "number_of_shards": 1,
        "numbeer_of_replicas": 1
      },
      "mapping": {
        "mytype": {
          "properties": {
            "id": {
              "type": "string"
            },
            "name": {
              "name": "string"
            }
          }
        }
      }
    }

    b. 修改索引

    GET /myindex/_settings?pretty
    {
      "myindex": {
        "settings": {
          "number_of_replicas": 1,
          "index": {
            "analysis": {
              "analyzer": {
                "comma": {
                  "type": "pattern",
                  "pattern": ","
                }
              }
            }
          }
        }
      }
    }
    GET /myindex/mytype/_mapping?pretty
    {
      "myindex": {
        "mappings": {
          "mytype": {
            "properties": {
              "name": {
                "type": "string"
              },
              "age": {
                "type": "long"
              }
            }
          }
        }
      }
    }

    c. 查询索引

    GET /myindex
    返回
    {
      "myindex": {
        "aliases": {},
        "mappings": {},
        "settings": {
          "index": {
            "creation_date": "1583914355792",
            "number_of_shards": "5",
            "number_of_replicas": "1",
            "uuid": "Aeld8sxwTCu5__gLHTvBwQ",
            "version": {
              "created": "6020499"
            },
            "provided_name": "myindex"
          }
        }
      }
    }

    d. 删除索引

    DELETE /myindex
    返回 {
    "acknowledged": true }

    搜索

    GET /myindex/mytype/_search

    默认返回10条,查询表达式 , 它支持构建更加复杂和健壮的查询。领域特定语言 (DSL), 使用 JSON 构造了一个请求。

    姓氏为 Smith 年龄大于 30 的员工

    查询 match,过滤器 filter

    POST /myindex/mytype/_search
    {
      "query": {
        "bool": {
          "must": {
            "match": {
              "last_name": "smith"
            }
          },
          "filter": {
            "range": {
              "age": {
                "gt": 30
              }
            }
          }
        }
      }
    }

    文档
    最顶层或者根对象指定了唯一ID被序列化成JSON并存储到Elasticsearch中。

    a. 索引文档(创建文档)

    在Elasticsearch中_index、_type和_id的组合唯一标识一个文档

    自定义ID

    PUT /myindex/mytype/id
    { ... }

    自动生成ID

    把put换成post就可以,去掉指定ID

    POST /myindex/mytype
    { ... }

    创建一个完全新的文档,而不是覆盖现有的文档

    如果已经有自己的_id,那么我们必须告诉Elasticsearch,只有在相同的_index、_type和_id不存在时才接受我们的索引请求。有两种方式:

    第一种方法使用op_type查询-字符串参数

    PUT /myindex/mytype/id?op_type=create
    { ... }

    第二种方法是在URL末端使用/_create

    PUT /myindex/mytype/id/_create
    { ... }

    如果创建新文档的请求成功执行,Elasticsearch 会返回元数据和一个 201 Created 的 HTTP 响应码

    另一方面,如果具有相同的_index、_type和_id的文档已经存在,Elasticsearch将会返回409Conflict响应码

     b. 查询文档

    GET /myindex/mytype/{id}?pretty

    返回部分文档,单个字段能用_source参数请求得到,多个字段也能使用逗号分隔的列表来指定。

    GET /myindex/mytype/id?_source=age,first_name

     带请求体

    POST /myindex/mytype/_search
    {
      "_source": [
        "age",
        "first_name"
      ]
    }

    返回_source部分

    GET /myindex/mytype/id/_source

    c. 更新文档

    在Elasticsearch中文档是不可改变的,不能修改它们。相反,如果想要更新现有的文档,需要重建索引或者进行替换

    PUT /myindex/mytype/id
    {
      "name": "alice",
    }

    d. 删除文档 

    DELETE /myindex/mytype/id

    成功"result": "deleted"、 失败"result": "not_found"

    统计文档件数

    curl -XGET 'http://localhost:9200/_count?pretty' -d '
    {
        "query": {
            "match_all": {}
        }
    }
    '

    更新 fielddata

    PUT my_index/_mapping/my_type
    {
      "properties": {
        "id": { 
          "type":     "text",
          "fielddata": true
        }
      }
    }

    7、翻页检索

    A、from, size

    获取实时数据,但是随着翻页深度增加效率会越来越差,window默认是10000

    可通过,设置max_result_window临时解决

    PUT movies/_settings
    { 
        "index" : { 
            "max_result_window" : 20000
        }
    }

    B、Search After

    官方文档

    可用于实时翻页,不能指定页码

    POST my_index/my_type/_search
    {
      "size": 1,
      "query": {
        "match_all": {}
      },
      "sort": [
        {
          "status": "desc"
        },
        {
          "id": "desc"
        }
      ]
    }

    返回的sort放在search_after中
    "sort": [2,"1235472336950333441"]

    POST my_index/my_type/_search
    {
      "size": 1,
      "query": {
        "match_all": {}
      },
      "search_after": [
        2,
        "1235472336950333441"
      ],
      "sort": [
        {
          "status": "desc"
        },
        {
          "id": "desc"
        }
      ]
    }

    C、Scroll

    官方文档

    非实时获取文档,处理大数据量,类似数据库的光标。指定scroll参数

    POST /my_index/_search?scroll=1m
    {
      "size": 1,
      "query": {
        "match": {
          "title": "文档"
        }
      }
    }

    返回结果中有"_scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAACYWaFh0R3k5Y3ZTYU9JSEZqNGV6ek14UQ==",拿到_scroll_id之后,用下面的方式往下翻

    POST /_search/scroll 
    {
        "scroll" : "1m", 
        "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAADgWaFh0R3k5Y3ZTYU9JSEZqNGV6ek14UQ==" 
    }

    查询当前有多少scroll

    默认情况下,打开的滚动的最大数量为500.可以使用search.max_open_scroll_context群集设置更新此限制

    GET /_nodes/stats/indices/search

    删除指定scroll_id

    DELETE /_search/scroll
    {
        "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
    }

    删除全部

    DELETE /_search/scroll/_all

    8、字符类型 使用 text 或 keyword 类型设置

    参考:https://blog.csdn.net/kakaluoteyy/article/details/80324553

    es从2.X版本一下子跳到了5.X版本,将string类型变为了过期类型,取而代之的是text和keyword数据类型,一直到现在最新的6以上版本

    按照官方文档的阐述

    text类型的数据被用来索引长文本,例如电子邮件主体部分或者一款产品的介绍,这些文本会被分析,在建立索引文档之前会被分词器进行分词,转化为词组。经过分词机制之后es允许检索到该文本切分而成的词语,但是text类型的数据不能用来过滤、排序和聚合等操作。

    keyword类型的数据可以满足电子邮箱地址、主机名、状态码、邮政编码和标签等数据的要求,不进行分词,常常被用来过滤、排序和聚合。

    综上,可以发现text类型在存储数据的时候会默认进行分词,并生成索引。而keyword存储数据的时候,不会分词建立索引,显然,这样划分数据更加节省内存。

    2.X版本设置分词使用"index":"not_analyzed"配置是否可以被用来搜索

    5以上的版本中,“index”参数用来配置该字段是否可以被用来搜索,true可以通过搜索该字段检索到文档,false为否,配置分词器,用analyzer参数

  • 相关阅读:
    ios面试题(二)
    ios之自定义UINavigationBar
    ios之自定义导航栏上的返回按钮
    ios之键盘的自定义
    ios之UITabelViewCell的自定义(xib实现2)
    ios之UITabelViewCell的自定义(xib实现)
    ios之UITabelViewCell的自定义(代码实现)
    ios 登录功能学习研究
    Create Table操作
    C#数据库查询和操作大全
  • 原文地址:https://www.cnblogs.com/yangchongxing/p/12240865.html
Copyright © 2020-2023  润新知