当采用Azure Search 来搜索结构化数据,在考虑怎样构建合理的结构,以及怎样合理使用query方法的同时,也需要考虑到怎样把数据同步到search index上。Azure Search对数据上传提供了两种方式。
- 通过rest API的方式上传数据。官方提供的SDK也是根据rest API来实现的。
- 通过Search Indexer直接将数据源映射到search index上,其会主动爬取数据源并上传到search index上去。
这两种方式差别在从数据提供方的角度来看是push和polling的区别。
第一种方式是数据提供方需要将自己的数据主动push到search index上,一般需要自建服务去抓去数据并组合成搜索需要的结构,并上传到search index,如图所示:
push-mode-of-upload-data-to-search-index.png第二种方式是数据提供方允许indexer接入自己的数据源,indexer就可以按照既定的查询数据源并mapping到search index上,这对于数据提供方来讲就是一种polling的方式,因为其不用自建服务。
polling-mode-of-upload-data-to-search-index.png那么选择使用哪种方式会更好呢?这个是要由不同的业务需求来驱动决定的,当然更需要明确每种方式的特点以及限制是什么,这里大概列一下。
对于第一种push方式的rest API而言,有如下特点及限制
- 每次提交的文本大小应该限制在16M以下
- 每次提交的文本数最大在1000个
- 两次顺序的push请求之间没有时间的限制,从而单个文档能够达到秒甚至毫秒级别的更新
对于第二种polling方式的Indexer而言,有如下的特点及限制
- 能够直接访问数据源,批量抓去数据的速度非常快,一分钟内能完成数万条数据的更新
- 对数据源有所限制,其可以是在Azure 云上的sql server数据库、blob storage等,具体请看链接:data source input
- Indexer运行的可以是一次性的,也可以是定时任务的方式。但是定时任务有最短5min的时间限制
以上是我认为可能会影响决策的这两种方式需要考虑的点。那么基于这些点,我们应该怎样做决策呢。分享一下心得,当你需要解决的问题是如何往search index上初始化数据时,你需要问这样一些问题:
- 你的数据源在哪儿?
- 能接受的最小时间的初始化时间是多少?
当你的数据源没法满足Indexer所需求的datasource的类型,那么你没有必要再在第二种方案上去纠结了。如果你希望整个过程耗时较短并且开发成本低,那么第二种方案会是一个好的选择。是否好调试也应该是需要考虑的一个点,但是两种方式其实都是可调试的,只是对与第二种方案来讲其调试方式可能仅仅只能使用azure提供的rest API通过获取indexer运行的状态来调试,第一种因为相当于要自己写一个抓取数据并上传数据的应用,调试方式可以相对灵活。
当你需要解决的问题是如何往search index上持续的更新数据,你可能需要问:项目对数据实时性的要求怎样?如果对实时性要求极高,那么显然indexer的绝对不是一个好的选择,因为它最多能做到5分钟去跑一次,也就是说最少需要5分钟才能将数据更新到search index上,况且当你的数据量较大的时候,甚至都不能保证,一次的运行能在5分钟之内跑完。说到这里就需要提一下Search indexer在运行时的特性:
- 每次运行都是以全量的方式运行,即每次都会抓取声明为dataSource中的所有数据
- 抓取的数据会以mergeOrUpload的方式同步到search index,即如果已经存在对应的文档就以合并的方式更新,如果不存在就创建一条新的文档
- 如果当前运行的indexer没有运行完,但是已经超过了下一次运行的时间,它会直接忽略下一次,直到当前次跑完
所以如果对实时性要求较高,应该使用push的方式去更新数据,关于这块具体的设计,我们将放到另一篇文章中说明。相反如果对实时性要求不高,那么Indexer也会是一个好的选择。
数据的初始化以及数据的持续更新是两个不同的问题,他们的解决方案有时候不能共用。我之前的项目就经历这样的情况,文档的数据量级在千万,客户需要我们至少在一分钟之内将更新数据同步到search-index中,并且要在尽量快的时间之内完成数据的初始化。所以在持续更新的问题上我们只有选择push的方式,但是当我们期待着使用这种push的方式也能完成初始化的工作的时候,发现我们需要做额外的数据准备工作,种种问题导致无法满足时间的短的需求,所以我们采用了indexer的方式去做了初始化,千万的数据也能在十多个小时之内完成。
关于第一种的实践及设计,计划在另一篇文章中讲。本文将继续介绍第二种方式的使用,分享一下自己在这个过程中遇到的一种常见的问题。
如何构建Indexer
Indexer是Azure提供同步数据到Search Index的一种方式。我们将基于从Azure sql server Database到Search Index去讲述Indexer的实现过程。
一个indexer的DataSource在sql server来讲可以是一张表或者一个视图,如果search index的结构比较简单,其字段和某个表对应的字段一样并且类型也一样,那么我们可以以这个表为dataSource。但情况可能常常不是这样的,因为我们在设计search index的时候其数据结构往往是比单个表要复杂的。例如我们search index的结构如下:
{
Id : GUID,
Name : "sring",
Age: num,
Addresses: [
{
LineOne: "string",
City: "string",
GeoLocation: {
type: "Point",
coordinates: [longitude, lititude]
}
},
],
GraduatedSchool: {
Name: "string",
Country: "string"
CountryCode: "string"
}
}
但是对应到数据库表却是一个较复杂的映射关系, 如下面的类图所示:
table-relation-map.png在这种情况下,我们不得不去构建一个视图,这个视图的结构应该和search index 的结构一致。你可能会问这样一些问题,面对复杂结构indexer真的能正确的映射吗,对此我们当时也有疑问,毕竟Azure在官方的文档上面也说,它只支持一些简单数据结构的映射。但是经过我们的几次尝试,发现只要能够保证从视图中查询出来的每一条数据能在JSON序列化之后和search index结构一模一样,那就能将其应用于indexer作为dataSource。
所以我们就花费了一些时间去尝试构建这种结构的视图。最终这个视图的创建sql如下。
CREATE VIEW IndividualView AS
SELECT
ind.Id,
ind.Age,
(
SELECT
CONCAT(
ind.FirstName,
ind.LastName,
)
) AS Name,
(
SELECT
s.Name,
mc.Code AS CountryCode,
mc.Name AS Country
FROM School AS s
LEFT JOIN MetadataCountry AS mc ON s.CountryCode = mc.Code
WHERE
s.Id = c.SchoolId FOR JSON PATH,
WITHOUT_ARRAY_WRAPPER
) AS GraduatedSchool,
(
SELECT
child_addresses.*
FROM (
(
SELECT
adr.LineOne,
adr.City,
'Point' AS 'GeoLocation.type',
JSON_QUERY (
FORMATMESSAGE(
'[%s,%s]',
FORMAT(
m.Longitude,
N'0.##################################################'
),
FORMAT(
m.Latitude,
N'0.##################################################'
)
)
)
AS 'GeoLocation.coordinates',
FROM Address AS adr
WHERE
adr.IndivdiualId = ind.Id
)
) AS child_addresses FOR JSON PATH
) AS Addresses,
FROM Individual AS ind;
涉及到一些SQL函数,在使用过程中也发现其实SQL提供了很多丰富的功能,所以对于构建复杂结构的VIEW基本都没有什么问题。
这里需要特殊说明的就是GeoLocation,在Search index上声明了一个字段是GeographyPoint类型,在上传的时候就需要将数据整理成GeoJson 的格式。具体来说就是如下的结构
{
type : 'Point',
coordinates : [longitude, latitude]
}
具体怎么转换可以参见以上创建View的sql。
成功构建了视图之后,dataSource的创建还没有结束,需要在Azure portal或者调用API来正真的创建,这个过程需要提供数据库的connection string, 并且还需要保证在Azure 云上面数据库的防火墙是允许被Search Service访问的。假设我们使用API的方式来创建,需要发送一下的请求:
POST /datasources?api-version=2019-05-06 HTTP/1.1
Host: {{service-host}}
api-key: {{api-key}}
Content-Type: application/json
{
"name" : "individual-data-source",
"type" : "azuresql",
"credentials" : { "connectionString" : ""}, //提供可访问的connection string
"container" : { "name" : "IndividualView" } //指定视图名
}
创建结束之后就可以创建Indexer了,这个过程也可以使用portal或者API的方式,这里给rest API的例子,需要发送一下的请求:
POST /indexers?api-version=2019-05-06 HTTP/1.1
Host: {{service-host}}
api-key: {{api-key}}
Content-Type: application/json
{
"name": "individual-indexer",
"description": "indexer for individual",
"dataSourceName": "individual-data-source",
"targetIndexName": "individual-index",
"parameters": {
"maxFailedItems": "15",
"batchSize": "500"
},
"schedule": {
"interval": "PT15M", //每15分钟跑一次
"startTime": "2020-01-01T00:00:00Z"
}
"fieldMappings": [
{ "sourceFieldName": "Id", "targetFieldName": "Id" },
{ "sourceFieldName": "Name", "targetFieldName": "Name" },
{ "sourceFieldName": "Age", "targetFieldName": "Age" },
{ "sourceFieldName": "Addresses", "targetFieldName": "Addresses" },
{ "sourceFieldName": "GraduatedSchool", "targetFieldName": "GraduatedSchool"}
]
}
可以看到,在创建过程中我们可以指定很多参数具体参数的说明可以参考create indexer rest api. 这里有一点需要说明的是,声明字段映射的时候,只用声明第一层结构的映射就可以,甚至如果你能确保View的结构以及字段名和search index的一样,你甚至可以不用指明fieldMappings参数。
至此indexer 的构建就完成了,如果指定了schedule参数,indexer就会在指定的开始时间开始运行,如果没有指定,在构建完成之后,indexer就会立马执行。后续的调试可以调用get indexer status API来查看运行状态。
网友评论