mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
fix(search): cap vector hydration window
This commit is contained in:
+11
-9
@@ -1060,17 +1060,18 @@ describe("search helpers", () => {
|
||||
expect(result[0].skill.slug).toBe("fallback-skill");
|
||||
});
|
||||
|
||||
it("hydrates the stable max vector window for ordinary load-more searches", async () => {
|
||||
it("hydrates a bounded vector window for ordinary load-more searches", async () => {
|
||||
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
||||
|
||||
// Ordinary first-page and load-more searches use a stable recall floor, so
|
||||
// candidateLimit starts at the Convex vector maximum.
|
||||
const batch = Array.from({ length: 256 }, (_, i) => ({
|
||||
const batch = Array.from({ length: 128 }, (_, i) => ({
|
||||
_id: `skillEmbeddings:e${i}`,
|
||||
_score: 0.5 - i * 0.001,
|
||||
}));
|
||||
|
||||
const vectorSearchMock = vi.fn().mockResolvedValueOnce(batch);
|
||||
const vectorSearchMock = vi.fn(
|
||||
async (_table: unknown, _index: unknown, opts: { limit: number }) =>
|
||||
batch.slice(0, opts.limit),
|
||||
);
|
||||
|
||||
const hydrateCalls: string[][] = [];
|
||||
const runQuery = vi.fn(
|
||||
@@ -1101,9 +1102,10 @@ describe("search helpers", () => {
|
||||
{ query: "test", limit: 50 },
|
||||
);
|
||||
|
||||
expect(vectorSearchMock).toHaveBeenCalledTimes(1);
|
||||
expect(hydrateCalls).toHaveLength(1);
|
||||
expect(hydrateCalls[0]).toHaveLength(256);
|
||||
expect(vectorSearchMock).toHaveBeenCalledTimes(2);
|
||||
expect(hydrateCalls).toHaveLength(2);
|
||||
expect(hydrateCalls[0]).toHaveLength(100);
|
||||
expect(hydrateCalls[1]).toHaveLength(28);
|
||||
});
|
||||
|
||||
it("merges fallback matches without duplicate skill ids", () => {
|
||||
@@ -1239,7 +1241,7 @@ describe("soul search", () => {
|
||||
|
||||
it("hydrates only new soul embedding ids across vector iterations", async () => {
|
||||
generateEmbeddingMock.mockResolvedValueOnce([0, 1, 2]);
|
||||
const firstBatch = Array.from({ length: 200 }, (_, i) => ({
|
||||
const firstBatch = Array.from({ length: 100 }, (_, i) => ({
|
||||
_id: i === 0 ? "soulEmbeddings:a" : `soulEmbeddings:filler${i}`,
|
||||
_score: i === 0 ? 0.9 : 0.1,
|
||||
}));
|
||||
|
||||
+14
-9
@@ -60,6 +60,8 @@ const POPULARITY_WEIGHT = 0.08;
|
||||
const FALLBACK_SCAN_LIMIT = 2000;
|
||||
const MIN_STABLE_SEARCH_RECALL_LIMIT = 100;
|
||||
const MAX_DIRECT_SKILL_SEARCH_CANDIDATES = 100;
|
||||
const MIN_VECTOR_SEARCH_CANDIDATES = 50;
|
||||
const MAX_VECTOR_SEARCH_CANDIDATES = 128;
|
||||
const SKILL_CAPABILITY_TAG_SET = new Set<string>(SKILL_CAPABILITY_TAGS);
|
||||
|
||||
function getNextCandidateLimit(current: number, max: number) {
|
||||
@@ -188,11 +190,13 @@ export const searchSkills: ReturnType<typeof action> = action({
|
||||
// Keep ordinary first-page and load-more requests ranking the same recall pool
|
||||
// before slicing, so expanding the display limit does not reshuffle the prefix.
|
||||
const recallLimit = Math.max(limit, MIN_STABLE_SEARCH_RECALL_LIMIT);
|
||||
// Convex vectorSearch max limit is 256; clamp candidate sizes accordingly.
|
||||
// Keep the initial pool large enough to catch moderate-vector matches
|
||||
// that win after lexical and popularity scoring, even for small limits.
|
||||
const maxCandidate = Math.min(Math.max(recallLimit * 10, 200), 256);
|
||||
let candidateLimit = Math.min(Math.max(recallLimit * 3, 200), 256);
|
||||
// Keep the vector pool bounded; exact slug, prefix, and lexical fallback cover
|
||||
// literal recall without hydrating hundreds of semantic candidates per search.
|
||||
const maxCandidate = Math.min(
|
||||
Math.max(limit * 4, MIN_VECTOR_SEARCH_CANDIDATES),
|
||||
MAX_VECTOR_SEARCH_CANDIDATES,
|
||||
);
|
||||
let candidateLimit = Math.min(Math.max(limit * 2, MIN_VECTOR_SEARCH_CANDIDATES), maxCandidate);
|
||||
let hydrated: SkillSearchEntry[] = [];
|
||||
const seenEmbeddingIds = new Set<Id<"skillEmbeddings">>();
|
||||
let scoreById = new Map<Id<"skillEmbeddings">, number>();
|
||||
@@ -664,10 +668,11 @@ export const searchSouls: ReturnType<typeof action> = action({
|
||||
vector = null;
|
||||
}
|
||||
const limit = args.limit ?? 10;
|
||||
// Convex vectorSearch max limit is 256; clamp candidate sizes accordingly.
|
||||
// Match searchSkills so soul search does not miss boosted exact matches.
|
||||
const maxCandidate = Math.min(Math.max(limit * 10, 200), 256);
|
||||
let candidateLimit = Math.min(Math.max(limit * 3, 200), 256);
|
||||
const maxCandidate = Math.min(
|
||||
Math.max(limit * 4, MIN_VECTOR_SEARCH_CANDIDATES),
|
||||
MAX_VECTOR_SEARCH_CANDIDATES,
|
||||
);
|
||||
let candidateLimit = Math.min(Math.max(limit * 2, MIN_VECTOR_SEARCH_CANDIDATES), maxCandidate);
|
||||
let hydrated: HydratedSoulEntry[] = [];
|
||||
const seenEmbeddingIds = new Set<Id<"soulEmbeddings">>();
|
||||
let scoreById = new Map<Id<"soulEmbeddings">, number>();
|
||||
|
||||
Reference in New Issue
Block a user