BT

你的观点很重要! 快来参与InfoQ调研吧!

如何使用Elasticsearch构建企业级搜索方案?

| 作者 陈智发 关注 0 他的粉丝 发布于 2017年9月7日. 估计阅读时间: 30 分钟 | ArchSummit社交架构图谱:Facebook、Snapchat、Tumblr等背后的核心技术

A note to our readers: As per your request we have developed a set of features that allow you to reduce the noise, while not losing sight of anything that is important. Get email and web notifications by choosing the topics you are interested in.

说到搜索,大家第一印象一般都是像百度、Google 这样的互联网搜索引擎,这些搜索引擎主要通过网络爬虫抓取互联网上的数据,比如网页、图片、文档、音视频等,任何人都可以在上面输入关键词查询自己想要的信息。然而明略数据搜索技术专家陈智发将在今天的分享中告诉我们:搜索应用远不止查找网页和文件。

以下内容整理自 8 月 17 日 AI 前线微信社群内容分享。

(点击放大图像)

大家好,我是明略数据的陈智发,很高兴今天能跟大家分享我们明略在企业级搜索服务方面的一些心得。

(点击放大图像)

今天我想从以上四个方面内容展开介绍,首先对企业级搜索进行简单的介绍,然后重点讲一下我们的企业级搜索方案,最后会介绍一下语义分析在搜索方案中的运用和一些实际的案例。

企业级搜索简介

(点击放大图像)

说到搜索,大家第一印象一般都是像百度、Google 这样的互联网搜索引擎,这些搜索引擎主要通过网络爬虫抓取互联网上的数据,比如网页、图片、文档、音视频等,任何人都可以在上面输入关键词查询自己想要的信息;而另一方面,平时大家关注得比较少的企业搜索,则是对企业提供一整套的搜索技术方案,它们处理的数据大部分是来自于企业内网,形式上跟互联网搜索的数据不太一样,可能包含内部文档、邮件、数据库、第三方办公软件,当然也可以包括外部的数据,而企业搜索的用户一般也只能在企业内网里使用这些搜索服务。

这里列出了几个比较知名的企业搜索服务商,比如 Googla GSA, HP 的 Autonomy, 基于 Solr 的商业方案 Lucidworks,还有我们今天会介绍到的 Elasticsearch。所以无论是从数据还是使用方式上,互联网搜索和企业搜索都有着较多的不同点

(点击放大图像)

互联网搜索与企业级搜索的区别还体现在其他几个方面:

  1. 数据获取方式和更新频率:

    互联网搜索需要爬虫去被动抓取,而爬虫覆盖到新的内容是需要一定的时间的,所以在数据的更新上往往不会那么及时;而企业搜索的数据一般是主动生成的,所以搜索服务很容易知道什么时候需要更新数据(要么是通过用户操作要么是 API 调用),数据的更新可以做到近实时的

  2. 在数据完整性的要求上,互联网搜索很难也不可能覆盖到所有的公开数据,更何况由于暗网、政策和 Robots 协议的存在,互联网搜索本来就触及不了或不能显示某些数据,用户进行互联网搜索时如果找不到想要的信息也可以理解;反观企业搜索,处理过哪些数据都是计划内的,如果搜不到本应搜到的内容,用户可能会暴跳如雷。

  3. 结果排序:互联网搜索的排序是以 PageRank 算法为基础的,而企业级搜索常常依赖于特定的业务逻辑

  4. 权限:企业级搜索经常需要进行严格的权限控制,不该让用户看到的数据不能显示,而互联网搜索没有限制,即使有也是由于政策原因,与用户本身无关

  5. 对抗博弈:由于互联网的内容有可能来自于有商业诉求的个人或群体,互联网搜索需要考虑如何对用户的搜索优化行为进行公平的评判(尤其是恶意作弊),而企业级搜索是企业自身控制可搜索内容不需要考虑

  6. 用户在互联网搜索时是无法对搜索结果进行显式的控制的(但可以通过搜索引擎提供的个性化方案和点击流取得隐性的影响),而在企业内部,用户不但可以修改各种排序策略还可以做到直接控制显示的结果

(点击放大图像)

那么企业级搜索应该怎么去做呢?

显然光靠技术是解决不了问题的,虽然我们有 Elasticsearch、Solr 这样的底层技术支持,但它们更多的还是作为技术人员的核心工具,单靠这些无法实现用户的业务价值。

