data: calculate best scores for autocompletion matches by moving indices

This commit is contained in:
Eberhard Graether
2016-10-10 16:51:40 +02:00
parent 95859d9605
commit 072cccf852
2 changed files with 282 additions and 157 deletions
+269 -156
View File
@@ -2,6 +2,7 @@
#include <algorithm>
#include <ctype.h>
#include <iterator>
#include "utility/utility.h"
#include "utility/utilityString.h"
@@ -107,139 +108,33 @@ void SearchIndex::clear()
std::vector<SearchResult> SearchIndex::search(const std::string& query, size_t maxResultCount) const
{
std::string lowerCaseQuery = utility::toLowerCase(query);
// find paths containing query
Path startPath;
startPath.node = m_root;
std::vector<Path> paths;
search(startPath, lowerCaseQuery, &paths);
searchRecursive(startPath, utility::toLowerCase(query), &paths);
std::set<char> noLetters;
noLetters.insert(' ');
noLetters.insert('.');
noLetters.insert(',');
noLetters.insert('_');
noLetters.insert(':');
noLetters.insert('<');
noLetters.insert('>');
// create scored search results
std::multiset<SearchResult> searchResults = createScoredResults(paths, maxResultCount * 3);
// scoring paths
std::multiset<std::pair<int, Path>, bool(*)(const std::pair<int, Path>&, const std::pair<int, Path>&)> scoredPaths(
[](const std::pair<int, Path>& a, const std::pair<int, Path>& b)
{
return a.first > b.first;
}
);
for (size_t i = 0; i < paths.size(); i++)
// find best scores
std::map<std::string, SearchResult> scoresCache;
std::multiset<SearchResult> bestResults;
for (const SearchResult& result : searchResults)
{
const std::vector<size_t>& currentIndices = paths[i].indices;
const std::string& currentText = paths[i].text;
const int unmatchedLetterBonus = -1;
const int consecutiveLetterBonus = 5;
const int camelCaseBonus = 5;
const int noLetterBonus = 3;
const int delayedStartBonus = -3;
const int minDelayedStartBonus = -15;
int unmatchedLetterScore = 0;
int consecutiveLetterScore = 0;
int camelCaseScore = 0;
int noLetterScore = 0;
for (size_t j = 0; j < currentIndices.size(); j++)
{
// unmatched and consecutive
if (j > 0)
{
unmatchedLetterScore += (currentIndices[j] - currentIndices[j-1] - 1) * unmatchedLetterBonus;
consecutiveLetterScore += (currentIndices[j] - currentIndices[j-1] == 1 ? consecutiveLetterBonus : 0);
}
size_t index = currentIndices[j];
// camel case
if (isupper(currentText[index]))
{
bool prevIsLower = (index > 0 && islower(currentText[index-1]));
bool nextIsLower = (index + 1 == currentText.size() || islower(currentText[index+1]));
if (prevIsLower && nextIsLower)
{
camelCaseScore += camelCaseBonus;
}
}
// after no letter
bool prevIsNoLetter = (index > 0 && noLetters.find(currentText[index-1]) != noLetters.end());
if (prevIsNoLetter)
{
noLetterScore += noLetterBonus;
}
}
int leadingStartScore = std::max(int(currentIndices[0]) * delayedStartBonus, minDelayedStartBonus);
int score =
unmatchedLetterScore +
consecutiveLetterScore +
camelCaseScore +
noLetterScore +
leadingStartScore;
scoredPaths.insert(std::make_pair(score, paths[i]));
bestResults.insert(bestScoredResult(result, &scoresCache));
}
// preparing results
std::vector<SearchResult> searchResults;
for (const std::pair<int, Path> currentResult : scoredPaths)
// narrow down to max result count
auto it = bestResults.end();
if (maxResultCount && bestResults.size() > maxResultCount)
{
if (maxResultCount > 0 && searchResults.size() >= maxResultCount)
{
break;
}
int currentScore = currentResult.first;
std::vector<Path> currentPaths;
currentPaths.push_back(currentResult.second);
while (currentPaths.size() > 0)
{
std::vector<Path> nextPaths;
for (size_t j = 0; j < currentPaths.size(); j++)
{
Path& currentPath = currentPaths[j];
if (currentPath.node->elementIds.size() > 0 && (maxResultCount == 0 || searchResults.size() < maxResultCount))
{
SearchResult result;
result.elementIds = currentPath.node->elementIds;
result.indices = currentPath.indices;
result.text = currentPath.text;
result.score = currentScore;
searchResults.push_back(result);
}
for (size_t k = 0; k < currentPath.node->edges.size(); k++)
{
Path nextPath;
nextPath.indices = currentPath.indices;
nextPath.node = currentPath.node->edges[k]->target;
nextPath.text = currentPath.text + currentPath.node->edges[k]->s;
nextPaths.push_back(nextPath);
}
}
currentPaths = nextPaths;
if (!(maxResultCount == 0 || searchResults.size() < maxResultCount))
{
break;
}
}
it = bestResults.begin();
std::advance(it, maxResultCount);
}
return searchResults;
return std::vector<SearchResult>(bestResults.begin(), it);
}
void SearchIndex::populateEdgeGate(Edge* e)
@@ -257,53 +152,271 @@ void SearchIndex::populateEdgeGate(Edge* e)
}
}
void SearchIndex::search(const Path& path, const std::string& remainingQuery, std::vector<SearchIndex::Path>* results) const
void SearchIndex::searchRecursive(
const Path& path, const std::string& remainingQuery, std::vector<SearchIndex::Path>* results) const
{
if (remainingQuery.size() == 0)
{
results->push_back(path);
return;
}
else
for (const Edge* currentEdge : path.node->edges)
{
for (size_t i = 0; i < path.node->edges.size(); i++)
// test if s passes the edge's gate.
bool passesGate = true;
for (const char& c : remainingQuery)
{
const Edge* currentEdge = path.node->edges[i];
// test if s passes the edge's gate.
bool passesGate = true;
for (size_t j = 0; j < remainingQuery.size(); j++)
if (currentEdge->gate.find(c) == currentEdge->gate.end())
{
if (currentEdge->gate.find(tolower(remainingQuery[j])) == currentEdge->gate.end())
passesGate = false;
break;
}
}
if (passesGate)
{
// consume characters for edge
const std::string& edgeString = currentEdge->s;
std::vector<size_t> indices = path.indices;
size_t j = 0;
for (size_t i = 0; i < edgeString.size() && j < remainingQuery.size(); i++)
{
if (tolower(edgeString[i]) == remainingQuery[j])
{
passesGate = false;
break;
indices.push_back(path.text.size() + i);
j++;
}
}
if (passesGate)
{
// consume characters for edge
const std::string& edgeString = currentEdge->s;
Path currentPath;
currentPath.node = currentEdge->target;
currentPath.indices = indices;
currentPath.text = path.text + edgeString;
std::vector<size_t> currentFoundIds = path.indices;
std::string currentRemainingQuery = remainingQuery;
for (size_t j = 0; j < edgeString.size() && currentRemainingQuery.size() > 0; j++)
{
if (currentRemainingQuery[0] == tolower(edgeString[j]))
{
currentFoundIds.push_back(path.text.size() + j);
currentRemainingQuery = currentRemainingQuery.substr(1);
}
}
Path currentPath;
currentPath.node = currentEdge->target;
currentPath.indices = currentFoundIds;
currentPath.text = path.text + edgeString;
search(currentPath, currentRemainingQuery, results);
}
searchRecursive(currentPath, remainingQuery.substr(j), results);
}
}
}
std::multiset<SearchResult> SearchIndex::createScoredResults(const std::vector<Path>& paths, size_t maxResultCount) const
{
// score and order initial paths
std::multiset<std::pair<int, Path>, bool(*)(const std::pair<int, Path>&, const std::pair<int, Path>&)> scoredPaths(
[](const std::pair<int, Path>& a, const std::pair<int, Path>& b)
{
return a.first > b.first;
}
);
for (const Path& path : paths)
{
scoredPaths.insert(std::make_pair(score(path.text, path.indices), path));
}
// score paths and subpaths
std::multiset<SearchResult> searchResults;
for (const std::pair<int, Path>& p : scoredPaths)
{
std::vector<Path> currentPaths;
currentPaths.push_back(p.second);
while (currentPaths.size())
{
std::vector<Path> nextPaths;
for (const Path& path : currentPaths)
{
if (path.node->elementIds.size())
{
SearchResult result;
result.text = path.text;
result.elementIds = path.node->elementIds;
result.indices = path.indices;
result.score = score(path.text, path.indices);
searchResults.insert(result);
if (maxResultCount && searchResults.size() >= maxResultCount)
{
return searchResults;
}
}
for (const Edge* edge : path.node->edges)
{
Path nextPath;
nextPath.indices = path.indices;
nextPath.node = edge->target;
nextPath.text = path.text + edge->s;
nextPaths.push_back(nextPath);
}
}
currentPaths = nextPaths;
}
}
return searchResults;
}
SearchResult SearchIndex::bestScoredResult(SearchResult result, std::map<std::string, SearchResult>* scoresCache) const
{
if (result.text.size() > 100)
{
return result;
}
const std::vector<size_t>& indices = result.indices;
std::map<std::string, SearchResult>::const_iterator it = scoresCache->find(result.text.substr(0, indices.back() + 1));
if (it != scoresCache->end())
{
result.score = it->second.score;
result.indices = it->second.indices;
}
int oldScore = result.score;
bool consecutive = (indices.size() == 1);
for (size_t i = 0; i < indices.size() - 1; i++)
{
if (indices[i + 1] - indices[i] != 1)
{
consecutive = false;
break;
}
}
if (!consecutive)
{
bestScoredResultRecursive(utility::toLowerCase(result.text), indices, indices.size() - 1, scoresCache, &result);
}
if (result.score != oldScore || it == scoresCache->end())
{
scoresCache->emplace(result.text.substr(0, result.indices.back() + 1), result);
}
return result;
}
void SearchIndex::bestScoredResultRecursive(
const std::string& lowerText, const std::vector<size_t>& indices, size_t indicesPos,
std::map<std::string, SearchResult>* scoresCache, SearchResult* result) const
{
size_t oldTextPos = indices[indicesPos];
size_t nextTextPos = (indicesPos + 1 == indices.size() ? result->text.size() : indices[indicesPos + 1]);
for (size_t i = oldTextPos + 1; i < nextTextPos; i++)
{
if (lowerText[i] == lowerText[oldTextPos])
{
std::vector<size_t> newIndices = indices;
newIndices[indicesPos] = i;
int newScore = score(result->text, newIndices);
if (newScore > result->score)
{
result->score = newScore;
result->indices = newIndices;
}
bestScoredResultRecursive(lowerText, newIndices, indicesPos, scoresCache, result);
break;
}
}
if (indicesPos + 1 == indices.size())
{
std::map<std::string, SearchResult>::const_iterator it = scoresCache->find(result->text.substr(0, indices.back() + 1));
if (it != scoresCache->end())
{
result->score = it->second.score;
result->indices = it->second.indices;
return;
}
}
if (indicesPos > 0)
{
size_t newIndicesPos = indicesPos - 1;
while (newIndicesPos > 0 && indices[newIndicesPos + 1] - indices[newIndicesPos] == 1)
{
newIndicesPos--;
}
if (indices[newIndicesPos + 1] - indices[newIndicesPos] > 1)
{
bestScoredResultRecursive(lowerText, indices, newIndicesPos, scoresCache, result);
}
}
}
int SearchIndex::score(const std::string& text, const std::vector<size_t>& indices) const
{
const int unmatchedLetterBonus = -1;
const int consecutiveLetterBonus = 5;
const int camelCaseBonus = 4;
const int noLetterBonus = 3;
const int delayedStartBonus = -1;
const int minDelayedStartBonus = -20;
int unmatchedLetterScore = 0;
int consecutiveLetterScore = 0;
int camelCaseScore = 0;
int noLetterScore = 0;
static std::set<char> noLetters;
if (!noLetters.size())
{
noLetters.insert(' ');
noLetters.insert('.');
noLetters.insert(',');
noLetters.insert('_');
noLetters.insert(':');
noLetters.insert('<');
noLetters.insert('>');
}
for (size_t i = 0; i < indices.size(); i++)
{
// unmatched and consecutive
if (i > 0)
{
unmatchedLetterScore += (indices[i] - indices[i - 1] - 1) * unmatchedLetterBonus;
consecutiveLetterScore += (indices[i] - indices[i - 1] == 1 ? consecutiveLetterBonus : 0);
}
size_t index = indices[i];
// camel case
if (isupper(text[index]))
{
bool prevIsLower = (index > 0 && islower(text[index - 1]));
bool nextIsLower = (index + 1 == text.size() || islower(text[index + 1]));
if (prevIsLower || nextIsLower)
{
camelCaseScore += camelCaseBonus;
}
}
// after no letter
bool prevIsNoLetter = (index > 0 && noLetters.find(text[index - 1]) != noLetters.end());
if (prevIsNoLetter)
{
noLetterScore += noLetterBonus;
}
}
int leadingStartScore = std::max(int(indices[0]) * delayedStartBonus, minDelayedStartBonus);
int score =
unmatchedLetterScore +
consecutiveLetterScore +
camelCaseScore +
noLetterScore +
leadingStartScore;
return score;
}
+13 -1
View File
@@ -17,6 +17,11 @@ struct SearchResult
std::set<Id> elementIds;
std::vector<size_t> indices;
int score;
bool operator<(const SearchResult& other) const
{
return score > other.score;
}
};
class SearchIndex
@@ -57,7 +62,14 @@ private:
};
void populateEdgeGate(Edge* e);
void search(const Path& path, const std::string& remainingQuery, std::vector<SearchIndex::Path>* results) const;
void searchRecursive(const Path& path, const std::string& remainingQuery, std::vector<SearchIndex::Path>* results) const;
std::multiset<SearchResult> createScoredResults(const std::vector<Path>& paths, size_t maxResultCount) const;
SearchResult bestScoredResult(SearchResult result, std::map<std::string, SearchResult>* scoresCache) const;
void bestScoredResultRecursive(
const std::string& lowerText, const std::vector<size_t>& indices, size_t indicesPos,
std::map<std::string, SearchResult>* scoresCache, SearchResult* result) const;
int score(const std::string& text, const std::vector<size_t>& indices) const;
std::vector<std::shared_ptr<Node>> m_nodes;
std::vector<std::shared_ptr<Edge>> m_edges;