Xavier's Blog

电商检索系统总结——功能篇

| Comments

自己作为后端研发工程师,一直在公司电商项目中参与和检索相关的工作。工作的时间也不短了,一直希望能写一些文章来总结、整理下自己接触到的知识点,一方面是为了梳理自己的思路,另一方面也作为一种分享和交流。

本文简单总结一下,电商检索系统需要向用户提供哪些功能。

搜索页面结构

下图是一个电商搜索结果页的基本结构:

搜索页结构

大家可以看到,页面基本上有以下几种元素构成:

  • 搜索栏
  • 商品列表
  • 面包屑
  • 分类树
  • 筛选项
  • 商品推荐

每一种元素,都为用户展现了不同纬度的检索结果;同时,部分元素也为用户提供了进一步的检索、过滤功能。

搜索栏,提供了query检索的功能,用户最常用的寻找商品的方式;

分类树和面包屑,一方面从分类纬度展示了搜索结果,同时用户也可以对于上面的结果进行分类检索

筛选项,提供了对于检索出的商品属性的聚合,同时用户又可以对于自己感兴趣的属性进行单独筛选;

商品列表,是呈现给用户的最终结果;

商品推荐,除了自然的检索结果,还会根据用户当前的检索行为以及历史行为,进行商品推荐。

功能

上面提到的元素,都是为了让用户使用电商检索系统的某些功能,或者向用户展现某些功能的最终结果。下面就具体讲一下电商检索系统需要具备的基本功能:

Query检索

即关键词检索,用户通过输入一个检索词来描述自己的需求,比如“iphone5s”、“三星Galaxy”、“Nike运动鞋”等等。关键词检索,涉及到建立一个检索系统的一些基本步骤:

  • 切词(将一段文本转化为一个一个单元,即term)
  • 建立倒排索引(Inverted Index)
  • 索引归并
  • 排序

切词之前,首先需要确定的是:商品的哪些字段需要被切词并且建入索引。商品的标题是需要建索引的,另外,一般来说,商品的品牌名称、商家名称、分类名也是需要建索引。选择建索引字段的范围,其实是需要一些权衡的,范围选得过大,当然可以提高召回率,但这样也会出现一些bad case(比如将商品描述中一些不相关的term建进了索引),同时倒排拉链过长也会影响性能。

分类检索

一般来说,综合型电商网站的首页,都会有一个分类树全集,供用户直接点击查询。例如下图:

除了Query检索,用户按照商品的分类进行检索的比例也会较大。分类检索和Query检索相比,不同点只是少了切词步骤,另外将term改为商品的分类ID。

说到分类,就要涉及到分类体系。一般来说,有两种分类体系:后端分类体系,和前端分类体系。后端分类体系相对稳定,几乎不变,用户感知不到后端分类;前端分类体系结构可以很灵活,随意变化,一般由运营同学来维护。前、后端分类体系都是树状的结构,而后端分类树的任意节点可以“挂载”至一个或者多个前端分类树的叶子节点上面,这样两套分类体系之间就产生了关联。

这两个分类体系可以类比为超市的货物分类(严格来说应该是电商参考了零售行业的分类方式),一开始货物都是放在后台的库房里面的,它们按照一种分类体系(后端分类系统)来存放,非工作人员是看不到的;而等到货物需要从库房摆放到货架时,超市工作人员可以以时令、促销活动等为依据,让货架上的商品按照另一种体系(前端分类体系)进行组织,顾客只能看到这种组织形式。

排序

用户通过query或者分类检索出的商品结果,默认都是按照相关性排序的。(关于相关性排序,内容还是比较复杂的,另外自己也不是专门做这一块的,这里就不展开讲了)除了按照相关性进行排序,用户还可以按照其它条件进行排序,例如:

  • 价格
  • 折扣
  • 评论数
  • 好评度
  • 上架时间
  • 是否正在促销
  • ……

上面都是用户可以看得到、自己可以选择的排序方式。除了这些,还会有一些其它因素影响商品结果的排序。

首先是一些基本的业务逻辑,比如在自然排序下,有库存的商品排在前面,无库存的排在后面;SPU商品排在前面,SKU商品排在后面。(SKU、SPU的概念后面会讲到)

另外还有一些运营方面的考虑。比如,发现搜索结果中有一个很不相关的商品出现,这时就急需在query粒度上对这个商品进行打压、甚至是不允许展现。或者,由于某种合作关系,在某些query或者分类下,必须将某个商家的商品排在前面。因此,检索系统后台就需要维护这么一份各个维度的商品“黑白名单”。

标签聚合

所谓标签,就是用一些“键-值”的概念来描述一个商品的特点。比如说MacBookPro,可以有如下标签:

  • 品牌:Apple
  • 尺寸:13寸
  • 处理器:Intel i7
  • 价格:9288 RMB
  • ……

当用户检索商品时,检索系统除了直接展示商品以外,还会将商品上面的标签进行聚合,一般都是通过“标签名 + 标签值的列表”的形式展现给用户(如下图),方便用户通过标签进行进一步的筛选。

分类树

当用户进行query检索时,检索系统会进行query分析,将这个query可能对应的分类,通过分类树的形式展现给用户。比如用户搜索“小米”,query分析出的分类既有“手机通讯”,又有“粮油米面”。

一般来说,检索系统为了保证query的准确率,会在检索条件中添加query的预测分类,使得检索结果不至于各种分类的商品混杂在一起,影响用户体验。所以当用户搜索“小米”时,检索结果会限定在“手机通讯”这个分类下,但是如果用户真的是想搜索“粮油米面”下的小米,也没关系,只需在点击分类树中相应分类进行限定即可。

面包屑

面包屑,原来是用于在网站上面显示当前页面在整个sitemap中的位置,方便用户跳转至网站其它地方。在电商网站中,就变成了展现网站所在的分类路径( + 品牌名称 + query),例如

电脑、办公 > 电脑整机 > 笔记本 > 清华同方(THTF) > 清华同方锋锐T200

点击面包屑上面的每一级分类,就可以在某个分类下进行商品检索,方便用户扩大或者缩小检索范围。

过滤

除了进行各种触发(query检索、分类检索等),还需要在触发结果的基础上面,再进行过滤。上面说到的标签过滤、分类树限定,都属于过滤。总结下来,会有这么几种过滤方式:

  • 分类过滤
  • 标签过滤
  • 价格区间过滤
  • 地域过滤
  • 库存过滤
  • 是否自营
  • 商家过滤(针对于微购这样的电商平台)

Query提示

所谓query提示,就是当用户在搜索框中建入query时,系统能提供给用户一个query list,或者一些分类建议,方便用户向检索系统提供给准确的query以及分类范围,减少用户进行重复搜索的次数。

以下是京东的query提示截屏,有拼音翻译为query、有分类预测、有每个query对应的检索商品数,做的比较完善。

相对而言,微购做的query提示就原始许多,输入“shouji”,居然连本身的“手机”都没有,囧……

Query改写

Query分析中的一项功能就是做“query correction”,通过算法或者人工标注的形式,判断出用户真正需要搜索的query是什么。比如用户输入了“按着手机”,检索系统需要能判断出用户搜索的真正query可能是“安卓手机”,当然,好的产品肯定能让用户自行选择,而不是强奸用户,就像上面提到的用户可以选择分类树上的分类,用以明确告知系统自己所需要查找的分类范围。

以下是在京东搜索“按着手机”的截图:

SPU聚合

首先需要提供两个概念:SKU,以及SPU。

根据我在网上查找到的资料,SKU是Stock Keeping Unit,指的是库存的最小单位;而SPU是Standard Product Unit,是指商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性

简单的理解就是,“iPhone4S”是一个SPU,“iPhone4S 白色 16G 电信版”就是一个SKU;“MacBookPro”是一个SPU,“MacBookPro 13寸 8G内存 128G硬盘”就是一个SKU。

因此,当用户进行商品检索时,需要将SKU粒度的商品聚合成SPU粒度,使得检索结果比较多样,从而不至于满屏都是各种颜色、型号的同一款商品。等到用户进行商品详情页之后,再来选择具体的型号。

下图是微购检索结果页SPU、SKU排列结果:

以下是京东商品详情页的截屏,红框中的选项的每一种组合,都代表着不同的SKU。

推荐

推荐系统,是和检索系统同样负责的系统,另外我也并不熟悉相关的知识,所以这里只是根据自己的理解,简单的说一下。

从页面角度来说,几乎所有页面上面都可以进行商品推荐:首页、搜索结果页、详情页、购物车页面、下单成功页、错误页,等等。而不同的页面,推荐的侧重点也会不尽相同。

比如首页推荐,用户这次购物流程还没有任何行为,所以一般都是通过该用户的历史行为向用户进行推荐。

在详情页,用户则已经表现出对于这个商品的较强的需求,一般会有两种类型的推荐:

  • 推荐和该商品类似的商品
  • 推荐可以和该商品进行组合的商品

第一种推荐,在各分类商品中出现的都比较多,一般的推荐理由是“看(购买)过该商品的用户也看(购买)了”;

第二种推荐,一般出现在数码产品中。比如用户在看一款手机时,向用户推荐手机套、手机耳机、SD卡,让用户可以“一页式”完成许多商品的购买,减少用户决策的过程,激发用户的购物欲望(原来根本没想到手机套这回事,既然你推荐了,又不贵,就买一个呗)。

到了购物车页面,用户的购物流程即将结束,能让用户在这个阶段再购买的一个主要动力是:凑单,这样可以节省运费或者参加活动。所以在这个阶段推荐的商品一般是:同店铺的相似商品,以及一些单价较低的、日常消费的商品。

总结

本文并没有讲解与电商检索相关的技术细节,只是单独从产品的角度,罗列了一下一个电商检索系统需要具备的功能,只能算是自己粗浅的整理和归纳,肯定有许多遗漏或者错误之处。有问题的话,欢迎大家反馈,我也会及时进行更正。以后有机会的话,还会对电商检索系统中的技术细节进行一些归纳和整理。

— EOF —

获取最近几分钟的日志

| Comments

最近在整理对于各个模块的监控,需要有一定的实时性。比如,需要获取最近几分钟内的日志,然后看某些请求的数量以及响应时间是否符合要求。但是,线上服务的日志,通常都是按照小时粒度进行切分的,你不可能对一个文件进行直接的过滤操作。在此之前需要解决一个问题:在一份文件中,获取最近一段时间的日志

当然,还有一个最最基础的问题:你的日志内容里面是表示时间的字段的。(不打时间和请求ID的日志简直就是耍流氓!)

我一开始的想法是:估算平均请求压力下,每5分钟的日志会有多少条,然后直接将cat替换为tail -n XXX就可以了。虽然修改起来很方便,但是是有明显缺陷的:

  1. 随着流量变化,tail出来的日志的时间粒度是不一样的。如果用来监控实时请求响应时间还算能接受,用来监控请求量就不行了;
  2. 如果以后模块升级,增加或者减少了请求日志,tail出来的数字需要不断调整。

看来还是要精确的获取某个时间段的日志才行。其实思路还是比较清晰的:

  1. 计算出开始时间结束时间两个字段
  2. 提取日志行中的日志时间
  3. 比较三个值,如果日志行的时间符合要求,则将其打印,作为过滤程序的输入
  4. 执行数日志数量或者统计请求响应时间的命令

对于步骤1,使用date命令就可以获取,这个简单。

对于步骤2,一般日志中的时间都会比较在日志前面几个字段,比较好提取,也不难。

步骤4嘛,就看需求是什么了,如果是获取请求数目,直接用grepwc -l就OK了。如果涉及到提取日志字段,简单的也可以用cut搞定,复杂就得用grep或者awk了。

最关键是步骤3如何实现,我想到的是在awk中进行逻辑判断,获取日志中的时间字段不难,但是如果时间字段是通过多个字段拼接而来的,比如2014-05-0217:25:00,怎么把他们放到一个变量里面呢?要是有像sprintf这样的函数就好了,没想到,还真有!类似于下面这样:

1
    cat xxx.log | awk '{t=sprintf("%s %s", $2, $3);}'

还有一个问题,就是如何将BASH中的开始时间结束时间变量传入awk呢?也有办法的!awk里面有-v选项,支持将外部变量传入其中。那么程序就类似于这样了:

1
2
3
    start_time=`date -d"$last_minutes minutes ago" +"%Y-%m-%d %H:%M:%S"`
    end_time=`date +"%Y-%m-%d %H:%M:%S"`
    cat xxx.log | awk -v st="$start_time" -v et="$end_time" '{t=sprintf("%s %s", $2, $3); if(t>=st && t<=et){print $0}}'

最后,还有一个小问题,因为会定期切分日志,所以需要考虑临界时间点的情况,把当前时间段和上个时间段的日志同时作为输入即可。

这样,精确获取最近一段时间日志的需求就得到解决了。

—EOF—

仿函数——Functor

| Comments

简单的需求

比如,有一个简单需求:找到一个vector<string>中,长度小于3的字符串的数目。解决方法可能会是:

1
2
3
4
5
6
7
8
9
10
11
    int count(const std::vector<std::string>& str_vec, const size_t threshold)
    {
        int size = 0;
        std::vector<std::string>::const_iterator it;
        for (it = str_vec.begin(); it != str_vec.end(); ++ it) {
            if (it->length() < threshold) {
                ++ size;
            }
        }
        return size;
    }

其实,数据STL的同学应该知道有个count_if函数。count_if的功能就是对于某种容器,对符合条件的元素进行计数。count_if包含三个参数,容器的开始地址、容器的结束地址、以及参数为元素类型的函数。

使用count_if的代码可以这样写:

1
2
3
4
5
    bool test(const std::string& str) { return str.length() < 3; }
    int count(const std::vector<std::string>& str_vec)
    {
        return std::count_if(str_vec.begin(), str_vec.end(), test);
    }

但是,这样有个问题:没有扩展性。比如,判断的字符串由长度3变成5呢?将test函数上面再增加一个长度参数可以吗?不行,count_if的实现就决定了test必须是单一参数的。既想满足count_if的语法要求,又需要让判断的函数具有可扩展性,这时候就需要functor了。

functor登场

functor的含义是:调用它就像调用一个普通的函数一样,不过它的本质是一个类的实例的成员函数(operator()这个函数),所以functor也叫function object。 因此以下代码的最后两个语句是等价的:

1
2
3
4
5
6
7
8
9
10
11
12
    class SomeFunctor
    {
    public:
        void operator() (const string& str)
        {
            cout << "Hello " << str << end;
        }
    };

    SomeFunctor functor;
    functor("world");               //Hello world
    functor.operator()("world");    //Hello world

其实,它并不算是STL中的一部分,不过需要STL中的函数都把functor所谓参数之一,functor起到了定制化的作用。functor与其它普通的函数相比,有一个明显的特点:可以使用成员变量。这样,就提供了扩展性。

继续上面例子,写成functor的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    class LessThan
    {
    public:
        LessThan(size_t threshold): _threshold(threshold) {}
        bool operator() (const std::string str) { return str.length() < _threshold; }
    private:
        const size_t _threshold;
    };

    int count(const std::vector<std::string>& str_vec)
    {
        LessThan less_than_three(3);
        return std::count_if(str_vec.begin(), str_vec.end(), less_than_three);
        //LessThan less_than_five(5);
        //std::count_if(str_vec.begin(), str_vec.end(), less_than_five);
    }

    int count_v2(const std::vector<std::string>& str_vec, const size_t threshold)
    {
        return std::count_if(str_vec.begin(), str_vec.end(), LessThan(threshold));
    }

C++11的新玩法

有人可能会说,我已经有了自己实现的判断函数了,但是直接用又不行,有啥解决办法吗? 其实是有的!(我也是最近才发现的)

C++11的标准中,提供了一套函数,能将一个普通的、不符合使用方要求的函数,转变成一个符合参数列表要求的functor,这实在是太酷了!

比如用户自己实现的int test(const std::string& str_vec, const size_t threshold)函数,如果能将第二个参数进行绑定,不就符合count_if的要求了吗?

新标准的C++就提供了这样一个函数——bind

通过std::bind以及std::placeholders,就可以实现转化,样例代码如下:

1
2
3
4
5
6
7
8
9
10
    bool less_than_func(const std::string& str, const size_t threshold)
    {
            return str.length() < threshold;
    }

    //提供 _1 占位符
    using namespace std::placeholders;
    //绑定less_than_func第二个参数为5, 转化为functor
    auto less_than_functor = std::bind(less_than_func, _1, 5);
    std::cout << std::count_if(str_vec.begin(), str_vec.end(), less_than_functor) << std::endl;

参考资料

—EOF—

公司设立分部弊端之我见

| Comments

入职已经快两年了,因为当时考虑到离家近,就没有去北京的总部,而是选了上海的分部研发中心。但是心想,“多好啊,研发中心里面大多数都是搞技术的,我们技术人员之间沟通肯定很顺畅,也能学到不少东西”。现实情况和我想得其实差不多,不过还多了些我当初没有想到的地方。

老大们都在总部

虽然团队的项目经理或者技术经历基本上都在上海,但是,总监级别的老大却几乎都在北京的总部,有些上层的消息,上海分部就比较迟才能接收到。正式由于这种消息的滞后性,以及来上层管理人员的沟通不畅, 分部在资源分配上面就略显吃亏了。

人员结构单一

我入职前总以为,作为分部,按道理是按项目来分的。比如一部分项目划给总部的团队、另一部分划给分部的团队。但事实是:项目的开发人员放在分部,而其它人员全在总部。 我就见过一两个团队的全部人员都是在分部的,其余都是我前面说的那种情况。

这种的人员的划分方式,就直接导致一个问题:同一个团队的总部人员和分部人员的不团结。我一开始待的团队,是这样划分的:老大、产品经理、UE、FE都在总部,后端RD在分部。我们对于需求的沟通,基本上都是电话会议的形式。虽然在一个团队,但是毕竟在两个地方工作,彼此都没有见过,互相不熟悉,大家只是共事,而不算是朋友。每当有一些争论的时候,大家心里都是下意识的怀疑是不是对方想少干一些活儿,把东西都推给我来做。因此很容易有矛盾。本来如果面对面也就10分钟能沟通好的事情,相隔两地就是无法打成一致。

核心技术在总部

一般来说,掌握核心技术的团队都会在总部,分部一般都是由于业务需要而逐渐扩展出来的。到了大公司,渐渐明白, 大公司的项目组之间的差异实在是太大了!就好像不同公司之间的差距一样 。在一个比较核心的团队里面,无论是技术、资源、还是经验,都比其它团队要成熟、规范的多,对于自身的成长,是极其有益的。万一你进了一个不是那么受重视的团队的话,各种没技术含量的重复劳动、或者是为了争取一些资源的各种扯皮,还是会很多的。

我之前和同事聊到这个话题,总会拿星际争霸来打比方:分基地嘛,主要的任务就是采矿,给主基地提供资源,能搞点兵营、炮台守住就可以了,主要的高端兵种都是总基地先造出来了,就算分基地有高级兵种,也会出的比较慢。

—EOF—

日志规范实践

| Comments

问题

这两天在整理现有模块的日志格式规范,以便于自己团队和其它团队更好的分析目前的产品。看了下,遗留的问题还真不少,问题主要集中在以下几点:

日志级别不正确

不是请求粒度的日志打成了NOTICE,用于排查的日志打成了WARNING、甚至打成了FATAL。这对于线上模块监控很不利,一方面很难从众多的WARNINGFATAL日志中找到有价值的信息,另一方面这些日志多了,难免让人产生“狼来了”的麻痹心理。

字段命名不统一

同样一个参数,比如说是请求ID,在不同的请求日志中,有logidlog_idlogId各种不同风格的写法,有的参数是用[]括起来的,有的则没有。这些问题会给日志解析程序带来很大的负担。

字段含义不一致

不同请求日志中的参数A,在一种请求中表示一种含义(比如触发出的商品数),在其它请求中则表示另一种含义(比如一页展现的商品数)。

日志被公共Lib污染

自己的模块依赖了其它公共lib,但是公共lib中的日志级别比较随意,结果污染了自身模块日志。

日志信息不足

许多NOTICE日志只打印出了给上游的返回数据,请求数据却不全。一些WARNING日志只能看出只在代码的哪一行出了问题,请求参数是什么、甚至请求ID,都没有。线上出现了问题,很难根据这条日志找到线索。

小试Travis-Ci

| Comments

前段时间,好友小新给我看了一个工具(或者说是一种服务)——travis-ci,它提供了对于Github的项目上的持续集成服务(私有项目是要收费的)。正好最近把一个大学时写的小程序从Google Code迁移到Github上,就拿他来做个实验吧!

.travis.yml

查看travis-ci的官方文档,其实十分简单。如果需要在项目中使用travis-ci提供的服务,只需要在Repository中添加.travis.yml配置文件。进行持续集成嘛,配置文件里面无非就是这么几项吧:

  • 使用的编程语言
  • 编译器及其版本
  • 编译命令
  • 跑测试用例的命令
  • etc.

