TableStore多元索引复合类型的使用诀窍:array还是nested

表格存储是阿里云提供的一个分布式存储系统,可以用来存储海量结构化、半结构化的数据。多元索引(SearchIndex)可以支持基于属性列的丰富查询类型,帮你挖掘出数据的更多潜能。
对主表中需要查询的列(Column)建立多元索引,即可通过该列的值查询数据。索引列可以分为以下几种类型:

  • 基本类型:不可再分的字段类型,包括long, double, boolean, keyword, text, geo_point
  • 复合类型:由基本类型构成,包括数组类型(array)和嵌套类型(nested)。


本文将结合具体使用场景说明两种复合类型的使用——

  • 数组类型(array):数组元素类型必须一致,只能是基本类型,元素个数不固定;
  • 嵌套类型(nested):nested类型本质上也是数组,数组元素内部可以嵌套基本类型,查询时保证各元素的独立性。

数组类型(array)

数组类型是一种复合的索引字段类型。如同C++/Java中的数组,一个数组类型的索引字段由0个或多个相同基本类型组成,只需要在定义基本类型字段时指定IsArray: true即可。常见的使用场景如,为“用户信息表”创建多元索引,为每个用户创建一个“用户标签(tags)”索引列——类型为TEXT类型数组。当好友为该用户打标签时,会作为一个数组元素被追加到tags列中,数量从0个到多个不等,如下所示。

fieldAuthors := &tablestore.FieldSchema{
    FieldName:        proto.String("tags"),                //字段名
    FieldType:        tablestore.FieldType_TEXT,    //定义数组类型的每个元素为TEXT类型
    Index:            proto.Bool(true),               //开启索引
    IsArray:          proto.Bool(true),                        //定义字段类型为数组
}

使用限制

  • 每个数组元素都必须是相同类型。如果无法保证数组元素类型相同,可以考虑使用nested。
  • array类型字段的取值只能以“json字符串”的列表,才能被同步到array类型索引。

形如["Thomas Cormen", "Charles Leiserson"],["Thomas Cormen"],或[]。地理类型的数组类型如["41.12,-71.34", "41.13,71.34"]。

  • 数组元素只能是基本类型。

嵌套类型(nested)

nested类型是嵌套文档类型,指一个文档(一行数据)可以包含多个子文档,多个子文档保存在一个nested类型的列中。对于一个nested类型的列,需要指定其子文档的结构,即指定子文档中需包含哪些字段,以及每个字段的属性。在多元索引底层实现中,文档和子文档是平等独立的。
nested类型本质上也是数组。array类型的数组元素只能是基本类型,有时无法准确描述业务,或者不支持把数组元素视为独立对象进行复杂查询,这时您可以考虑使用nested类型。例如,为“用户信息表”创建多元索引,每个用户都有一个好友列表,列表中每个好友的信息包括名字、性别、年龄等,用一个基本类型的字段无法准确描述。于是我们定义"friends"这个NESTED类型字段——嵌套TEXT类型的name字段(表示好友姓名)和LONG类型的gender字段(表示好友性别)。

fieldFriends := &tablestore.FieldSchema{
    FieldName:        proto.String("friends"),          //好友列表
    FieldType:        tablestore.FieldType_NESTED,       //字段类型: NESTED类型
    FieldSchemas:     [] *tablestore.FieldSchema {
        {
            FieldName:    proto.String("name"),        //内部字段名: 好友姓名
            FieldType:    tablestore.FieldType_TEXT,    //内部字段类型: TEXT类型
        },
        {
            FieldName:    proto.String("gender"),    //内部字段名: 好友性别
            FieldType:    tablestore.FieldType_LONG,    //内部字段类型: LONG类型, 0(男), 1(女), 2(其他)
        },
    },
}


嵌套类型不能直接查询,需要通过NestedQuery包装一下,即可对嵌套字段进行复杂组合查询。NestedQuery中需要指定嵌套类型的字段路径(Path)以及一个子Query(可以是任意Query)。如下面查询条件,将匹配“好友列表中有30岁以下女性好友”的那些用户。

