Improve cross-peer result ranking and add persistent language preference

- Normalize Solr scores per peer batch before inserting into the shared
  priority queue so results from different peers compete on a [0,1] scale
  rather than on their raw absolute scores, which vary widely across indexes
- Add a Language dropdown to the search form with 11 common languages
- Persist the selected language in a yacy-language cookie (1 year) so users
  no longer need to manually append /language/en to every query
- Fall back to cookie preference when no explicit lr param is submitted;
  selecting "any" clears the cookie and removes the filter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Leo TreasureandClaude Sonnet 4.6 committed 2026-04-25 13:27:17 +08:00
1 parent c33a2b8490
commit 11425263d4
3 files changed
+74 -11

No files matched your search

+18
View File
@@ -127,6 +127,24 @@ Use the RSS search result format to add static searches to your RSS reader, if y
</a>
#(/resortEnabled)#
</div>
</div>
<div style="font-size:11px;margin-top:3px;">
<label for="lang-select" style="color:#777;margin-right:3px;">Language:</label>
<select name="lr" id="lang-select" style="font-size:11px;padding:1px 2px;height:20px;">
<option value="">any</option>
<option value="lang_en">English</option>
<option value="lang_de">Deutsch</option>
<option value="lang_fr">Français</option>
<option value="lang_es">Español</option>
<option value="lang_it">Italiano</option>
<option value="lang_pt">Português</option>
<option value="lang_ru">Русский</option>
<option value="lang_nl">Nederlands</option>
<option value="lang_zh">中文</option>
<option value="lang_ja">日本語</option>
<option value="lang_ar">العربية</option>
</select>
<script>document.getElementById('lang-select').value='#[languageSel]#';</script>
</div>
#(authSearch)#::
<input type="hidden" name="auth" id="auth" value=""/>
+45 -10
View File
@@ -33,6 +33,7 @@ import static net.yacy.repository.BlacklistHelper.addBlacklistEntry;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import javax.servlet.http.Cookie;
import java.net.IDN;
import java.net.MalformedURLException;
import java.net.URLEncoder;
@@ -179,6 +180,22 @@ public class yacysearch {
boolean global = post == null || (!post.get("resource-switch", post.get("resource", "global")).equals("local") && p2pmode);
final boolean stealthmode = p2pmode && !global;
// Read persisted language preference from cookie so it is available in both the
// empty-search (early return) and full-search paths below.
String cookieLanguage = null;
final Cookie[] requestCookies = header.getCookies();
if (requestCookies != null) {
for (final Cookie c : requestCookies) {
if ("yacy-language".equals(c.getName())) {
final String v = c.getValue();
if (v != null && v.startsWith("lang_") && ISO639.exists(v.substring(5))) {
cookieLanguage = v.substring(5);
}
break;
}
}
}
if ( post == null || indexSegment == null || env == null || !searchAllowed ) {
if (indexSegment == null) ConcurrentLog.info("yacysearch", "indexSegment == null");
// we create empty entries for template strings
@@ -217,6 +234,7 @@ public class yacysearch {
prop.put("rss_queryenc", "");
prop.put("meanCount", 5);
prop.put("eventID",""); // mandatory parameter for yacysearchtrailer/yacysearchitem includes
prop.put("languageSel", cookieLanguage != null ? "lang_" + cookieLanguage : "");
return prop;
}
@@ -628,25 +646,42 @@ public class yacysearch {
if (urlmask == null || urlmask.isEmpty()) urlmask = ".*"; //if no urlmask was given
// read the language from the language-restrict option 'lr'
// if no one is given, use the user agent or the system language as default
String language = (post == null) ? null : post.get("lr");
if (language != null && language.startsWith("lang_") ) {
language = language.substring(5);
// read the language from the language-restrict option 'lr',
// then from a persisted cookie preference, then from Accept-Language header
final boolean languageFromParam = post.containsKey("lr"); // lr was explicitly submitted in this request
final String lrParam = languageFromParam ? post.get("lr") : null;
String language;
if (lrParam != null && lrParam.startsWith("lang_") && ISO639.exists(lrParam.substring(5))) {
language = lrParam.substring(5);
if (modifier.language == null) modifier.language = language;
}
if (language == null || !ISO639.exists(language) ) {
// find out language of the user by reading of the user-agent string
} else if (!languageFromParam && cookieLanguage != null) {
// no lr param submitted this request — use saved cookie preference
language = cookieLanguage;
if (modifier.language == null) modifier.language = language;
} else {
// lr was submitted as empty/invalid (clear preference) or no cookie exists
String agent = header.get(HeaderFramework.ACCEPT_LANGUAGE);
if ( agent == null ) {
if (agent == null) {
agent = System.getProperty("user.language");
}
language = (agent == null) ? "en" : ISO639.userAgentLanguageDetection(agent);
if ( language == null ) {
if (language == null) {
language = "en";
}
}
// persist language preference when user explicitly submits a change via the lr param
prop.put("languageSel", modifier.language != null ? "lang_" + modifier.language : "");
if (languageFromParam) {
final ResponseHeader outgoingHeader = prop.getOutgoingHeader();
if (modifier.language != null) {
outgoingHeader.setCookie("yacy-language", "lang_" + modifier.language, 365 * 24 * 60 * 60, "/", null, false);
} else {
outgoingHeader.setCookie("yacy-language", "", 0, "/", null, false);
}
prop.setOutgoingHeader(outgoingHeader);
}
// the query
final QueryGoal qg = new QueryGoal(querystring.trim());
final int maxDistance = (querystring.indexOf('"', 0) >= 0) ? qg.getIncludeHashes().size() - 1 : Integer.MAX_VALUE;
+11 -1
View File
@@ -977,6 +977,16 @@ public final class SearchEvent implements ScoreMapUpdatesListener {
this.remote_solr_peerCount.incrementAndGet();
}
// Compute the max Solr score in this peer's result batch so we can normalize
// individual scores into [0, 1] before inserting into the shared priority queue.
// Without this, peers with higher absolute Solr scores always dominate peers
// with lower absolute scores, producing biased cross-node result ordering.
float maxSolrScore = 0.0f;
for (final URIMetadataNode node : nodeList) {
final Float s = (Float) node.getFieldValue("score");
if (s != null && s > maxSolrScore) maxSolrScore = s;
}
long timer = System.currentTimeMillis();
// normalize entries
@@ -1111,7 +1121,7 @@ public final class SearchEvent implements ScoreMapUpdatesListener {
// so far Solr score is used (with abitrary factor to get value similar to rwi ranking values)
final Float scorex = (Float) iEntry.getFieldValue("score"); // this is a special field containing the ranking score of a Solr search result
if (scorex != null && scorex > 0)
score = (long) ((1000000.0f * scorex) - iEntry.urllength()); // we modify the score here since the solr score is equal in many cases and then the order would simply depend on the url hash which would be silly
score = (long) ((1000000.0f * (maxSolrScore > 0.0f ? scorex / maxSolrScore : scorex)) - iEntry.urllength()); // normalize to [0,1] across this peer's batch so scores are comparable across peers
else
score = this.order.cardinal(iEntry);
this.nodeStack.put(new ReverseElement<>(iEntry, score)); // inserts the element and removes the worst (which is smallest)