这篇文章翻译自 http://nathanmarz.com/blog/introducing-cascalog-a-clojure-based-query-language-for-hado.html。原文作者是写 Storm 和 Cascalog 项目的发起人。翻译这篇文章也为了下次需要参考的时候能有个中文版本,毕竟中文的看起来更快一些。
以下进入正文。
好吧,我们现在来看看 Cascalog 里都有哪些东西,我会通过一系列实例来说明。这些实例都用了 Cascalog 项目中的 “playground” ,所以我建议你把 Cascalog 的代码下载下来,然后在 REPL 环境里跟着我一起做(跟着 README 做,只需要花几分钟就能搞定)。
首先启动 REPL 环境,加载 “playground”:
lein repl
user=> (use 'cascalog.playground) (bootstrap)
这会把我们运行实例需要的所有东西都加载进来。你可以在 playground.clj 里看到我们等会查询需要用到的数据集。我们先来执行一条查询来找出所有 25 岁的人:
user=> (?<- (stdout) [?person] (age ?person 25))
这个查询可以当做 “Find all ?person for which ?person has an age that is equal to 25”。在作业运行的时候,你可以看到 Hadoop 的 log,几秒钟之后这个查询的结果就会被打印出来。
然后再来一个范围查询找出数据集中所有年龄小于 30 岁的人:
user=> (?<- (stdout) [?person] (age ?person ?age) (< ?age 30))
这也很简单,这次我们是把年龄绑定到 ?age,然后再加上一个约束说这个 ?age 要比 30 小。
然后再来一个查询,这次我们会在结果里包含 ages 和 people:
user=> (?<- (stdout) [?person ?age] (age ?person ?age)
(< ?age 30))
我们要做的只是在查询的 vector 里面加上 ?age。
再来一个查询找出 Emily 关注的所有男性(male people):
user=> (?<- (stdout) [?person] (follows "emily" ?person)
(gender ?person "m"))
你可能没注意,这其实是一个 Join 操作,两个 ?person 是同一个东西,而由于 “follows” 和 “gender” 是两个独立的数据源,Cascalog 会用一个 Join 操作来解析这个查询。
然后我们更具体的看一下查询的结构,以解析以下查询为例:
user=> (?<- (stdout) [?person ?a2] (age ?person ?age)
(< ?age 30) (* 2 ?age :> ?a2))
我们用的操作符是 ?<-
,这个操作符会定义个查询然后执行。?<-
其实是对 <-
(创建查询的操作符) 和 ?-
(执行查询的操作符) 的包装。我们会在后面创建更复杂的查询的时候再来看看怎么用。
首先,我们要在查询里说明想把结果发送到哪里,在这里我们用了 (stdout)
,(stdout)
会创建一个 Cascalog 的 tap,这个 tap 在查询结束之后把内容写到标准输出。任意的 Cascalog tap 都可以作为输出,也就是说,你可以把输出的数据写到任意的文件格式(如 Sequence file, 普通文本,等等)。
在定义完的输出之后,还需要在一个 Clojure vector 里定义查询的结果变量,这里我们用了 ?person 和 ?a2。
接下来,要定义一些 “谓词” 来定义和约束结果变量。一共有三种谓词:
1 生成器(Generators):生成器是一个数据源,包含两种:
<-
定以的查询2 操作(Operations):所有变量的一个隐式关系,可以是绑定新变量的函数,或者是一个过滤器(filter)。
3 累加器(Aggregators):count
,sum
,min
,max
,等等
谓词有一个名字,一串输入变量和一串输出变量,我们上面的谓词有:
:>
关键字用来分隔输入变量和输出变量,如果没有指定 :>
关键字,则变量会被当做操作(operations)的输入或者生成器(generators)和累加器(aggregators)的输出。
在 playground.clj 中,age
谓词指向一个 tap,所以它是一个生成器,生成了 ?person 和 ?age。
谓词 <
是一个 Clojure 函数,由于我们没有指定输出变量,这个谓词将作为一个过滤器,会过滤掉所有的 ?age 小于 30 的记录。如果我们在这里指定:
(< ?age 30 :> ?young)
那么 <
将作为一个函数,并将一个 boolean 类型的变量绑定到 ?young,表示年龄是否小于 30。
谓词的顺序没有关系。Cascalog 是纯定义型的。
变量是以 ?
或 !
开始的符号,如果有时不需要输出变量的值可以用 _
符号来略过。查询里的其他部分都会被求值然后作为常量插入,这个功能叫 “常量替换”,到目前为止我们已经用了很多。如果把常量作为输出变量,会对函数的结果做一些过滤,比如:
(* 4 ?v2 :> 100)
这里有两个常量:4 和 100。4 替换了一个输入变量,而 100 作为过滤条件,将只保留乘 4 后等于 100 的 ?v2 的值。字符串,数字,其他基本单元以及任意在 Hadoop serializers 注册过的 Object 都可以作为常量。
再回到例子,我们来找出 follow 关系中关注比自己年龄小的人:
user=> (?<- (stdout) [?person1 ?person2]
(age ?person1 ?age1) (follows ?person1 ?person2)
(age ?person2 ?age2) (< ?age2 ?age1))
再执行一遍加上年龄差:
user=> (?<- (stdout) [?person1 ?person2 ?delta]
(age ?person1 ?age1) (follows ?person1 ?person2)
(age ?person2 ?age2) (- ?age2 ?age1 :> ?delta)
(< ?delta 0))
现在来看一下我们的第一个累加器,我们来找出年龄小于 30 岁的人的数量:
user=> (?<- (stdout) [?count] (age _ ?a) (< ?a 30)
(c/count ?count))
这个查询会算出记录中所有人的数量,我们可以按分组累计数量,比如,要找出每个人关注的人的数量可以这样:
user=> (?<- (stdout) [?person ?count] (follows ?person _)
(c/count ?count))
因为我们在查询的结果变量里定义了 ?person,Cascalog 会按照 ?person 分组,然后对每个分组执行 c/count
累加器。
你也可以在一个查询中使用多个累加器,它们会对同一个记录的分组执行。例子:通过 count
和 sum
的组合来得到一个国家的平均年龄:
user=> (?<- (stdout) [?country ?avg]
(location ?person ?country _ _) (age ?person ?age)
(c/count ?count) (c/sum ?age :> ?sum)
(div ?sum ?count :> ?avg))
注意,我们对最后累计后的结果用了 div
操作。依赖与累加器的输出变量的操作都是在累加器运行完之后再执行的。
接下来来写一个统计一组句子中每个次出现的次数,首先用这个查询定义一个自定义操作:
user=> (defmapcatop split [sentence]
(seq (.split sentence "\\s+")))
user=> (?<- (stdout) [?word ?count] (sentence ?s)
(split ?s :> ?word) (c/count ?count))
defmapcatop split
定义的操作把只有一个字段的 sentence 作为输入,输出 0 个或多个元组。
deffilterop
定义返回布尔值的过滤操作,表示这个元组会不会被过滤掉。
defmapop
定义的函数只返回一个元组。
defaggregateop
定义一个累加器。
这些操作也可以在 Cascalog 的 workflow API 中被直接使用。
我们的 word count 的查询还有一个问题,就是在会区分大小写,我们可以这样修改:
user=> (defn lowercase [w] (.toLowerCase w))
user=> (?<- (stdout) [?word ?count]
(sentence ?s) (split ?s :> ?word1)
(lowercase ?word1 :> ?word) (c/count ?count))
就如你所看到的,普通的 Clojure 函数也可以被当做操作来使用。Clojure 函数在没有输入参数的时候是一个过滤器,在有输入参数的时候是一个 map 操作。想要输出 0 个或多个元组必须用 defmapcatop
。
这还有一个查询,会统计按年龄和性别分组后各组人的数量:
user=> (defn agebucket [age]
(find-first (partial <= age) [17 25 35 45 55 65 100 200]))
user=> (?<- (stdout) [?bucket ?gender ?count]
(age ?person ?age) (gender ?person ?gender)
(agebucket ?age :> ?bucket) (c/count ?count))
Cascalog 有个叫 “非空变量” 的功能,可以让你更优雅的处理空值。我们到目前为止一直在用非空变量。以 ?
开头的变量都是非空变量,以 !
开头的变量是可以为空的变量,当非空变量被绑上控制时,Cascalog 会过滤掉。
我们比较下面两个查询来看看非空变量的效果:
user=> (?<- (stdout) [?person ?city] (location ?person _ _ ?city))
user=> (?<- (stdout) [?person !city] (location ?person _ _ !city))
第二个查询在结果集中会包含一些空值。
最后,我们来看一看由子查询组成的复杂查询。先查出一对 follow 关系中的两个人都 follow 2 个以上人的集合:
user=> (let [many-follows (<- [?person] (follows ?person _)
(c/count ?c) (> ?c 2))]
(?<- (stdout) [?person1 ?person2] (many-follows ?person1)
(many-follows ?person2) (follows ?person1 ?person2)))
这里我们用 let
定义一个 many-follow 子查询,这个子查询使用 <-
来定义。然后就可以在 let
的 body 里使用 many-follow 子查询。
还可以运行包含多个输出的查询。如果我们还想要上面查询中 many-follow 子查询的结果,可以这样写:
user=> (let [many-follows (<- [?person] (follows ?person _)
(c/count ?c) (> ?c 2))
active-follows (<- [?p1 ?p2] (many-follows ?p1)
(many-follows ?p2) (follows ?p1 ?p2))]
(?- (stdout) many-follows (stdout) active-follows))
这里我们先定义了两个查询,但是没有执行。在最后才用查询操作符 ?-
来顺序执行两个查询。