我们需要结合领域知识、用户的业务去搭建一个定制化的方案。

在设计一个企业级搜索方案时,首先要了解清楚,我们要处理的数据内容是什么样的,用户是谁以及他们想要什么样的输出结果;在设计完方案后,需要持续收集来自用户的反馈,修正我们对领域业务的理解、调整数据架构、进一步改进搜索的相关度效果,从而形成一个良性的闭环。

不同领域中的政企客户,很有可能会有不同的需求:

比如在公共安全领域,搜索出来的信息一定要全(由于搜索不到影响破案那就麻烦了),且要求搜索结果尽量的准确,不相关的结果用户不希望看到(而不是放在靠后一点的位置),而且数据的更新一定要及时;

而在新闻或论坛搜索里,用户需求可能更侧重于信息的覆盖面以及推荐内容的匹配程度上。

(点击放大图像)

由此可见,企业搜索的难点在于以下几个方面:需要支持多种数据接入方式,除了原始的数据内容之外,我们往往还需要对数据进行适当的清洗、治理,添加标记 (tagging),丰富数据,定义关联。作为服务提供商,我们的索引配置足够的灵活、支持根据业务和数据定义合适的索引结构(对结果排序同样重要)。

(点击放大图像)

基于 Elasticsearch 构建企业级搜索方案

下面我们来看看怎么基于 Elasticsearch 构建企业级搜索方案

(点击放大图像)

在开始正题之前,我觉得还是先简单介绍一下 Elasticsearch 好让大家有个上下文。

Elasticsearch(常简称 ES)是一个基于 Apache Lucene 的分布式搜索引擎,左边是 ES 大体上的架构,它在 Lucene 支持的功能之上,提供了在节点管理、节点发现、建索引、查询的功能,并提供了用户友好的 REST API。

ES(实际上是 Lucene)建索引的过程如右图所示,把要建索引的数据从数据源抽取出来后,文本字段经过分词后变成一个个的词,最终这些词会存入一个大词典中排序编号,并且创建或更新每个词对应的倒排链,用户搜索时即可根据倒排链快速查找包含一个或多个关键词的文档。

(点击放大图像)

为了支持更多的业务场景,我们搭建了这样一套企业级搜索服务的整体架构:首先大家看右侧,是我们底层对接的搜索引擎,目前主要是 Elasticsearch,当然也正在接入 Solr Cloud。

当初设计的时候就是考虑到我们服务的不同企业的平台环境中可能有的使用 Solr Cloud 有的使用 Elasticsearch,所以我们对通用的索引、查询功能进行了抽象和封装,使得底层引擎可以便于切换。

抽象出来的核心搜索服务可以从两个方向看,向上的方向对应的是建索引的过程:

  • 虽然 Elasticsearch 可以通过 Beats 接入各种数据源,但出于可维护性和项目定制化、底层引擎去耦合等考虑,还是自己创建多种数据源连接器

  • 数据源从连接器输出后我们可以得到最原始的数据,之后可以在一个插件化的数据预处理流水线中进行多种处理,这一步对于搜索服务提供的整体功能至关重要,直接决定了我们搜索什么内容;

  • 经过处理的干净数据如何构建索引是由索引配置和索引模块决定的,在索引配置中,用户可以指定数据字段与索引字段的对应关系以及每个索引字段的具体索引配置(比如分词器等),而索引模块则根据索引字段的元属性创建 Elasticsearch 或 Solr 的索引配置,并根据索引配置为数据指定一个索引进行写入操作,还有索引别名的管理等等

  • 配置的内容以及应用相关的数据将由核心存储模块的元数据库进行统一管理自顶向下的方向对应的是查询的过程:

  • 系统会先对用户输入的查询内容(比如关键词)进行一系列的 NLP(自然语言处理)分析,比如搜索意图识别。当然其实这个 NLP 模块在预处理流水线也可以用到

  • 初步的查询分析结果还可以经过一个同样插件化的流水线过程进行进一步的处理,这个在后面会展开介绍

  • 排序模块负责在发送 Query 到底层引擎前根据用户配置的业务逻辑对查询计划进行一定的修改,也负责在结果返回后对排序的结果进行调整

  • 用户除了可以使用定制的搜索应用页面访问搜索服务之外,第三方应用也可以通过统一的 API 方式使用搜索服务,便于我们的合作伙伴进行集成。

