Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use regex to search the handbook for all words separately #98

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 36 additions & 69 deletions Systems/Handbook/Gui/GuiDialogHandbook.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Vintagestory.API.Client;
using Vintagestory.API.Common;
using Vintagestory.API.Config;
Expand Down Expand Up @@ -451,87 +452,53 @@ protected void FilterItemsBySearchText(string text)
FilterItems();
}


private int CountMatches(string text, Regex regex)
{
return regex.Matches(text).Count;
}

public void FilterItems()
{
string text = currentSearchText?.ToLowerInvariant();
string[] texts;
bool logicalAnd = false; // true if "and" is present; false if "or" or no logical operator is present
if (text == null)
{
texts = new string[0];
}
else
{
if (text.Contains(" or ", StringComparison.Ordinal))
{
texts = text.Split(new string[] { " or " }, StringSplitOptions.RemoveEmptyEntries).OrderBy(str => str.Length).ToArray();
}
else if (text.Contains(" and ", StringComparison.Ordinal))
{
texts = text.Split(new string[] { " and " }, StringSplitOptions.RemoveEmptyEntries).OrderBy(str => str.Length).ToArray();
logicalAnd = texts.Length > 1;
}
else
{
texts = new string[] { text };
}
int countEmpty = 0;
for (int i = 0; i < texts.Length; i++)
{
texts[i] = texts[i].ToSearchFriendly().Trim(); // Only remove diacritical marks etc after splitting on " or ", helps with languages such as Icelandic where "ör" is a word (a type of bow)
if (texts[i].Length == 0) countEmpty++;
}
if (countEmpty > 0)
{
string[] newTexts = new string[texts.Length - countEmpty];
int j = 0;
for (int i = 0; i < texts.Length; i++)
{
if (texts[i].Length == 0) continue;
newTexts[j++] = texts[i];
}
texts = newTexts;
logicalAnd = logicalAnd && texts.Length > 1;
}
}

List<WeightedHandbookPage> foundPages = new List<WeightedHandbookPage>();
shownHandbookPages.Clear();

if (!loadingPagesAsync)
{
for (int i = 0; i < allHandbookPages.Count; i++)
{
GuiHandbookPage page = allHandbookPages[i];
if (currentCatgoryCode != null && page.CategoryCode != currentCatgoryCode) continue;
if (page.IsDuplicate) continue;
string searchText = currentSearchText ?? "";
string[] searchWords = Regex.Split(searchText, "\\s+", RegexOptions.Multiline);
var pattern = $"({String.Join("|", searchWords.Where(w => w != "").Select(w => $"{Regex.Escape(w.ToSearchFriendly().Trim())}"))})";
var regex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.Multiline);

float weight = 1;
bool matched = logicalAnd; // Normally (for no logical operator or logical operator OR) no match unless any found; if it's logical AND then we have no match if any in texts are not found (and texts length cannot be 0)
var weightedPages = new List<WeightedHandbookPage>();
allHandbookPages.ForEach(page =>
{
if (currentCatgoryCode != null && page.CategoryCode != currentCatgoryCode || page.IsDuplicate) return;

for (int j = 0; j < texts.Length; j++)
{
weight = page.GetTextMatchWeight(texts[j]);
if (weight > 0)
{
if (!logicalAnd) { matched = true; break; }
}
else
var pageText = page.GetPageText();
var titleMathces = CountMatches(pageText.Title ?? "", regex);
var textMatches = CountMatches(pageText.Text ?? "", regex);
if (titleMathces > 0 || textMatches > 0)
weightedPages.Add(new WeightedHandbookPage
{
if (logicalAnd) { matched = false; break; };
}
}
if (!matched && texts.Length > 0) continue;

foundPages.Add(new WeightedHandbookPage() { Page = page, Weight = weight });
}

foreach (var val in foundPages.OrderByDescending(wpage => wpage.Weight))
Page = page,
TitleMatches = titleMathces,
TitleLength = pageText.Title?.Length ?? 0,
TextMatches = textMatches,
});
});
if (searchText.Length > 0)
{
shownHandbookPages.Add(val.Page);
weightedPages.Sort((a, b) =>
{
var titleSort = b.TitleMatches - a.TitleMatches;
if (titleSort != 0) return titleSort;
var textSort = b.TextMatches - a.TextMatches;
if (textSort != 0) return textSort;
// Prefer shorter matches, efffectively prioritizing more "exact" matches
// e. g. "Iron Plate" over "Plate Armor (Iron)" when searching "Iron Plate"
return a.TitleLength - b.TitleLength;
});
}
weightedPages.ForEach(page => shownHandbookPages.Add(page.Page));
}

GuiElementFlatList stacklist = overviewGui.GetFlatList("stacklist");
Expand Down
13 changes: 5 additions & 8 deletions Systems/Handbook/Gui/GuiHandbookCommandPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,12 @@ protected virtual RichTextComponentBase[] GetPageText(ICoreClientAPI capi, ItemS
return VtmlUtil.Richtextify(capi, "<font size=\"24\"><strong>" + Command.CallSyntax + "</strong></font>\n\n" + TextCacheAll, CairoFont.WhiteSmallText());
}

public override float GetTextMatchWeight(string searchText)
public override PageText GetPageText()
{
string title = TextCacheTitle;
if (title.Equals(searchText, StringComparison.InvariantCultureIgnoreCase)) return searchWeightOffset + 3;
if (title.StartsWith(searchText + " ", StringComparison.InvariantCultureIgnoreCase)) return searchWeightOffset + 2.75f + Math.Max(0, 15 - title.Length) / 100f;
if (title.StartsWith(searchText, StringComparison.InvariantCultureIgnoreCase)) return searchWeightOffset + 2.5f + Math.Max(0, 15 - title.Length) / 100f;
if (title.CaseInsensitiveContains(searchText)) return searchWeightOffset + 2;
if (TextCacheAll.CaseInsensitiveContains(searchText)) return searchWeightOffset + 1;
return 0;
return new PageText {
Title = TextCacheTitle,
Text = TextCacheAll,
};
}
}

Expand Down
14 changes: 6 additions & 8 deletions Systems/Handbook/Gui/GuiHandbookItemStackPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,13 @@ protected virtual RichTextComponentBase[] GetPageText(ICoreClientAPI capi, ItemS
return Stack.Collectible.GetBehavior<CollectibleBehaviorHandbookTextAndExtraInfo>()?.GetHandbookInfo(dummySlot, capi, allStacks, openDetailPageFor) ?? new RichTextComponentBase[0];
}

public override float GetTextMatchWeight(string searchText)
public override PageText GetPageText()
{
string title = TextCacheTitle;
if (title.Equals(searchText, StringComparison.InvariantCultureIgnoreCase)) return searchWeightOffset + 3;
if (title.StartsWith(searchText + " ", StringComparison.InvariantCultureIgnoreCase)) return searchWeightOffset + 2.75f + Math.Max(0, 15 - title.Length) / 100f;
if (title.StartsWith(searchText, StringComparison.InvariantCultureIgnoreCase)) return searchWeightOffset + 2.5f + Math.Max(0, 15 - title.Length) / 100f;
if (title.CaseInsensitiveContains(searchText)) return searchWeightOffset + 2;
if (TextCacheAll.CaseInsensitiveContains(searchText)) return searchWeightOffset + 1;
return 0;
return new PageText
{
Title = TextCacheTitle,
Text = TextCacheAll,
};
}
}

Expand Down
13 changes: 10 additions & 3 deletions Systems/Handbook/Gui/GuiHandbookPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ namespace Vintagestory.GameContent
{
public struct WeightedHandbookPage
{
public float Weight;
public int TitleMatches;
public int TextMatches;
public int TitleLength;
public GuiHandbookPage Page;
}

public struct PageText
{
public string Title;
public string Text;
}

public abstract class GuiHandbookPage : IFlatListItem
{
public int PageNumber;
Expand All @@ -25,8 +33,7 @@ public abstract class GuiHandbookPage : IFlatListItem
public abstract void RenderListEntryTo(ICoreClientAPI capi, float dt, double x, double y, double cellWdith, double cellHeight);
public abstract void Dispose();
public bool Visible { get; set; } = true;

public abstract float GetTextMatchWeight(string text);
public abstract PageText GetPageText();
public abstract bool IsDuplicate { get; }

public abstract void ComposePage(GuiComposer detailViewGui, ElementBounds textBounds, ItemStack[] allstacks, ActionConsumable<string> openDetailPageFor);
Expand Down
13 changes: 6 additions & 7 deletions Systems/Handbook/Gui/GuiHandbookTextPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,13 @@ public void Recompose(ICoreClientAPI capi)
Texture = new TextTextureUtil(capi).GenTextTexture(Lang.Get(Title), CairoFont.WhiteSmallText());
}

public override float GetTextMatchWeight(string searchText)
public override PageText GetPageText()
{
if (titleCached.Equals(searchText, StringComparison.InvariantCultureIgnoreCase)) return 4;
if (titleCached.StartsWith(searchText + " ", StringComparison.InvariantCultureIgnoreCase)) return 3.5f;
if (titleCached.StartsWith(searchText, StringComparison.InvariantCultureIgnoreCase)) return 3f;
if (titleCached.CaseInsensitiveContains(searchText)) return 2.75f;
if (Text.CaseInsensitiveContains(searchText)) return 1.25f;
return 0;
return new PageText
{
Title = titleCached,
Text = Text,
};
}

public override void RenderListEntryTo(ICoreClientAPI capi, float dt, double x, double y, double cellWidth, double cellHeight)
Expand Down
10 changes: 0 additions & 10 deletions Systems/Handbook/Tutorial/GuiHandbookTutorialPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,16 +149,6 @@ public override void ComposePage(GuiComposer detailViewGui, ElementBounds textBo
detailViewGui.AddRichtext(comps, textBounds, "richtext");
}

public override float GetTextMatchWeight(string text)
{
/*string title = TextCacheTitle;
if (title.Equals(searchText, StringComparison.InvariantCultureIgnoreCase)) return 3;
if (title.StartsWith(searchText, StringComparison.InvariantCultureIgnoreCase)) return 2.5f;
if (title.CaseInsensitiveContains(searchText)) return 2;
if (TextCacheAll.CaseInsensitiveContains(searchText)) return 1;*/
return 0;
}

Comment on lines -152 to -161
Copy link
Author

@uramer uramer Jan 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not add the same override as I was not sure of its intended purpose, but obviously it's not difficult to bring back.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetTextMatchWeight is intended to return weighted results based on things like titles being higher in the list than descriptions. Does your code adequately replace this weighting to make sure different things have different weights to them in the results?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the purpose of GetTextMatchWeight, I was specifically referring to the commented out code.

Yes, it sorts all pages by title matches first, then breaks ties by page content matches, and then by title length to prefer more exact matches.


public override void Dispose()
{
Expand Down