zzzhc's Blog

stay curious

Lucene中文分词器比较

当前支持lucene 3.1的中文分词器

  • mmseg4j mmseg4j 用 Chih-Hao Tsai 的 MMSeg 算法(http://technology.chtsai.org/mmseg/ )实现的中文分词器,并实现 lucene 的 analyzer 和 solr 的TokenizerFactory 以方便在Lucene和Solr中使用。
  • paoding Paoding’s Knives 中文分词具有极 高效率 和 高扩展性 。引入隐喻,采用完全的面向对象设计,构思先进。 高效率:在PIII 1G内存个人机器上,1秒 可准确分词 100万 汉字。 采用基于 不限制个数 的词典文件对文章进行有效切分,使能够将对词汇分类定义。 能够对未知的词汇进行合理解析
  • ik-analyzer IKAnalyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包。从2006年12月推出1.0版开始,IKAnalyzer已经推出了3个大版本。最初,它是以开源项目Luence为应用主体的,结合词典分词和文法分析算法的中文分词组件。新版本的IKAnalyzer3.0则发展为面向Java的公用分词组件,独立于Lucene项目,同时提供了对Lucene的默认优化实现。
  • imdict imdict-chinese-analyzer 是imdict智能词典的智能中文分词模块,算法基于隐马尔科夫模型(Hidden Markov Model, HMM),是中国科学院计算技术研究所的ictclas中文分词程序的重新实现(基于Java),可以直接为lucene搜索引擎提供简体中文分词支持。 已加入lucene contributor, smartcn.

支持的特性

name CharTermAttribute OffsetAttribute TypeAttribute PositionIncrementAttribute KeywordAttribute
mmseg4j Y Y Y N N
paoding Y Y Y N N
ik-analyzer Y Y N N N
imdict Y Y Y N Y

性能比较

本机cpu: Intel® Core™2 Duo CPU P8600 @ 2.40GHz

name speed(chars/second)
StandardAnalyzer 5227272
mmseg4j 771812
paoding 847145
ik-analyzer 657142
imdict 269242

测试代码:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//CNAnalyzerBenchmark.java
import java.io.IOException;
import java.io.StringReader;
import java.util.HashSet;
import java.util.Iterator;

import net.paoding.analysis.analyzer.PaodingAnalyzer;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.util.Attribute;
import org.apache.lucene.util.Version;
import org.wltea.analyzer.lucene.IKAnalyzer;

import com.chenlb.mmseg4j.analysis.MMSegAnalyzer;

public class CNAnalyzerBenchmark {

    public static void main(String[] args) throws IOException {
        IKAnalyzer ikAnalyzer = new IKAnalyzer();
        testAnalyzer(ikAnalyzer);

        MMSegAnalyzer mmsegAnalyzer = new MMSegAnalyzer();
        testAnalyzer(mmsegAnalyzer);

        PaodingAnalyzer paodingAnalyzer = new PaodingAnalyzer();
        testAnalyzer(paodingAnalyzer);

        SmartChineseAnalyzer smartChineseAnalyzer = new SmartChineseAnalyzer(
                Version.LUCENE_31, false);
        testAnalyzer(smartChineseAnalyzer);

        StandardAnalyzer standardAnalyzer = new StandardAnalyzer(
                Version.LUCENE_31, new HashSet<String>());
        testAnalyzer(standardAnalyzer);
    }

    static void testAnalyzer(Analyzer a) throws IOException {
        String data = "中文(chinese)与西方语言最大的区别" + "就在于语句的词汇之间没有明显的分词界限,"
                + "但是计算机自然语言处理是按词汇来进行分析的," + "因此中文分词的效果直接影响中文检索和自然语言处理的准确性。";
        StringBuilder ss = new StringBuilder();
        for (int i = 0; i < 10000; i++) {
            ss.append(data);
        }
        String s = ss.toString();

        long startTime = System.currentTimeMillis();

        String attributes = "";
        TokenStream stream = a.tokenStream("", new StringReader(s));
        Iterator<Class<? extends Attribute>> iterator = stream
                .getAttributeClassesIterator();
        while (iterator.hasNext()) {
            Class<? extends Attribute> attrClass = iterator.next();
            attributes += " " + attrClass.getSimpleName();
        }
        stream = a.tokenStream("", new StringReader(s));
        while (stream.incrementToken()) {
        }
        long endTime = System.currentTimeMillis();
               System.out.println(a.getClass().getSimpleName() + " attributes: "
                   + attributes);
               double seconds = (endTime - startTime) / 1000.0;
               System.out.println("chars=" + s.length() +
                       ",time=" + seconds + "seconds" +
                      ",speed=" + (int) (s.length() / seconds) + "chars/second"
               );
    }
}

Solr Faceted Search

从用户角度看,faceted search把搜索结果分入多个类别(一般还会显示各个类别下有多少结果),并允许用户分类别查看。在电子商务类网站经常可以看到,如

FacetFields有两种,枚举型和分词型。枚举型适合类别比较少的场景,如卓越。分词型适合于类别很多,但每个document所属的类别较少的情况。Facet field不需要定义成stored,但需要是indexed. facet主要是用在drill-down到搜索结果的一个子集,实际查询是会生成一个对应类别的filter query.

实现原理,枚举型与分词型有些区别

  • 枚举型,遍历field的所有terms, 得到各个term对应的document BitSet, 这个结果与query result作与
  • 分词型,遍历搜索结果中的每个document, 从field cache里得到它对应的terms, 累加terms出现次数

Solr

solr是一个基于lucene,高度可配置的企业级搜索平台。它自己的介绍:

Solr is the popular, blazing fast open source enterprise search platform from the Apache Lucene project. Its major features include powerful full-text search, hit highlighting, faceted search, dynamic clustering, database integration, rich document (e.g., Word, PDF) handling, and geospatial search. Solr is highly scalable, providing distributed search and index replication, and it powers the search and navigation features of many of the world’s largest internet sites.

Features有一堆,觉得比较实用的几点有

  • 高度可配置,index和query的各部分都能通过配置定制
  • 与数据库表结构类似的索引定义,支持主键,支持基本数据类型(如int), 很容易与表结构关联起来,更新方便
  • 支持multiple indexes, 方便对多个表分别建索引
  • 简单的HTTP接口,开发改进client都很方便
  • 支持replication, 对HA和search scalability提供了一些安慰
  • 简单的后台管理界面,支持查看索引(LukeRequestHandler),执行查询,有助于debug
  • 丰富的文档,主要是wiki

几个不足的地方:

  • 配置太繁琐,需要反复查文档
  • 管理界面太简陋

Wiki Markup

wordpress的编辑器实在不怎么样,想念conflucenewiki markup.

confluence的wiki markup是从textile改进过来的,在plugins里找了会,没有好用的textile plugin,像textile 2会把原来的内容变成乱七八遭的,没法忍。

接着试了下markdown, 因为在github的项目上经常看到README.md, 知道是用markdown语法写的。markdown on save看起来是没有侵入性的, markdown version放post_content_formatted字段里,对于wordpress 3.1来说放在meta里更好. 参考markdown syntax练习了一下,基本还好,有两个地方不太满意:

  • ordered list要自己用数字开头,像1.这样来写,同时又忽略这些值的内容,相当反直觉,还是textile里的#爽快
  • 不直接支持table, 虽然可以自己直接写 <table>…</table> confluence里的||column||column||很简单快捷。

先用它了。

Nodejs

nodejs Evented I/O for V8 JavaScript.

node的目标是提供一种简单的方式来构建可伸缩的网络程序。在node里几乎没有函数会直接执行IO操作,进程从不会阻塞在某个IO等待上。event, callback是node中两个主要的概念,因为IO操作不阻塞,通过event, callback来实现是很自然的。

这一两年node发展很快,有了包管理器(npm),也出现了各种各样的module, 如

有了这些之后要用javascript开发一个web app相对容易多了。

这两天简单试了下,用express, hamljs, node-mongodb-native写了一个最简单的blog app.

安装环境 * install node

1
2
3
4
5
#download source code from http://nodejs.org/dist/
$ wget http://nodejs.org/dist/node-v0.4.5.tar.gz
$ tar -xf node-v0.4.5.tar.gz
$ cd node-v0.4.5
$ ./configure &amp;&amp; make &amp;&amp; sudo make install
  • install mongodb
1
2
3
4
5
6
7
#http://www.mongodb.org/downloads
$ wget http://fastdl.mongodb.org/linux/mongodb-linux-i686-1.8.1.tgz
$ tar -xf mongodb-linux-i686-1.8.1.tgz
$ cd mongodb-linux-i686-1.8.1
$ sudo mkdir -p /data/db
$ sudo chown `id -u` -R /data
$ bin/mongod # start mongodb server
  • install npm
1
2
3
$ git clone http://github.com/isaacs/npm.git
$ cd npm
$ sudo make install
  • install modules
1
2
3
$ sudo npm install express
$ sudo npm install hamljs
$ sudo npm install mongodb

express指南见http://expressjs.com/guide.html,它的route方式跟rails比算很弱的,没有对restful的直接支持,但在加上methodOverride后都可以自己定义出来。 模板引擎用hamljs, 通过app.register(‘.haml’, require(‘hamljs’))指定。在hamljs layout里要嵌入其它模板内容时可以用!= body。partial可以用!= partial(“post.haml”, posts)。!=的意思是对后面的内容不作escape, 细节可以看hamljs代码,很短,不到700行。 mongodb client用起来比较费劲,一层一层的callback, 适当封装下应该会好使点。 最终的代码放在https://github.com/nodepress/draft 性能看起来还是很好的,production模式下(NODE_ENV=production node blog.js) 并发5个连接(ab -c 5 -n 1000 http://localhost:3000/posts/)的结果 Time per request: 1.145 [ms] (mean, across all concurrent requests)

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
35
36
37
38
$ ab -c 5 -n 1000 http://localhost:3000/posts/
....
Server Software:
Server Hostname:        localhost
Server Port:            3000

Document Path:          /posts/
Document Length:        2950 bytes

Concurrency Level:      5
Time taken for tests:   1.145 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      3073000 bytes
HTML transferred:       2950000 bytes
Requests per second:    873.02 [#/sec] (mean)
Time per request:       5.727 [ms] (mean)
Time per request:       1.145 [ms] (mean, across all concurrent requests)
Transfer rate:          2619.90 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:     3    6   1.8      5      22
Waiting:        3    6   1.8      5      22
Total:          3    6   1.8      5      22

Percentage of the requests served within a certain time (ms)
  50%      5
  66%      5
  75%      6
  80%      6
  90%      7
  95%      8
  98%     14
  99%     16
  100%     22 (longest request)

Ruby Eventmachine

eventmachine 是一个快速轻量的网络协议框架,有不少ruby应用基于它实现,如thin, ruby-amqp. eventmachine在不同os上自动选择最佳的底层网络通知机制,在linux上用epoll,freebsd上用kqueue.

eventmachine对网络事件进行封装,有事件发生时回调预设的handler module。

事件处理都需要放在EventMachine::run里,可以分server, client两种模式

1
2
3
4
5
6
7
8
#server
EventMachine::run {
  EventMachine::start_server "0.0.0.0", port, ServerHandler
}
#client
EventMachine::run {
  EventMachine::connect remote_server, port, ClientHandler #client
}

在一个网络连接的生存周期中,可能发生的回调事件有:

  1. post_init handler对象创建后,注意client模式时,即使连接还未建立也会被调用
  2. connection_completed 主动连接远端服务器,连接建立时
  3. receive_data 有数据可读时, 在这里需要处理协议细节
  4. unbind 连接关闭,主动关闭,对方关闭或网络错误

在handler上可以使用send_data发送数据.

最简单的echo server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env ruby

require 'rubygems'
require 'eventmachine'

module EchoServer
  def post_init
    puts "-- someone connected to the echo server!"
  end

  def receive_data data
    send_data ">>>you sent: #{data}"
    close_connection if data =~ /quit/i
  end

  def unbind
    puts "-- someone disconnected from the echo server!"
  end
end

EventMachine::run {
  EventMachine::start_server "127.0.0.1", 8081, EchoServer
}

一个简单的http client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env ruby

require 'rubygems'
require 'eventmachine'

module HttpClient
  def post_init
    puts "sending request to server"
    send_data "GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: close\r\n\r\n"
  end

  def receive_data data
    puts "recv: #{data}"
  end

  def unbind
    puts "connection closed"
    EventMachine::stop
  end
end

EventMachine::run {
  EventMachine::connect "www.baidu.com", 80, HttpClient
}

连接失败的例子:

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
#!/usr/bin/env ruby

require 'rubygems'
require 'eventmachine'

module Client
  def connection_completed
    puts "connected"
  end

  def post_init
    puts "handler inited"
  end

  def receive_data data
    puts "recv: #{data}"
  end

  def unbind
    puts "connection closed"
    EventMachine::stop
  end
end

EventMachine::run {
  EventMachine::connect "api.jquery.com", 8088, Client
}

Qpid Ruby Client

Qpid对ruby的官方支持:https://svn.apache.org/repos/asf/qpid/trunk/qpid/ruby/

感觉是对python client的port, 代码风格看起来很不ruby, 用了挺多锁,信号,线程,想要停止一个connection都比较困难。对AMQP协议的实现细节被序列化到一个spec_cache下,不可读。基本没有文档,总体来说不像是一个可以正式用的东西。

大概的用法,用rake gem生成gem包,安装gem install pkg/qpid-0.10.2.gem

用qpid java broker的时候要注意下,创建connection的时候要指定用户名密码。 conn = Qpid::Connection.new(TCPSocket.new(“localhost”, 5672), :username => “guest”, :password => “guest”)

参考example写了两个简单例子 producer.rb

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
#!/usr/bin/env ruby

require "rubygems"
require "qpid"
require "socket"

conn = Qpid::Connection.new(TCPSocket.new("localhost", 5672),
                                         :username => "guest",
                                         :password => "guest")
conn.start(10)

ssn = conn.session("test_producer")

# create a queue
ssn.queue_declare("test-queue")
ssn.exchange_declare("test-exchange", :type => "direct")

dp = ssn.delivery_properties(:routing_key => "test-queue")
mp = ssn.message_properties(:content_type => "text/plain")

ssn.message_transfer(:message => Qpid::Message.new(dp, mp, "Hello QPID!"))
ssn.message_transfer(:message => Qpid::Message.new(dp, mp, "Hello RUBY!"))
while line = gets.strip
  break if line =~ /^(exit|done)$/i
  ssn.message_transfer(:message => Qpid::Message.new(dp, mp, line.strip))
end
ssn.message_transfer(:message => Qpid::Message.new(dp, mp, "done"))
ssn.sync

ssn.close()
conn.close()

consumer.rb

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
#!/usr/bin/env ruby

require "rubygems"
require "qpid"
require "socket"

conn = Qpid::Connection.new(TCPSocket.new("localhost", 5672),
                                         :username => "guest",
                                         :password => "guest")
conn.start(10)

ssn = conn.session("test_consumer")

incoming = ssn.incoming("messages")
ssn.message_subscribe(
  :destination => "messages",
  :queue => "test-queue",
  :accept_mode => ssn.message_accept_mode.none
)

# start incoming message flow
incoming.start()

while true
 body = incoming.get().body
 puts body
 break if body == "done"
end

ssn.close()
conn.close()

另一个ruby AMQP协议的实现是ruby-amqp ,基于event-machine, 目前只支持amqp 0-8.

主要是为与rabbitmq通讯打造,简单试了下simple example也可用在qpid上,其它自动的example基本跑不过。现在开发比较活跃,更看好它。

AMQP

AMQP(Advanced Message Queuing Protocol) 是一个为消息驱动中间件提供的应用层协议,binary protocol. 协议规范有多个版本,0-8版本是支持得最多的。

AMQP的原始用途只是为金融界提供一个可以彼此协作的消息协议,而现在的目标则是为通用消息队列架构提供通用基础架构。因此,面向消息的中间件 (MOM)系统,例如发布/订阅队列,没有作为基本元素实现,而是通过发送简化的AMQ实体来完成。这些实体也是规范的一 部分,形成了在线路层协议顶端的一个层级:AMQP模型。这个模型统一了消息模式,包括发布/订阅,队列,事务以及流数据,路由等。

几个主要概念:

  • broker,即AMQP server
  • message, 内容不可变,没有长度限制
  • queue, message最终总会进入一个queue
  • exchange, 为message提供分发服务,按binding规则将message分发到一个或多个queue, 有多种类型,如direct, fanout, topic
  • binding, 消息路由规则

exchange的类型说明:

 

brokers

Qpid c++ broker性能测试结果

qpid-perftest —worker-threads 16 —default-queue-limit 1024000 —queue-purge-interval 30 —tcp-nodelay —mgmt-enable no

Message size (bytes) Total transfers/sec Total Mbytes/sec
32 708,159 21.6113
64 627,766 38.3158
128 537,001 65.5519
256 487,070 118.914
512 278,192 135.445
1024 176,901 172.755

ruby client