下面我们分模块介绍一下搜索系统各个部分。

(点击放大图像)

在给企业客户制定一套搜索方案时,需要先了解他们的数据种类和规模、以及业务模式等情况,从而可以设计好一个适当的索引分布方案。

这里举两个例子:

第一个例子是,对于数据规模相对稳定的情况,我们可以根据数据规模和现场的资源情况(比如服务器节点数,CPU 核数,可用内存资源等),智能地计算出一个合适的索引分配方案。

比方说,有三类数据,人口的数据比较多,而手机和车辆的数据比较少,那么 Partition Planning 程序就会判断把手机和车辆的数据存放在一个 ES index 下(分属不同的 Type),并且只需要分配 4 个 shards(分块),而人口数据则需要单独创建一个 ES index,并且设置 10 个 shards,这样既充分利用了硬件的资源,同时也可以在性能和占用资源上取得较好的平衡(毕竟每个索引都要占用 cache 等资源,虽然使用多个 Type 也有一些不利的地方)

(点击放大图像)

第二个例子是,对于数据规模不稳定且随时间增长的场景,可以以一天或一周为单位创建索引(根据具体需求而定),而且往往可以把实时 / 在线的数据索引存放在在线查询集群里,而历史的全量数据进行存放在离线计算集群,一般来说在线查询集群保留半年或一年内的数据,并可以使用更高端的配置(如 SSD)进行加速。

数据在两个集群中的分配可以有多种做法,简单的做法是半年数据可以同时写入在线和离线集群,然后定期删除(比如每天)在线集群中的过时数据。

(点击放大图像)

由于我们是一家企业服务提供商,在索引配置上,我们需要做一些权衡,既要满足项目实施团队容易定制的需求,同时也要尽量降低使用的门槛。

因此我们对 ES 索引的配置项也做了一层抽象,把一些跟业务功能不太相关的配置项隐藏起来,只保留业务和项目人员比较容易理解的配置项。

比方说,索引分词方案,我们会默认提供几种选择:不分词(即 keyword 分词)、按单字分词、按单词分词、混合分词(即按单字又按单词切分)。

这些默认的选项能满足 80%~90% 的搜索需求,如果有定制需求,项目人员可以在这基础上自己扩展新的分词配置,一般来说是通过创建新的 TokenFilter 即可满足需求。

每个字段的分词方案决定了它能够提供的业务功能,比如在高级检索中是否支持模糊匹配、是否支持关键字查询、是否可以作为排序字段和聚合字段等等。这些支持的能力通过接口反馈到我们的上层应用中。

另外,还支持用户配置复合索引字段(即包含多个原始内容字段的索引)、全文索引字段(用于全文检索)和控制哪些字段进行高亮输出等等。

(点击放大图像)

在不同项目中,我们可能会遇到各种各样特定的功能需求,而我们需要为之提供特定的索引配置方案。

举个例子,有些客户要求可以根据身份证号中的出生日期筛选人口数据,那为了完成这个功能,我们可以有两种方案:

  1. 使用 Elasticsearch 的通配查询,比如输入 19900203 可以获取正确的结果,但这样的查询性能是很差的,尤其是数据量大的情况下,因为要使用字符串匹配

  2. 第二种就是我们给定制一种新的分词方案,比如使用 Elasticsearch 的 Pattern Capture Filter,通过正则匹配把出生日期部分提取出来作为单独的 Term 进行索引,这样性能上会很好(尽管索引速度可能会受到一点点微不足道的影响)

除了每个字段的索引配置,还有其他的配置内容,比如影响索引输出结果的停用词表(把“得”,“的”,“了”等无用词去掉),影响分词准确性的分词词典等。这些资源也可以由项目人员进行修改更新。

(点击放大图像)

在数据预处理方面,我们曾经考虑过多个方案:

第一个是使用 Elasticsearch 的 ingest API,这种方案好处是不需要自己开发额外程序,但 ingest API 是基于脚本的,使用起来不是很灵活,并且如果语法或者逻辑上有问题也不方便调试,所以我们放弃了。

第二个方案是我们可以去扩展 ES 的插件,这样会比使用 Ingest API 的脚本灵活,但需要部署到 ES 并经常维护时需要重启服务,对于我们的场景不是很适合,并且在 ES 里也很难调试。

最后我们决定自己开发预处理程序,并做成可插件化,这样好处有很多,一是灵活且项目可以很容易去定制,二是不需要部署并重启 ES 就可以直接使用(因为是外部程序),三是我们控制能力比较强,可以在并行性能上做优化,同时也减轻 ES 的压力。最后就是我们可以做到与底层引擎解耦,便于集成其他引擎,比如 Solr。

在预处理的过程里,我们预设了几种可用的处理插件,比如解析原始数据内容(比如文本内容抽取)、数据清洗操作(比如去重)、语言分析(识别数据所使用的语言种类)、通过 NLP 模块提取更有价值的信息,进行同义词扩展和拼音扩展等。在特定的业务场景里,项目人员也可以通过定制新的插件为索引的内容增加新的丰富信息。

(点击放大图像)

那么在查询(Query)处理方面,我们也可以通过多种不同的操作对原始查询进行修改 (rewrite):

这里举了一个流水线的实例,首先查询内容经过语法分析,会分别分派到不同的处理器上进行处理,比如关键词中包含 * 或? 的字眼,我们会使用通配查询,对于包含逻辑操作符的查询我们会使用 QueryString 查询;

对于普通的文本关键词查询,会经过一个搜索意图分析模块,智能判断用户想查找的大概率是什么目标,这个分析结果会对后续查询的逻辑和排序造成一定的影响(比如通过设置权重)。

再者,通过结合用户的配置,可以识别出查询内容中包含的过滤条件项(比如“男性”)等,也可以进行查询阶段的同义词扩展,常见的场景就是对于行政区划地名会有不同的说法但其实指向的是同一个东西。

查询流水线跟预处理流水线一样也可以扩展新的插件实现不同的逻辑,最终得到的查询会被发送到底层搜索引擎进行查询。

(点击放大图像)

搜索其实就是为了查找用户最想要的东西,那么搜索逻辑和相关度排序逻辑的结合就显得特别重要了。

在企业级搜索中,搜索逻辑往往要与业务需求相结合。

有些场景下,需要以字段为中心,即要求一个字段里必须包含查询的几个关键词;而在另外一些场景下则需要以关键词为中心,即用户只要求我输入的每个关键词都被命中了,不在乎是哪个字段命中。有些场景则需要结合两种思路才能得到合理的相关的结果。

作为示例,我们在搜索服务中默认提供两种搜索模式——精确模式和模糊模式,在精确模式下,要去输入的内容都有匹配,这个模式适合那些要求结果比较准确,无关结果尽量少的应用场景;而模糊搜索则只要求一个或部分关键词命中即可,这样召回的结果会比精确模式多,但相对也不那么精确,命中的越多也会更相关。

在排序方面,我们除了要考虑普通的文本匹配相似度之外(比如命中关键词个数、关键词在原文中距离、内容长度),经常还需要考虑其他的元属性(如日期、点赞数、评论数)。还可以考虑其他的业务规则。

常用手段包括 field boosting(加权重),重排序、词组匹配和 Function score query 等等

(点击放大图像)

对于查询结果,除了使用 Elasticsearch 自带的 Highlighter 之外,我们还曾经在一些项目上提取文本的核心摘要内容来对输出结果进行概括,这一块也包括在后面提到的语义部分内容中。

客户的数据源有可能有多个来源,那么在展示结果的时候,我们支持对同源的结果进行聚合显示(单独查询)或者融合显示(多类结果合并排序)。

前面提到,在企业级搜索中安全是很重要的因素,因此我们在输出结果时也会根据当前系统用户配置的角色权限来判断是否要过滤 / 脱敏某些数据内容。

(点击放大图像)

为了优化搜索效果,我们除了直接收集用户的评价之外,还需要通过程序化的方式收集用户的反馈,这属于搜索管理的一部分。通过结合 FileBeat、Logstash 和 Kibana,我们可以及时收集用户使用搜索的日志数据,并通过 kibana 的图形界面进行搜索点击结果的统计、性能的分析、badcase 的分析等等,通过分析日志可以改善我们的排序逻辑并形成正反馈。

(点击放大图像)

结合语义的搜索方案

下面我们再介绍一下结合语义的搜索方案

(点击放大图像)

