标签 Apache 下的文章

搭建你自己的Go Runtime metrics环境

自从Go 1.5开始,每次Go release, Gopher Brian Hatfield都会将自己对新版Go的runtime的性能数据(与之前Go版本的比较)在twitter上晒出来。就连Go team staff在世界各地做speaking时也在slide中引用Brian的图片。后来,Brian Hatfield将其用于度量runtime性能数据的代码打包成library并放在github上开源了,我们也可以使用这个library来建立我们自己的Go Runtime metrics设施了。这里简要说一下搭建的步骤。

一、环境与原理

Brian Hatfield的go-runtime-metrics library实现的很简单,其runtime data来自于Go runtime package中的MemStats、NumGoroutine和NumCgoCall等。被测试目标程序只需要import该library即可输出runtime states数据:

import _ "github.com/bmhatfield/go-runtime-metrics"

go-runtime-metrics library将启动一个单独的goroutine,并定时上报runtime数据。目前该library仅支持向statsD输出数据,用户可以通过配置将statsD的数据导入graphite并使用graphite web查看,流程如下图:

img{512x368}

本次实验环境为ubuntu 16.04.1:

$ uname -rmn
tonybai-ThinkCentre-M6600s-N000 4.4.0-83-generic x86_64

二、搭建步骤

1、安装go-runtime-metrics library

我们直接go get就可以下载go-runtime-metrics library:

$ go get github.com/bmhatfield/go-runtime-metrics

我们编写一个目标程序:

//main.go
package main

import (
    "flag"
    "log"
    "net/http"
    "os"

    _ "github.com/bmhatfield/go-runtime-metrics"
)

func main() {
    flag.Parse()

    cwd, err := os.Getwd()
    if err != nil {
        log.Fatal(err)
    }

    srv := &http.Server{
        Addr:    ":8000", // Normally ":443"
        Handler: http.FileServer(http.Dir(cwd)),
    }
    log.Fatal(srv.ListenAndServe())
}

我的ubuntu主机上安装了四个go版本,它们分别是go 1.5.4、go 1.7.6、go 1.8.3和go1.9beta2,于是我们分别用这四个版本的server作为被测程序进行go runtime数据上报,以便对比。

$ GOROOT=~/.bin/go154 ~/.bin/go154/bin/go build -o server-go154 main.go
$ GOROOT=~/.bin/go174 ~/.bin/go174/bin/go build -o server-go174 main.go
$ GOROOT=~/.bin/go183 ~/.bin/go183/bin/go build -o server-go183 main.go
$ GOROOT=~/.bin/go19beta2 ~/.bin/go19beta2/bin/go build -o server-go19beta2 main.go

$ ls -l

-rwxr-xr-x 1 tonybai tonybai 6861176 7月   4 13:49 server-go154
-rwxrwxr-x 1 tonybai tonybai 5901876 7月   4 13:50 server-go174
-rwxrwxr-x 1 tonybai tonybai 6102879 7月   4 13:51 server-go183
-rwxrwxr-x 1 tonybai tonybai 6365648 7月   4 13:51 server-go19beta2

2、安装、配置和运行statsD

statsD这个工具用于收集统计信息,并将聚合后的信息发给后端服务(比如:graphite)。statsD是采用js实现的服务,因此需要安装nodejsnpm和相关modules:

$ sudo apt-get install nodejs
$ sudo apt-get install npm

接下来,我们将statsD项目clone到本地并根据exampleConfig.js模板配置一个我们自己用的goruntimemetricConfig.js(基本上就是保留默认配置):

// goruntimemetricConfig.js
{
  graphitePort: 2003
, graphiteHost: "127.0.0.1"
, port: 8125
, backends: [ "./backends/graphite" ]
}

启动statsD:

$ nodejs stats.js goruntimemetricConfig.js
3 Jul 11:14:20 - [7939] reading config file: goruntimemetricConfig.js
3 Jul 11:14:20 - server is up INFO

启动成功!

3、安装、配置和运行graphite

graphite是一种存储时序监控数据,并可以按用户需求以图形化形式展示数据的工具,它包括三个组件:

whisper是一种基于file的时序数据库格式,同时whisper也提供了相应的命令和API供其他组件调用以操作时序数据库;

carbon用于读取外部推送的metrics信息,进行聚合并写入db,它还支持缓存热点数据,提升访问效率。

graphite-web则是针对用户的图形化系统,用于定制展示监控数据的。

Graphite的安装和配置是略微繁琐的,我们一步一步慢慢来。

a) 安装graphite

$sudo apt-get install graphite-web graphite-carbon

whisper将作为依赖自动被安装。

b) local_settings.py

graphite的主配置文件在/etc/graphite/local_settings.py,文件里面有很多配置项,这里仅列出有关的,且本次生效的配置:

// /etc/graphite/local_settings.py

TIME_ZONE = 'Asia/Shanghai'

LOG_RENDERING_PERFORMANCE = True
LOG_CACHE_PERFORMANCE = True
LOG_METRIC_ACCESS = True

GRAPHITE_ROOT = '/usr/share/graphite-web'

CONF_DIR = '/etc/graphite'
STORAGE_DIR = '/var/lib/graphite/whisper'
CONTENT_DIR = '/usr/share/graphite-web/static'

WHISPER_DIR = '/var/lib/graphite/whisper'
LOG_DIR = '/var/log/graphite'
INDEX_FILE = '/var/lib/graphite/search_index'  # Search index file

DATABASES = {
    'default': {
        'NAME': '/var/lib/graphite/graphite.db',
        'ENGINE': 'django.db.backends.sqlite3',
        'USER': '',
        'PASSWORD': '',
        'HOST': '',
        'PORT': ''
    }
}

c) 同步数据库

接下来执行下面两个命令来做database sync(同步):

$ sudo graphite-manage migrate auth
.. ....
Operations to perform:
  Apply all migrations: auth
Running migrations:
  Rendering model states... DONE
  Applying contenttypes.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0001_initial... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK

$ sudo graphite-manage syncdb

Operations to perform:
  Synchronize unmigrated apps: account, cli, render, whitelist, metrics, url_shortener, dashboard, composer, events, browser
  Apply all migrations: admin, contenttypes, tagging, auth, sessions
Synchronizing apps without migrations:
  Creating tables...
    Creating table account_profile
    Creating table account_variable
    Creating table account_view
    Creating table account_window
    Creating table account_mygraph
    Creating table dashboard_dashboard
    Creating table events_event
    Creating table url_shortener_link
    Running deferred SQL...
  Installing custom SQL...
Running migrations:
  Rendering model states... DONE
  Applying admin.0001_initial... OK
  Applying sessions.0001_initial... OK
  Applying tagging.0001_initial... OK

You have installed Django's auth system, and don't have any superusers defined.
Would you like to create one now? (yes/no): yes
Username (leave blank to use 'root'):
Email address: xx@yy.com
Password:
Password (again):
Superuser created successfully.

这里我们创建一个superuser:root,用于登录graphite-web时使用。

d) 配置carbon

涉及carbon的配置文件如下,我们保持默认配置不动:

/etc/carbon/carbon.conf(内容太多,这里不列出来了)

/etc/carbon/storage-schemas.conf
[carbon]
pattern = ^carbon\.
retentions = 60:90d

[default_1min_for_1day]
pattern = .*
retentions = 60s:1d

[stats]
pattern = ^stats.*
retentions = 10s:6h,1min:6d,10min:1800d

carbon有一个cache功能,我们通过下面步骤可以将其打开:

打开carbon-cache使能开关:

$ vi /etc/default/graphite-carbon
CARBON_CACHE_ENABLED=true

启动carbon-cache:

$ sudo cp /usr/share/doc/graphite-carbon/examples/storage-aggregation.conf.example /etc/carbon/storage-aggregation.conf
$ systemctl start carbon-cache

e) 启动graphite-web

graphite-web支持多种主流web server,这里以apache2为例,graphite-web将mod-wsgi方式部署在apache2下面:

$sudo apt-get install apache2 libapache2-mod-wsgi

$ sudo service apache2 start

$ sudo a2dissite 000-default
Site 000-default disabled.

$ sudo service apache2 reload

$ sudo cp /usr/share/graphite-web/apache2-graphite.conf /etc/apache2/sites-available

$ sudo  a2ensite apache2-graphite
Enabling site apache2-graphite.
To activate the new configuration, you need to run:
  service apache2 reload

$ sudo systemctl reload apache2

由于apache2的Worker process默认以www-data:www-data用户权限运行,但数据库文件的访问权限却是:_graphite:_graphite:

$ ll /var/lib/graphite/graphite.db
-rw-r--r-- 1 _graphite _graphite 72704 7月   3 13:48 /var/lib/graphite/graphite.db

我们需要修改一下apache worker的user:

$ sudo vi /etc/apache2/envvars

export APACHE_RUN_USER=_graphite
export APACHE_RUN_GROUP=_graphite

重启apache2生效!使用Browser打开:http://127.0.0.1,如无意外,你将看到下面graphite-web的首页:

img{512x368}

三、执行benchmarking

这里我将使用wrk这个http benchmarking tool分别对前面的四个版本的目标程序(server-go154 server-go174 server-go183 server-go19beta2)进行benchmarking test,每个目标程序接收10分钟的请求:

$ ./server-go154
$ wrk -t12 -c400 -d10m http://127.0.0.1:8000

$ ./server-go174
$ wrk -t12 -c400 -d10m http://127.0.0.1:8000

$ ./server-go183
$ wrk -t12 -c400 -d10m http://127.0.0.1:8000

$ ./server-go19beta2
$ wrk -t12 -c400 -d10m http://127.0.0.1:8000

四、结果展示

用浏览器打开graphite-web,在左边的tree标签下以此打开树形结构:Metrics -> stats -> gauges -> go -> YOUR_HOST_NAME -> mem -> gc -> pause,如果顺利的话,你将会在Graphite Composer窗口看到折线图,我们也以GC pause为例,GC pause也是gopher们最为关心的:

img{512x368}

通过这幅图(左侧坐标轴的单位为nanoseconds),我们大致可以看出:

Go 1.5.4的GC pause约在600μs左右;
Go 1.7.4的GC pause约在300μs左右;
Go 1.8.3和Go 1.9beta2的GC pause基本都在100μs以下了。Go 1.9的GC改进似乎不大。不过这里我的程序也并不足够典型。

其他结果:

Go routines number:

img{512x368}

GC count:

img{512x368}

memory allocations:

img{512x368}

除了查看单个指标曲线,你也可以通过graphite-web提供的dashboard功能定制你要monitor的面板,这里就不赘述了。

五、参考资料


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

使用wukong全文搜索引擎

近期项目中有一个全文索引和全文搜索的业务需求,组内同事在这方面都没啥经验,找一个满足我们需求的开源的全文搜索引擎势在必行。我们这一期对全文搜索引擎的需求并不复杂,最主要的是引擎可以很好的支持中文分词、索引和搜索,并能快速实现功能。在全文搜索领域,基于Apache luceneElasticSearch舍我其谁,其强大的分布式系统能力、对超大规模数据的支持、友好的Restful API以及近实时的搜索性能都是业内翘楚,并且其开发社区也是相当活跃,资料众多。但也正式由于其体量较大,我们并没有在本期项目中选择使用ElasticSearch,而是挑选了另外一个“fame”不是那么响亮的引擎:wukong

一、wukong简介

wukong,是一款golang实现的高性能、支持中文分词的全文搜索引擎。我个人觉得它最大的特点恰恰是不像ElasticSearch那样庞大和功能完备,而是可以以一个Library的形式快速集成到你的应用或服务中去,这可能也是在当前阶段选择它的最重要原因,当然其golang技术栈也是让我垂涎于它的另外一个原因:)。

第一次知道wukong,其实是在今年的GopherChina大会上,其作者陈辉作为第一个演讲嘉宾在大会上分享了“Go与人工智能”。在这个presentation中,chen hui详细讲解了wukong搜索引擎以及其他几个关联的开源项目,比如:sego等。

