From e45d62ba26ab6565cb78c47bfe5d47d163981550 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 7 Mar 2026 22:41:38 +0000 Subject: [PATCH] fix(memory): preserve BM25 relevance ordering (#33757, thanks @lsdcc01) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Land #33757 by @lsdcc01 without the unrelated dependency bump. Preserve negative FTS5 BM25 ordering in hybrid scoring and add changelog coverage for #5767. Co-authored-by: 丁春才0668000523 --- CHANGELOG.md | 1 + src/memory/hybrid.test.ts | 13 ++++++++++++- src/memory/hybrid.ts | 10 ++++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5d749e95..edd5a441b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Security/Config: fail closed when `loadConfig()` hits validation or read errors so invalid configs cannot silently fall back to permissive runtime defaults. (#9040) Thanks @joetomasone. +- Memory/Hybrid search: preserve negative FTS5 BM25 relevance ordering in `bm25RankToScore()` so stronger keyword matches rank above weaker ones instead of collapsing or reversing scores. (#33757) Thanks @lsdcc01. - LINE/`requireMention` group gating: align inbound and reply-stage LINE group policy resolution across raw, `group:`, and `room:` keys (including account-scoped group config), preserve plugin-backed reply-stage fallback behavior, and add regression coverage for prefixed-only group/room config plus reply-stage policy resolution. (#35847) Thanks @kirisame-wang. - Onboarding/local setup: default unset local `tools.profile` to `coding` instead of `messaging`, restoring file/runtime tools for fresh local installs while preserving explicit user-set profiles. (from #38241, overlap with #34958) Thanks @cgdusek. - Gateway/Telegram stale-socket restart guard: only apply stale-socket restarts to channels that publish event-liveness timestamps, preventing Telegram providers from being misclassified as stale solely due to long uptime and avoiding restart/pairing storms after upgrade. (openclaw#38464) diff --git a/src/memory/hybrid.test.ts b/src/memory/hybrid.test.ts index 98e67f034..134e7bfe7 100644 --- a/src/memory/hybrid.test.ts +++ b/src/memory/hybrid.test.ts @@ -14,7 +14,18 @@ describe("memory hybrid helpers", () => { expect(bm25RankToScore(0)).toBeCloseTo(1); expect(bm25RankToScore(1)).toBeCloseTo(0.5); expect(bm25RankToScore(10)).toBeLessThan(bm25RankToScore(1)); - expect(bm25RankToScore(-100)).toBeCloseTo(1); + expect(bm25RankToScore(-100)).toBeCloseTo(1, 1); + }); + + it("bm25RankToScore preserves FTS5 BM25 relevance ordering", () => { + const strongest = bm25RankToScore(-4.2); + const middle = bm25RankToScore(-2.1); + const weakest = bm25RankToScore(-0.5); + + expect(strongest).toBeGreaterThan(middle); + expect(middle).toBeGreaterThan(weakest); + expect(strongest).not.toBe(middle); + expect(middle).not.toBe(weakest); }); it("mergeHybridResults unions by id and combines weighted scores", async () => { diff --git a/src/memory/hybrid.ts b/src/memory/hybrid.ts index af045ade7..00c5985d7 100644 --- a/src/memory/hybrid.ts +++ b/src/memory/hybrid.ts @@ -44,8 +44,14 @@ export function buildFtsQuery(raw: string): string | null { } export function bm25RankToScore(rank: number): number { - const normalized = Number.isFinite(rank) ? Math.max(0, rank) : 999; - return 1 / (1 + normalized); + if (!Number.isFinite(rank)) { + return 1 / (1 + 999); + } + if (rank < 0) { + const relevance = -rank; + return relevance / (1 + relevance); + } + return 1 / (1 + rank); } export async function mergeHybridResults(params: {