Skip to content

Commit

Permalink
improved search results (#2116)
Browse files Browse the repository at this point in the history
* improved search results

* added mode for debug info

* update match threshold

* update search logic

* remove commented code

* small refactoring

* added context comments
  • Loading branch information
ABSitf committed Feb 1, 2024
1 parent d1ba234 commit 7af2a5e
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 43 deletions.
20 changes: 18 additions & 2 deletions source/MRViewer/MRRibbonMenuSearch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ void RibbonMenuSearch::drawWindow_( const Parameters& params )
{
if ( ImGui::IsKeyPressed( ImGuiKey_Escape ) )
deactivateSearch_();
#ifndef NDEBUG
if ( ImGui::IsKeyPressed( ImGuiKey_F11 ) )
showResultWeight_ = !showResultWeight_;
#endif

const float minSearchSize = cSearchSize * params.scaling;
if ( isSmallUI() )
Expand All @@ -75,7 +79,7 @@ void RibbonMenuSearch::drawWindow_( const Parameters& params )
ImGui::SetNextItemWidth( minSearchSize );
if ( ImGui::InputText( "##SearchLine", searchLine_ ) )
{
searchResult_ = RibbonSchemaHolder::search( searchLine_ );
searchResult_ = RibbonSchemaHolder::search( searchLine_, &searchResultWeight_ );
hightlightedSearchItem_ = -1;
}
if ( !ImGui::IsWindowAppearing() &&
Expand Down Expand Up @@ -125,6 +129,18 @@ void RibbonMenuSearch::drawWindow_( const Parameters& params )
params.btnDrawer.drawButtonItem( *foundItem.item, dbParams );
if ( foundItem.item->item->isActive() != pluginActive )
deactivateSearch_();
#ifndef NDEBUG
if ( showResultWeight_ && !searchLine_.empty() )
{
const auto& weights = searchResultWeight_[i];
ImGui::SameLine();
ImGui::Text( "%s", "(?)" );
if ( ImGui::IsItemHovered() )
ImGui::SetTooltip( "caption = %.3f\ncaption order = %.3f\ntooltip = %.3f\ntooltip order = %.3f",
weights.captionWeight, weights.captionOrderWeight,
weights.tooltipWeight, weights.tooltipOrderWeight );
}
#endif
}
ImGui::PopStyleVar( 1 );
ImGui::PopStyleColor( 3 );
Expand Down Expand Up @@ -164,7 +180,7 @@ void RibbonMenuSearch::drawMenuUI( const Parameters& params )
}
if ( searchInputText_( "##SearchLine", searchLine_, params ) )
{
searchResult_ = RibbonSchemaHolder::search( searchLine_ );
searchResult_ = RibbonSchemaHolder::search( searchLine_, &searchResultWeight_ );
hightlightedSearchItem_ = -1;
}
if ( mainInputFocused_ && !ImGui::IsItemFocused() )
Expand Down
4 changes: 4 additions & 0 deletions source/MRViewer/MRRibbonMenuSearch.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class MRVIEWER_CLASS RibbonMenuSearch

std::string searchLine_;
std::vector<RibbonSchemaHolder::SearchResult> searchResult_;
std::vector<RibbonSchemaHolder::SearchResultWeight> searchResultWeight_;
std::vector<RibbonSchemaHolder::SearchResult> recentItems_;
int hightlightedSearchItem_{ -1 };

Expand All @@ -58,6 +59,9 @@ class MRVIEWER_CLASS RibbonMenuSearch
bool mainInputFocused_ = false;
bool blockSearchBtn_ = false;
bool setMainInputFocus_ = false;
#ifndef NDEBUG
bool showResultWeight_ = false;
#endif
};

}
168 changes: 128 additions & 40 deletions source/MRViewer/MRRibbonSchema.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,51 +32,102 @@ bool RibbonSchemaHolder::addItem( std::shared_ptr<RibbonMenuItem> item )
return true;
}