query := &search.NestedQuery{                //nested查询
    Path:        "friends",
    Query:        &search.BoolQuery{
        MustQueries: []search.Query {
            &search.TermQuery{
                FieldName:    "friends.gender",    //查询字段friends.gender(nested内部字段)
                Term:         1,                //查询字段取值: 1 (女)
            },
            &search.RangeQuery{
                FieldName:    "friends.age",    //查询字段friends.age(nested内部字段)
                To:           30,                //年龄<30岁
                IncludeUpper: false,            //不包括上限
            },
        },
    },
    ScoreMode:    search.ScoreMode_Avg,
}

使用限制

  • nested类型字段的取值只能以“json字符串”的列表,才能被同步到array类型索引。形如[{"name": "王二", "gender": 0, "age": 30}, {"name": "小谭", "gender": 1, "age": 25}]。
  • NESTED不允许嵌套,即nested字段的内部字段只能是基本类型。
  • 最多100个NESTED字段。

代码示例

用户信息表及其多元索引,定义了array类型字段"tags",以及nested类型字段"friends",并演示如何进行NestedQuery匹配“好友列表中有30岁以下女性好友”的那些用户。

func putUser(client *tablestore.TableStoreClient, tableName string, uid string, name string, desc string, tags string, friends string) {
    putRowRequest := new(tablestore.PutRowRequest)
    putRowChange := new(tablestore.PutRowChange)
    putRowChange.TableName = tableName                            //设置表名
    putPk := new(tablestore.PrimaryKey)
    putPk.AddPrimaryKeyColumn("uid", uid)        //设置主键

    putRowChange.PrimaryKey = putPk
    putRowChange.AddColumn("name", name)            //用户姓名
    putRowChange.AddColumn("desc", desc)            //用户描述
    putRowChange.AddColumn("tags", tags)            //用户标签
    putRowChange.AddColumn("friends", friends)        //用户描述
    putRowChange.SetCondition(tablestore.RowExistenceExpectation_EXPECT_NOT_EXIST)
    putRowRequest.PutRowChange = putRowChange
    _, err := client.PutRow(putRowRequest)
    if err != nil {
        fmt.Println("PutRow failed with error:", err)
    } else {
        fmt.Println("PutRow ok")
    }
}

func ArrayDemo(client *tablestore.TableStoreClient, tableName string, indexName string) {
    //1. 创建表
    createtableRequest := new(tablestore.CreateTableRequest)

    tableMeta := new(tablestore.TableMeta)
    tableMeta.TableName = tableName                //设置表名
    tableMeta.AddPrimaryKeyColumn("uid", tablestore.PrimaryKeyType_STRING)    //主键列
    tableOption := new(tablestore.TableOption)
    tableOption.TimeToAlive = -1                //TTL无限
    tableOption.MaxVersion = 1                    //每列最多1个数据版本
    reservedThroughput := new(tablestore.ReservedThroughput)
    reservedThroughput.Readcap = 0
    reservedThroughput.Writecap = 0
    createtableRequest.TableMeta = tableMeta
    createtableRequest.TableOption = tableOption
    createtableRequest.ReservedThroughput = reservedThroughput  //设置预留读写吞吐量

    _, err := client.CreateTable(createtableRequest)            //创建主表
    if err != nil {
        fmt.Println("Failed to create table with error:", err)
        return
    } else {
        fmt.Println("Create table finished")
    }

    //2. 创建多元索引
    request := &tablestore.CreateSearchIndexRequest{}
    request.TableName = tableName                            //设置表名
    request.IndexName = indexName                            //设置索引名

    schemas := []*tablestore.FieldSchema{}                    //索引字段列表(根据业务,您还可以添加更多字段)
    fieldTitle := &tablestore.FieldSchema{
        FieldName:        proto.String("name"),            //用户名
        FieldType:        tablestore.FieldType_TEXT,        //字段类型: TEXT类型
        Index:            proto.Bool(true),               //开启索引
    }
    fieldTime := &tablestore.FieldSchema{
        FieldName:        proto.String("desc"),            //用户描述
        FieldType:        tablestore.FieldType_TEXT,           //字段类型: TEXT类型
        Index:            proto.Bool(true),               //开启索引
    }
    fieldTags := &tablestore.FieldSchema{
        FieldName:        proto.String("tags"),          //个人标签
        FieldType:        tablestore.FieldType_TEXT,           //字段类型: TEXT类型
        Index:            proto.Bool(true),               //开启索引
        IsArray:          proto.Bool(true),                //是数组
    }
    fieldFriends := &tablestore.FieldSchema{
        FieldName:        proto.String("friends"),          //好友列表
        FieldType:        tablestore.FieldType_NESTED,       //字段类型: NESTED类型
        FieldSchemas:     [] *tablestore.FieldSchema {
            {
                FieldName:    proto.String("name"),        //内部字段名: 好友姓名
                FieldType:    tablestore.FieldType_TEXT,    //内部字段类型: TEXT类型
            },
            {
                FieldName:    proto.String("gender"),    //内部字段名: 好友性别
                FieldType:    tablestore.FieldType_LONG,    //内部字段类型: LONG类型, 0(男), 1(女), 2(其他)
            },
            {
                FieldName:    proto.String("age"),        //内部字段名: 好友年龄
                FieldType:    tablestore.FieldType_LONG,    //内部字段类型: LONG类型
            },
        },
    }
    schemas = append(schemas, fieldTitle, fieldTime, fieldTags, fieldFriends)

    request.IndexSchema = &tablestore.IndexSchema{
        FieldSchemas: schemas,                                //设置索引字段
    }

    _, err = client.CreateSearchIndex(request)                //创建多元索引
    if err != nil {
        fmt.Println("Failed to create index with error:", err)
        return
    } else {
        fmt.Println("Create index finished")
    }
    time.Sleep(time.Duration(20) * time.Second)               //等待数据表加载

    //3. 插入一些测试数据
    putUser(client, tableName, "0001", "Thomas Corment", "<Introduction to Algorithms>作者", "[\"技术牛人\", scientist]", "[{\"name\": \"老周\", \"gender\": 0}]")
    putUser(client, tableName, "0002", "小明", "资深研发", "[\"资深研发\", \"聪明可爱\"]",
        "[{\"name\": \"王二\", \"gender\": 0, \"age\": 30}, {\"name\": \"小谭\", \"gender\": 1, \"age\": 25}]")

    time.Sleep(time.Duration(20) * time.Second)       //等待数据同步到多元索引(通常,稳定后延迟在1s~10s级别)

    //4. 查询
    searchRequest := &tablestore.SearchRequest{}
    searchRequest.SetTableName(tableName)       //设置主表名
    searchRequest.SetIndexName(indexName)       //设置多元索引名

    query := &search.NestedQuery{                //nested查询
        Path:        "friends",
        Query:        &search.BoolQuery{
            MustQueries: []search.Query {
                &search.TermQuery{
                    FieldName:    "friends.gender",    //查询字段friends.gender(nested内部字段)
                    Term:         1,                //查询字段取值: 1 (女)
                },
                &search.RangeQuery{
                    FieldName:    "friends.age",    //查询字段friends.age(nested内部字段)
                    To:           30,                //年龄<30岁
                    IncludeUpper: false,            //不包括上限
                },
            },
        },
        ScoreMode:    search.ScoreMode_Avg,
    }

    searchQuery := search.NewSearchQuery()
    searchQuery.SetQuery(query)
    searchRequest.SetColumnsToGet(
        &tablestore.ColumnsToGet{
            ReturnAll:false,
            Columns: []string{"name", "friends"},
        })
    searchRequest.SetSearchQuery(searchQuery)

    searchResponse, err := client.Search(searchRequest)   //查询
    if err != nil {
        fmt.Println("Failed to search with error:", err)
        return
    }
    fmt.Println("IsAllSuccess: ", searchResponse.IsAllSuccess)  //查看返回结果是否完整
    fmt.Println("TotalCount: ", searchResponse.TotalCount)    //返回所有匹配到的行数
    fmt.Println("RowCount: ", len(searchResponse.Rows))         //返回的行数
    for _, row := range searchResponse.Rows {           //打印本次返回的行
        jsonBody, err := json.Marshal(row)
        if err != nil {
            panic(err)
        }
        fmt.Println("Row: ", string(jsonBody))
    }
}
上一篇:*** $CI =& get_instance() 用法:关于CodeIgniter中get_instance() 函数


下一篇:【PHP框架CodeIgniter学习】使用辅助函数—建立自己的JSONHelper