fix(memory): preserve BM25 relevance ordering (#33757, thanks @lsdcc01)

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 <ding.chuncai1@xydigit.com>
This commit is contained in:
Peter Steinberger
2026-03-07 22:41:38 +00:00
parent 99de6515a0
commit e45d62ba26
3 changed files with 21 additions and 3 deletions

View File

@@ -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)

View File

@@ -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 () => {

View File

@@ -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: {