std::vector<RibbonSchemaHolder::SearchResult> RibbonSchemaHolder::search( const std::string& searchStr )
std::vector<RibbonSchemaHolder::SearchResult> RibbonSchemaHolder::search( const std::string& searchStr,
std::vector<SearchResultWeight>* weights /*= nullptr*/ )
{
std::vector<SearchResult> res;
std::vector<std::pair<SearchResult, SearchResultWeight>> rawResult;

if ( searchStr.empty() )
return res;
return {};
auto words = split( searchStr, " " );
std::erase_if( words, [] ( const auto& str ) { return str.empty(); } );
std::vector<std::pair<float, SearchResult>> resultListForSort;
auto enweight = [] ( const std::vector<std::string>& searchWords, const std::string& testLine )->float
auto calcWeight = [&words] ( const std::string& sourceStr )->Vector2f
{
if ( testLine.empty() )
return 1.0f;
float sumWeight = 0.0f;
for ( const auto& word : searchWords )
if ( sourceStr.empty() )
return { 1.0f, 1.f };

auto sourceWords = split( sourceStr, " " );
std::erase_if( sourceWords, [] ( const auto& str ) { return str.empty(); } );
if ( sourceWords.empty() )
return { 1.0f, 1.f };

const int sourceWordsSize = int( sourceWords.size() );
//std::vector<int> errorArr( words.size() * sourceWordsSize, -1 );
std::vector<bool> busyWord( sourceWordsSize, false );
int sumError = 0;
int searchCharCount = 0;
int posWeight = sourceWordsSize;
for ( int i = 0; i < words.size(); ++i )
{
int minDistance = std::abs( int( word.size() ) - int( testLine.size() ) );
int maxDistance = std::max( int( word.size() ), int( testLine.size() ) );
int distance = calcDamerauLevenshteinDistance( word, testLine, false );
sumWeight += float( distance - minDistance ) / ( maxDistance - minDistance );
searchCharCount += int( words[i].size() );
int minError = int( words[i].size() );
int minErrorIndex = -1;
for ( int j = 0; j < sourceWordsSize; ++j )
{
if ( busyWord[j] )
continue;
int error = calcDamerauLevenshteinDistance( words[i], sourceWords[j], false );
if ( i == words.size() - 1 )
error -= std::max( int( sourceWords[j].size() ) - int( words[i].size() ), 0 );
if ( error < minError )
{
minError = error;
minErrorIndex = j;
}
}
if ( minErrorIndex != -1 )
{
busyWord[minErrorIndex] = true;
posWeight += minErrorIndex;
}
sumError += minError;
}
return std::clamp( sumWeight / float( searchWords.size() ), 0.0f, 1.0f );
return { std::clamp( float( sumError ) / searchCharCount, 0.0f, 1.0f ), float( posWeight ) / sourceWordsSize / words.size()};
};

const float maxWeight = 0.25f;
bool exactMatch = false;
// check item (calc difference from search item) and add item to raw results if difference less than threshold
auto checkItem = [&] ( const MenuItemInfo& item, int t )
{
const auto& caption = item.caption.empty() ? item.item->name() : item.caption;
const auto& tooltip = item.tooltip;
auto captionRes = enweight( words, caption );
auto tooltipRes = enweight( words, tooltip );
if ( captionRes > 0.1f && tooltipRes > 0.1f )
std::pair<SearchResult, SearchResultWeight> itemRes;
itemRes.first.tabIndex = t;
itemRes.first.item = &item;
const auto posCE = findSubstringCaseInsensitive( caption, searchStr );
if ( posCE != std::string::npos )
{
if ( !exactMatch )
{
rawResult.clear();
exactMatch = true;
}
itemRes.second.captionWeight = 0.f;
itemRes.second.captionOrderWeight = float( posCE ) / caption.size();
rawResult.push_back( itemRes );
return;
for ( const auto& word : words )
}
else if ( exactMatch )
{
auto posC = findSubstringCaseInsensitive( caption, word );
auto posT = findSubstringCaseInsensitive( tooltip, word );
if ( posC == std::string::npos )
captionRes += 10.0f;
else
captionRes += 0.5f * ( float( posC ) / caption.size() );
if ( posT == std::string::npos )
tooltipRes += 10.0f;
else
tooltipRes += 0.5f * ( float( posT ) / tooltip.size() );
const auto posTE = findSubstringCaseInsensitive( tooltip, searchStr );
if ( posTE == std::string::npos )
return;
itemRes.second.tooltipWeight = 0.f;
itemRes.second.tooltipOrderWeight = float( posTE ) / tooltip.size();
rawResult.push_back( itemRes );
return;
}
resultListForSort.push_back( { captionRes + 0.5f * tooltipRes, SearchResult{t,&item} } );

Vector2f weightEP = calcWeight( caption );
itemRes.second.captionWeight = weightEP.x;
itemRes.second.captionOrderWeight = weightEP.y;
weightEP = calcWeight( tooltip );
itemRes.second.tooltipWeight = weightEP.x;
itemRes.second.tooltipOrderWeight = weightEP.y;
if ( itemRes.second.captionWeight > maxWeight && itemRes.second.tooltipWeight > maxWeight )
return;
rawResult.push_back( itemRes );
};
const auto& schema = RibbonSchemaHolder::schema();
auto lookUpMenuItemList = [&] ( const MenuItemsList& list, int t )
Expand Down Expand Up @@ -121,25 +172,62 @@ std::vector<RibbonSchemaHolder::SearchResult> RibbonSchemaHolder::search( const
lookUpMenuItemList( schema.headerQuickAccessList, -1 );
lookUpMenuItemList( schema.sceneButtonsList, -1 );

std::sort( resultListForSort.begin(), resultListForSort.end(), [] ( const auto& a, const auto& b )
// clear duplicated results
std::sort( rawResult.begin(), rawResult.end(), [] ( const auto& a, const auto& b )
{
return intptr_t( a.second.item ) < intptr_t( b.second.item );
return intptr_t( a.first.item ) < intptr_t( b.first.item );
} );
resultListForSort.erase(
std::unique( resultListForSort.begin(), resultListForSort.end(),
rawResult.erase(
std::unique( rawResult.begin(), rawResult.end(),
[] ( const auto& a, const auto& b )
{
return a.second.item == b.second.item;
return a.first.item == b.first.item;
} ),
resultListForSort.end() );
rawResult.end() );

std::sort( resultListForSort.begin(), resultListForSort.end(), [] ( const auto& a, const auto& b )
std::sort( rawResult.begin(), rawResult.end(), [] ( const auto& a, const auto& b )
{
return a.first < b.first;
if ( a.second.captionWeight < b.second.captionWeight )
return true;
else if ( a.second.captionWeight > b.second.captionWeight )
return false;
else if ( a.second.captionOrderWeight < b.second.captionOrderWeight )
return true;
else if ( a.second.captionOrderWeight > b.second.captionOrderWeight )
return false;
else if ( a.second.tooltipWeight < b.second.tooltipWeight )
return true;
else if ( a.second.tooltipWeight > b.second.tooltipWeight )
return false;
else if ( a.second.tooltipOrderWeight < b.second.tooltipOrderWeight )
return true;
else
return false;
} );
res.reserve( resultListForSort.size() );
for ( const auto& sortedRes : resultListForSort )
res.push_back( sortedRes.second );

// filter results with error threshold as 3x minimum caption error
if ( !rawResult.empty() && rawResult[0].second.captionWeight < maxWeight / 3.f )
{
const float maxWeightNew = rawResult[0].second.captionWeight * 3.f;
if ( rawResult.back().second.captionWeight > maxWeightNew )
{
auto tailIt = std::find_if( rawResult.begin(), rawResult.end(), [&] ( const auto& a )
{
return a.second.captionWeight > maxWeightNew && a.second.tooltipWeight > maxWeightNew;
} );
rawResult.erase( tailIt, rawResult.end() );
}
}

std::vector<SearchResult> res( rawResult.size() );
if ( weights )
*weights = std::vector<SearchResultWeight>( rawResult.size() );
for ( int i = 0; i < rawResult.size(); ++i )
{
res[i] = rawResult[i].first;
if ( weights )
( *weights )[i] = rawResult[i].second;
}

return res;
}
Expand Down
10 changes: 9 additions & 1 deletion source/MRViewer/MRRibbonSchema.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,15 @@ class MRVIEWER_CLASS RibbonSchemaHolder
int tabIndex{ -1 }; // -1 is default value if item has no tab
const MenuItemInfo* item{ nullptr }; // item info to show correct caption
};
MRVIEWER_API static std::vector<SearchResult> search( const std::string& searchStr );
// ancillary struct to hold information for search result order
struct SearchResultWeight
{
float captionWeight{ 1.f };
float captionOrderWeight{ 1.f };
float tooltipWeight{ 1.f };
float tooltipOrderWeight{ 1.f };
};
MRVIEWER_API static std::vector<SearchResult> search( const std::string& searchStr, std::vector<SearchResultWeight>* weights = nullptr );
private:
RibbonSchemaHolder() = default;
};
Expand Down

0 comments on commit 7af2a5e

Please sign in to comment.