在golang世界中,做full text search的可不止wukong一个。另外一个比较知名的是bleve,但默认情况下,bleve并不支持中文分词和搜索,需要结合中文分词插件才能支持,比如:gojieba

wukong基本上是陈辉一个人打造的项目,在陈辉在阿里任职期间,他将其用于阿里内部的一些项目中,但总体来说,wukong的应用还是很小众的,相关资料也不多,基本都集中在其github站点上。关于wukong源码的分析,倒是在国外站点上发现一篇:《Code reading: wukong full-text search engine》。

本文更多聚焦于应用wukong引擎,而不是来分析wukong代码。

二、全文索引和检索

1、最简单的例子

我们先来看一个使用wukong引擎编写的最简单的例子:

//example1.go

package main

import (
    "fmt"

    "github.com/huichen/wukong/engine"
    "github.com/huichen/wukong/types"
)

var (
    searcher = engine.Engine{}
    docId    uint64
)

const (
    text1 = `在苏黎世的FIFA颁奖典礼上,巴萨球星、阿根廷国家队队长梅西赢得了生涯第5个金球奖,继续创造足坛的新纪录`
    text2 = `12月6日,网上出现照片显示国产第五代战斗机歼-20的尾翼已经涂上五位数部队编号`
)

func main() {
    searcher.Init(types.EngineInitOptions{
        IndexerInitOptions: &types.IndexerInitOptions{
            IndexType: types.DocIdsIndex,
        },
        SegmenterDictionaries: "./dict/dictionary.txt",
        StopTokenFile:         "./dict/stop_tokens.txt",
    })
    defer searcher.Close()

    docId++
    searcher.IndexDocument(docId, types.DocumentIndexData{Content: text1}, false)
    docId++
    searcher.IndexDocument(docId, types.DocumentIndexData{Content: text2}, false)

    searcher.FlushIndex()

    fmt.Printf("%#v\n", searcher.Search(types.SearchRequest{Text: "巴萨 梅西"}))
    fmt.Printf("%#v\n", searcher.Search(types.SearchRequest{Text: "战斗机 金球奖"}))
}

在这个例子中,我们创建的wukong engine索引了两个doc:text1和text2,建立好索引后,我们利用引擎进行关键词查询,我们来看看查询结果:

$go run example1.go
2016/12/06 21:40:04 载入sego词典 ./dict/dictionary.txt
2016/12/06 21:40:08 sego词典载入完毕
types.SearchResponse{Tokens:[]string{"巴萨", "梅西"}, Docs:[]types.ScoredDocument{types.ScoredDocument{DocId:0x1, Scores:[]float32{0}, TokenSnippetLocations:[]int(nil), TokenLocations:[][]int(nil)}}, Timeout:false, NumDocs:1}
types.SearchResponse{Tokens:[]string{"战斗机", "金球奖"}, Docs:[]types.ScoredDocument{}, Timeout:false, NumDocs:0}

可以看出当查询“巴萨 梅西”时,引擎正确匹配到了第一个文档(DocId:0×1)。而第二次查询关键词组合“战斗机 金球奖”则没有匹配到任何文档。从这个例子我们也可以看出,wukong引擎对关键词查询支持的是关键词的AND查询,只有文档中同时包含所有关键词,才能被匹配到。这也是目前wukong引擎唯一支持的一种关键词搜索组合模式。

wukong引擎的索引key是一个uint64值,我们需要保证该值的唯一性,否则将导致已创建的索引被override。

另外我们看到:在初始化IndexerInitOptions时,我们传入的IndexType是types.DocIdsIndex,这将指示engine在建立的索引和搜索结果中只保留匹配到的DocId信息,这将最小化wukong引擎对内存的占用。

如果在初始化EngineInitOptions时不给StopTokenFile赋值,那么当我们搜索”巴萨 梅西”时,引擎会将keywords分成三个关键词:”巴萨”、空格和”梅西”分别搜索并Merge结果:

