portal-auth P1(#24 #25):KBDB library filter 地基(不 merge,待總管審) (#50)
This commit was merged in pull request #50.
This commit is contained in:
+22
-3
@@ -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 過濾與語義共用)、
|
||||
// library(portal-auth P1「庫」filter)。library 在寫入端正規化:未標記='general'(design §3.2
|
||||
// 「未蓋章的舊資料視同 general」——D1 側用查詢端 COALESCE fallback,Vectorize 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.$.library(portal-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')。模組未開 → 回 null(caller 降級 keyword + 告知缺能力)。
|
||||
* owner_id / source / entry_type 過濾走 Vectorize metadata filter(entry_type 已 index,見上 upsert metadata)。
|
||||
* entry_type 是 base 通用 filter(caller 傳任意 type,base 不寫死語意)。
|
||||
* library(portal-auth P1):多值庫 filter 走 `$in`(官方支援已核實 2026-07-14:文件明列 $in/$nin+
|
||||
* workers-types 原生 typing;design §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,
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user