一个中等的电商平台,天天都要产生百万条原始数据,上亿条用户行为数据。通常来讲,电商数据通常有3种主要类型的数据系统:node
目前搜索引擎技术已经有很是成熟的开源解决方案,最出名的ElasticSearch和Solr都是基于lucence的。不少中小型互联网公司搜索引擎都是基于这两个开源系统搭建的,可是即使如此,一个搜索引擎团队想把搜索引擎质量作到商用标准,从系统熟悉,服务搭建,功能定制,一般须要花费较长时间。mysql
通用搜索引擎应用在互联网商用搜索 一般会遇到以下几个问题 :算法
笔者是有赞大数据架构师,从自身的搜索实践出发,分享搜索引擎实际的架构和解决的问题。sql
有赞搜索引擎实践,上半部分主要介绍搜索引擎的架构和性能优化方面的经验;下半部分是算法篇,介绍有赞实际须要的搜索算法的问题和解决方案。文章仅仅介绍一个中型电商公司实际的使用现状和笔者我的的经验,不表明搜索引擎最佳实践方法,也不表明能够适用全部的场景。数据库
有赞搜索引擎基于分布式实时引擎elasticsearch(ES)。ES构建在开源社区最稳定成熟的索引库lucence上,支持多用户租用,高可用,可水平扩展;并有自动容错和自动伸缩的机制。咱们同事还实现了es与mysql和hadoop的无缝集成;咱们自主开发了高级搜索模块提供灵活的相关性计算框架等功能。编程
互联网索引的特色是实时性高,数据量大。时效性要求用户和客户的各类行为可以第一时间进入索引;数据量大要求一个有效分布式方案能够在常数时间内建立不断增加的TB数量级索引。缓存
实时索引咱们采用 面向队列的架构 ,数据首先写入DB(或文件),而后经过数据库同步机制将数据流写入kafka队列。这种同步机制和数据库主从同步的原理相同,主要的开源产品有mypipe和阿里推出的canal。es经过订阅相应的topic实现实时创建索引。性能优化
若是数据源是文件,则使用flume实时写入Kafka。网络
另一个索引问题是全量索引。有以下几个场景让 全量索引是一个必要过程 :session
咱们采用 Hadoop-es 利用hadoop分布式的特性来建立索引。hadoop-es让分布式索引对用户透明,就像单机更新索引同样。一个是分布式的数据平台,一个是分布式搜索引擎,若是能把这两个结合就可以实现分布式的全量索引过程。Hadoop-es正式咱们想要的工具。
咱们给出一个经过Hive sql建立索引的栗子 :
drop table search.goods_index;
CREATE EXTERNAL TABLE search.goods_index (
is_virtual int,
created_time string,
update_time string,
title string,
tag_ids array
) STORED BY ‘org.elasticsearch.hadoop.hive.EsStorageHandler’ TBLPROPERTIES (
‘es.batch.size.bytes’=’1mb’,
‘es.batch.size.entries’=’0’,
‘es.batch.write.refresh’=’false’,
‘es.batch.write.retry.count’=’3’,
‘es.mapping.id’=’id’,
‘es.write.operation’=’index’,
‘es.nodes’=’192.168.1.10:9200’,
‘es.resource’=’goods/goods’);
系统把es映射成hive的一个外部表,更新索引就像是写入一个hive表同样。实际上全部分布式问题都被系统透明了。
不建议从数据库或文件系统来全量索引。一方面这会对业务系统形成很大的压力,另外一方面由于数据库和文件系统都不是真正分布式系统,本身写程序保证全量索引的水平扩展性很容易出问题,也没有必要这么作。
全量索引和增量索引的架构以下图所示。另一点是hadoop也是订阅kafka备份数据库和日志的。我我的建议一个公司全部DB和文件都存储在hadoop上,这样作起码有二个 好处 :
数据仓库的话题不在本篇文章的讨论范围,这里只是简单提一下。
为何咱们选择Kafka?Kafka 是一个以高吞吐著名的消息系统。Kafka开启了日志合并(log compaction)功能后,能够永久保存每条消息。每一条消息都有一个key,正好对应数据库的主键,Kafka始终保存一个key最新的一条消息,历史版本会被垃圾回收掉。有了这个特性,Kafka不只能够保存数据库最新的快照,并且能够实现实时更新的消息系统。
第一次同步的时候,数据表中每行记录都转化成以主键为key的消息进入Kafka,而且能够被任意数量的broker消费。以后数据库的每次更新(insert,updated,delete)都会被转化成Kafka的消息。若是一行记录频繁被更改,Kafka会识别这些重复的消息,把旧的消息回收掉。
Kafka既保存数据库最新的全量数据,又提供实时数据流的这个特性为架构的可维护性提供极大便捷。若是你想从零扫描整个数据库,你只须要从开始消费这个Kafka的topic便可完成,当读到topic末端,自动得到实时更新的特性。
Kakfa的另外一个特性是 支持从任意断点读取数据 ,好比咱们全量索引是从HDFS中读取,咱们能够根据HDFS保存的数据的最后一条的时间戳,直接切换到Kafka读取以后的数据。
高级搜索模块(AS)在商业搜索引擎起到相当重要的做用。在各大商业搜索引擎公司里面AS已经成为标配,也是变动最为频繁的模块。
AS在商业搜索引擎中主要起到以下做用:
AS 一个主要的功 能 是经过一个个业务插件来表明相应的搜索。一个最简单的插件只须要包含对应的ES search API,它实际上就是一个配置项,说明es的地址。 这样AS就是一个纯代理。可是商业搜索的需求都是否是ES自己可以支持的,因此就须要根据需求写相应的Query rewriter,rerank等算法插件。这样就实现了框架和业务分离,AS具备极强的扩展性和复用性。
AS 另外一个功能 是提供通用算法库,实际上它只为每种算法提供编程框架。 算法也是经过插件的方式加入算法库的。这种方法可让算法工程师抽象公共算法库供业务方使用,避免从新造轮子。一个具体业务要么使用已经存在的算法(并修改参数),要么本身实现算法。
上图是一个 实例 。商品搜索和分销搜索各自实现一个rerank的的算法,同时都调用了系统提供的rerank1的算法库,并加入了本身特有的逻辑。
AS除了基本proxy功能外,还提供基于query的cache功能用于应用级别的缓存。内部有一个缓冲队列,防止出现雪崩现象。下一节性能优化中会详细说明。
下面几个小结,咱们写了几个咱们遇到的性能优化场景。
ES一个问题是在高峰期时候极容易发生雪崩。ES有健全的线程池系统来保证并发与稳定性问题。 可是在流量突变的状况下(好比双十一秒杀)仍是很容易发生瘫痪的现象,主要的缘由以下:
在AS里咱们实现了面向请求的全局队列来保证稳定性。 它主要作了3件事情。
应用级队列解决雪崩问题有点粗暴,若是一个应用自己查询就很是慢,很容易让一个应用持续超时好久。咱们根据搜索引擎的特色编写了自动降级功能。
好比商品搜索的例子,商品搜索最基本的功能是布尔查询,可是还须要按照相关性分数和质量度排序等功能,甚至还有个性化需求。完成简单的布尔查询,ES使用bitsets操做就能够作到,可是若是若是须要相关性分,就必须使用倒排索引,并有大量CPU消耗来计算分数。ES的bitsets比倒排索引快50倍左右。
对于有降级方案的slide,AS在队列响应过慢时候直接使用降级query代替正常query。这种方法让咱们在不扩容的状况下成功度过了双十一的流量陡增。
理解lucence filter工做原理对于写出高性能查询语句相当重要。许多搜索性能优化都和filter的使用有关。filter使用bitsets进行布尔运算,quey使用倒排索引进行计算,这是filter比query快的缘由。 bitsets的优点 主要体如今:
举个例子:
query:bool:
tag:'mac'
region:'beijing'
title: "apple"
lucence处理这个query的方式是在倒排索引中寻找这三个term的倒排链,并使用跳指针技术求交,在运算过程当中须要对每一个doc进行算分。实际上tag和region对于算分并无做用,他们充当是过滤器的做用。
这就是过滤器使用场景,它只存储存在和不存在两种状态。 若是咱们把tag和region使用bitsets进行存储,这样这两个过滤器能够一直都被缓存在内存里面,这样会快不少。 另外tag和region之间的求交很是迅速,由于64位机器能够时间一个CPU周期同时处理64个doc的位运算。
一个lucence金科玉律是: 能用filter就用filter,除非必须使用query(当且仅当你须要算分的时候)。
query:
filtered:
query:
title: "apple"
filter:
tag:"mac"
region:"beijing"
lucence的filtered query会智能的先计算filter语句,而后才计算query语句,尽量在进行复杂的倒排算法前减小计算空间。
线上集群关闭分片自动均衡。分片的自动均衡主要目的防止更新形成各个分片数据分布不均匀。可是若是线上一个节点挂掉后,很容易触发自动均衡,一时间集群内部的数据移动占用全部带宽。建议采用闲时定时均衡策略来保证数据的均匀。
尽量延长refresh时间间隔。为了确保实时索引es索引刷新时间间隔默认为1秒,索引刷新会致使查询性能受影响,在确保业务时效性保证的基础上能够适当延长refresh时间间隔保证查询的性能。
除非有必要把all字段去掉。索引默认除了索引每一个字段外,还有额外建立一个all的字段,保存全部文本,去掉这个字段能够把索引大小下降50%。
建立索引时候,尽量把查询比较 慢 的索引和 快 的 索引物理分离 。
本文对es自己的优化写的很少,由于es官网和其余的博客有不少es优化的意见,就不在一一枚举。本文的主要目的是可以对搭建商用电商搜索引擎给读者一个通常性的建议,另外,困扰商用搜索引擎的最多见的问题是排序和算法问题。
在上半部分中,咱们介绍了有赞搜索引擎的基本框架。搜索引擎主要3个部件构成。第一,hadoop集群,用于生成大规模搜索和实时索引; 第二,ElasticSearch集群,提供分布式搜索方案; 第三,高级搜索集群,用于提供商业搜索的特殊功能。
建立索引过程从原始数据建立倒排索引的过程。这个过程当中咱们对商品(doc)进行分析,计算商品静态分,并对商品进行类似度计算。商品的静态分对于提高搜索引擎质量起到相当重要的做用,至关于网页搜索的pagerank,想象一下若是没有pagerank算法,网页搜索的质量会有多么差。在电商搜索中,最多见的问题是类似商品太多,必须在创建索引过程当中就对商品间的类似度进行预计算,以便在检索过程当中进行有效去重。
建立索引的过程以下:
step 1,计算每一个doc的静态分;
step 2,计算两两doc的类似度;
step 3,根据类似度和其余信息对数据进行分库;
step 4,创建ES索引。
检索过程是搜索引擎接收用户的query进行一系列处理并返回相关结果的过程。商业搜索引擎在检索过程当中须要考虑2个因素:1) 相关性,2) 重要性。
相关性是指返回结果和输入query是否相关,这是搜索引擎基本问题之一,目前经常使用的算法有BM25和空间向量模型。这个两个算法ElasticSearch都支持,通常商业搜索引擎都用BM25算法。BM25算法会计算每一个doc和query的相关性分,咱们使用Dscore表示。
重要性是指商品被信赖的程度,咱们应该吧最被消费之信赖的商品返回给消费者,而不是让消费之本身鉴别。尤为是在商品充分竞争的电商搜索,咱们必须赋予商品合理的重要性分数,才能保证搜索结果的优质。重要性分,又叫作静态分,使用Tscore表示。
搜索引擎最终的排序依据是:
Score = Dscore * Tscore
即综合考虑静态分和动态分,给用户相关且重要的商品。
检索的过程大体抽象为以下几个步骤。
step 1,对原始query进行query分析;
step 2,在as中根据query分析结果进行query重写;
step 3,在as中使用重写后的query检索es;
step 4,在es查询过程当中根据静态分和动态分综合排序;
step 5,在as中吧es返回的结果进行重排;
step 6,返回结果。
在电商搜索引擎里面商品的静态分是有网页搜索里面的pagerank同等的价值和重要性,他们都是doc固有的和查询query无关的价值度量。pagerank经过doc之间的投票关系进行运算,相对而言商品的静态分的因素会更多一些。商品静态计算过程和pagerank同样 须要解决以下2个问题 :
咱们假设商品的静态分有3个决定性因素:1,下单数;2,好评率;3,发货速度。
静态分咱们使用Tsocre表示,Tscore能够写成以下形式:
Tscore = a * f(下单数) + b * g(好评率) + c * h(发货速度)
a,b,c是权重参数,用于平衡各个指标的影响程度。f,g,h是表明函数用于把原始的指标转化成合理的度量。
首先,咱们须要寻找合理的表明函数。
z-score 标准化方法
“几率论”告诉咱们对于知足正态分布的数据来讲,均值先后3个z-score的范围能够覆盖99%的数据。经验地,咱们把>5个zscore 或者小于 -5个zscore的分数设置成5*zscore或者-5zscore。特别说明的是,咱们不建议使用min-max标准化方法。这种方法又叫离差标准化,是对原始数据的线性变换,使结果值映射到[0-1]之间,转化函数以下:
这种方法很是不稳定,假设一个奇异点是第二大的值的1000倍,会让大部分的值都集中在0~0.01,一样失去了归一化的目的。
图一是使用min-max归一化后的数据分布,显然大部分数据被”压扁”在很小的范围; 图二使用log归一化后的数据分布,因为log缓解了增加速度,能够看出来已经有一个不错的结果了;图三是在log的基础上进行z-score归一化,能够看出来,z-score让数据变得很是平滑。
(图一: min-max归一化)
(图二: log归一化)
(图三: log-zscore归一化)
最后,选择合适的权重 通过log-zscore归一化之后,咱们基本上吧f,g,h的表示的表明函数说明清楚。Tscore = af(下单数) + bg(好评率) + c*h(发货速度),下一步就是肯定a,b,c的参数。通常有两个方法:
商品标题去重在电商搜索中起到重要做用,根据数据,用户经过搜索页购买商品80%选择搜索的前4页。商品标题的重复会致使重要的页面没有含金量,极大下降了搜索的购买率。
举个例子:
Title1:美味/香蕉/包邮/广东/高州/香蕉/banana//无/催熟剂/
Title2:美味/香蕉/广东/高州/香蕉//非/粉蕉/包邮/
这里用到 “bag of word” 技术,将词汇表做为空间向量的维度,标题的每一个term的词频做为这个feature的值。以这个例子来讲。这个词汇的维度为: 美味(0),香蕉(1),包邮(2),广东(3),高州(4),banana(5),无(6),催熟剂(7),非(8),粉蕉(9) 位置: 0,1,2,3,4,5,6,7,8,9
Title1: 1,2,1,1,1,1,1,1,0,0
Title2: 1,2,1,1,1,0,0,0,1,1
这个每一个title都用一个固定长度的向量表示。
再次,计算两两类似度。
类似度通常是经过计算两个向量的距离实现的,不失通常性,在这里咱们使用1-cosine(x,y)来表示两个向量的距离。这是一个”All Pair Similarity”的问题,即须要两两比较,复杂度在O(n^2)。在商品量巨大的时候单机很难处理。咱们给出两种方法用于实现”All Pair Similarity”。
方法一:spark的矩阵运算。
rddRows = sc.parallelize([“1 0 2 0 0 1”, “0 0 4 2 0 0″])
rddRows.map(lambda x: Vectors.dense([float(each) for each in str(x).split(” “)]))
mat = RowMatrix(rddRows)
simsPerfect = mat.columnSimilarities()
方法二:map-reduce 线性方法。
这个方法参考论文”Pairwise Document Similarity in Large Collections with MapReduce”。能够实现几乎线性的时间复杂度。相对于矩阵运算在大规模(10亿以上)pair similarity 运算上面有优点。这个方法简单的描述以下: 首先,按照倒排索引的计算方式计算每一个term到doc的映射。好比3个doc:
doc1 = 我 爱 北京
doc2 = 我 北京 天安门
doc3 = 我 天安门
转化为倒排格式,这个须要一次mapper reduce
我 -> doc1, doc2, doc3
爱 -> doc1
北京 -> doc1, doc2
天安门 -> doc2, doc3
而后,对于value只有一个元素的过滤掉,对于value大于2个doc的两两组合:
doc1,doc2 <—- from: 我 -> doc1, doc2, doc3
doc1,doc3 <—- from: 我 -> doc1, doc2, doc3
doc2,doc3 <—- form: 我 -> doc1, doc2, doc3
doc1,doc2 <—- from: 北京 -> doc1, doc2
doc2,doc3 <—- from: 天安门 -> doc2, doc3
最后,对于输出进行聚合,value为重复次数和两个doc乘积开根号的比。
doc1,doc2 -> 2/(len(doc1)*len(doc2))^1/2 = 0.7
doc1,doc3 -> 1/(len(doc1)*len(doc3))^1/2 = 0.3
doc2,doc3 -> 2/(len(doc2)*len(doc3))^1/2 = 0.3
对于2个title1,title2,若是X(title1,title2) > 0.7 则认为title1和title2类似,对于类似的两个doc,静态分大的定义为主doc,静态分小的定义为辅doc。主doc和辅doc分别建库。
区别于网页搜索(网页搜索直接将辅doc删除),咱们将主doc和辅doc分别建库。每一次搜索按比例分别搜主库和辅库,并将结果融合返回。这样能够保证结果的多样性。
店铺去重和商品标题去重有点不一样。因为电商特定场景的须要,不但愿搜索结果一家独大,这样会引起强烈的马太效应。店铺去重不能使用如上的方法进行。由于上面的方法的主要依据是文本类似,在结果都相关的前提下,进行适当的取舍。可是店铺去重不是这样的特性。
设想一下,若是咱们根据店铺是否相同,把同一店铺的商品分到主库和从库中,以下图所示。
A和B表明不一样的店铺。
在搜索香蕉的时候,的确能够控制A店铺结果的数量,可是在搜索”梨”的时候就错误的吧B店铺的梨排在前面了(假设A:梨比B:梨静态分高)。
实际上想达到店铺去重的效果经过分桶搜索是很容易作的事情。咱们假设每页搜索20个结果,咱们把索引库分红4个桶,每一个商品对桶数取模获得所在桶的编号。这样能够保证同一店铺的商品仅在一个桶里面。搜索的过程每一个桶平均分摊搜索任务的25%,并根据静态分合并成一页的结果。这样同一保证结果的相对顺序,又达到了店铺去重的目的。
如上图所示,搜索”香蕉”,虽然A店铺有10个知足需求的结果,可是每页搜索醉倒只有5个结果能够展现。
query分析与Query改写技术
上面介绍了几个创建索引过程当中几项技术,检索过程当中的关键技术有不少。其中最著名的是query分析技术。咱们使用的query分析技术主要包括核心词识别,同义词拓展,品牌词识别等等。query分析技术大部分都是NLP研究范围,本文就不详细阐述不少理论知识。咱们重点介绍同义词拓展技术。这个技术通常都须要根据本身的商品和和用户日志特定训练,没法像分词技术和品牌词识别同样有标准的库能够适用。
同义词拓展通常是经过分析用户session日志获取。若是一个用户输入”苹果手机”没有获得想要的结果,他接着输入”iphone”,咱们在”苹果手机”和”iphone”之间建立一个转移关系。基于统计,咱们能够把用户query建立一个相互联系的权重图。
用户输入query “苹果手机”,根据query分析,”苹果手机”有 “iphone”0.8,”iphone 6″0.5 两个同义词。0.8和0.5分别表示同义的程度。咱们想要”苹果手机”,”iphone”,”iphone 6” 3个query同时输入,而且按照同义的程度对不一样的query赋予不一样的权重。ElasticSearch提供的BoostingQuery能够支持这个需求。
原始的query:
{
“query”{
“match”: {
“query”: ”苹果手机”
}
}
}
优化后的query:
{
"query": {
"should": [
{
"match": {
"content": {
"query": "苹果手机",
"boost": 10
}
}
},
{
"match": {
"content": {
"query": "iphone",
"boost": 8
}
}
},
{
"match": {
"content": {
"query": "iphone6",
"boost": 5
}
}
}
]
}
}
其余好比核心词识别,歧义词纠正等方法差很少,本文不作详细阐述。
商业电商搜索算法另外两个重要技术,一个是类目体系创建和应用,另外一个是个性化技术。这个两项技术咱们还处在探索阶段。类目体系咱们主要使用机器学习的方法进行训练,个性化主要经过用户画像进行Query改写来实现。等咱们上线有效果在与你们分享。
搜索算法是一个很是值得一个电商产品持续投入的技术。一方面若是技术人员要有良好的技术背景,能够借鉴不少成熟的技术,避免重复造轮子; 另外一方面,每一个产品的搜索都有自身的特色,须要深刻研究产品的特性给出合理的解决方案。