$go run example1.go
2016/12/06 21:57:47 载入sego词典 ./dict/dictionary.txt
2016/12/06 21:57:51 sego词典载入完毕
types.SearchResponse{Tokens:[]string{"巴萨", " ", "梅西"}, Docs:[]types.ScoredDocument{}, Timeout:false, NumDocs:0}
types.SearchResponse{Tokens:[]string{"战斗机", " ", "金球奖"}, Docs:[]types.ScoredDocument{}, Timeout:false, NumDocs:0}

2、FrequenciesIndex和LocationsIndex

wukong Engine的IndexType支持的另外两个类型是FrequenciesIndex和LocationsIndex,分别对应的是保留词频信息以及关键词在文档中出现的位置信息,这两类IndexType对内存的消耗量也是逐渐增大的,毕竟保留的信息是递增的:

当IndexType = FrequenciesIndex时:

$go run example1.go
2016/12/06 22:03:47 载入sego词典 ./dict/dictionary.txt
2016/12/06 22:03:51 sego词典载入完毕
types.SearchResponse{Tokens:[]string{"巴萨", "梅西"}, Docs:[]types.ScoredDocument{types.ScoredDocument{DocId:0x1, Scores:[]float32{3.0480049}, TokenSnippetLocations:[]int(nil), TokenLocations:[][]int(nil)}}, Timeout:false, NumDocs:1}
types.SearchResponse{Tokens:[]string{"战斗机", "金球奖"}, Docs:[]types.ScoredDocument{}, Timeout:false, NumDocs:0}

当IndexType = LocationsIndex时:

$go run example1.go
2016/12/06 22:04:31 载入sego词典 ./dict/dictionary.txt
2016/12/06 22:04:38 sego词典载入完毕
types.SearchResponse{Tokens:[]string{"巴萨", "梅西"}, Docs:[]types.ScoredDocument{types.ScoredDocument{DocId:0x1, Scores:[]float32{3.0480049}, TokenSnippetLocations:[]int{37, 76}, TokenLocations:[][]int{[]int{37}, []int{76}}}}, Timeout:false, NumDocs:1}
types.SearchResponse{Tokens:[]string{"战斗机", "金球奖"}, Docs:[]types.ScoredDocument{}, Timeout:false, NumDocs:0}

3、分词对结果的影响

在前面,当不给StopTokenFile赋值时,我们初步看到了分词对搜索结果的影响。wukong的中文分词完全基于作者的另外一个开源项目sego实现的。分词的准确程度直接影响着索引的建立和关键词的搜索结果。sego的词典和StopTokenFile来自于网络,如果你需要更加准确的分词结果,那么是需要你定期更新dictionary.txt和stop_tokens.txt。

举个例子,如果你的源文档内容为:”你们很感兴趣的 .NET Core 1.1 来了哦”,你的搜索关键词为:兴趣。按照我们的预期,应该可以搜索到这个源文档。但实际输出却是:

types.SearchResponse{Tokens:[]string{"兴趣"}, Docs:[]types.ScoredDocument{}, Timeout:false, NumDocs:0}

其原因就在于sego对”你们很感兴趣的 .NET Core 1.1 来了哦”这句话的分词结果是:

你们/r 很感兴趣/l 的/uj  /x ./x net/x  /x core/x  /x 1/x ./x 1/x  /x 来/v 了/ul 哦/zg

sego并没有将“兴趣”分出来,而是将“很感兴趣”四个字放在了一起,wukong引擎自然就不会单独为“兴趣”单独建立文档索引了,搜索不到也就能理解了。因此,sego可以被用来检验wukong引擎分词情况,这将有助于你了解wukong对文档索引的建立情况。

三、持久化索引和启动恢复

上面的例子中,wukong引擎建立的文档索引都是存放在内存中的,程序退出后,这些数据也就随之消失了。每次启动程序都要根据源文档重新建立索引显然是一个很不明智的想法。wukong支持将已建立的索引持久化到磁盘文件中,并在程序重启时从文件中间索引数据恢复出来,并在后续的关键词搜索时使用。wukong底层支持两种持久化引擎,一个是boltdb,另外一个是cznic/kv。默认采用boltdb。

我们来看一个持久化索引的例子(考虑文章size,省略一些代码):

// example2_index_create.go
... ...
func main() {
    searcher.Init(types.EngineInitOptions{
        IndexerInitOptions: &types.IndexerInitOptions{
            IndexType: types.DocIdsIndex,
        },
        UsePersistentStorage:    true,
        PersistentStorageFolder: "./index",
        SegmenterDictionaries:   "./dict/dictionary.txt",
        StopTokenFile:           "./dict/stop_tokens.txt",
    })
    defer searcher.Close()

    os.MkdirAll("./index", 0777)

    docId++
    searcher.IndexDocument(docId, types.DocumentIndexData{Content: text1}, false)
    docId++
    searcher.IndexDocument(docId, types.DocumentIndexData{Content: text2}, false)
    docId++
    searcher.IndexDocument(docId, types.DocumentIndexData{Content: text3}, false)

    searcher.FlushIndex()
    log.Println("Created index number:", searcher.NumDocumentsIndexed())
}

这是一个创建持久化索引的源文件。可以看出:如果要持久化索引,只需在engine init时显式设置UsePersistentStorage为true,并设置PersistentStorageFolder,即索引持久化文件存放的路径。执行一下该源文件:

$go run example2_index_create.go
2016/12/06 22:41:49 载入sego词典 ./dict/dictionary.txt
2016/12/06 22:41:53 sego词典载入完毕
2016/12/06 22:41:53 Created index number: 3

执行后,我们会在./index路径下看到持久化后的索引数据文件:

$tree index
index
├── wukong.0
├── wukong.1
├── wukong.2
├── wukong.3
├── wukong.4
├── wukong.5
├── wukong.6
└── wukong.7

0 directories, 8 files

现在我们再建立一个程序,该程序从持久化的索引数据恢复索引到内存中,并针对搜索关键词给出搜索结果:

// example2_index_search.go
... ...
var (
    searcher = engine.Engine{}
)

func main() {
    searcher.Init(types.EngineInitOptions{
        IndexerInitOptions: &types.IndexerInitOptions{
            IndexType: types.DocIdsIndex,
        },
        UsePersistentStorage:    true,
        PersistentStorageFolder: "./index",
        SegmenterDictionaries:   "./dict/dictionary.txt",
        StopTokenFile:           "./dict/stop_tokens.txt",
    })
    defer searcher.Close()

    searcher.FlushIndex()
    log.Println("recover index number:", searcher.NumDocumentsIndexed())

    fmt.Printf("%#v\n", searcher.Search(types.SearchRequest{Text: "巴萨 梅西"}))
}

执行这个程序:

$go run example2_index_search.go
2016/12/06 22:48:37 载入sego词典 ./dict/dictionary.txt
2016/12/06 22:48:41 sego词典载入完毕
2016/12/06 22:48:42 recover index number: 3
types.SearchResponse{Tokens:[]string{"巴萨", "梅西"}, Docs:[]types.ScoredDocument{types.ScoredDocument{DocId:0x1, Scores:[]float32{0}, TokenSnippetLocations:[]int(nil), TokenLocations:[][]int(nil)}}, Timeout:false, NumDocs:1}

该程序成功从前面已经建立好的程序中恢复了索引数据,并针对Search request给出了正确的搜索结果。

需要注意的是:boltdb采用了flock保证互斥访问底层文件数据的,因此当一个程序打开了boltdb,此时如果有另外一个程序尝试打开相同的boltdb,那么后者将阻塞在open boltdb的环节。

四、动态增加和删除索引

wukong引擎支持运行时动态增删索引,并实时影响搜索结果。

我们以上一节建立的持久化索引为基础,启动一个支持索引动态增加的程序:

//example3.go

func main() {
    searcher.Init(types.EngineInitOptions{
        IndexerInitOptions: &types.IndexerInitOptions{
            IndexType: types.DocIdsIndex,
        },
        UsePersistentStorage:    true,
        PersistentStorageFolder: "./index",
        PersistentStorageShards: 8,
        SegmenterDictionaries:   "./dict/dictionary.txt",
        StopTokenFile:           "./dict/stop_tokens.txt",
    })
    defer searcher.Close()
    searcher.FlushIndex()
    log.Println("recover index number:", searcher.NumDocumentsIndexed())
    docId = searcher.NumDocumentsIndexed()

    os.MkdirAll("./source", 0777)

    go func() {
        for {
            var paths []string

            //update index dynamically
            time.Sleep(time.Second * 10)
            var path = "./source"
            err := filepath.Walk(path, func(path string, f os.FileInfo, err error) error {
                if f == nil {
                    return err
                }
                if f.IsDir() {
                    return nil
                }

                fc, err := ioutil.ReadFile(path)
                if err != nil {
                    fmt.Println("read file:", path, "error:", err)
                }

                docId++
                fmt.Println("indexing file:", path, "... ...")
                searcher.IndexDocument(docId, types.DocumentIndexData{Content: string(fc)}, true)
                fmt.Println("indexed file:", path, " ok")
                paths = append(paths, path)

                return nil
            })
            if err != nil {
                fmt.Printf("filepath.Walk() returned %v\n", err)
                return
            }

            for _, p := range paths {
                err := os.Remove(p)
                if err != nil {
                    fmt.Println("remove file:", p, " error:", err)
                    continue
                }
                fmt.Println("remove file:", p, " ok!")
            }

            if len(paths) != 0 {
                // 等待索引刷新完毕
                fmt.Println("flush index....")
                searcher.FlushIndex()
                fmt.Println("flush index ok")
            }
        }
    }()

    for {
        var s string
        fmt.Println("Please input your search keywords:")
        fmt.Scanf("%s", &s)
        if s == "exit" {
            break
        }

        fmt.Printf("%#v\n", searcher.Search(types.SearchRequest{Text: s}))
    }
}

example3这个程序启动了一个goroutine,定期到source目录下读取要建立索引的源文档,并实时更新索引数据。main routine则等待用户输入关键词,并通过引擎搜索返回结果。我们来Run一下这个程序:

$go run example3.go
2016/12/06 23:07:17 载入sego词典 ./dict/dictionary.txt
2016/12/06 23:07:21 sego词典载入完毕
2016/12/06 23:07:21 recover index number: 3
Please input your search keywords:
梅西
types.SearchResponse{Tokens:[]string{"梅西"}, Docs:[]types.ScoredDocument{types.ScoredDocument{DocId:0x1, Scores:[]float32{0}, TokenSnippetLocations:[]int(nil), TokenLocations:[][]int(nil)}}, Timeout:false, NumDocs:1}
Please input your search keywords:
战斗机
types.SearchResponse{Tokens:[]string{"战斗机"}, Docs:[]types.ScoredDocument{types.ScoredDocument{DocId:0x2, Scores:[]float32{0}, TokenSnippetLocations:[]int(nil), TokenLocations:[][]int(nil)}}, Timeout:false, NumDocs:1}
Please input your search keywords:

可以看到:基于当前已经恢复的索引,我们可以正确搜索到”梅西”、”战斗机”等关键词所在的文档。

这时我们如果输入:“球王”,我们得到的搜索结果如下:

Please input your search keywords:
球王
types.SearchResponse{Tokens:[]string{"球王"}, Docs:[]types.ScoredDocument{}, Timeout:false, NumDocs:0}

没有任何文档得以匹配。

没关系,现在我们就来增加一个文档,里面包含球王等关键字。我们创建一个文档: soccerking.txt,内容为:

《球王马拉多纳》是一部讲述世界上被公认为现代足球坛上最伟大的传奇足球明星迭戈·马拉多纳的影片。他出身于清贫家庭,九岁展露过人才华,十一岁加入阿根廷足球青少年队,十六岁便成为阿根廷甲级联赛最年轻的>球员。1986年世界杯,他为阿根廷队射入足球史上最佳入球,并带领队伍勇夺金杯。他的一生充满争议、大起大落,球迷与人们对他的热爱却从未减少过,生命力旺盛的他多次从人生谷底重生。

