Redis实战之文本检索

问题

检索包含特定单词,但不包含其他特定单词的文本。

约定:
检索语句中,+表示为前一个字的同义词,-表示检索结果中不得包含该词。

如:“你+您-可-以搜索”,表示检索包含“你”(“您”)、“搜”、“索”,且不包含“可”、“以”的所有文本。

思路

1、建立反向索引(类似于书本末尾的索引),即从每个被索引的文档中抽取部分单词,并为每个单词创建一个集合set(key=idx:单词),用来记录单词所包括含于的文档。如,idx:ring docA docB docC
2、并集运算,对同义词按组进行并集运算,检索至少包括其中一个的所有文本。如,sunionstore idx:并集ID idx:你 idx:您
3、交集运算,检索同时包括特定单词的文本。如,sinterstore idx:交集ID idx:并集ID idx:搜 idx:索
4、差集运算,从交集运算结果中剔除包含特定单词的记录。如,sdiffstore idex:差集ID idx:交集ID idx:可 idx:以

索引更新

若文本内容可变,则需为文本删除无效索引并添加新索引 —>>> 可使用json编码的列表存储文本中已建立索引的单词,然后使用set命令将列表存储到一个键里。

实现

//---------------------------------------------------------------SearchManager.h

#ifndef _SEARCHMANAGER_H_
#define _SEARCHMANAGER_H_

struct redisContext;

class SearchManager
{
public:

    // @param   strDir                                          [in]    文件搜索目录
    SearchManager(QString &strDir);
    ~SearchManager();

    // @param   strQuery                                        [in]    检索条件 字可带前缀:+表示为前一个字的同义词,-表示检索结果中不得包含该词
    // @param   varPKValue                                      [out]   运算结果集 idx:nID
    // @param   nTTL                                            [in]    运算结果有效时间
    bool parse_and_search(QString &strQuery, QVariant &varPKValue, int nTTL = 30);

private:

    // 连接Redis
    // @param   strHost                                         [in]    Redis服务器IP
    // @param   nPort                                           [in]    Redis服务器Port
    int _initRedisConnect(QString &strHost = QString("127.0.0.1"), int nPort = 6001);

    // 获取目录下所有文件及文件内单词
    // @param   strDir                                          [in]    文件搜索目录
    bool _tokenize(QString &strDir);

    // 为目录下所有文件内单词建立反向索引 索引键值形式为 idx:单词
    // @param   strDir                                          [in]    文件搜索目录
    bool _index_document(QString &strDir);

    // 通用函数 执行交集/并集/差集运算
    // @param   strMethod                                       [in]    交集/并集/差集运算名称
    // @param   vecWord                                         [in]    单词集合
    // @param   nID                                             [out]   运算结果集 idx:nID
    // @param   nTTL                                            [in]    运算结果有效时间
    bool _set_common(QString &strMethod, QVector<QString> &vecWord, qint64 &nID, int nTTL = 30);

    // 执行交集运算
    // @param   vecWord                                         [in]    单词集合
    // @param   nID                                             [out]   运算结果集 idx:nID
    // @param   nTTL                                            [in]    运算结果有效时间
    bool _intersect(QVector<QString> &vecWord, qint64 &nID, int nTTL = 30);

    // 执行并集运算
    // @param   vecWord                                         [in]    单词集合
    // @param   nID                                             [out]   运算结果集 idx:nID
    // @param   nTTL                                            [in]    运算结果有效时间
    bool _union(QVector<QString> &vecWord, qint64 &nID, int nTTL = 30);

    // 执行差集运算
    // @param   vecWord                                         [in]    单词集合
    // @param   nID                                             [out]   运算结果集 idx:nID
    // @param   nTTL                                            [in]    运算结果有效时间
    bool _difference(QVector<QString> &vecWord, qint64 &nID, int nTTL = 30);

    // 对检索条件进行分析 获取需要进行交集/并集/差集运算的单词
    // @param   strQuery                                        [in]    检索条件 字可带前缀:+表示为前一个字的同义词,-表示检索结果中不得包含该词
    // @param   vecAll                                          [out]   运算结果集 idx:nID
    // @param   vecUnWanted                                     [out]   运算结果有效时间
    bool _parse(QString &strQuery, QVector<QVector<QString>> &vecAll, QVector<QString> &vecUnWanted);


private:

    redisContext *m_pRedisContext;                      // Redis连接对象

    QMap<QString, QVector<QString>> m_mapFileName2Word; // key:文本文件名称 value:文本中建立索引的单词
};

#endif
//---------------------------------------------------------------SearchManager.cpp

#include "stdafx.h"
#include "SearchManager.h"
#include "redis-3.0\deps\hiredis\hiredis.h"
#include "redis-3.0\src\Win32_Interop\Win32_APIs.h"

SearchManager::SearchManager(QString &strDir)
{
    //1、连接Redis
    _initRedisConnect();

    //2、建立反向索引
    _index_document(strDir);
}

SearchManager::~SearchManager()
{
}

// 连接Redis
int SearchManager::_initRedisConnect(QString &strHost, int nPort)
{
    m_pRedisContext = redisConnect(strHost.toLocal8Bit().data(), nPort);
    if (NULL == m_pRedisContext || m_pRedisContext->err)
    {
        redisFree(m_pRedisContext);
        m_pRedisContext = NULL;
        return REDIS_ERR;
    }

    return REDIS_OK;
}

// 获取目录下所有文件及文件内单词
bool SearchManager::_tokenize(QString &strDir)
{
    QDir dir(strDir);
    dir.setFilter(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::NoSymLinks);
    dir.setSorting(QDir::Time | QDir::Reversed);

    QStringList listFileName = dir.entryList();
    for (int nIndex = 0; nIndex < listFileName.size(); nIndex++)
    {
        QString strChildPath = QString("%1\\%2").arg(strDir).arg(listFileName.at(nIndex));

        QFileInfo fileInfo(strChildPath);
        if (fileInfo.isDir())
        {   
            // 递归遍历子目录
            _tokenize(strChildPath);
        }
        else
        {
            //遍历文件
            QFile file(strChildPath);
            if (!file.open(QIODevice::ReadOnly))
            {
                return false;
            }

            QString strContent(file.readAll());
            QStringList strListContent = strContent.toLower().split(QRegExp("\\W+"), QString::SkipEmptyParts);
            for (int nIndex = 0; nIndex < strListContent.size(); nIndex++)
            {
                QString strSentence = strListContent.at(nIndex).trimmed();
                for (int nWordIndex = 0; nWordIndex < strSentence.size(); nWordIndex++)
                {
                    QString strWord = strSentence.mid(nWordIndex, 1);
                    if (strWord.isEmpty())
                    {
                        continue;
                    }

                    m_mapFileName2Word[strChildPath].append(strWord);
                }
            }
        }
    }

    return true;
}

// 为目录下所有文件内单词建立反向索引 索引键值形式为 idx:单词
bool SearchManager::_index_document(QString &strDir)
{
    //1、获取目录下所有文件及文件内单词
    _tokenize(strDir);

    //2、为每个单词建立反向索引 记录单词所包括于的文档
    //索引键值形式为 idx:单词
    QMap<QString, QVector<QString>>::Iterator iter = m_mapFileName2Word.begin();
    for (; iter != m_mapFileName2Word.end();iter++)
    {
        QString strFileName = iter.key();
        QVector<QString> vecWord = iter.value();

        for (int nIndex = 0; nIndex < vecWord.size(); nIndex++)
        {
            QString strWord = vecWord.at(nIndex);

            redisReply *pRedisReply = (redisReply *)redisCommand(m_pRedisContext, "SADD idx:%s %s", strWord.toLocal8Bit().data(), strFileName.toLocal8Bit().data());
            if (pRedisReply == NULL)
            {
                return false;
            }

            freeReplyObject(pRedisReply);
        }
    }

    return true;
}

