portal-auth P1(#24 #25):KBDB library filter 地基(不 merge,待總管審) (#50)

This commit was merged in pull request #50.
This commit is contained in:
Leo
2026-07-14 03:50:06 +00:00
parent 18c0846af0
commit 7f409646e5
6 changed files with 274 additions and 14 deletions
+22 -3
View File
@@ -44,11 +44,15 @@ export async function embedOnWrite(env: Bindings, entry: Entry): Promise<boolean
{
id: entry.id,
values: vec,
// metadata 走 indexed 範圍:owner_id(租戶隔離)、entry_type、source#5.1 過濾與語義共用)
// metadata 走 indexed 範圍:owner_id(租戶隔離)、entry_type、source#5.1 過濾與語義共用)
// libraryportal-auth P1「庫」filter)。library 在寫入端正規化:未標記='general'design §3.2
// 「未蓋章的舊資料視同 general」——D1 側用查詢端 COALESCE fallbackVectorize filter 做不了
// COALESCE,故在 upsert 時蓋 'general',查詢端單純 $in 即可)。
metadata: {
owner_id: entry.owner_id ?? '',
entry_type: entry.entry_type,
source: readSource(entry) ?? '',
library: readLibrary(entry) ?? 'general',
},
},
]);
@@ -69,6 +73,13 @@ function readSource(entry: Entry): string | null {
return typeof s === 'string' ? s : null;
}
/** metadata_json.$.libraryportal-auth P1)。非字串/空字串一律視同未標記(→ caller fallback 'general')。 */
function readLibrary(entry: Entry): string | null {
const meta = parseMeta(entry.metadata_json);
const l = meta?.library;
return typeof l === 'string' && l.trim() !== '' ? l : null;
}
function parseMeta(json: string | null): Record<string, unknown> | null {
if (!json) return null;
try {
@@ -150,6 +161,7 @@ export async function backfillEmbeddings(
owner_id: x.e.owner_id ?? '',
entry_type: x.e.entry_type,
source: readSource(x.e) ?? '',
library: readLibrary(x.e) ?? 'general', // 同 embedOnWrite:寫入端正規化(P1
},
}));
if (vectors.length > 0) {
@@ -199,25 +211,31 @@ export interface SemanticHit {
owner_id?: string;
entry_type?: string;
source?: string;
library?: string;
}
/**
* 語義搜尋(mode:'semantic')。模組未開 → 回 nullcaller 降級 keyword + 告知缺能力)。
* owner_id / source / entry_type 過濾走 Vectorize metadata filterentry_type 已 index,見上 upsert metadata)。
* entry_type 是 base 通用 filtercaller 傳任意 typebase 不寫死語意)。
* libraryportal-auth P1):多值庫 filter 走 `$in`(官方支援已核實 2026-07-14:文件明列 $in/$nin
* workers-types 原生 typingdesign §3.3 的 fan-out fallback 不需啟用)。未帶=行為不變。
* 註:向量 metadata 的 library 在寫入端已正規化(未標記='general'),故 $in 不需 NULL 處理;
* 但「建 library metadata index 之前」upsert 的既有向量沒有此欄 → 部署清單強制 reindex backfill。
*/
export async function semanticSearch(
env: Bindings,
q: string,
opts: { owner_id?: string; source?: string; entry_type?: string; topK?: number } = {},
opts: { owner_id?: string; source?: string; entry_type?: string; library?: string[]; topK?: number } = {},
): Promise<SemanticHit[] | null> {
if (!embedEnabled(env)) return null;
const vec = await embedText(env, q);
if (!vec) return [];
const filter: Record<string, string> = {};
const filter: VectorizeVectorMetadataFilter = {};
if (opts.owner_id) filter.owner_id = opts.owner_id;
if (opts.source) filter.source = opts.source;
if (opts.entry_type) filter.entry_type = opts.entry_type;
if (opts.library && opts.library.length > 0) filter.library = { $in: opts.library };
const res = await env.VECTORIZE!.query(vec, {
topK: Math.min(opts.topK ?? 20, 100),
returnMetadata: 'indexed',
@@ -229,5 +247,6 @@ export async function semanticSearch(
owner_id: m.metadata?.owner_id as string | undefined,
entry_type: m.metadata?.entry_type as string | undefined,
source: m.metadata?.source as string | undefined,
library: m.metadata?.library as string | undefined,
}));
}