目錄
- 介紹
- 安裝
- 安裝kibana
- 安裝ES客戶端
- 使用
介紹
Elasticsearch, 簡稱 ES,它是個開源分布式搜索引擎,它的特點有:分布式,零配置,自動發現,索引自動分片,索引副本機制,restful 風格接口,多數據源,自動搜索負載等。它可以近乎實時的存儲、檢索數據;本身擴展性很好,可以擴展到上百臺服務器,處理 PB 級別的數據。es 也使用 Java 開發并使用 Lucene 作為其核心來實現所有索引和搜索的功能,但是它的目的是通過簡單的 RESTful API 來隱藏 Lucene 的復雜性,從而讓全文搜索變得簡單。
Elasticsearch 是面向文檔(document oriented)的,這意味著它可以存儲整個對象或文檔(document)。然而它不僅僅是存儲,還會索引(index)每個文檔的內容使之可以被搜索。在 Elasticsearch 中,你可以對文檔(而非成行成列的數據)進行索引、搜索、排序、過濾。
安裝
1.添加倉庫密鑰
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
2.添加鏡像源倉庫
echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elasticsearch.list
3.更新軟件包列表
sudo apt update
4.安裝es
sudo apt-get install elasticsearch=7.17.21
5.安裝ik分詞器插件
sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/7.17.21
6.啟動es
sudo systemctl start elasticsearch
6.5如果啟動es失敗
調整 ES 虛擬內存,虛擬內存默認最大映射數為 65530,無法滿足 ES 系統要求,需要調整為 262144 以上
sysctl -w vm.max_map_count=262144
增加虛擬機內存配置
vim /etc/elasticsearch/jvm.options
新增如下內容:
-Xms512m
-Xmx512m
設置完后重啟ubuntu
7.查看es服務的狀態
sudo systemctl status elasticsearch.service
8.驗證es是否安裝成功
curl -X GET "http://localhost:9200/"
9.設置能夠外部訪問: 如果新配置完成默認只能在本機進行訪問
vim /etc/elasticsearch/elasticsearch.yml
新增配置
network.host: 0.0.0.0
http.port: 9200
cluster.initial_master_nodes: ["node-1"]
重啟后sudo systemctl restart elasticsearch.service
瀏覽器訪問http://自己的IP:9200/
安裝kibana
kibana可以支持通過網頁對ES進行訪問(增刪查改), 這可以讓我們的測試更加直觀一些
1.安裝kibana
sudo apt install kibana
2.配置kibana
sudo vim /etc/kibana/kibana.yml
在7行修改為server.host: "0.0.0.0"
在32行修改為elasticsearch.hosts: ["http://0.0.0.0:9200"]
3.啟動kibana服務
sudo systemctl start kibana
4.驗證安裝
sudo systemctl status kibana
5.訪問kibana
在瀏覽器訪問kibana, http://你的ip:5601
安裝ES客戶端
1.克隆代碼
git clone https://github.com/seznam/elasticlient
注意: 這里不能從github下載源碼然后拖拽進來, 因為內部有git子模塊, 需要去更新子模塊之后, 才能去編譯
如果無法始終clone不下來, 也有對應的解決方案:https://blog.csdn.net/eyuyanniniu/article/details/145807381
2.切換目錄
cd elasticlient
3.安裝 MicroHTTPD 庫
sudo apt-get install libmicrohttpd-dev
4.更新子模塊
git submodule update --init --recursive
5.編譯安裝代碼
mkdir build
cd build
cmake ..
make
make install
6.配置環境變量
因為我們的庫默認安裝路徑是/usr/local/lib, 編譯器可能找不到這個庫目錄的位置
所以我們需要配置(這些文件最好都進行配置):
全局設置: /etc/profile
當前用戶設置: .bash_profil或.bashrc
在文件末尾加上 export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
使用
封裝icsearch.hpp文件
#include <elasticlient/client.h>
#include <cpr/cpr.h>
#include <json/json.h>
#include <iostream>
#include <memory>
#include "logger.hpp"//ES的二次封裝, 原因: 為了簡化ES的使用操作, 我們可以看到, 請求的時候, 正文很長, 我們希望只設置我們關心的參數即可, 而且能自動的構造完成
//封裝四個操作: 索引創建, 數據新增, 數據查詢, 數據刪除namespace wufan_im{
bool UnSerialize(const std::string& src, Json::Value& val)
{// 同樣的Read類, 需要先構造出工廠類Json::CharReaderBuilder crb;std::unique_ptr<Json::CharReader> cr(crb.newCharReader());std::string err;bool ret = cr->parse(src.c_str(), src.c_str() + src.size(), &val, &err);if (ret == false) {std::cout << "json反序列化失敗: " << err << std::endl;return false;}return true;
}bool Serialize(const Json::Value& val, std::string& dst)
{// Writer(StreamWriter)類, 這個類就是用來序列化的, 但是這個類不能直接構造, 因為使用了工廠模式// 先定義Json::SreamWriter 工廠類 Json::StreamWriterBuilderJson::StreamWriterBuilder swb; //構造出工廠類std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());// 通過Json::StreamWriter中的write接口進行序列化std::stringstream ss;int ret = sw->write(val, &ss); //將其序列化到字符流里面if (ret != 0) {std::cout << "Json反序列化失敗!\n";return false;}dst = ss.str();return true;
}// 索引創建:
// 傳兩個參數, 索引名稱 和 索引類型 就可以創建出索引
// 能夠添加字段, 并設置字段類型, 設置分詞器類型, 是否構造索引
class ESIndex{public:ESIndex(std::shared_ptr<elasticlient::Client>& client, const std::string& name, const std::string& type = "_doc"):_name(name), _type(type), _client(client){Json::Value analysis; //可以把Value當做Json里的{ }Json::Value analyzer;Json::Value ik;Json::Value tokenizer;tokenizer["tokenizer"] = "ik_max_word";ik["ik"] = tokenizer;analyzer["analyzer"] = ik;analysis["analysis"] = analyzer;_index["settings"] = analysis;}// 創建索引, 就相當于在設置表結構 - ai說的// 添加字段, 就相當于設置表的字段屬性ESIndex& append(const std::string& key, const std::string& type = "text", const std::string& analyzer = "ik_max_word", bool enabled = true) {Json::Value fields;fields["type"] = type;fields["analyzer"] = analyzer;if (enabled == false) fields["enabled"] = enabled;_properties[key] = fields;return *this;}bool create(const std::string& index_id = "default_index_id") {Json::Value mappings;mappings["dynamic"] = true;mappings["properties"] = _properties;_index["mappings"] = mappings;std::string body;bool ret = Serialize(_index, body);if (ret == false) {LOG_ERROR("索引序列化失敗! ");return false;}LOG_DEBUG("{}", body);// 2. 發起搜索請求try{ //因為請求失敗就可能會拋異常, 異常你不接住, 程序就會崩潰auto rsp = _client->index(_name, _type, index_id, body);if (rsp.status_code < 200 || rsp.status_code >= 300) {LOG_ERROR("創建ES索引 {} 失敗, 響應狀態碼異常: {}", _name, rsp.status_code);return false;}} catch(std::exception& e) {LOG_ERROR("創建ES索引 {} 失敗: {}", _name, e.what());return false;}return true;}private:std::string _name;std::string _type;Json::Value _properties;Json::Value _index;std::shared_ptr<elasticlient::Client> _client;
};// 數據新增
class ESInsert{public:ESInsert(std::shared_ptr<elasticlient::Client>& client, const std::string& name,const std::string& type = "_doc"):_name(name), _type(type), _client(client){}ESInsert& append(const std::string& key, const std::string& val){_item[key] = val;return *this;}// 插入到哪個id里面 - 這個ID就相當于是每一次插入時數據的唯一標識bool insert(const std::string id = ""){std::string body;bool ret = Serialize(_item, body);if (ret == false) {LOG_ERROR("索引序列化失敗! ");return false;}LOG_DEBUG("{}", body);// 2. 發起搜索請求try{ //因為請求失敗就可能會拋異常, 異常你不接住, 程序就會崩潰auto rsp = _client->index(_name, _type, id, body);if (rsp.status_code < 200 || rsp.status_code >= 300) {LOG_ERROR("新增數據 {} 失敗, 響應狀態碼異常: {}", body, rsp.status_code);return false;}} catch(std::exception& e) {LOG_ERROR("新增數據 {} 失敗: {}", body, e.what());return false;}return true;}private:std::string _name;std::string _type;Json::Value _item;std::shared_ptr<elasticlient::Client> _client;
};// 數據刪除
class ESRemove{public:ESRemove(std::shared_ptr<elasticlient::Client>& client, const std::string& name, const std::string& type):_name(name), _type(type), _client(client){}bool remove(const std::string& id) {try{ //因為請求失敗就可能會拋異常, 異常你不接住, 程序就會崩潰auto rsp = _client->remove(_name, _type, id);if (rsp.status_code < 200 || rsp.status_code >= 300) {LOG_ERROR("刪除數據 {} 失敗, 響應狀態碼異常: {}", id, rsp.status_code);return false;}} catch(std::exception& e) {LOG_ERROR("刪除數據 {} 失敗: {}", id, e.what());return false;}return true;}private:std::string _name;std::string _type;std::shared_ptr<elasticlient::Client> _client;
};//數據查詢
class ESSearch{public: //用戶還會設置過濾條件,以及應該包含的字段ESSearch(std::shared_ptr<elasticlient::Client>& client, const std::string& name, const std::string& type = "_doc"):_name(name), _type(type), _client(client){}ESSearch& append_must_not_terms(const std::string& key, const std::vector<std::string>& vals){Json::Value fields;for (const auto& val : vals) {fields[key].append(val);}Json::Value terms;terms["terms"] = fields;_must_not.append(terms);return *this;}ESSearch& append_should_match(const std::string& key, const std::string& val) {Json::Value field;field[key] = val;Json::Value match;match["match"] = field;_should.append(match);return *this;}Json::Value search() {Json::Value cond;if (_must_not.empty() == false) cond["must_not"] = _must_not;if (_should.empty() == false) cond["should"] = _should;Json::Value query;query["bool"] = cond;Json::Value root;root["query"] = query;std::string body;bool ret = Serialize(root, body);if (ret == false) {LOG_ERROR("索引序列化失敗! ");return Json::Value();}LOG_DEBUG("{}", body);// 2. 發起搜索請求cpr::Response rsp;try{ //因為請求失敗就可能會拋異常, 異常你不接住, 程序就會崩潰rsp = _client->search(_name, _type, body);if (rsp.status_code < 200 || rsp.status_code >= 300) {LOG_ERROR("檢索數據 {} 失敗, 響應狀態碼異常: {}", body, rsp.status_code);return Json::Value();}} catch(std::exception& e) {LOG_ERROR("檢索數據 {} 失敗: {}", body, e.what());return Json::Value();}//3. 需要對響應正文進行反序列化LOG_DEBUG("檢索響應正文: [{}]", rsp.text);Json::Value json_res;ret = UnSerialize(rsp.text, json_res);if (ret == false) {LOG_ERROR("檢索數據 {} 結果反序列化失敗", rsp.text);return Json::Value();}return json_res["hits"]["hits"];}private:std::string _name;std::string _type;//用戶還會設置過濾條件,以及應該包含的字段Json::Value _must_not; //必須不包含的Json::Value _should; //必須包含的, 多選一即可std::shared_ptr<elasticlient::Client> _client;
};
}
main.cc文件
#include "../../common/icsearch.hpp"
#include <gflags/gflags.h>DEFINE_bool(run_mode, false, "程序的運行模式, false-調試; true-發布;");
DEFINE_string(log_file, "", "發布模式下, 用于指定日志的輸出文件");
DEFINE_int32(log_level, 0, "發布模式下, 用于指定日志輸出等級");int main(int argc, char* argv[])
{google::ParseCommandLineFlags(&argc, &argv, true);wufan_im::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);std::shared_ptr<elasticlient::Client> client(new elasticlient::Client({"http://127.0.0.1:9200/"}));bool ret = wufan_im::ESIndex(client, "test_user")// 創建索引, 就相當于在設置表結構 - ai說的// 添加字段, 就相當于設置表的字段屬性.append("nickname").append("phone", "keyword", "standard", true) //手機號是不能進行分詞的, 是一個關鍵字, 分詞器用標準分詞器, 需要構造索引.create();if (ret == false) {LOG_INFO("索引創建失敗!");return -1;}LOG_INFO("索引創建成功");// 新增數據ret = wufan_im::ESInsert(client, "test_user").append("nickname", "張三").append("phone", "155666777").insert("00001"); // 這個ID就相當于是每一次插入時數據的唯一標識if (ret == false) {LOG_ERROR("數據插入失敗!");return -1;}// 數據的修改ret = wufan_im::ESInsert(client, "test_user").append("nickname", "張三").append("phone", "1334444555").insert("00001");if (ret == false) {LOG_ERROR("數據更新失敗!");return -1;}LOG_INFO("數據新增成功");Json::Value user = wufan_im::ESSearch(client, "test_user").append_should_match("phone.keyword", "1334444555") //檢索的時候, 告訴ES, 這個關鍵詞不要進行分詞// .append_must_not_terms("nickname.keyword", {"張三"}).search();if (user.empty() || user.isArray() == false) {LOG_ERROR("結果為空, 或者結果不是數組類型");return -1;}LOG_INFO("數據檢索成功");int sz = user.size();LOG_DEBUG("檢索結果條目數量: {}", sz);for (int i = 0; i < sz; ++i) {LOG_INFO("nickname: {}", user[i]["_source"]["nickname"].asString());}ret = wufan_im::ESRemove(client, "test_user", "_doc").remove("00001");if (ret == false) {LOG_ERROR("刪除數據失敗");return -1;}LOG_INFO("數據刪除成功");return 0;
}
運行程序: