一直以来,为了优化本博客站内搜索效果和速度,我使用 bing 的 site: 站内搜索作为数据源,在服务端获取、解析、处理并缓存搜索结果,直接输出 HTML。这个方案惟一的问题是时效性难以保证,尽管我能够在发布和修改文章时主动告诉 bing,但它何时更新索引则彻底不受我控制。javascript
本着不折腾就浑身不自在的原则,我最终仍是使用 Elasticsearch 搭建了本身的搜索服务。Elasticsearch 是一个基于 Lucene 构建的开源、分布式、RESTful 搜索引擎,不少大公司都在用,程序员的好伙伴 Github 的搜索也用的是它。本文记录我使用 Elasticsearch 搭建站内搜索的过程,目前支持中文分词、同义词、标题匹配优先等常见策略,能够点击这里体验。html
部署 Elasticsearch 最简单的方法是使用 Elasticsearch Dockerfile 。为了更完全地折腾,我没有使用 Docker,好在手动安装过程也不复杂。java
个人虚拟机和线上环境都是 Ubuntu 14.04.3 LTS,Elasticsearch 用的是目前最新的 2.1.1。一切开始以前,先要检查机器上是否装有 java 环境,若是没有能够经过如下命令安装:git
sudo apt-get install openjdk-7-jre-headless
下载 Elasticsearch 2.1.1 压缩包并解压:程序员
wget -c https://download.elasticsearch.org/elasticsearch/release/org/elasticsearch/distribution/zip/elasticsearch/2.1.1/elasticsearch-2.1.1.zip unzip elasticsearch-2.1.1.zip
我将解压获得的 elasticsearch-2.1.1 目录重命名为 ~/es_root (名称及位置没有限制,能够将它挪到你认为合适的任何位置)。Elasticsearch 无需安装,直接能够运行:github
cd ~/es_root/bin/ chmod a+x elasticsearch ./elasticsearch
若是屏幕上没有打印错误信息,说明 Elasticsearch 服务已经成功启动。新建一个终端,用 curl 验证下:docker
curl -XGET http://127.0.0.1:9200/?pretty { "name" : "Goblyn", "cluster_name" : "elasticsearch", "version" : { "number" : "2.1.1", "build_hash" : "40e2c53a6b6c2972b3d13846e450e66f4375bd71", "build_timestamp" : "2015-12-15T13:05:55Z", "build_snapshot" : false, "lucene_version" : "5.3.1" }, "tagline" : "You Know, for Search" }
若是看到以上信息,说明一切正常,不然请根据屏幕上的错误信息查找缘由。尽管 Elasticsearch 自己是用 java 写的,但它对外能够经过 RESTful 接口交互,十分方便。npm
默认状况下 Elasticsearch 的 RESTful 服务只有本机才能访问,也就是说没法从主机访问虚拟机中的服务。为了方便调试,能够修改 ~/es_root/config/elasticsearch.yml 文件,加入如下两行:api
network.bind_host: "0.0.0.0" network.publish_host: _non_loopback:ipv4_
但线上环境切忌不要这样配置,不然任何人均可以经过这个接口修改你的数据。promise
Elasticsearch 自带的分词器会粗暴地把每一个汉字直接分开,没有根据词库来分词。为了处理中文搜索,还须要安装中文分词插件。我使用的是 elasticsearch-analysis-ik ,支持自定义词库。
首先,下载与 Elasticsearch 2.1.1 匹配的 elasticsearch-analysis-ik 插件。根据文档,当前须要使用 master 版:
wget -c https://github.com/medcl/elasticsearch-analysis-ik/archive/master.zip unzip master.zip
解压后,进入插件源码目录编译:
sudo apt-get install maven cd elasticsearch-analysis-ik-master/ mvn package
若是一切顺利,在 target/releases/ 目录下能够找到编好的文件。将其解压并拷到 ~/es_root 对应目录:
mkdir -p ~/es_root/plugins/ik/ unzip target/releases/elasticsearch-analysis-ik-1.6.2.zip -d ~/es_root/plugins/ik/
再将 elasticsearch-analysis-ik 的配置也拷贝到 ~/es_root 对应目录:
mkdir -p ~/es_root/config/ik cp -r config/ik/* ~/es_root/config/ik/
elasticsearch-analysis-ik 的配置文件中不少都是词表,直接用文本编辑器打开就能够修改,改完记得保存为 utf-8 格式。
如今再启动 Elasticsearch 服务,若是看到相似下面这样的信息,说明 IK Analysis 插件已经装好了:
[plugins] [Libra] loaded [elasticsearch-analysis-ik]
Elasticsearch 自带一个名为 synonym 的同义词 filter。为了能让 IK 和 synonym 同时工做,咱们须要定义新的 analyzer,用 IK 作 tokenizer,synonym 作 filter。听上去很复杂,实际上要作的只是加一段配置。
打开 ~/es_root/config/elasticsearch.yml 文件,加入如下配置:
index: analysis: analyzer: ik_syno: type: custom tokenizer: ik_max_word filter: [my_synonym_filter] ik_syno_smart: type: custom tokenizer: ik_smart filter: [my_synonym_filter] filter: my_synonym_filter: type: synonym synonyms_path: analysis/synonym.txt
以上配置定义了 ik_syno 和 ik_syno_smart 这两个新的 analyzer,分别对应 IK 的 ik_max_word 和 ik_smart 两种分词策略。根据 IK 的文档,两者区别以下:
ik_syno 和 ik_syno_smart 都会使用 synonym filter 实现同义词转换。为了方便后续测试,建议建立 ~/es_root/config/analysis/synonym.txt 文件,输入一些同义词并存为 utf-8 格式。例如:
ua,user-agent,userAgent js,javascript
经过前面的示例,咱们知道经过 curl 或者 Chrome 的 Postman 扩展能轻松地与 Elasticsearch 服务交互。为了更好与已有系统集成,咱们还可使用 Elasticsearch Client。Elasticsearch Client 只是将 RESTful 接口包装了一层,常见语言都有对应的实现( 查看官方 Client ),本身写一套也不难。
个人博客系统是 Node.js 写的,在项目里直接 npm install elasticsearch --save 就能够安装 Elasticsearch 的 Node.js 包。
不管进行什么操做,首先都须要实例化 Elasticsearch Client 对象:
var elasticsearch = require('elasticsearch'); var client = new elasticsearch.Client({ host: '10.211.55.23:9200', //服务 IP 和端口 log: 'trace' //输出详细的调试信息 });
而后就能够调用 client 对象提供的各类方法了,client 对象拥有大量方法,请查看 官方文档 。这个库支持两种调用方式:callback 和 promise:
//callback client.info({}, function(err, data) { if(!err) { console.log('result:', data); } else { console.log('error:', err); } }); //promise client.info({}).then(function(data) { console.log('result:', data); }, function(err) { console.log('error:', err); });
为了节约篇幅,本文后续贴出的代码都采用 promise 写法,而且省略 then 函数。
到如今为止,全部准备工做都已经完成,立刻就要大功告成了。在进行下一步以前,先简单介绍一下 Elasticsearch 几个名词:
Elasticsearch 集群能够包含多个索引(Index),每一个索引能够包含多个类型(Type),每一个类型能够包含多个文档(Document),每一个文档能够包含多个字段(Field)。如下是 MySQL 和 Elasticsearch 的术语类比图,帮助理解:
MySQL | Elasticsearch |
---|---|
Database | Index |
Table | Type |
Row | Document |
Column | Field |
Schema | Mappping |
Index | Everything Indexed by default |
SQL | Query DSL |
就像使用 MySQL 必须指定 Database 同样,要使用 Elasticsearch 首先须要建立 Index:
client.indices.create({index : 'test'});
这样就建立了一个名为 test 的 Index。Type 不用单首创建,在建立 Mapping 时指定就能够。Mapping 用来定义 Document 中每一个字段的类型、所使用的 analyzer、是否索引等属性,很是关键。建立 Mapping 的代码示例以下:
client.indices.putMapping({ index : 'test', type : 'article', body : { article: { properties: { title: { type: 'string', term_vector: 'with_positions_offsets', analyzer: 'ik_syno', search_analyzer: 'ik_syno', }, content: { type: 'string', term_vector: 'with_positions_offsets', analyzer: 'ik_syno', search_analyzer: 'ik_syno', }, tags: { type: 'string', term_vector: 'no', analyzer: 'ik_syno', search_analyzer: 'ik_syno', }, slug: { type: 'string', term_vector: 'no', }, update_date: { type : 'date', term_vector: 'no', index : 'no', } } } } });
以上代码为 test 索引下的 article 类型指定了字段特征: title 、 content 和 tags 字段使用 ik_syno 作为 analyzer,说明它使用 ik_max_word 作为分词,而且应用 synonym 同义词策略; slug 字段没有指定 analyzer,说明它使用默认分词;而 update_date 字段则不会被索引。
接着,写入测试数据并索引:
client.index({ index : 'test', type : 'article', id : '100', body : { title : '什么是 JS?', slug :'what-is-js', tags : ['JS', 'JavaScript', 'TEST'], content : 'JS 是 JavaScript 的缩写!', update_date : '2015-12-15T13:05:55Z', } })
id 参数若是不指定,系统会自动生成一个并返回,后续在更新、删除时都要用到它。至于如何更新、删除,这里就不写了,请自行 查看文档 。
搜一下试试:
client.search({ index : 'test', type : 'article', q : 'JS', }).then(function(data) { console.log('result:'); console.log(JSON.stringify(data)); }, function(err) { console.log('error:'); console.log(err); });
没有问题,能够搜出来!查询结果数量和具体内容都在 hits 字段中:
result: {"took":50,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":1,"max_score":0.076713204,"hits":[{"_index":"test","_type":"article","_id":"100","_score":0.076713204,"_source":{"title":"什么是 JS?","slug":"what-is-js","tags":["JS","JavaScript","TEST"],"content":"JS 是 JavaScript 的缩写!","update_date":"2015-12-15T13:05:55Z"}}]}}
若是要实现更复杂的查询策略该怎么办?那就要请出前面表格中与 SQL 对应的 Query DSL 了。例如如下是本博客站内搜索所使用的 Query DSL:
{ index : 'test', type : 'article', from : start, body : { query : { dis_max : { queries : [ { match : { title : { query : keyword, minimum_should_match : '50%', boost : 6, } } }, { match : { content : { query : keyword, minimum_should_match : '75%', boost : 4, } } }, { match : { tags : { query : keyword, minimum_should_match : '100%', boost : 2, } } }, { match : { slug : { query : keyword, minimum_should_match : '100%', boost : 1, } } } ], tie_breaker : 0.3 } }, highlight : { pre_tags : ['<b>'], post_tags : ['</b>'], fields : { title : {}, content : {}, } } } }
from 参数指定从开始跳过多少条结果,用来实现分页。这份复杂的 Query DSL 搜出来的结果以下:
result: {"took":108,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":1,"max_score":0.29921508,"hits":[{"_index":"test","_type":"article","_id":"100","_score":0.29921508,"_source":{"title":"什么是 JS?","slug":"what-is-js","tags":["JS","JavaScript","TEST"],"content":"JS 是 JavaScript 的缩写!","update_date":"2015-12-15T13:05:55Z"},"highlight":{"content":["<b>JS</b> 是 <b>JavaScript</b> 的缩写!"],"title":["什么是 <b>JS</b>?"]}}]}}
能够看到,同义词策略和关键词高亮功能都正常。跑通 Elasticsearch 基本流程,剩余工做就是导入更多数据、配置更多词表和尝试不一样策略了,略过不写。
我接触 Elasticsearch 一共才几小时,个人出发点也很简单,只是为了给博客加上站内搜索,故本文既不全面也不深刻,甚至还包含各类错误,仅供参考。Elasticsearch 功能十分强大和复杂,远远不是花几个小时就能玩明白的。最后推荐「 Elasticsearch 权威指南(中文版) 」这本书,很是细致和全面,我对 Elasticsearch 仅有的一点了解都来自于这本书和官方文档。
本文连接: https://imququ.com/post/elasticsearch.html , 参与评论 。
-- EOF --
http://www.open-open.com/lib/view/open1452046497511.html