Qt 数字报阅读器 图文版 改用 Elasticsearch 检索,首先通过三篇文章的节选介绍一下 Elasticsearch 集成应用场景。
聊聊八种架构模式-51CTO.COM
https://developer.51cto.com/article/711543.html
如上图所示,这种模式较单库单应用模式与内容分发模式多了几个部分,一个是业务数据库的主从分离,一个是引入了ES,为什么要这样?都解决了哪些痛点,下面具体结合业务需求场景进行叙述。
场景一:全文关键词检索
我想这个需求,绝大多数应用都会有,如果使用传统的数据库技术,大部分可能都会使用like这种SQL语句,高级一点可能是先分词,然后通过分词index相关的记录。SQL语句的性能问题与全表扫描机制导致了非常严重的性能问题,现在基本上很少见到。这里的ES是ElasticSearch的缩写,是一种查询引擎,类似的还有Solr等,都差不多的技术,ES较Solr配置简单、使用方便,所以这里选用了它。另外,ES支持横向扩展,理论上没有性能的瓶颈。同时,还支持各种插件、自定义分词器等,可扩展性较强。在这里,使用ES不仅可以替代数据库完成全文检索功能,还可以实现诸如分页、排序、分组、分面等功能。具体的,请同学们自行学习之。那怎么使用呢?一个一般的流程是这样的:
服务端把一条业务数据落库
服务端异步把该条数据发送到ES
ES把该条记录按照规则、配置放入自己的索引库
客户端查询的时候,由服务端把这个请求发送到ES,得到数据后,根据需求拼装、组合数据,返回给客户端
实际中怎么用,还请同学们根据实际情况做组合、取舍。
场景二:大量的普通查询
这个场景是指我们的业务中的大部分辅助性的查询,如:取钱的时候先查询一下余额,根据用户的ID查询用户的记录,取得该用户最新的一条取钱记录等。我们肯定是要天天要用的,而且用的还非常多。同时呢,我们的写入请求也是非常多的,导致大量的写入、查询操作压向同一数据库,然后,数据库挂了,系统挂了,领导生气了,被开除了,还不起房贷了,露宿街头了,老婆跟别人跑了,......
不敢想,所以要求我们必须分散数据库的压力,一个业界较成熟的方案就是数据库的读写分离,写的时候入主库,读的时候读从库。这样就把压力分散到不同的数据库了,如果一个读库性能不行,扛不住的话,可以一主多从,横向扩展。可谓是一剂良药啊!那怎么使用呢?一个一般的流程是这样的:
服务端把一条业务数据落库
数据库同步或异步或半同步把该条数据复制到从库
服务端读数据的时候直接去从库读相应的数据
比较简单吧,一些聪明的、爱思考的、上进的同学可能发现问题了,也包括上面介绍的场景一,就是延迟问题,如:数据还没有到从库,我就马上读,那么是读不到的,会发生问题的。对于这个问题,各家公司解决的思路不一样,方法不尽相同。一个普遍的解决方案是:读不到就读主库,当然这么说也是有前提条件的,但具体的方案这里就不一一展开了,我可能会在接下来的分享中详解各种方案。另外,关于数据库的复制模式,还请同学们自行学习,太多了,这里说不清。该总结一下这种模式的优缺点的了,如下:
优点:减少数据库的压力,理论上提供无限高的读性能,间接提高业务(写)的性能,专用的查询、索引、全文(分词)解决方案。
缺点:数据延迟,数据一致性的保证。
架构师之路,从「存储选型」起步-51CTO.COM
https://www.51cto.com/article/711630.html
MySQL数据库擅长在线业务(OLTP)读写,不擅长做统计、分析型业务(OLAP)。因此,一般会通过MySQL做持久化存储,ES构建索引进行查询、分析。
适用于搜索场景:
复杂查询
模糊搜索
全文搜索
统计分析
Elasticsearch的使用场景深入详解_铭毅天下的博客-CSDN博客_elasticsearch使用场景和注意事项
https://blog.csdn.net/laoyang360/article/details/52227541
场景二:在现有系统中增加elasticsearch
由于ES不能提供存储的所有功能,一些场景下需要在现有系统数据存储的基础上新增ES支持。
例1:ES不支持事务、复杂的关系(至少1.X版本不支持,2.X有改善,但支持的仍然不好),如果你的系统中需要上述特征的支持,需要考虑在原有架构、原有存储的基础上的新增ES的支持。
举例2:如果你已经有一个在运行的复杂的系统,你的需求之一是在现有系统中添加检索服务。一种非常冒险的方式是重构系统以支持ES,而相对安全的方式是将ES作为新的组件添加到现有系统中。
如果你使用了如下图所示的SQL数据库和ES存储,你需要找到一种方式使得两存储之间实时同步。需要根据数据的组成、数据库选择对应的同步插件,可供选择的插件包括:
1)mysql、oracle选择 logstash-input-jdbc 插件。
2)mongo选择 mongo-connector工具。
假设你的在线零售商店的产品信息存储在SQL数据库中。为了快速且相关的搜索,你安装了Elasticsearch。为了索引数据,您需要部署一个同步机制,该同步机制可以是Elasticsearch插件或你建立的一个自定义的服务。此同步机制可以将对应于每个产品的所有数据和索引都存储在Elasticsearch,每个产品作为一个document存储(这里的document相当于关系型数据库中的一行/row数据)。
当在该网页上的搜索条件中输入“用户的类型”,店面网络应用程序通过Elasticsearch查询该信息。Elasticsearch返回符合标准的产品documents,并根据你喜欢的方式来分类文档。排序可以根据每个产品的被搜索次数所得到的相关分数,或任何存储在产品document的信息,例如:最新最近加入的产品、平均得分,或者是那些插入或更新信息。所以你可以只使用Elasticsearch处理搜索,这取决于同步机制来保持Elasticsearch获取最新变化。
Qt 数字报阅读器图文版改用ES检索便是采用在现有系统中增加ES的方案,将ES作为新的组件添加到现有系统中,建立自定义服务同步MySQL与ES。
现有数据库如下,每日数字报一张表,数据保存和查询通过日期区分,为了和现有数据库操作一致,ES也是每日建一个同名index。
引题、主题、副题、图片描述、内容为text,使用ik分词,其它为keyword
PUT /t_epaper_2022-03-05
{
"mappings": {
"properties": {
"md5_str": {
"type": "keyword"
},
"md5_url": {
"type": "keyword"
},
"paper_name": {
"type": "keyword"
},
"paper_date": {
"type": "keyword"
},
"paper_layout": {
"type": "keyword"
},
"pre_title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"sub_title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"author": {
"type": "keyword"
},
"image_text": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"content": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"html_url": {
"type": "keyword"
},
"seq_num": {
"type": "integer"
},
"update_time": {
"type": "keyword"
}
}
}
}
需要实现的功能有实时数据同步、MySQL历史数据导入ES、改用ES检索。由于_id是doc的唯一属性,为了避免重复数据,使用PUT指定id;实时数据同步,数据库建表时建ES index,使用一个全局id,每次数据库操作时同步ES,构造doc,执行index,本系统不实现,只通过阅读器客户端同步数据。
由于现有的阅读页面已有高亮功能,不使用ES的高亮,避免无用大数据的传送,但现有高亮只是对整个关键词高亮,ik分词打散后无法高亮,所以加ik打散关键词给现有系统使用,这样便能达到和ES高亮同样功能。现有系统已有前端伪分页,为了最小改动现有系统,不使用ES分页,但ES默认只返回10条,query要加size限制,本系统体量小,1000足够返回所有数据。
使用ik_smart打散关键词给现有系统使用,mapping search_analyzer就是ik_smart,这样便能达到和ES高亮同样功能。
QStringList MainWindow::getIKLst(QStringList wordLst)
{
if (mIKLst.contains(wordLst))
{
return mIKLst.value(wordLst);
}
QString text;
foreach (QString word, wordLst)
{
text.append(QStringLiteral("\"%1\", ").arg(word));
}
text.remove(text.lastIndexOf(", "), 2);
QString queryES = QStringLiteral("{\"analyzer\": \"ik_smart\",\"text\": [%1]}").arg(text);
QJsonDocument doc = QJsonDocument::fromJson(queryES.toUtf8().data());
QByteArray body = doc.toJson();
QString strInput = body;
QString strMessage;
QString strResult;
QString strUrl = QStringLiteral("http://localhost:9200/_analyze");
SendAndGetText(strUrl, "POST", strInput, strMessage, strResult);
doc = QJsonDocument::fromJson(strResult.toUtf8().data());
QJsonObject object = doc.object();
QJsonObject object2;
QStringList highLightLst;
if (object.contains("tokens"))
{
QJsonValue value = object.value("tokens");
QJsonArray array = value.toArray();
int retSize = array.size();
for (int i = 0; i < retSize; ++i)
{
object2 = array.at(i).toObject();
highLightLst.append(object2.value("token").toString());
}
}
mIKLst.insert(wordLst, highLightLst);
return highLightLst;
}
数据同步,按日期建ES index,查数据库,PUT提交
mESMapping = "{\"mappings\": {\"properties\": {\"md5_str\": {\"type\": \"keyword\"},\"md5_url\": {\"type\": \"keyword\"},\"paper_name\": {\"type\": \"keyword\"},\"paper_date\": {\"type\": \"keyword\"},\"paper_layout\": {\"type\": \"keyword\"},\"pre_title\": {\"type\": \"text\",\"analyzer\": \"ik_max_word\",\"search_analyzer\": \"ik_smart\"},\"title\": {\"type\": \"text\",\"analyzer\": \"ik_max_word\",\"search_analyzer\": \"ik_smart\"},\"sub_title\": {\"type\": \"text\",\"analyzer\": \"ik_max_word\",\"search_analyzer\": \"ik_smart\"},\"author\": {\"type\": \"keyword\"},\"image_text\": {\"type\": \"text\",\"analyzer\": \"ik_max_word\",\"search_analyzer\": \"ik_smart\"},\"content\": {\"type\": \"text\",\"analyzer\": \"ik_max_word\",\"search_analyzer\": \"ik_smart\"},\"html_url\": {\"type\": \"keyword\"},\"seq_num\": {\"type\": \"integer\"},\"update_time\": {\"type\": \"keyword\"}}}}";
int MainWindow::importES(QString date)
{
QString retStr = mDBHelper.getConnectDB();
if (!retStr.isEmpty())
{
informationMessageBox(QStringLiteral("提示"), QStringLiteral("数据库连接失败:\n%1").arg(retStr), true);
return 0;
}
QJsonDocument doc = QJsonDocument::fromJson(mESMapping.toUtf8().data());
QByteArray body = doc.toJson();
QString strMessage;
QString strResult;
QString strUrl = QStringLiteral("http://localhost:9200/t_epaper_%1").arg(date);
QString strInput = body;
int code = SendAndGetText(strUrl, "PUT", strInput, strMessage, strResult);
ui->textBrowser_es->append(QStringLiteral("%1 %2 %3\n").arg(code).arg(strMessage).arg(qUtf8Printable(strResult)));
QStringList columnLst;
columnLst << "md5_str" << "md5_url" << "paper_name" << "paper_date" << "paper_layout" << "pre_title" << "title" << "sub_title" << "author" << "image_text" << "content" << "html_url" << "seq_num" << "update_time";
QString str = QStringLiteral("select columns from `t_epaper_%1`").arg(date);
QList<QStringList> retLst = mDBHelper.getSqlSelect(str, columnLst);
QStringList docLst;
QString docStr;
QString strUrlDoc;
int count = 1;
bool abnormal = false;
QDateTime start = QDateTime::currentDateTime();
foreach (QStringList ret, retLst)
{
QString md5_str = ret[0];
QString md5_url = ret[1];
QString paper_name = ret[2];
QString paper_date = ret[3];
QString paper_layout = ret[4];
QString pre_title = ret[5].replace("\\", "").replace("\"", "\\\"");
QString title = ret[6].replace("\\", "").replace("\"", "\\\"");
QString sub_title = ret[7].replace("\\", "").replace("\"", "\\\"");
QString author = ret[8].replace("\\", "").replace("\"", "\\\"");
QString image_text = ret[9].replace("\\", "").replace("\"", "\\\"");
QString content = ret[10].replace("\\", "").replace("\"", "\\\"");
QString html_url = ret[11];
QString seq_num = ret[12];
QString update_time = ret[13];
docLst.clear();
docLst.append(QStringLiteral("\"md5_str\": \"%1\"").arg(md5_str));
docLst.append(QStringLiteral("\"md5_url\": \"%1\"").arg(md5_url));
docLst.append(QStringLiteral("\"paper_name\": \"%1\"").arg(paper_name));
docLst.append(QStringLiteral("\"paper_date\": \"%1\"").arg(paper_date));
docLst.append(QStringLiteral("\"paper_layout\": \"%1\"").arg(paper_layout));
docLst.append(QStringLiteral("\"pre_title\": \"%1\"").arg(pre_title));
docLst.append(QStringLiteral("\"title\": \"%1\"").arg(title));
docLst.append(QStringLiteral("\"sub_title\": \"%1\"").arg(sub_title));
docLst.append(QStringLiteral("\"author\": \"%1\"").arg(author));
docLst.append(QStringLiteral("\"image_text\": \"%1\"").arg(image_text));
docLst.append(QStringLiteral("\"content\": \"%1\"").arg(content));
docLst.append(QStringLiteral("\"html_url\": \"%1\"").arg(html_url));
docLst.append(QStringLiteral("\"seq_num\": \"%1\"").arg(seq_num));
docLst.append(QStringLiteral("\"update_time\": \"%1\"").arg(update_time));
docStr = "{" + docLst.join(",") + "}";
doc = QJsonDocument::fromJson(docStr.toUtf8().data());
body = doc.toJson();
strUrlDoc = QStringLiteral("http://localhost:9200/t_epaper_%1/_doc/%2").arg(date).arg(count++);
strInput = body;
code = SendAndGetText(strUrlDoc, "PUT", strInput, strMessage, strResult);
if (code > 300)
{
ui->textBrowser_abnormal_data->append(QStringLiteral("%1 %2 %3\n%4\n\n").arg(code).arg(strMessage).arg(qUtf8Printable(strResult)).arg(qUtf8Printable(docStr)));
abnormal = true;
}
ui->textBrowser_es->append(QStringLiteral("%1 %2 %3\n").arg(code).arg(strMessage).arg(qUtf8Printable(strResult)));
}
if (!abnormal)
{
ui->textBrowser_es->append(QStringLiteral("%1数据导入ES完成,%2/%3,用时%4s\n").arg(date).arg(count - 1).arg(retLst.size()).arg(start.secsTo(QDateTime::currentDateTime())));
}
else
{
ui->textBrowser_es->append(QStringLiteral("%1数据导入ES完成,%2/%3,用时%4s,有异常数据\n").arg(date).arg(count - 1).arg(retLst.size()).arg(start.secsTo(QDateTime::currentDateTime())));
}
return retLst.size();
}
void MainWindow::deleteES(QString date)
{
QString strMessage;
QString strResult;
QString strUrl = QStringLiteral("http://localhost:9200/t_epaper_%1").arg(date);
QString strInput;
int code = SendAndGetText(strUrl, "DELETE", strInput, strMessage, strResult);
ui->textBrowser_es->append(QStringLiteral("%1 %2 %3\n").arg(code).arg(strMessage).arg(qUtf8Printable(strResult)));
}
查数据库所有表名,提取日期
QStringList MainWindow::getAllTableDateLst()
{
QString retStr = mDBHelper.getConnectDB();
if (!retStr.isEmpty())
{
informationMessageBox(QStringLiteral("提示"), QStringLiteral("数据库连接失败:\n%1").arg(retStr), true);
return QStringList();
}
QStringList columnLst;
columnLst << "table_name";
QString str = QStringLiteral("select columns from information_schema.tables where table_schema='epaper';");
QList<QStringList> retLst = mDBHelper.getSqlSelect(str, columnLst);
QStringList tableLst;
foreach (QStringList ret, retLst)
{
tableLst.append(ret[0]);
}
tableLst.removeOne("t_epaper");
QStringList dateLst;
foreach (QString table, tableLst)
{
dateLst.append(table.replace("t_epaper_", ""));
}
qSort(dateLst.begin(), dateLst.end());
return dateLst;
}
单个日期同步、全部同步
void MainWindow::on_pushButton_es_date_clicked()
{
ui->textBrowser_es->clear();
importES(ui->dateEdit_es->date().toString("yyyy-MM-dd"));
}
void MainWindow::on_pushButton_es_all_clicked()
{
QStringList dateLst = getAllTableDateLst();
if (dateLst.isEmpty())
{
return;
}
ui->textBrowser_es->clear();
ui->textBrowser_es->append(QStringLiteral("共有%1张表").arg(dateLst.size()));
QDateTime start = QDateTime::currentDateTime();
int count = 0;
int total = 0;
foreach (QString date, dateLst)
{
count = importES(date);
total += count;
}
ui->textBrowser_es->append(QStringLiteral("共导入%1数据,用时%2s").arg(total).arg(start.secsTo(QDateTime::currentDateTime())));
}
单个日期删除、全部删除
void MainWindow::on_pushButton_es_date_delete_clicked()
{
ui->textBrowser_es->clear();
deleteES(ui->dateEdit_es->date().toString("yyyy-MM-dd"));
}
void MainWindow::on_pushButton_es_all_delete_clicked()
{
QStringList dateLst = getAllTableDateLst();
if (dateLst.isEmpty())
{
return;
}
ui->textBrowser_es->clear();
foreach (QString date, dateLst)
{
deleteES(date);
}
ui->textBrowser_es->append(QStringLiteral("删除%1数据").arg(dateLst.size()));
}
ES检索,主要是json结果的处理,需要注意的是要用POST,如果用GET只会使用url,相当于不带body,查询了全部。
QString keyWord = ui->lineEdit_keyword->text();
QString keyWordRelationship = ui->comboBox_keyword_relation->currentText();
QString searchRange = ui->comboBox_search_range->currentText();
QString operatorES;
if (keyWordRelationship == QStringLiteral("并且"))
{
operatorES = "and";
}
else if (keyWordRelationship == QStringLiteral("或者"))
{
operatorES = "or";
}
QStringList fieldsES;
if (searchRange == QStringLiteral("全部"))
{
fieldsES.append("pre_title");
fieldsES.append("title");
fieldsES.append("sub_title");
fieldsES.append("image_text");
fieldsES.append("content");
}
else if (searchRange == QStringLiteral("仅标题"))
{
fieldsES.append("pre_title");
fieldsES.append("title");
fieldsES.append("sub_title");
}
else if (searchRange == QStringLiteral("仅内容"))
{
fieldsES.append("image_text");
fieldsES.append("content");
}
QString fieldsStr;
foreach (QString fields, fieldsES)
{
fieldsStr.append(QStringLiteral("\"%1\", ").arg(fields));
}
fieldsStr.remove(fieldsStr.lastIndexOf(", "), 2);
mSearchDataLst.clear();
QString queryES;
QJsonDocument doc;
QJsonObject object;
QByteArray body;
QString strMessage;
QString strResult;
QString strUrl;
QString strInput;
foreach (QString paperName, paperNameLst)
{
queryES = QStringLiteral("{\"query\": {\"bool\": {\"must\": [{\"match\": {\"paper_name\": \"%1\"}}],\"filter\": [{\"multi_match\": {\"query\": \"%2\",\"fields\": [%3],\"operator\": \"%4\"}}]}},\"_source\": [\"paper_layout\",\"pre_title\",\"title\",\"sub_title\",\"author\",\"md5_str\",\"md5_url\"]}").arg(paperName).arg(keyWord).arg(fieldsStr).arg(operatorES);
doc = QJsonDocument::fromJson(queryES.toUtf8().data());
body = doc.toJson();
strInput = body;
foreach (QString paperDate, paperDateLst)
{
strUrl = QStringLiteral("http://localhost:9200/t_epaper_%1/_search").arg(paperDate);
SendAndGetText(strUrl, "POST", strInput, strMessage, strResult);
doc = QJsonDocument::fromJson(strResult.toUtf8().data());
object = doc.object();
if (object.contains("hits"))
{
QJsonValue value = object.value("hits");
if (value.isObject())
{
QJsonObject object2 = value.toObject();
QJsonValue hits = object2.value("hits");
QJsonArray array = hits.toArray();
int retSize = array.size();
if (retSize > 0)
{
for (int i = 0; i < retSize; ++i)
{
QStringList tmpLst;
tmpLst.append(paperName);
tmpLst.append(paperDate);
QJsonObject object3 = array.at(i).toObject();
QJsonObject object4 = object3.value("_source").toObject();
tmpLst.append(object4.value("paper_layout").toString());
tmpLst.append(object4.value("pre_title").toString());
tmpLst.append(object4.value("title").toString());
tmpLst.append(object4.value("sub_title").toString());
tmpLst.append(object4.value("author").toString());
tmpLst.append(QString::asprintf("%.6f", object3.value("_score").toDouble()));
tmpLst.append(object4.value("md5_str").toString());
tmpLst.append(object4.value("md5_url").toString());
mSearchDataLst.append(tmpLst);
}
}
}
}
}
}