// 通用函数 执行交集/并集/差集运算
bool SearchManager::_set_common(QString &strMethod, QVector<QString> &vecWord, qint64 &nID, int nTTL)
{
    nID = QDateTime::currentDateTime().toMSecsSinceEpoch();

    QString strCommand = QString("%1 idx:%2").arg(strMethod).arg(nID);

    for (int nIndex = 0; nIndex < vecWord.size(); nIndex++)
    {
        QString strWord = vecWord.at(nIndex);

        strCommand = QString("%1 idx:%2").arg(strCommand).arg(strWord);
    }

    redisReply *pRedisReply = (redisReply *)redisCommand(m_pRedisContext, strCommand.toLocal8Bit().data());
    if (pRedisReply == NULL)
    {
        return false;
    }

    pRedisReply = (redisReply *)redisCommand(m_pRedisContext, "EXPIRE idx:%ld %d", nID, nTTL);
    if (pRedisReply == NULL)
    {
        return false;
    }

    return true;
}

// 执行交集运算
bool SearchManager::_intersect(QVector<QString> &vecWord, qint64 &nID, int nTTL)
{
    return _set_common(QString("sinterstore"), vecWord, nID, nTTL);
}

// 执行并集运算
bool SearchManager::_union(QVector<QString> &vecWord, qint64 &nID, int nTTL)
{
    return _set_common(QString("sunionstore"), vecWord, nID, nTTL);
}

// 执行差集运算
bool SearchManager::_difference(QVector<QString> &vecWord, qint64 &nID, int nTTL)
{
    return _set_common(QString("sdiffstore"), vecWord, nID, nTTL);
}

// 对检索条件进行分析 获取需要进行交集/并集/差集运算的单词
bool SearchManager::_parse(QString &strQuery, QVector<QVector<QString>> &vecAll, QVector<QString> &vecUnWanted)
{
    strQuery = strQuery.replace(" ", "");

    QStringList listWord;
    int pos = 0;
    QRegExp rx("[\\+\\-\\s]{1}[^\\+\\-\\s]{1}|[^\\+\\-\\s]{1}");

    while ((pos = rx.indexIn(strQuery, pos)) != -1) 
    {
        listWord << rx.capturedTexts();
        pos += rx.matchedLength();
    }

    // 你+您-可-以搜索;
    QVector<QString> vecSynonym;
    for (int nIndex = 0; nIndex < listWord.size(); nIndex++)
    {
        QString strWord = listWord.at(nIndex);
        QString strPrefix = strWord.left(1);
        if (strPrefix == "+" || strPrefix == "-")
        {
            strWord = strWord.right(strWord.length() - 1);
        }
        else
        {
            strPrefix = "";
        }

        if (strPrefix == "-")
        {
            vecUnWanted.append(strWord);
            continue;
        }
        else if (strPrefix != "+" && vecSynonym.size() > 0)
        {
            vecAll.append(vecSynonym);
            vecSynonym.clear();
        }

        vecSynonym.append(strWord); 
    }

    if (vecSynonym.size() > 0)
    {
        vecAll.append(vecSynonym);
    }

    return true;
}

bool SearchManager::parse_and_search(QString &strQuery, QVariant &varPKValue, int nTTL)
{
    QVector<QVector<QString>> vecAll;
    QVector<QString> vecUnWanted;

    // 对检索条件进行分析 获取需要进行交集/并集/差集运算的单词
    _parse(strQuery, vecAll, vecUnWanted);

    QVector<QString> vecIntersect;
    // 遍历同义词
    for (int nSynonymIndex = 0; nSynonymIndex < vecAll.size(); nSynonymIndex++)
    {
        QVector<QString> vecSynonym = vecAll.at(nSynonymIndex);

        if (vecSynonym.size() > 1)
        {
            qint64 nID;
            _union(vecSynonym, nID, nTTL);
            vecIntersect.append(QString("%1").arg(nID));
        }
        else
        {
            vecIntersect.append(vecSynonym.at(0));
        }
    }

    // 交集
    if (vecIntersect.size() > 1)
    {
        qint64 nID;
        _intersect(vecIntersect, nID, nTTL);

        varPKValue = nID;
    }
    else
    {
        varPKValue = vecIntersect.at(0);
    }

    // 差集
    if (vecUnWanted.size() > 0)
    {
        qint64 nID;
        vecUnWanted.push_front(varPKValue.toString());
        _difference(vecUnWanted, nID, nTTL);

        varPKValue = nID;
    }

    return true;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值