大家都知道,现在已经是语义的时代,Google 在推出知识图谱搜索的时候有一句话“Things, not strings ”,意思是我们要寻找数据深层的含义,寻找更深层的答案。

我们认为目前搜索发展的方向是一个更广义的搜索,是要跟知识图谱结合起来的。传统的搜索虽然构建了索引帮助用户加速查找过程,但无法体现出数据的真正价值,因此语义搜索要解决的问题,就是更直接地给用户他想要的答案。而在广义搜索中,索引已经不仅仅是以倒排的方式存在了。

(点击放大图像)

目前我们的搜索服务可以作为其他应用的一个入口,通过支持自然语言的搜索,可以使用户更快地获取到他想要的知识结果,比如查找 2016 年 8 月到 9 月从北京坐火车去过石家庄的男性,这个查询经过我们的查询处理和意图分析后,会得到右上角这些细致的条件信息,并在搜索系统中执行这一复杂的查询。可想而知,如果使用传统的功能需要经过多少步骤才能得到这个答案。

前面提到,我们会在数据预处理和查询预处理时进行一些智能的分析,这里会包括浅层和深层的语义分析,比如实体识别, 情感分析等,这些是用于丰富我们索引的数据,而深层语义分析则是可以帮助我们从原始数据中构建出一个具象的知识图谱。因此语义搜索,不单单是查询,也不单单是索引的问题。

说到这一块,我们下周会有一个关于知识图谱和语义搜索的产品发布会,也欢迎大家关注哈。

(点击放大图像)

搜索案例介绍

最后我们来介绍一下我们做过的一些案例吧。

(点击放大图像)

在公共安全领域,我们使用搜索来满足用户对知识图谱中的实体、关系和事件的查询,并支持多类数据的混合碰撞分析,前面提到的自然语言搜索就是在这个领域得到了较好的应用,因为数据本身比较复杂。而在社交分析领域,我们主要使用 ES 进行较多的数据聚合分析,前提也是做了比较多的浅层和深层语义分析;更常见的是,企业客户内部需要以搜索的方式查找内部的文档或者获取相关的文档内容。此外,在我们重点关注的公共安全领域,我们还通过搜索服务实现从人到案件,从案件到人,从案件到案件的案件比对系统,以及事件预警系统,给业务人员推荐有重点嫌疑的人和事。

所以,搜索应用远不止查找网页和文件。

问答环节 ES 的 index 的 shard 数量怎么确定?单个 shard 大小不能超过多少?

答:像我刚才提到,我们有一个索引分布的计算程序,通过指定数据规模和资源情况来计算,大概原理就是让每个 shard 能分到尽量多的计算资源,同时我们一般限制一个 shard 不超过 20GB。

ES 是建一个大索引好,还是建多个索引;这两种方式如何权衡?

答:ES 是一个分布式的方案,设计本意是充分利用分布式的性能,所以如果是大索引,也需要分成好多 shard,一个大索引好多 shard 跟好多个索引 + 较少的 shard,如果查询请求面向的数据范围是一样的,那其实没有太大区别。分成多个索引有一个好处,就是相当于分库了,比如人、车、手机三类数据分别用三个索引,那当你只想搜人的时候,是不需要搜其他两类的,而放在一个大索引会涉及到过滤其他数据,性能上还是会有损耗。

基于语义的 ES,关于这个“语义”是如何定义的,是语义相关性,还是可推导的语义搜索?关于这个语义模型,是 offline training 还是 online trainning。

答:我这里提到的语义,一方面是理解用户输入的内容从而自动地完成一个复杂的查询过程,另一方面是挖掘到数据里细粒度的有价值信息以及它们之间的关联; 2. 我们的语义模型更多的是 offline training。

目前我们利用 ELasticsearch 做基于地理位置的搜索,但是 ES 的 geo 搜索特别差,请问一下,怎么有效地提高 ES 的经纬度查询?另外,贵公司有没有 geo_point 类似的 case?

答: 我们目前也用了 ES 的 geo 搜索,但还没遇到特别差的情况(可能也跟具体项目的数据量有关),优化方式就要跟具体的数据情况和业务需求一起考虑了,可以考虑分库、二次过滤之类的方案;有的,前面我介绍时提到过地图搜索的业务功能。