将soccerking.txt移动到source目录中,片刻后,可以看到程序输出以下日志:

indexing file: source/soccerking.txt ... ...
indexed file: source/soccerking.txt  ok
remove file: source/soccerking.txt  ok!
flush index....
flush index ok

我们再尝试搜索”球王”、”马拉多纳”等关键词:

Please input your search keywords:
球王
types.SearchResponse{Tokens:[]string{"球王"}, Docs:[]types.ScoredDocument{types.ScoredDocument{DocId:0x4, Scores:[]float32{0}, TokenSnippetLocations:[]int(nil), TokenLocations:[][]int(nil)}}, Timeout:false, NumDocs:1}
Please input your search keywords:
马拉多纳
types.SearchResponse{Tokens:[]string{"马拉多纳"}, Docs:[]types.ScoredDocument{types.ScoredDocument{DocId:0x4, Scores:[]float32{0}, TokenSnippetLocations:[]int(nil), TokenLocations:[][]int(nil)}}, Timeout:false, NumDocs:1}

可以看到,这回engine正确搜索到了对应的Doc。

五、分布式索引和搜索

从前面的章节内容,我们大致了解了wukong的工作原理。wukong将索引存储于boltdb中,每个wukong instance独占一份数据,无法共享给其他wukong instance。当一个node上的内存空间不足以满足数据量需求时,需要将wukong引擎进行分布式部署以实现分布式索引和搜索。关于这点,wukong官方提供了一段方案描述:

分布式搜索的原理如下:

当文档数量较多无法在一台机器内存中索引时,可以将文档按照文本内容的hash值裂分(sharding),不同块交由不同服务器索引。在查找时同一请求分发到所有裂分服务器上,然后将所有服务器返回的
结果归并重排序作为最终搜索结果输出。

为了保证裂分的均匀性,建议使用Go语言实现的Murmur3 hash函数:

https://github.com/huichen/murmur

按照上面的原理很容易用悟空引擎实现分布式搜索(每个裂分服务器运行一个悟空引擎),但这样的分布式系统多数是高度定制的,比如任务的调度依赖于分布式环境,有时需要添加额外层的服务器以
均衡负载

实质就是索引和搜索的分片处理。目前我们项目所在阶段尚不需这样一个分布式wukong,因此,这里也没有实战经验可供分享。

六、wukong引擎的局限

有了上面的内容介绍,你基本可以掌握和使用wukong引擎了。不过在选用wukong引擎之前,你务必要了解wukong引擎的一些局限:

1、开发不活跃,资料较少,社区较小
wukong引擎基本上是作者一个人的项目,社区参与度不高,资料很少。另外由于作者正在创业,忙于造轮子^_^,因此wukong项目更新的频度不高。

2、缺少计划和愿景

似乎作者并没有持续将wukong引擎持续改进和发扬光大的想法和动力。Feature上也无增加。这点和bleve比起来就要差很多。

3、查询功能简单,仅支持关键词的AND查询

如果你要支持灵活多样的全文检索的查询方式,那么当前版本的wukong很可能不适合你。

4、搜索的准确度基于dictionary.txt的规模

前面说过,wukong的索引建立和搜索精确度一定程度上取决于分词引擎的分词精确性,这样dictionary.txt文件是否全面,就会成为影响搜索精确度的重要因素。

5、缺少将索引存储于关系DB中的插件支持

当前wukong引擎只能将索引持久化存储于文件中,尚无法和MySQL这样的数据库配合索引的存储和查询。

总之,wukong绝非一个完美的全文搜索引擎,是否选用,要看你所处的context。

七、小结

选用wukong引擎和我们的项目目前所处的context情况不无关系:我们需要快速实现出一个功能简单却可用的全文搜索服务。也许在后续版本中,对查询方式、数据规模有进一步要求时,就是可能考虑更换引擎的时刻了。bleve、elasticsearch到时候就都会被我们列为考虑对象了。

本文代码在可在这里下载。

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats