表格存储是阿里云提供的一个分布式存储系统,可以用来存储海量结构化、半结构化的数据。多元索引(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))
}
}