使用 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

相關文章
相關標籤/搜索