使用 Elasticsearch 实现博客站内搜索

 

一直以来,为了优化本博客站内搜索效果和速度,我使用 bing 的 site: 站内搜索作为数据源,在服务端获取、解析、处理并缓存搜索结果,直接输出 HTML。这个方案惟一的问题是时效性难以保证,尽管我能够在发布和修改文章时主动告诉 bing,但它何时更新索引则彻底不受我控制。javascript

本着不折腾就浑身不自在的原则,我最终仍是使用 Elasticsearch 搭建了本身的搜索服务。Elasticsearch 是一个基于 Lucene 构建的开源、分布式、RESTful 搜索引擎,不少大公司都在用,程序员的好伙伴 Github 的搜索也用的是它。本文记录我使用 Elasticsearch 搭建站内搜索的过程,目前支持中文分词、同义词、标题匹配优先等常见策略,能够点击这里体验。html

安装 Elasticsearch

部署 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

安装 IK Analysis

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_max_word:会将文本作最细粒度的拆分,例如「中华人民共和国国歌」会被拆分为「中华人民共和国、中华人民、中华、华人、人民共和国、人民、人、民、共和国、共和、和、国国、国歌」,会穷尽各类可能的组合;
  • ik_smart:会将文本作最粗粒度的拆分,例如「中华人民共和国国歌」会被拆分为「中华人民共和国、国歌」;

ik_syno 和 ik_syno_smart 都会使用 synonym filter 实现同义词转换。为了方便后续测试,建议建立 ~/es_root/config/analysis/synonym.txt 文件,输入一些同义词并存为 utf-8 格式。例如:

ua,user-agent,userAgent
js,javascript

使用 JavaScript API

经过前面的示例,咱们知道经过 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

相关文章
相关标签/搜索