Qt 数字报阅读器 图文版 改用 Elasticsearch 检索

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);
                        }
                    }
                }
            }
        }
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值