feat(ai): unify AI architecture, implement RAG and legacy migration
CI / CD Pipeline / build (push) Failing after 5m36s
CI / CD Pipeline / deploy (push) Has been skipped

This commit is contained in:
2026-05-15 11:10:44 +07:00
parent 0240d80da5
commit 6cb3ae10ee
56 changed files with 6051 additions and 304 deletions
+104
View File
@@ -0,0 +1,104 @@
// File: src/modules/ai/qdrant.service.ts
// Change Log
// - 2026-05-14: เพิ่ม Qdrant gateway สำหรับ AI Module พร้อม project payload filter.
// - 2026-05-14: เพิ่ม OnModuleInit เพื่อ auto-call ensureCollection() (💡 S2).
import {
Injectable,
Logger,
OnModuleInit,
ServiceUnavailableException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { QdrantClient } from '@qdrant/js-client-rest';
const AI_COLLECTION_NAME = 'lcbp3_vectors';
const AI_VECTOR_SIZE = 768;
export interface AiVectorSearchResult {
pointId: string | number;
score: number;
payload: Record<string, unknown>;
}
/** Gateway กลางสำหรับ Qdrant ที่บังคับ project_public_id ทุก search */
@Injectable()
export class AiQdrantService implements OnModuleInit {
private readonly logger = new Logger(AiQdrantService.name);
private readonly client: QdrantClient;
constructor(private readonly configService: ConfigService) {
const url =
this.configService.get<string>('AI_QDRANT_URL') ??
this.configService.get<string>('QDRANT_URL') ??
'http://localhost:6333';
this.client = new QdrantClient({ url });
}
/** เรียก ensureCollection() อัตโนมัติเมื่อโมดูลถูก bootstrap */
async onModuleInit(): Promise<void> {
try {
await this.ensureCollection();
} catch (err) {
this.logger.error(
`AiQdrantService: collection init failed — ${err instanceof Error ? err.message : String(err)}`
);
}
}
/** เตรียม collection และ tenant payload index สำหรับ project isolation */
async ensureCollection(): Promise<void> {
const collections = await this.client.getCollections();
const exists = collections.collections.some(
(collection) => collection.name === AI_COLLECTION_NAME
);
if (!exists) {
await this.client.createCollection(AI_COLLECTION_NAME, {
vectors: { size: AI_VECTOR_SIZE, distance: 'Cosine' },
});
await this.client.createPayloadIndex(AI_COLLECTION_NAME, {
field_name: 'project_public_id',
field_schema: { type: 'keyword', is_tenant: true } as Parameters<
QdrantClient['createPayloadIndex']
>[1]['field_schema'],
});
this.logger.log(`Created Qdrant collection ${AI_COLLECTION_NAME}`);
}
}
/** ค้นหา vector โดยบังคับ projectPublicId เพื่อป้องกันข้อมูลข้ามโครงการ */
async searchByProject(
vector: number[],
projectPublicId: string,
limit: number
): Promise<AiVectorSearchResult[]> {
if (!projectPublicId) {
throw new ServiceUnavailableException('AI_QDRANT_PROJECT_SCOPE_REQUIRED');
}
const results = await this.client.search(AI_COLLECTION_NAME, {
vector,
limit,
filter: {
must: [{ key: 'project_public_id', match: { value: projectPublicId } }],
},
with_payload: true,
});
return results.map((result) => ({
pointId: result.id,
score: result.score,
payload: result.payload ?? {},
}));
}
/** ลบ vector ของเอกสารด้วย publicId ผ่าน queue processor ในขั้นถัดไป */
async deleteByDocumentPublicId(documentPublicId: string): Promise<void> {
await this.client.delete(AI_COLLECTION_NAME, {
wait: true,
filter: {
must: [{ key: 'public_id', match: { value: documentPublicId } }],
},
});
}
}