zoukankan      html  css  js  c++  java
  • ElasticSearch(5)-Mapping

    一、Mapping概述

    映射

    为了能够把日期字段处理成日期,把数字字段处理成数字,把字符串字段处理成全文本(Full-text)或精确的字符串值,Elasticsearch需要知道每个字段里面都包含了什么类型。这些类型和字段的信息存储(包含)在映射(mapping)中。

    正如《数据吞吐》一节所说,索引中每个文档都有一个类型(type)。 每个类型拥有自己的映射(mapping)或者模式定义(schema definition)。一个映射定义了字段类型,每个字段的数据类型,以及字段被Elasticsearch处理的方式。映射还用于设置关联到类型上的元数据。

    这里只是入门。

    例如,可以使用映射来定义:

    • 字符串字段是否作为全文本搜索字段
    • 哪些字段包含数字,日期或地理信息
    • 文档中所有字段的值是否应该被索引到_all字段
    • 日期值的格式
    • 自定义规则来控制动态添加的字段的映射

    映射类型与type:即一个索引中有多个type,从逻辑上对文档进行划分、每个索引有一个或多个映射类型,类型是对Document划分的逻辑组,索引中每个文档都有一个类型(type),每个类型拥有自己的映射或者模式定义(schema definition) 。每个映射类型包括:

    1. 关联到类型上的元数据,比如:_index, _type, _id, and _source
    2. 字段或属性的定义,比如:字段类型,每个字段的数据类型,以及字段被ES处理的方式。

    数据类型大致分类:

    • 简单类型,比如:string, date, long, double, boolean 或 ip等
    • 嵌套对象
    • 特殊类型,比如:geo_point, geo_shape, 或completion等

    二、分词器

    这里的分词器和lucene中的分词器差不多。

    lucene基础:http://www.cnblogs.com/carl10086/p/6020379.html

    分析和分析器

    分析(analysis)是这样一个过程:

    • 首先,标记化一个文本块为适用于倒排索引单独的词(term)
    • 然后标准化这些词为标准形式,提高它们的“可搜索性”或“查全率”

    这个工作是分析器(analyzer)完成的。一个分析器(analyzer)只是一个包装用于将三个功能放到一个包里:

    功能1:字符过滤器

    首先字符串经过字符过滤器(character filter),它们的工作是在标记化前处理字符串。字符过滤器能够去除HTML标记,或者转换"&""and"

    功能2:分词器

    下一步,分词器(tokenizer)被标记化成独立的词。一个简单的分词器(tokenizer)可以根据空格或逗号将单词分开(译者注:这个在中文中不适用)。

    功能3:标记过滤

    最后,每个词都通过所有标记过滤(token filters),它可以修改词(例如将"Quick"转为小写),去掉词(例如停用词像"a""and""the"等等),或者增加词(例如同义词像"jump""leap"

    Elasticsearch提供很多开箱即用的字符过滤器,分词器和标记过滤器。这些可以组合来创建自定义的分析器以应对不同的需求。我们将在《自定义分析器》章节详细讨论。

    内建的分析器

    不过,Elasticsearch还附带了一些预装的分析器,你可以直接使用它们。下面我们列出了最重要的几个分析器,来演示这个字符串分词后的表现差异:

    "Set the shape to semi-transparent by calling set_trans(5)"

    标准分析器

    标准分析器是Elasticsearch默认使用的分析器。对于文本分析,它对于任何语言都是最佳选择(译者注:就是没啥特殊需求,对于任何一个国家的语言,这个分析器就够用了)。它根据Unicode Consortium的定义的单词边界(word boundaries)来切分文本,然后去掉大部分标点符号。最后,把所有词转为小写。产生的结果为:

    set, the, shape, to, semi, transparent, by, calling, set_trans, 5

    简单分析器

    简单分析器将非单个字母的文本切分,然后把每个词转为小写。产生的结果为:

    set, the, shape, to, semi, transparent, by, calling, set, trans

    空格分析器

    空格分析器依据空格切分文本。它不转换小写。产生结果为:

    Set, the, shape, to, semi-transparent, by, calling, set_trans(5)

    语言分析器

    特定语言分析器适用于很多语言。它们能够考虑到特定语言的特性。例如,english分析器自带一套英语停用词库——像andthe这些与语义无关的通用词。这些词被移除后,因为语法规则的存在,英语单词的主体含义依旧能被理解(译者注:stem English words这句不知道该如何翻译,查了字典,我理解的大概意思应该是将英语语句比作一株植物,去掉无用的枝叶,主干依旧存在,停用词好比枝叶,存在与否并不影响对这句话的理解。)。

    english分析器将会产生以下结果:

    set, shape, semi, transpar, call, set_tran, 5

    注意"transparent""calling""set_trans"是如何转为词干的。

    当分析器被使用

    当我们索引(index)一个文档,全文字段会被分析为单独的词来创建倒排索引。不过,当我们在全文字段搜索(search)时,我们要让查询字符串经过同样的分析流程处理,以确保这些词在索引中存在。

    全文查询我们将在稍后讨论,理解每个字段是如何定义的,这样才可以让它们做正确的事:

    • 当你查询全文(full text)字段,查询将使用相同的分析器来分析查询字符串,以产生正确的词列表。
    • 当你查询一个确切值(exact value)字段,查询将不分析查询字符串,但是你可以自己指定。

    现在你可以明白为什么《映射和分析》的开头会产生那种结果:

    • date字段包含一个确切值:单独的一个词"2014-09-15"
    • _all字段是一个全文字段,所以分析过程将日期转为三个词:"2014""09""15"

    当我们在_all字段查询2014,它一个匹配到12条推文,因为这些推文都包含词2014

    GET /_search?q=2014              # 12 results

    当我们在_all字段中查询2014-09-15,首先分析查询字符串,产生匹配任一词20140915的查询语句,它依旧匹配12个推文,因为它们都包含词2014

    GET /_search?q=2014-09-15        # 12 results !

    当我们在date字段中查询2014-09-15,它查询一个确切的日期,然后只找到一条推文:

    GET /_search?q=date:2014-09-15   # 1  result

    当我们在date字段中查询2014,没有找到文档,因为没有文档包含那个确切的日期:

    GET /_search?q=date:2014         # 0  results !

    测试分析器

    尤其当你是Elasticsearch新手时,对于如何分词以及存储到索引中理解起来比较困难。为了更好的理解如何进行,你可以使用analyze API来查看文本是如何被分析的。在查询字符串参数中指定要使用的分析器,被分析的文本做为请求体:

    GET /_analyze?analyzer=standard&text=Text to analyze

    结果中每个节点在代表一个词:

    {
       "tokens": [
          {
             "token":        "text",
             "start_offset": 0,
             "end_offset":   4,
             "type":         "<ALPHANUM>",
             "position":     1
          },
          {
             "token":        "to",
             "start_offset": 5,
             "end_offset":   7,
             "type":         "<ALPHANUM>",
             "position":     2
          },
          {
             "token":        "analyze",
             "start_offset": 8,
             "end_offset":   15,
             "type":         "<ALPHANUM>",
             "position":     3
          }
       ]
    }

    token是一个实际被存储在索引中的词。position指明词在原文本中是第几个出现的。start_offsetend_offset表示词在原文本中占据的位置。

    analyze API 对于理解Elasticsearch索引的内在细节是个非常有用的工具,随着内容的推进,我们将继续讨论它。

    指定分析器

    当Elasticsearch在你的文档中探测到一个新的字符串字段,它将自动设置它为全文string字段并用standard分析器分析。

    你不可能总是想要这样做。也许你想使用一个更适合这个数据的语言分析器。或者,你只想把字符串字段当作一个普通的字段——不做任何分析,只存储确切值,就像字符串类型的用户ID或者内部状态字段或者标签。

    为了达到这种效果,我们必须通过映射(mapping)人工设置这些字段。

    三、例子

    PUT /gb <1>
    {
      "mappings": {
        "tweet" : {
          "properties" : {
            "tweet" : {
              "type" :    "string",
              "analyzer": "english"
            },
            "date" : {
              "type" :   "date"
            },
            "name" : {
              "type" :   "string"
            },
            "user_id" : {
              "type" :   "long"
            }
          }
        }
      }
    }

    即使没有在es中显示指定映射,ES可以自动猜测字段类型,进行自动映射。如果不符要求,运行期再手动修改也一样。

    {
        "mappings": {
            "user": {
                "_all": {
                    "enabled": false
                },
                "properties": {
                    "title": {
                        "type": "string"
                    },
                    "name": {
                        "type": "string"
                    },
                    "age": {
                        "type": "integer"
                    }
                }
            },
            "blogpost": {
                "properties": {
                    "title": {
                        "type": "string"
                    },
                    "body": {
                        "type": "string"
                    },
                    "user_id": {
                        "type": "string",
                        "index": "not_analyzed"
                    },
                    "created": {
                        "type": "date",
                        "format": "strict_date_optional_time||epoch_millis"
                    }
                }
            }
    }

    以上是官网提供的SAMPLE,提供了2个type,user和blogpost

    如何查看某个具体type的映射:

    curl -XGET 'http://localhost:9200/mytest/_mapping/product?pretty'

    我们将映射的最高一层称为根对象,此处的根对象有properties和_all设置

    通常根对象可能包含以下内容:

    • 一个 properties 节点,列出了文档中可能包含的每个字段的映射
    • 多个元数据字段,每一个都以下划线开头,例如 _type, _id 和 _source
    • 设置项,控制如何动态处理新的字段,如analyzer, dynamic_date_formats 和dynamic_templates
    • 其他设置,可以同时应用在根对象和其他 object 类型的字段上,如enabled, dynamic 等

    properties属性:

    1. type: 字段的数据类型,例如 string 和 date,形如:

    {
    "字段1": {
    "type": "integer"
    }
    }

    2. index: index参数控制字符串以何种方式被索引,它包含以下三个值当中的一个:

    (1)analyzed: 分词 索引。换言之,以全文形式索引此字段。
    (2)not_analyzed: 索引 不分词,使之可以被搜索,但是索引内容和指定值一样。不分析此字段。
    (3)no:不索引这个字段。这个字段不能为搜索到。
    其中string类型字段默认值是analyzed。如果我们想映射字段为确切值,需要设置它为not_analyzed:

    "字段1": { "type": "string",
    "index": "not_analyzed"
    }

    注意:除了String外的其他简单类型(long、double、date等等)也接受index参数,但相应的值只能是no和not_analyzed,它们的值不能被分析

    3. analyzer: 确定在索引和或搜索时全文字段使用的 分析器,形如:
    对于analyzed类型的字符串字段,使用analyzer参数来指定哪一种分析器将在搜索和索引的时候
    使用。默认的,ES使用standard分析器,但是你可以通过指定一个内建的分析器来更改它,例如
    whitespace、simple或english。

    {
      "字段1": {
        "type": "string",
        "analyzer": "english"
      }
    }

     4. fielddata: 设置对字段数据装载内存的处理方式,对同一个索引下相同的名字的设置必须是一样的

    (1) fielddata.format: 设置是否装载字段数据到内存,只有装载到内存的数据才能执行排序,聚合等等操作

    "properties": {
        "text": {
            "type": "string",
            "fielddata": {
                "format": "disabled"
            }
        }
    }

    (2) fielddata.loading: 装载方式,支持 lazy, eager, eager_global_ordinals三种

    (3) fielddata.filter: 装载时对数据进行过滤,支持频率,支持正则表达式等等

    "properties": {
        "tag": {
            "type": "string",
            "fielddata": {
                "filter": {
                    "frequency": {
                        "min": 0.001,
                        "max": 0.1,
                        "min_segment_size": 500
                    }
                }
            }
        }
    }

    三、常见字段类型简介

    以下内容主要是介绍在设计mapping映射关系时可以选择的字段类型以及相关参数。

    字段数据类型大致上可以分为以下几类,几乎是够用了。。
    1:核心数据类型,包括:

    • String类型:string
    • Numeric类型:long, integer, short, byte, double, float
    • Date类型:date
    • Boolean类型:boolean
    • Binary类型:binary

    2:复杂数据类型,包括

    • Array类型:Array,ES并没有专用的Array,直接使用[]来表示数组
    • Object类型:单一的 JSON 对象
    • Nested类型:嵌套的多个 JSON 对象数组

    3:地理数据类型,包括

    • Geo-point类型:geo_point的纬度/经度点
    • Geo-Shape类型:复杂的地理形状,比如多边形

    4:特殊数据类型,包括:

    • IPv4类型:IPv4地址
    • Completion类型:提供自动完成建议
    • Token count类型:计算字符串中token的数量
    • mapper-murmur3类型:murmur3用来计算索引时的hash值,并把它们存储在索引里面
    • Attachment类型:映射附件的格式,比如:Ms Office,ePub等

    下面逐个类型进行详细说明

    (1) Binary类型

    这个类型可以看做是可以以Base64编码的字符串,这个类型缺省是只存储,但是不可查询。接受的参数如下:
    1:doc_values:是否被存储在磁盘,以参加后续的排序、聚合等,缺省true
    2:store:是否存储到_source,并能从_source检索,缺省false

    关于store的说明:如果设置为false,当_source=false时,无法获取,演示如下:

    //1. 创建索引mytest,或者先删除,再创建
    //2. 定义mytest/t2的mapping映射
    curl -XPUT 'http://localhost:9200/mytest/_mapping/t2?pretty' -d '
    {
        "_source": {
            "enabled": false
        },
        "properties": {
            "content": {
                "type": "string",
                "store": "true"
            },
            "name": {
                "type": "string",
                "store": "false"
            }
        }
    }                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           
    '
    //3. 插入数据
    curl -XPUT 'http://localhost:9200/mytest/t2/1' -d '
    {
        "content": "hello",
        "type": "world"
    }
    '
    //4. 通过fields查询,返回结果只有content
    
    curl -XGET 'http://localhost:9200/mytest/t2/1?pretty&fields=content,type'

    (2) Boolean类型

    认为是false的值有很多,比如:false, "false", "off", "no", "0", "" (empty string), 0, 0.0
    接受的参数:
    1:doc_values 、 store
    2:boost:权重,缺省1.0
    3:index:这个字段是否可以被查询,接受not_analyzed (缺省) 和no
    4:null_value:设置为null时候的值,缺省是null

     

    (3) 日期类型

    符合缺省或者指定日期格式的值会被当作日期类型,缺省是
    "strict_date_optional_time||epoch_millis" ,支持很多格式,通常我们会明确指定自己的格式。
    接受的参数:

    1: boost、 doc_values、 store、 index、 null_value
    2:format:日期格式,比如:yyyy-MM-dd HH:mm:ss
    3:ignore_malformed:数据不满足格式的时候,true表示忽略,false抛出一个异常,并拒绝整个文档
    4:include_in_all:是否把字段值包含在_all中,如果index设置为no或者父对象的字段的这个参数设置的是
    false,缺省是false;其它情况缺省是true
    5:precision_step:设置索引的term的数量,以加快范围查询,缺省16

    (4) String类型

    熟悉的字符串,支持以下参数:

    1: boost、doc_values、include_in_all、store、index、null_value
    2:analyzer:用来分析字符串字段值的分析器,包括索引和查询(如果没有设置查询索引的分析器)两个阶段,缺省使用index的分析器或者standard分析器
    3:fielddata:字段值是否能放置到内存中,用于排序、聚合等,接受disabled或paged_bytes(缺省)
    4:fields:Multi-fields允许这同一个字符串值,以多种方式索引,以满足不同的目的。
    5:ignore_above:不要索引或分析任何比这个值长的字符串。缺省为0(disabled)。
    6:index_options:为了查询或高亮显示,应该在index中存储什么信息。缺省对analyzed字段是positions,对not_analyzed是docs
    7:norms:计算查询得分时,是否要考虑字段长度,缺省对analyzed字段是
    { “enabled”: true,“loading”: “lazy” } ,对not_analyzed是{ "enabled": false },后面会详述查询得分
    8:position_increment_gap:距离查询时,最大允许查询的距离,默认是100
    9:search_analyzer:查询时使用的分析器,缺省是analyzer设置的分析器
    10:search_quote_analyzer:搜索中碰到短语使用的分析器,缺省是search_analyzer设置的分析器
    11:similarity:设置计算相似度的算法,缺省TF/IDF
    12:term_vector:是否为一个analyzed字段保存term词计数的值,缺省是no

    (5) Token Count类型

    这个类型实质上是integer,功能是分析字符串,然后记录字符串分析后的token数量,例如:

    PUT my_index
    {
        "mappings": {
            "my_type": {
                "properties": {
                    "name": {
                        "type": "string",
                        "fields": {
                            "length": {
                                "type": "token_count",
                                "analyzer": "standard"
                            }
                        }
                    }
                }
            }
        }
    }

    接受的参数:
    1:analyzer、boost、doc_values、include_in_all、store、index、null_value
    2:precision_step:控制索引的额外term的数量,以加快范围查询,缺省32


    (6) 多值字段-数组

    比如可以索引一个标签数组来代替单一字符串:
    { "tag": [ "search", "nosql" ]}
    对于数组不需要特殊的映射。任何一个字段可以包含零个、一个或多个值,同样对于全文字段将被分析并产生多个词。
    这意味着数组中所有值必须为同一类型,不能把日期和字符串混合。如果创建一个新字段,这个字段索引了一个数组,ES将使用第一个值的类型来确定这个新字段的类型。

    (7) 空字段

    Lucene没法存放null值,所以一个null值的字段被认为是空字段。这四个字段将被识别为空字段而不被索引:
    "empty_string": "",
    "null_value": null,
    "empty_array": [],
    "array_with_null_value": [ null ]

    (8) 内部对象

    ES会动态的检测新对象的字段,并映射它们为object类型,然后将每个字段加到properties字段
    下,例如:

    curl -XPUT http://localhost:9200/mytest2?pretty -d '
    {
        "mappings": {
            "blog": {
                "properties": {
                    "createTime": {
                        "type": "date",
                        "format": "yyyy-MM-dd HH:mm:ss"
                    },
                    "name": {
                        "type": "object",
                        "properties": {
                            "full": {
                                "type": "string"
                            },
                            "first": {
                                "type": "string"
                            },
                            "last": {
                                "type": "string"
                            }
                        }
                    },
                    "title": {
                        "type": "string"
                    }
                }
            }
        }
    }
    '

    说明:内部对象是怎样被索引的
    Lucene并不了解内部对象。 一个 Lucene 文件包含一个键-值对应的扁平表单。 为了让 ES可以有效的索引内部对象,将文件转换为以下格式:注意都是数组...

    {
        "tweet": [
            elasticsearch,
            flexible,
            very
        ],
        "user.id": [
            @johnsmith
        ],
        "user.gender": [
            male
        ],
        "user.age": [
            26
        ],
        "user.name.full": [
            john,
            smith
        ],
        "user.name.first": [
            john
        ],
        "user.name.last": [
            smith
        ]
    }

    内部栏位可被归类至name,例如"first"。 为了区别两个拥有相同名字的栏位,我们可以使用完整路径,例如"user.name.first" 或甚至类型名称加上路径:"tweet.user.name.first"。
    注意: 在以上扁平化文件中,并没有栏位叫作user也没有栏位叫作user.name。Lucene 只索引阶层或简单的值,而不会索引复杂的文档结构

    (9) 内部对象数组

    一个包含内部对象的数组如何索引,假如有个数组如下所示:

    {
        "followers": [
            {
                "age": 35,
                "name": "Mary White"
            },
            {
                "age": 26,
                "name": "Alex Jones"
            },
            {
                "age": 19,
                "name": "Lisa Smith"
            }
        ]
    }

    此文件会如我们以上所说的被扁平化,但其结果会像如此:

    {
        "followers.age": [
            19,
            26,
            35
        ],
        "followers.name": [
            alex,
            jones,
            lisa,
            smith,
            mary,
            white
        ]
    }


    但是这种合并数组的方式会带来问题,{age: 35}与{name: Mary White}之间的关联会消失,因每个多值的栏位会变成一个值集合,而非有序的阵列。 这让我们可以知道:是否有26岁的追随者?
    但我们无法取得准确的资料如:
    是否有26岁的追随者且名字叫Alex Jones?
    可以通过关联内部对象(Nested Object)解决此类问题,称之为嵌套对象。

    四、元数据类型

    ES支持的元数据类型分成如下几类:
    1:Identity meta-fields:_index、_type、_id、_uid(_type和_id的组合值)
    2:Document source meta-fields:_source、_size(_source的bytes)
    3:Indexing meta-fields:_all、_field_names(文档中值非null的字段名称)、_timestamp(文档关联
    的时间戳,手动设置或自动生成)、_ttl(文档的存活时间)
    4:Routing meta-fields:_parent(设置文档的parent-child关系)、_routing
    5:Other meta-fielde:_meta(具体应用使用的特殊的元数据)

    (1) _source

     默认情况下,ES用JSON字符串来表示源数据,即没有经过分词等等处理的数据,并保存在_source字段中。

    可以在mapping中定义_source的处理方式: false表示禁用

    {
        "mappings": {
            "my_type": {
                "_source": {
                    "enabled": false
                }
            }
        }
    }

    (2) _all

    泛指所有的字段,也可以是所有的类型,索引等,泛指所有的

    _all指字段时可以看做是一个特殊的字段,所有被include_in_all选项控制的字段都包含在内。但是要注意_all会被当作用一个字符串对象。哪怕字段中原本的mapping类型是date,在_all中依然会当做一个字符串,而date和string的索引方式是不同的。

    同样可以在mapping中禁用:

    {
        "my_type": {
            "_all": {
                "enabled": false
            }
       

    可以在type,字段多个层次去控制是否需要被包含在_all中

    PUT /my_index/my_type/_mapping
    {
        "my_type": {
            "include_in_all": false,
            "properties": {
                "title": {
                    "type": "string",
                    "include_in_all": true
                },
                ...
            }
        }
    }

    _all字段仅仅是一个经过分词的string字段。它使用默认的分析器来分析它的值,而不管这值本来所在字段指定的分析器。

    PUT /my_index/my_type/_mapping
    {
        "my_type": {
            "_all": {
                "analyzer": "whitespace"
            }
        }
    }

     

    (3) _id

    即唯一id,注意一个唯一的文档由index type id唯一决定,其中type仅仅只是一个逻辑上的概念

    最好是所有type中不同文档的字段名都不相同

    五、动态映射

    ES支持自动的动态映射,我们很少去修改ES默认的设置,仅仅是某些特殊需求下才会修改动态映射的默认行为

    如何禁止自动映射?

    生产环境中,可能出于更加规范的目的强制要求必须手动建立映射去禁止自动映射,可以在配置文件中配置。也可以使用api

    PUT /_settings
    { "index.mapper.dynamic":false}

    _default_: 为没有 手动指定mapping的type指定默认mapping内容

    PUT my_index
        {
            "mappings": {
                "_default_": {
                    "_all": {
                        "enabled": false
                    }
                },
                "user": {},
                "blogpost": {
                    "_all": {
                        "enabled": true
                    }
                }
            }
        }

    _default的意思就表示是类型的默认映射关系,如果没有指定,就用default,比如上面的us.

    动态映射,不重要,仅仅演示

    缺省的,ES会为没有设置映射的字段进行动态映射,可以通过 dynamic 设置来控制动态映射,它接受下面几个选项:

    (1)true:自动添加字段(默认)

    (2)false:忽略字段

    (3)strict:当遇到未知字段时抛出异常

    dynamic 设置可以用在根对象或任何 object 对象上。你可以将 dynamic 默认设置为 strict,

    而在特定内部对象上启用它,例如:

    PUT /my_index
    
    {
    
        "mappings": {
    
            "my_type": {
    
                "dynamic": "strict",
    
                "properties": {
    
                    "title": {
    
                        "type": "string"
    
                    },
    
                    "name": {
    
                        "type": "object",
    
                        "dynamic": true
    
                    }
                }
            }
        }
    }

    六、总结

    ES通过映射来控制索引的行为,并逻辑上对文档进行划分,拥有相同映射的称为一个type。

    通常,建议在以下情况中使用自定义字段映射:

    (1) 区分full-text和exact value,即是否需要分词

    (2) 使用特定的分词器,比如中文需要使用ik

    (3) 优化部分匹配字段

    (4) 指定自定义的日期格式

    ...

    当然ES也支持指定完映射之后修改,但是请注意,已经存在的字段不要随意修改,因为当前这个字段可能已经建立了索引分词等等。修改必然伴随着代价。

    可以新增字段和设置其类型,对已有数据修改,可能导致错误并且不能正确的被搜索到。

     

  • 相关阅读:
    Docker容器启动时初始化Mysql数据库
    使用Buildpacks高效构建Docker镜像
    Mybatis 强大的结果集映射器resultMap
    Java 集合排序策略接口 Comparator
    Spring MVC 函数式编程进阶
    换一种方式编写 Spring MVC 接口
    【asp.net core 系列】6 实战之 一个项目的完整结构
    【asp.net core 系列】5 布局页和静态资源
    【asp.net core 系列】4. 更高更强的路由
    【Java Spring Cloud 实战之路】- 使用Nacos和网关中心的创建
  • 原文地址:https://www.cnblogs.com/carl10086/p/6055974.html
Copyright © 2011-2022 走看看