我现在搞一套拼音搜索,想利用 ES,但就目前来看,ES 适合于中文搜索,很少用于拼音搜索。有什么好的建议么?ES 提供了丰富的查询方式,能给讲讲这些查询方式之间的性能区别么?

答: ES 有支持的拼音插件,可以在网上查一下,应该基本够用,如果要把拼音消岐做好(比如加上概率模型)那就需要自己研发模型了。ES 的查询方式太多了,不好简单回答,但最基本的查询是 Lucene 的 term query,match query 会转换为 term query,wildcard query 和 prefix query 的性能是比较差的,尤其是 wildcard 用 * 前缀时。

filebeat 与 logstash 在系统中收集采集的分工如何?logstash 的性能问题

答: filebeat 负责读取日志文件,把结果输出到 logstash,logstash 再把数据解析后送到 ES 建索引。

ES 目前的索引规模最大是多大啊?数据信息和元数据信息都是存在哪里的?当时选 ES 做搜索引擎的原因是什么?为什么实时数据和其他数据是分别存放和提供搜索的?

答: 我们目前接触过最大的索引规模是百亿量级的记录;在大量数据的场景下我们会考虑把原数据存放在其他数据库,ES 只放索引; 开源方案一般就 ES 和 Solr,其实功能上 ES 跟 Solr 虽各有侧重但是大同小异,并没有说用 Solr 不好,我们也在对接 Solr,只是 ES 对使用者是比较友好的,发展速度也比较快,而且在一些管理功能上做得比较好所以一开始选择了 ES; 实时数据和其他数据由于服务的场景不一样,所以分开放可以有的放矢。

知识图谱有没有用图数据库?是单机还是分布式,如何支持高并发?

答:我们构建知识图谱有使用图数据库,一开始使用的是 titan(我们公司同事曾经在 QCon 上做过知识图谱构建的分享,可以回顾下)。titan 是支持分布式的。

ES 有哪些比较好的性能可视化工具

答:我想是问免费的工具,有一个 elasticHQ 的可视化工具,可以试用下。

某种日志一天有一个 t 的量,希望同时实现入库快和查询快,这种情况是分多个索引存还是单索引设置很多分片,一般的配置原则是怎样的?

答: 面对大数据,无论是入库还是查询,都是要占用计算和内存资源的,所以首要条件是硬件尽量好,资源多,可以考虑进行集群的读写分离,分散不同请求的压力。关于是多个索引还是多个分片,前面有类似的问题,可以参考。在日志的这个场景,建议可以考虑按时间再细分索引(比如按小时),或者设置 ES routing 参数。

能否在 o2o 的场景下,利用 ES 来作为主搜,提供 C 端用户场景的搜索?除了初创的企业用 ES,面向用户端,能否也用 ES 作为搜索引擎,提供亿级用户的流量入口?

答: 可以使用 ES 作为面向 C 端的搜索,但在并发和集群优化上需要下很多功夫,我们主要是面向 B 端,所以能分享的不多见谅。

请教个问题: 我们在实际使用中 有很多聚合的页面,一个页面很多聚合。想问问老师这种应用场景之前有吗?适合用 ES 吗?

答: 有类似的项目,像刚才介绍的社交数据分析场景,就是用 ES 做的,ES 区别于 Solr 的一点就是更侧重数据分析,聚合功能做得还是比较细的。

ES 如何做好查询的相关性呢,既要保证查询的准确性,又要保证查询的查全?

答:准确率和查全率是两个有点互斥的指标,往往要根据业务指标从中间取个平衡点,比如结果虽然多但不会出现乱起八糟的结果(往往是索引方式的缘故)。相关度是个长期工作,没有 silver bullet。

请问你们是用什么语言开发 ES 插件的?

答:Java。

请问 ES 在节点超过 10 个,分片超过 1w 后,集群非常不稳定,查询经常出现超时问题,master 节点 ping 失败,不知是否和单索引 20 个分片有关?偶尔还会出现 old gc,但是日志中并没有明显异常会导致 gc,请教有没有好的分析方法?

答: 这个很难说是什么原因,ES 有很多底层的操作是不太明显的,比如数据迁移、比如 index merge,数据多了之后都有可能,一般可以先从系统资源(io, cpu, mem)分析,以及 ES 提供的 task 查询 API 看看在做什么。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

nice job by Zhou Jianhui

nice job

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

1 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT