天天看點

Elasticsearch實戰:給部落格打造全文檢索一、建立索引二、全量資料導入es3、增量資料同步es4、檢索資料

學習和使用Elasticsearch有一段時間了,項目中大量使用到了es,但對于我來說都是部分或者局部地去使用,是以得找個時間好好整理并且再完整實踐一下es,于是就有了這篇文章。

首先系統架構是LNMP,很簡單的個人部落格網站(逐漸前行STEP),

使用laravel架構,實作全文檢索的引擎是elasticsearch,使用的分詞工具是ik-analyzer然後是安裝元件:elasticsearch/elasticsearch,以下清單是本次實踐所用到的軟體/架構/元件的版本:

  1. PHP 7.1.3
  2. Larvel 5.8
  3. Mysql 5.7
  4. elasticsearch 5.3
  5. elasticsearch/elasticsearch 7.2

以下預設上述環境已經準備完畢。

實戰主要分為4部分:

  1. 建立索引
  2. 全量資料導入es
  3. 增量資料同步es
  4. 關鍵詞檢索

一、建立索引

部落格的以下屬性需要納入檢索:

字段 備注 屬性
id ID int(11)
title 标題 varchar(255)
description 摘要 varchar(255)
content 内容 text
category_id 分類ID int(11)
keyword_ids 關鍵詞 varchar(255)
read_cnt 閱讀量 int(11)
created_at 釋出時間 TIMESTAMP
updated_at 更新時間 TIMESTAMP

其中,title、description、content既需要分詞來做全文檢索,又需要保留部分原字元串便于直接搜尋,是以使用

fields

将字段映射出不同類型:

"title": {
    "type": "text",
    "fields": {
        "keyword": {
            "type": "keyword",
            "ignore_above": 256
        }
    }
},
           

而在分詞器的選擇上,為了既能對文檔分詞更細,又能對檢索更精确,在對文檔字段分詞和對檢索時的輸入分詞使用不同的分詞器:

"title": {
   "type": "text",
     "fields": {
         "keyword": {
             "type": "keyword",
             "ignore_above": 256
         }
     },
     "analyzer": "ik_max_word",
     "search_analyzer": "ik_smart"
 },
           

比如,title為”重走絲綢之路“,ik_max_word分詞如下:

{
    "tokens": [
        {
            "token": "重走",
            "start_offset": 0,
            "end_offset": 2,
            "type": "CN_WORD",
            "position": 0
        },
        {
            "token": "絲綢之路",
            "start_offset": 2,
            "end_offset": 6,
            "type": "CN_WORD",
            "position": 1
        },
        {
            "token": "絲綢",
            "start_offset": 2,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 2
        },
        {
            "token": "之路",
            "start_offset": 4,
            "end_offset": 6,
            "type": "CN_WORD",
            "position": 3
        }
    ]
}
           

而ik_smart分詞粒度更粗:

{
    "tokens": [
        {
            "token": "重走",
            "start_offset": 0,
            "end_offset": 2,
            "type": "CN_WORD",
            "position": 0
        },
        {
            "token": "絲綢之路",
            "start_offset": 2,
            "end_offset": 6,
            "type": "CN_WORD",
            "position": 1
        }
    ]
}
           

鍵搜尋詞為”重走絲綢之路“,我們當然希望原文盡可能多比對到這個檢索詞,而不是每個字都可能檢索出一堆文檔,這就是比對的精确度。

對于keyword_ids、category_id,導入到es中時,就要裝換成具體的内容了,才能要支援使用者使用文字檢索,而不是限制使用ID,這兩個字段分别在es中字段名設定為keywords、category。

而且,一般來說關鍵詞的檢索,隻考慮精确比對,比如說關鍵詞”全文檢索“,如果要分詞的話就會變成:

{
    "tokens": [
        {
            "token": "全文",
            "start_offset": 0,
            "end_offset": 2,
            "type": "CN_WORD",
            "position": 0
        },
        {
            "token": "檢索",
            "start_offset": 2,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 1
        }
    ]
}
           

而實際上,全文可能比對一部分文檔,檢索頁比對一部分文檔,這對于關鍵詞這個屬性定義來說,是沒有意義的,是以,我們對keywords、category使用”keyword“類型。

考慮到該實戰隻是最小實作,忽略别名(aliases),分片配置使用預設,相應的需建立索引

articles

如下:

{
        "mappings": {
            "doc": {
                "properties": {
                    "id": {
                        "type": "long"
                    },
                    "keywords": {
                        "type": "keyword",
                        "ignore_above": 256
                    },
                    "categorys": {
                        "type": "keyword",
                        "ignore_above": 256
                    },
                    "read_cnt": {
                        "type": "long"
                    },
                    "title": {
                        "type": "text",
                        "fields": {
                            "keyword": {
                                "type": "keyword",
                                "ignore_above": 256
                            }
                        },
                        "analyzer": "ik_max_word",
                        "search_analyzer": "ik_smart"
                    },
                    "description": {
                        "type": "text",
                        "fields": {
                            "keyword": {
                                "type": "keyword",
                                "ignore_above": 256
                            }
                        },
                        "analyzer": "ik_max_word",
                        "search_analyzer": "ik_smart"
                    },
                    "created_at": {
                        "type": "date",
                        "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
                    },
                    "updated_at": {
                        "type": "date",
                        "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
                    }
                }
            }
        }
    
}
           

使用

PUT /articles

API建立索引成功後會傳回:

{
    "acknowledged": true,
    "shards_acknowledged": true
}
           

二、全量資料導入es

因為是對已有的部落格網站打造全文檢索,是以首先需要進行一次全量導入ES。第一步的操作都是直接使用es api完成的,而這一步涉及到資料查詢與轉換,則需要在我們的項目内完成。

首先我們需要熟悉es元件

elasticsearch/elasticsearch

的使用,以下介紹本次實戰涉及到的一些功能,更多可以直接看文檔:Elasticsearch-PHP 中文文檔。

我們先在配置檔案

config/elastic.php

定義好es的連接配接資訊:

<?php

return array(
    'default' => [
         'hosts'     => [
            [
                 'host' => ‘xxx.xxx.xxx.xxx’,
                 'port' => '9200',
                 'scheme' => 'http',
             ]
         ],
        'retries'   => 1,

        /*
        |--------------------------------------------------------------------------
        | Default Index Name
        |--------------------------------------------------------------------------
        |
        | This is the index name that elasticquent will use for all
        */
        'default_index' => ‘default_index’,
    ],
);


           

再使用批量批量索引文檔的方法:bulk,示例:

for($i = 0; $i < 100; $i++) {
    $params['body'][] = [
        'index' => [
            '_index' => 'my_index',
            '_type' => 'my_type',
    	]
    ];

    $params['body'][] = [
        'my_field' => 'my_value'
    ];
}

$responses = ClientBuilder::create()->build()->bulk($params);

           

這裡不能直接使用查庫後的資料,需要做一些轉換工作,比如keyword_ids 轉換成keywords,我們封裝一個函數:

getDoc()

public function getDoc()
{
    $fields = [
        'id',
        ’title,
        ‘description’,
        ‘read_cnt’,
        'created_at’,
        ‘updated_at’
    ];

    $data = array_only($this->getAttributes(), $fields);

    $data[‘keywords’] = ArticleKeyword::whereIn(‘id’, $this->keyword_ids)->pluck(‘name’)->toArray();

    $data[‘category’] = ArticleCategory::find($this->category_id);

    return $data;
}

           

直接調用該方法擷取需要同步的文檔資料。

注意使用該方法批量索引時,index + 一組資料是成對的。

按照第一步建立的索引,直接使用元件提供的批量索引功能全量将查詢出的資料同步到es中。

3、增量資料同步es

對于新增的資料,需要在寫入庫中的同時同步到es,這裡使用到的方案是Eloquent 的模型事件。

在 Eloquent 模型類上進行查詢、插入、更新、删除操作時,會觸發相應的模型事件,不管你有沒有監聽它們。這些事件包括:

retrieved 擷取到模型執行個體後觸發

creating 插入到資料庫前觸發

created 插入到資料庫後觸發

updating 更新到資料庫前觸發

updated 更新到資料庫後觸發

saving 儲存到資料庫前觸發(插入/更新之前,無論插入還是更新都會觸發)

saved 儲存到資料庫後觸發(插入/更新之後,無論插入還是更新都會觸發)

deleting 從資料庫删除記錄前觸發

deleted 從資料庫删除記錄後觸發

restoring 恢複軟删除記錄前觸發

restored 恢複軟删除記錄後觸發

而我們需要使用到的事件是:saved、deleted,監聽這兩個事件,在觸發後同步到es,這樣文章的增、改、删操作都能實時将資料變化同步到es。

我們使用

fireModelEvent

設定事件觸發的同步操作,這裡用到了元件中的單文檔索引功能:index,示例:

$params = [
    'index' => 'my_index',
    'type' => 'my_type',
    'id' => 'my_id',
    'body' => [ 'testField' => 'abc']
];

$response = $client->index($params);
           

使用第2步中的

getDoc()

方法來擷取待更新的資料。

具體實作如下:

public function fireModelEvent($event, $halt = true)
    {
        if (in_array($event, ['saved', 'deleted']))
        {
            if($event == 'deleted')
            {
                ClientBuilder::create()->build()->delete(['id' => $this->id]);
            }

            if($event == 'saved')
            {
                $params = [
                    'index' => 'articles',
                    'type' => 'doc',
                    'id' => $this->id,
                    'body' => $this->getDoc()
                ];

                ClientBuilder::create()->build()->index($params);
            }
        }
    }
           

4、檢索資料

通過2、3步驟,我們的文章已經實時同步到es上了,這一步我們需要将es的全文檢索開放給使用者使用,在我的網站中,我在文章清單增加了一個搜尋框給使用者輸入需檢索的文本:

Elasticsearch實戰:給部落格打造全文檢索一、建立索引二、全量資料導入es3、增量資料同步es4、檢索資料

這裡有兩個需求:

1、對title、description、keywords、category 做 query_string 查詢

2、将查詢結果轉化為Eloquent集合,便于結果展示

封裝的檢索函數:

public static function search($keyword, $page = 1, $per_page = 20, $conditions = [], $sort = null)
    {
        $page = max(1, intval($page));

        $from = ($page - 1) * $per_page;

        $query = [];
        //搜尋文本字段
        $search_fields = ['title', 'keywords', 'category', 'description'];

        if($keyword)
        {
            foreach ($search_fields as $key => $search_field)
            {
                $query['must']['bool']['should'][] = [
                    'query_string' => [
                        'default_field' => $search_field,
                        'query' => strtolower($keyword),
                        'default_operator' => 'AND',
                    ]
                ];
            }
        }

        $params = [
            'index' => 'articles',
            'type' => 'doc',
            'body' => [
                'query' => $query
            ]
        ];

        $response = ClientBuilder::create()->build()->search($params);

        $total_count = array_get($response, 'hits.total', 0);

        $collection = new Collection();

        foreach (array_get($response, 'hits.hits', []) as $key => $item)
        {
            $self = new static;

            $self->setRawAttributes($item['_source'], true);

            $collection->add($self);
        }

        return new LengthAwarePaginator($collection, $total_count, $per_page, intval($from/$per_page) + 1);
    }
           

繼續閱讀