Typelist && Abstract Fatory

| Comments

上一篇博客讲到了模板元编程中的Typelist,这种技术能够让编译器帮你生成许多结构类似的代码,省去了程序员自己编写代码的时间以及一些运行时的效率损失。设计模式中的Abstract Factory,其Template MetaProgramming版本也依赖于TypeList技术。

普通实现

Abstract Factory模式,即规定了一组抽象产品(Abstract Product)的接口,再由各个具体的Factory生成不同组的具体产品(Concrete Product)。我一下子想到了公司的RD、QA以及PM,就以这三种角色为例吧。(虽然不是很恰当,因为不同级别的不同职位是可以共存的)

普通的代码一般会写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class AbstractFactory
{
public:
    virtual RD* create_RD() = 0;
    virtual QA* create_QA() = 0;
    virtual PM* create_PM() = 0;
};

class JuniorFactory: public AbstractFactory
{
public:
    RD* create_RD() {return new JuniorRD();}
    QA* create_QA() {return new JuniorQA();}
    PM* create_PM() {return new JuniorPM();}
};

class SeniorFactory: public AbstractFactory
{
public:
    RD* create_RD() {return new SeniorRD();}
    QA* create_QA() {return new SeniorQA();}
    PM* create_PM() {return new SeniorPM();}
};

//使用方代码
AbstractFactory* fact = NULL;
if (/**/) {
    fact = new JuniorFactory();
} else (/**/) {
    fact = new SeniorFactory();
}

RD* rd = fact->create_RD();
//...

可以看出来,每一个具体的Factory类只是重复的实现AbstractFactory提供的接口,唯一的不同就是填写具体的类名,其它的语句都是重复的。(我不得不在编写以上代码的时候使用了Vim的替换功能)

模板元编程中的Typelist

| Comments

前段时间在看其他同事的代码,无意间看了类似下面的代码:

1
2
3
4
5
6
TypeList< FilterA,
TypeList< FilterB,
TypeList< FilterC,
TypeList< FilterD,
TypeList< FilterE,
    void > > > > > Filters;

感觉代码风格略微诡异了些,怎么像是LISP,呵呵。后来查了些资料,得知这就是我一直心仪已久的模板元编程中的一种技巧—— Typelist

从2013到2014

| Comments

pic

2013年就这么过去了,感觉时间如白驹过隙,转瞬即逝。我现在仍然清晰地记得2013年过年回家时的情景,仿佛就是前段时间发生的,可是一转眼,马上又要过春节了。

回首2013,最大的感觉还是时间过得飞快,一周又一周,感觉没做啥事情就这样过去了。每天忙忙碌碌的工作,晚上属于自己的时间越来越少了,即使有一些,也感觉没有精力去充分利用了。我使劲想了想,大概可以说的,也就这么几件事吧。

部门变动了

年初和同事一起做广告方面的项目,感觉做的还是挺有意思的。但是没过多久,就被Boss调去做另外一个电商方面的项目,被调过去的原因是我之前“兼职”负责过那个项目的一些统计工作。(不由感慨,一旦你在其他人心中被打上了某些tag,想要改变就会十分困难。比如你做过统计相关的事情,以后当有统计相关的其它工作时,Boss第一个想到的估计就是你,因为你这方面最容易上手,人力成本最低。)之后,就在新的团队一直干着,但是由于项目前景一直很惨淡却又倍受领导重视,直接导致项目组的每一位成员的工作强度都很大,有时候上线会上到凌晨2、3点,实实在在的狼性了一把。期间一些同学一方面承受不了这样的强度,一方面估计也觉得项目没啥前途,纷纷离开了项目组或者公司,其中包括了我的Boss和项目的大Boss。不过,大部分的同学还是任劳任怨,一直坚持到现在。

锻炼算是一直坚持着

虽然工作的强度略大,不过我还是一有空就去健身房锻炼,工作日如果能正常下班(19点之前),并且还有体力的话,我就会去健身房;周末只要不回南京老家,我基本上两天都会去。去跑跑步、做做器械,虽然我还没有练出健壮的肌肉,但至少身材还是一直保持着,没有向有些搞IT的同学,自从上班之后身体就逐渐发福。

另外,在气候适合的情况下,我也会找同事一起打打篮球,周末或者过节在家的话,我有机会也会去爬紫金山,算是锻炼比较勤快了。

经常回家看看

自从来上海工作之后,我基本都保持每两周就回家一次的习惯,回家多陪陪父母,和父亲聊聊体育、聊聊新闻,听母亲唠叨一些家长里短,另外偶尔也会回学校看看学弟学妹以及老师。在上海,每隔两三天,我也会打个电话给父母,报个平安。

经常回家的另外一个原因,是可以吃到好吃的东西,在公司周围吃饭比较贵,而且吃饭的地方也就这么几家,早就吃腻了。

多认识了些朋友

工作方面,由于加入了新的项目组,所以又多认识了些同事,其中包括只在hi上交流过的大搜的同事、“人数众多”的产品经理们、和我们一起开发一起上线的FE同学等等,可以说自己这一年的绝大部分的时间都花在了和他们交流上。

生活方面,又多认识了一些女生(我目前是一名单身男性,你懂的),虽然没能和她们进一步的发展,虽然有的只匆匆见过一面,但是和大部分还算是交了朋友,偶尔也会在微博或者微信上面有所交流,对于身在异乡的我来说,这还是挺不错的,不是吗?

————————————-华丽的分割线———————————————–

2013年,有很多地方做的还不是很好。比如,技术方面感觉没太大长进,一直想学的英语也没有坚持下去。2014年,也不想去过度设计这一年,只希望自己能好好利用时间静下心来做一些事情,比如技术、比如健身、比如旅游。

  • 希望能将后端相关的技术再深入研究、平时多写一些开源项目、多写些技术博客,自认为自己是个做技术的,技术才是我的立足之本!我就是想好好做一名技术“工匠”,尽可能的少一些扯皮的事情。
  • 希望能继续坚持锻炼,保持良好的作息习惯,13年做的不错,14年继续保持!
  • 希望2014年能多到些地方看看,13年准备的台湾行没能如愿,希望14年能够实现。
  • 如果时间和精力允许的情况下,我还想把英语好好学起来,以便我能和国外的同行交流,多学习学习国外的先进技术。

就列这四点吧,如果这些都能完成,已经相当不错了。

—EOF—

《Information Retrieval in Practice》笔记——整体架构

| Comments

对于一个典型的搜索引擎,至少包含两大部分:1)建立索引;2)query查询。

建立索引

主要流程如下图所示:

主要工作为:获取需要检索的数据,将其转化为系统可以识别的方式,做一些预处理,最后持久化存储起来。

本文获取 (Text Accquisition)

爬虫

本文获取就是将所需要的数据“抓”下来。其中,依据搜索引擎的使用场景不同(网站内部检索/全网检索/文件系统检索/…),需要检索的数据可以是全网、某个网站的网页、数据库的表、或者磁盘上面的文件等等,这样就需要不同类型的“爬虫”。另外,也可以像RSS这样的流式访问数据的标准。

获取到所需要的数据,这只完成了一部分。另外,当数据源有更新时(网页修改/数据库新增了数据/…),爬虫需要能及时的感知并且重新抓取数据提供给检索系统。而且,不同类型的数据,对于实时性的要求是不一样的。比如,一个发布新闻网站和和一个保存历史资料的网站,它们网页的实时性要求肯定是不一样的。

转换

文本获取到了,需要将其统一成搜索引擎可以识别的格式,文本转换包含两层含义:

  • 将不同格式标准的数据转换为统一的格式

例如,同样的记录书籍内容的文档,一个是XML格式的,一个是JSON格式的。爬虫如果想获取书籍的名称、作者、ISBN等信息,就需要采用不同的解析方式。

  • 将不同编码的文本转化为统一编码

有的网页是UTF-8编码的,有的是GB18030编码的,都需要统一成一个编码。

存储

数据(数据本身以及MetaData)获取到了,也转成统一格式了,下面就得想办法把数据持久化。持久化的方式有很多种,比如可以是本地文件系统、分布式文件系统(HDFS)、各种数据库(MYSQL / MongoDB)等等。

文本变形 (Text Transformation)

切词 && 归一化

检索索引之前,需要先将文本转化为一系列的term,term可以理解为有意义的最小单位词语,比如:

GitHub is the best place to share code with friends, co-workers, classmates, and complete strangers.

就会生成github, best, place, share, code, friend, co-worker, classmate, complete, stranger这些term。其中会将is, the等没有实际意义的单词去掉,然后做归一化,比如大小写转换,单复数转换等。

中文一句话中没有空格表示停顿,转化term会更复杂,比如上学时老师举的一个例子:

南京市长江大桥

应该需要切成南京市, 长江以及大桥,而不是南京, 市长, 江大桥

质量度

除了获取已经归一化的、数据所需要表达的信息之外,还需要对这份数据本身的质量做一个判断,相当于一个小网站发布的新闻和一个大网站发布的新闻,按照常理明显后者的质量、可信度等因素要优化前者。(在某些国家,事实真的是这样吗? :P )。PageRank算法就是一个例子,采用迭代的方式,通过网页链接(也就是网页的出度和入度)来计算该网页的质量度。

构建索引

一般来说,倒排索引的结构是这样的:

term1 -> (docid_1, data_11), (docid_2, data_12), ...
term2 -> (docid_2, data_21), (docid_3, data_22), ...

其中,term就是某一个切词的结果的签名,当然,这里的term也可以是其它信息,比如商品的分类、文章的类型等等。其实应用中需要什么样的触发方式,就可以建立对应类型的倒排。docid就是用来唯一标识一份数据的,数据可以是一个网页、一个商品等等,一般都会称其Document。至于data,主要是为计算doc相对于term的权重服务的,里面存储了计算权重所需要的数据。举个简单的例子,如果我们假设一篇文章中出现某个term的次数越多,这篇文章对于这个term就越相关,这时候在data数据里面存储这个term在该文章中出现的次数即可。另外,有些权重计算在建立索引的阶段就可以完成,因此这部分权重结果也会存放在data中。

所以,建立倒排索引的第一步,就是扫描整个数据全集,收集有关term和document的统计信息(term在doc中出现的频率,term在整个doc全集中出现的频率,term的偏移量,doc的更新时间,etc.)。接着,将数据集合,由doc -> (term1, term2, term3, ...)转化为term -> (doc1, doc2, doc3, ...)的倒排形式,其中包含了对term以及doc的权重计算。最后,将倒排索引dump出来,有时出于数据量和性能的考虑,还需要将索引分库存储,分库方式有两种:按document分库,以及按term进行分库。

Query查询

query查询的主要步骤如下图所示:

pic

User Interface

对于用户来说,直接面对的是搜索引擎的Web界面,或者说是User Interface界面。UI有以下几个功能:

  1. 接收用户输入,将其转化为一棵query查询树,作为排序模块的输入。query查询树节点上面的操作一般是AND/OR/NOT这样的布尔运算;
  2. 进行query变换。比如拼写检查、query推荐、query扩展;
  3. 展现最终的排序结果。包括:填充document信息(物料)、生成内容摘要、对关键信息进行飘红或者加粗等等。

排序

排序模块决定了结果文档集合的先后顺序,文档的权重计算方法有很多种,最原始的形式可以表示成:

Sum(qi * di)

其中,qi表示输入query的第i个term的权重,di表示该document相对于第i个term的权重。

qi以及di权重的计算,一般是基于tf.idf的思想。tf(term frequency),表示term在document或者query中出现的频率;而idf(inverse document frequncy),则是term出现在整个document或者query全局中的频率的倒数。这种思想其实很好理解,如果一个词在一篇文章中出现了许多次,我们可以暂且认为这个词和这篇文章是相关的,但是,如果这个词在所有文章中出现的频率都很高,那么这个词就对于那篇文章来说就没有那么“特殊”了,并不能表明这个词和那篇文章就是很相关的。

评估

两层含义:

  • 排序相关性的评估
  • 性能的评估

通过算法,最终得到一份排序结果,但是由于算法的局限性、训练数据集合是否完备以及个性化等因素,用户并不一定就对排在前面的结果感兴趣。因此,需要一个评估模块,收集用户的点击行为,用于扩充算法训练数据集合,或者作为算法参数调整的依据,最终反映在排序模块中。

另一方面,搜索引擎的性能,也需要进行监控。通过日志就可以清楚的看到用户的一次检索,各个模块的耗时是多少,哪个模块是性能瓶颈,开发者可以有针对性的进行优化。在测试环节,QA也可以使用日志来反向构造请求,模拟线上请求。

(PS: 文章包含了自己的理解,可能不正确)

— EOF —