问题
检索包含特定单词,但不包含其他特定单词的文本。
约定:
检索语句中,+表示为前一个字的同义词,-表示检索结果中不得包含该词。如:“你+您-可-以搜索”,表示检索包含“你”(“您”)、“搜”、“索”,且不包含“可”、“以”的所有文本。
思路
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;
}