Update searchAsyncSingle.py
Browse files- searchAsyncSingle.py +222 -116
searchAsyncSingle.py
CHANGED
@@ -355,11 +355,11 @@ async def rebuild_faiss_index():
|
|
355 |
def _build_ivf_index():
|
356 |
dimension = item_vectors.shape[1]
|
357 |
# IVF 클러스터 수 - 데이터 크기에 따라 조정 (√n 규칙 사용)
|
358 |
-
nlist = int(np.sqrt(total_items) *
|
359 |
-
nlist = max(
|
360 |
|
361 |
# 양자화 파라미터 - 차원 수에 맞게 조정
|
362 |
-
M = min(
|
363 |
nbits = 8 # 비트 수
|
364 |
|
365 |
# 고속 IVF 인덱스 생성
|
@@ -378,7 +378,7 @@ async def rebuild_faiss_index():
|
|
378 |
|
379 |
# 검색 품질 향상을 위한 설정
|
380 |
# nprobe = 몇 개의 클러스터를 검색할지 (높을수록 정확도 ↑, 속도 ↓)
|
381 |
-
index.nprobe = min(
|
382 |
|
383 |
logger.info(f"✅ IVF 인덱스 구축 완료: clusters={nlist}, nprobe={index.nprobe}")
|
384 |
return index
|
@@ -410,21 +410,16 @@ async def check_faiss_index():
|
|
410 |
raise RuntimeError("FAISS 인덱스 초기화에 실패했습니다.")
|
411 |
|
412 |
# ✅ 최적화된 키워드 추출 함수
|
413 |
-
async def extract_keywords(query: str, top_n: int =
|
414 |
"""KeyBERT 최적화 키워드 추출 (성능 중심)"""
|
415 |
# 매우 짧은 쿼리는 그대로 반환 (처리 비용 절감)
|
416 |
if len(query) <= 3:
|
417 |
return [query]
|
418 |
|
419 |
-
# 단어가 2개 이하면 키워드 추출 과정 생략
|
420 |
-
words = query.split()
|
421 |
-
if len(words) <= 2:
|
422 |
-
return [words[0]] if words else [query]
|
423 |
-
|
424 |
loop = asyncio.get_event_loop()
|
425 |
|
426 |
def _optimized_extract():
|
427 |
-
# 성능 중심 설정
|
428 |
return kw_model.extract_keywords(
|
429 |
query,
|
430 |
keyphrase_ngram_range=(1, 1), # 단일 단어만 추출
|
@@ -437,22 +432,20 @@ async def extract_keywords(query: str, top_n: int = 1): # top_n을 2에서 1로
|
|
437 |
try:
|
438 |
keywords = await loop.run_in_executor(thread_pool, _optimized_extract)
|
439 |
# 가중치가 너무 낮은 키워드 제외
|
440 |
-
filtered = [(k, s) for k, s in keywords if s > 0.
|
441 |
return [k[0] for k in filtered]
|
442 |
except Exception as e:
|
443 |
logger.error(f"❌ 키워드 추출 오류: {str(e)}")
|
444 |
-
# 단어 분리로 폴백
|
445 |
-
return [
|
446 |
-
|
447 |
|
448 |
|
449 |
# ✅ 최적화된 키워드 확장 함수
|
450 |
-
async def expand_keywords_with_word2vec(keywords: list, max_new=
|
451 |
"""Word2Vec 키워드 확장 최적화"""
|
452 |
global word2vec_model
|
453 |
|
454 |
-
|
455 |
-
if not word2vec_model or not keywords or not keywords[0]:
|
456 |
return keywords
|
457 |
|
458 |
# 결과 저장을 위한 집합
|
@@ -461,28 +454,27 @@ async def expand_keywords_with_word2vec(keywords: list, max_new=1): # max_new
|
|
461 |
loop = asyncio.get_event_loop()
|
462 |
|
463 |
def _expand_keywords():
|
464 |
-
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
-
# 결과 변환 - 최대 3개로 제한 (이전 5개)
|
483 |
result = list(expanded)
|
484 |
-
|
485 |
-
|
|
|
486 |
return result
|
487 |
|
488 |
try:
|
@@ -494,9 +486,8 @@ async def expand_keywords_with_word2vec(keywords: list, max_new=1): # max_new
|
|
494 |
return keywords # 오류 시 원본 키워드 반환
|
495 |
|
496 |
|
497 |
-
|
498 |
# ✅ 최적화된 search_faiss_with_keywords 함수
|
499 |
-
async def search_faiss_with_keywords(query: str, top_k: int =
|
500 |
"""고속 키워드 기반 FAISS 검색 수행"""
|
501 |
global faiss_index, indexed_items
|
502 |
|
@@ -520,11 +511,8 @@ async def search_faiss_with_keywords(query: str, top_k: int = 3, keywords=None,
|
|
520 |
# 2. 벡터 인코딩 최적화 - 쿼리와 키워드 한 번에 처리
|
521 |
search_texts = [query] + expanded_keywords
|
522 |
|
523 |
-
# 짧은 텍스트는 벡터화 배치 크기 2배 증가 (성능 향상)
|
524 |
-
batch_size = 2048 if len(search_texts) < 10 else 1024
|
525 |
-
|
526 |
# 벡터 인코딩 - 최적화된 함수 사용
|
527 |
-
all_vectors = await encode_texts_parallel(search_texts
|
528 |
|
529 |
# 벡터 정규화 - 최적화된 방식
|
530 |
def normalize_batch(vectors):
|
@@ -544,47 +532,30 @@ async def search_faiss_with_keywords(query: str, top_k: int = 3, keywords=None,
|
|
544 |
else:
|
545 |
return [] # 벡터화 실패 시 빈 결과 반환
|
546 |
|
547 |
-
# 3. FAISS 검색 최적화 -
|
548 |
def _optimized_batch_search():
|
549 |
all_results = {}
|
550 |
|
551 |
-
# 쿼리 벡터 검색 (가중치
|
552 |
if query_vector.shape[0] > 0:
|
553 |
-
|
554 |
-
|
555 |
-
faiss_index.nprobe = min(8, original_nprobe) # nprobe 감소
|
556 |
-
|
557 |
-
# top_k의 1.5배만 검색 (이전 2배)
|
558 |
-
search_k = int(top_k * 1.5)
|
559 |
-
distances, indices = faiss_index.search(query_vector, search_k)
|
560 |
-
|
561 |
-
# 원래 nprobe로 복원
|
562 |
-
faiss_index.nprobe = original_nprobe
|
563 |
-
|
564 |
-
# 쿼리 결과 가중치 적용
|
565 |
for idx, dist in zip(indices[0], distances[0]):
|
566 |
if idx < len(indexed_items):
|
567 |
-
all_results[idx] = dist * 3.0 # 가중치 3.0
|
568 |
|
569 |
-
# 키워드 벡터 배치 검색
|
570 |
-
if keyword_vectors.shape[0] >
|
571 |
-
#
|
572 |
-
original_nprobe = faiss_index.nprobe
|
573 |
-
faiss_index.nprobe = max(2, original_nprobe // 4) # 더 낮은 nprobe
|
574 |
-
|
575 |
-
# top_k만큼만 검색 (이전과 동일)
|
576 |
k_distances, k_indices = faiss_index.search(keyword_vectors, top_k)
|
577 |
|
578 |
-
# 원래 nprobe로 복원
|
579 |
-
faiss_index.nprobe = original_nprobe
|
580 |
-
|
581 |
# 키워드별 가중치 적용 및 결과 병합
|
582 |
for i in range(keyword_vectors.shape[0]):
|
583 |
for j, (idx, dist) in enumerate(zip(k_indices[i], k_distances[i])):
|
584 |
if idx < len(indexed_items):
|
585 |
# 순위에 따라 가중치 차등 적용 (상위 결과 우대)
|
586 |
-
rank_weight = 1.0 / (1 + j * 0.
|
587 |
-
weight = 0.
|
588 |
|
589 |
# 기존 점수에 추가
|
590 |
all_results[idx] = all_results.get(idx, 0) + dist * weight
|
@@ -598,50 +569,44 @@ async def search_faiss_with_keywords(query: str, top_k: int = 3, keywords=None,
|
|
598 |
def _process_results():
|
599 |
# 임계값 필터링 및 정렬
|
600 |
filtered_items = [(idx, score) for idx, score in result_scores.items()
|
601 |
-
if score >= 0.
|
602 |
|
603 |
# 점수 기준 내림차순 정렬
|
604 |
sorted_items = sorted(filtered_items, key=lambda x: x[1], reverse=True)
|
605 |
|
606 |
-
# 최종 결과 변환
|
607 |
recommendations = []
|
608 |
-
|
609 |
-
|
610 |
-
|
611 |
-
|
612 |
-
|
613 |
-
|
614 |
-
|
615 |
-
|
616 |
-
|
617 |
-
|
618 |
-
|
619 |
-
|
620 |
-
|
621 |
-
|
622 |
-
|
623 |
-
if mask.any():
|
624 |
-
item_seq = items_df.loc[mask, "ITEMSEQ"].values[0]
|
625 |
-
recommendations.append({
|
626 |
-
"ITEMSEQ": item_seq,
|
627 |
-
"ITEMNAME": item_name,
|
628 |
-
"score": float(score)
|
629 |
-
})
|
630 |
-
|
631 |
return recommendations
|
632 |
|
633 |
# 결과 처리 실행
|
634 |
recommendations = await loop.run_in_executor(thread_pool, _process_results)
|
635 |
|
636 |
-
# 5. 직접 매칭
|
637 |
-
if len(recommendations)
|
638 |
-
direct_matches = await find_direct_matches(query,
|
|
|
|
|
639 |
if direct_matches:
|
640 |
recommendations.extend(direct_matches)
|
641 |
|
642 |
-
# 처리
|
643 |
elapsed = time.time() - start_time
|
644 |
-
if elapsed >
|
645 |
logger.info(f"🔍 검색 완료 | 소요시간: {elapsed:.2f}초 | 결과: {len(recommendations)}개")
|
646 |
|
647 |
return recommendations[:top_k]
|
@@ -708,18 +673,16 @@ async def recommend(request: RecommendRequest, background_tasks: BackgroundTasks
|
|
708 |
if not search_query:
|
709 |
raise HTTPException(status_code=400, detail="검색어를 입력해주세요")
|
710 |
|
711 |
-
top_k = min(max(1, request.top_k),
|
712 |
-
|
713 |
-
# 병렬 프로세싱을 위한 동시 실행
|
714 |
-
|
715 |
-
|
716 |
-
|
717 |
-
|
718 |
-
|
719 |
-
if request.use_expansion
|
720 |
-
|
721 |
-
else:
|
722 |
-
expanded_keywords = keywords
|
723 |
|
724 |
# 검색 실행 - 병렬 처리된 키워드 활용
|
725 |
recommendations = await search_faiss_with_keywords(
|
@@ -737,9 +700,9 @@ async def recommend(request: RecommendRequest, background_tasks: BackgroundTasks
|
|
737 |
"expanded_keywords": expanded_keywords if expanded_keywords and len(expanded_keywords) > 0 else None
|
738 |
}
|
739 |
|
740 |
-
# 응답 시간 측정 (
|
741 |
elapsed = time.time() - start_time
|
742 |
-
if elapsed > 0
|
743 |
logger.info(f"⏱️ API 응답 시간: {elapsed:.2f}초 | 쿼리: '{search_query}'")
|
744 |
|
745 |
return result
|
@@ -748,6 +711,47 @@ async def recommend(request: RecommendRequest, background_tasks: BackgroundTasks
|
|
748 |
logger.error(f"❌ 추천 처리 오류: {str(e)}")
|
749 |
raise HTTPException(status_code=500, detail=f"추천 처리 중 오류가 발생했습니다")
|
750 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
751 |
# ✅ FAISS 인덱스 갱신 API (명시적으로 요청할 때만 실행)
|
752 |
@app.post("/api/update_index")
|
753 |
async def update_index(background_tasks: BackgroundTasks):
|
@@ -772,6 +776,108 @@ async def rebuild_and_log_index():
|
|
772 |
except Exception as e:
|
773 |
logger.error(f"❌ 백그라운드 인덱스 재구축 중 오류: {str(e)}")
|
774 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
775 |
# ✅ FastAPI 실행
|
776 |
if __name__ == "__main__":
|
777 |
# 서버 시작 시 저장된 인덱스 로드 시도
|
|
|
355 |
def _build_ivf_index():
|
356 |
dimension = item_vectors.shape[1]
|
357 |
# IVF 클러스터 수 - 데이터 크기에 따라 조정 (√n 규칙 사용)
|
358 |
+
nlist = int(np.sqrt(total_items) * 4) # 클러스터 수 증가
|
359 |
+
nlist = max(32, min(nlist, 1024)) # 최소 32, 최대 1024개 제한
|
360 |
|
361 |
# 양자화 파라미터 - 차원 수에 맞게 조정
|
362 |
+
M = min(64, dimension // 2) # 서브벡터 수
|
363 |
nbits = 8 # 비트 수
|
364 |
|
365 |
# 고속 IVF 인덱스 생성
|
|
|
378 |
|
379 |
# 검색 품질 향상을 위한 설정
|
380 |
# nprobe = 몇 개의 클러스터를 검색할지 (높을수록 정확도 ↑, 속도 ↓)
|
381 |
+
index.nprobe = min(32, nlist // 4) # 클러스터의 25% 검색
|
382 |
|
383 |
logger.info(f"✅ IVF 인덱스 구축 완료: clusters={nlist}, nprobe={index.nprobe}")
|
384 |
return index
|
|
|
410 |
raise RuntimeError("FAISS 인덱스 초기화에 실패했습니다.")
|
411 |
|
412 |
# ✅ 최적화된 키워드 추출 함수
|
413 |
+
async def extract_keywords(query: str, top_n: int = 2): # top_n 감소
|
414 |
"""KeyBERT 최적화 키워드 추출 (성능 중심)"""
|
415 |
# 매우 짧은 쿼리는 그대로 반환 (처리 비용 절감)
|
416 |
if len(query) <= 3:
|
417 |
return [query]
|
418 |
|
|
|
|
|
|
|
|
|
|
|
419 |
loop = asyncio.get_event_loop()
|
420 |
|
421 |
def _optimized_extract():
|
422 |
+
# 성능 중심 설정
|
423 |
return kw_model.extract_keywords(
|
424 |
query,
|
425 |
keyphrase_ngram_range=(1, 1), # 단일 단어만 추출
|
|
|
432 |
try:
|
433 |
keywords = await loop.run_in_executor(thread_pool, _optimized_extract)
|
434 |
# 가중치가 너무 낮은 키워드 제외
|
435 |
+
filtered = [(k, s) for k, s in keywords if s > 0.2]
|
436 |
return [k[0] for k in filtered]
|
437 |
except Exception as e:
|
438 |
logger.error(f"❌ 키워드 추출 오류: {str(e)}")
|
439 |
+
# 단어 분리로 폴백
|
440 |
+
return query.split()[:2]
|
|
|
441 |
|
442 |
|
443 |
# ✅ 최적화된 키워드 확장 함수
|
444 |
+
async def expand_keywords_with_word2vec(keywords: list, max_new=2): # max_new 감소
|
445 |
"""Word2Vec 키워드 확장 최적화"""
|
446 |
global word2vec_model
|
447 |
|
448 |
+
if word2vec_model is None or not keywords:
|
|
|
449 |
return keywords
|
450 |
|
451 |
# 결과 저장을 위한 집합
|
|
|
454 |
loop = asyncio.get_event_loop()
|
455 |
|
456 |
def _expand_keywords():
|
457 |
+
for keyword in keywords:
|
458 |
+
# 단일 단어인 경우
|
459 |
+
if keyword in word2vec_model:
|
460 |
+
# 유사도가 높은 단어만 선택 (임계값 적용)
|
461 |
+
similar_words = word2vec_model.most_similar(keyword, topn=max_new)
|
462 |
+
for word, score in similar_words:
|
463 |
+
if score > 0.7: # 높은 유사도 임계값 적용
|
464 |
+
expanded.add(word)
|
465 |
+
# 복합어 처리 (첫 단어만)
|
466 |
+
elif len(keyword.split()) > 1:
|
467 |
+
word = keyword.split()[0]
|
468 |
+
if word in word2vec_model and len(word) > 1:
|
469 |
+
similar = word2vec_model.most_similar(word, topn=1)
|
470 |
+
if similar and similar[0][1] > 0.8: # 높은 임계값
|
471 |
+
expanded.add(similar[0][0])
|
472 |
+
|
473 |
+
# 결과 변환
|
|
|
|
|
474 |
result = list(expanded)
|
475 |
+
# 키워드가 너무 많으면 제한
|
476 |
+
if len(result) > 5:
|
477 |
+
return keywords + result[len(keywords):5]
|
478 |
return result
|
479 |
|
480 |
try:
|
|
|
486 |
return keywords # 오류 시 원본 키워드 반환
|
487 |
|
488 |
|
|
|
489 |
# ✅ 최적화된 search_faiss_with_keywords 함수
|
490 |
+
async def search_faiss_with_keywords(query: str, top_k: int = 5, keywords=None, expanded_keywords=None):
|
491 |
"""고속 키워드 기반 FAISS 검색 수행"""
|
492 |
global faiss_index, indexed_items
|
493 |
|
|
|
511 |
# 2. 벡터 인코딩 최적화 - 쿼리와 키워드 한 번에 처리
|
512 |
search_texts = [query] + expanded_keywords
|
513 |
|
|
|
|
|
|
|
514 |
# 벡터 인코딩 - 최적화된 함수 사용
|
515 |
+
all_vectors = await encode_texts_parallel(search_texts)
|
516 |
|
517 |
# 벡터 정규화 - 최적화된 방식
|
518 |
def normalize_batch(vectors):
|
|
|
532 |
else:
|
533 |
return [] # 벡터화 실패 시 빈 결과 반환
|
534 |
|
535 |
+
# 3. FAISS 검색 최적화 - 일괄 배치 처리
|
536 |
def _optimized_batch_search():
|
537 |
all_results = {}
|
538 |
|
539 |
+
# 쿼리 벡터 검색 (가중치 3배로 증가)
|
540 |
if query_vector.shape[0] > 0:
|
541 |
+
distances, indices = faiss_index.search(query_vector, top_k * 2)
|
542 |
+
# 쿼리 결과 가중치 적용 (중요도 상향)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
543 |
for idx, dist in zip(indices[0], distances[0]):
|
544 |
if idx < len(indexed_items):
|
545 |
+
all_results[idx] = dist * 3.0 # 가중치 3.0
|
546 |
|
547 |
+
# 키워드 벡터 배치 검색
|
548 |
+
if keyword_vectors.shape[0] > 0:
|
549 |
+
# 배치 검색 한 번에 처리
|
|
|
|
|
|
|
|
|
550 |
k_distances, k_indices = faiss_index.search(keyword_vectors, top_k)
|
551 |
|
|
|
|
|
|
|
552 |
# 키워드별 가중치 적용 및 결과 병합
|
553 |
for i in range(keyword_vectors.shape[0]):
|
554 |
for j, (idx, dist) in enumerate(zip(k_indices[i], k_distances[i])):
|
555 |
if idx < len(indexed_items):
|
556 |
# 순위에 따라 가중치 차등 적용 (상위 결과 우대)
|
557 |
+
rank_weight = 1.0 / (1 + j * 0.2) # 순위별 가중치 감소
|
558 |
+
weight = 0.6 * rank_weight # 기본 가중치 0.6
|
559 |
|
560 |
# 기존 점수에 추가
|
561 |
all_results[idx] = all_results.get(idx, 0) + dist * weight
|
|
|
569 |
def _process_results():
|
570 |
# 임계값 필터링 및 정렬
|
571 |
filtered_items = [(idx, score) for idx, score in result_scores.items()
|
572 |
+
if score >= 0.3] # 최소 점수 필터링
|
573 |
|
574 |
# 점수 기준 내림차순 정렬
|
575 |
sorted_items = sorted(filtered_items, key=lambda x: x[1], reverse=True)
|
576 |
|
577 |
+
# 최종 결과 변환
|
578 |
recommendations = []
|
579 |
+
for idx, score in sorted_items[:top_k]: # top_k개만 처리
|
580 |
+
item_name = indexed_items[idx]
|
581 |
+
try:
|
582 |
+
# 메모리 내 조회 최적화
|
583 |
+
mask = active_sale_items["ITEMNAME"] == item_name
|
584 |
+
if mask.any():
|
585 |
+
item_seq = active_sale_items.loc[mask, "ITEMSEQ"].values[0]
|
586 |
+
recommendations.append({
|
587 |
+
"ITEMSEQ": item_seq,
|
588 |
+
"ITEMNAME": item_name,
|
589 |
+
"score": float(score)
|
590 |
+
})
|
591 |
+
except (IndexError, KeyError):
|
592 |
+
continue
|
593 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
594 |
return recommendations
|
595 |
|
596 |
# 결과 처리 실행
|
597 |
recommendations = await loop.run_in_executor(thread_pool, _process_results)
|
598 |
|
599 |
+
# 5. 직접 매칭 추가 최적화 (필요한 경우에만)
|
600 |
+
if len(recommendations) < top_k:
|
601 |
+
direct_matches = await find_direct_matches(query,
|
602 |
+
top_k - len(recommendations),
|
603 |
+
[r["ITEMNAME"] for r in recommendations])
|
604 |
if direct_matches:
|
605 |
recommendations.extend(direct_matches)
|
606 |
|
607 |
+
# 처리 시간이 1초 이상인 경우에만 로깅
|
608 |
elapsed = time.time() - start_time
|
609 |
+
if elapsed > 1.0:
|
610 |
logger.info(f"🔍 검색 완료 | 소요시간: {elapsed:.2f}초 | 결과: {len(recommendations)}개")
|
611 |
|
612 |
return recommendations[:top_k]
|
|
|
673 |
if not search_query:
|
674 |
raise HTTPException(status_code=400, detail="검색어를 입력해주세요")
|
675 |
|
676 |
+
top_k = min(max(1, request.top_k), 20) # 1~20 범위로 제한
|
677 |
+
|
678 |
+
# 병렬 프로세싱을 위한 동시 실행
|
679 |
+
keywords, expanded_keywords = await asyncio.gather(
|
680 |
+
extract_keywords(search_query),
|
681 |
+
expand_keywords_with_word2vec(
|
682 |
+
[search_query.split()[0]] if search_query.split() else [search_query],
|
683 |
+
max_new=2
|
684 |
+
) if request.use_expansion else None
|
685 |
+
)
|
|
|
|
|
686 |
|
687 |
# 검색 실행 - 병렬 처리된 키워드 활용
|
688 |
recommendations = await search_faiss_with_keywords(
|
|
|
700 |
"expanded_keywords": expanded_keywords if expanded_keywords and len(expanded_keywords) > 0 else None
|
701 |
}
|
702 |
|
703 |
+
# 응답 시간 측정 (1초 이상만 로깅)
|
704 |
elapsed = time.time() - start_time
|
705 |
+
if elapsed > 1.0:
|
706 |
logger.info(f"⏱️ API 응답 시간: {elapsed:.2f}초 | 쿼리: '{search_query}'")
|
707 |
|
708 |
return result
|
|
|
711 |
logger.error(f"❌ 추천 처리 오류: {str(e)}")
|
712 |
raise HTTPException(status_code=500, detail=f"추천 처리 중 오류가 발생했습니다")
|
713 |
|
714 |
+
# 인덱스 상태 확인 함수 (백그라운드 태스크용)
|
715 |
+
async def check_index_health():
|
716 |
+
"""인덱스 상태를 주기적으로 확인하는 백그라운드 태스크"""
|
717 |
+
try:
|
718 |
+
# 인덱스 사용 상태 확인
|
719 |
+
if faiss_index is None:
|
720 |
+
logger.warning("⚠️ 백그라운드 체크: FAISS 인덱스가 로드되지 않았습니다.")
|
721 |
+
await check_faiss_index()
|
722 |
+
|
723 |
+
# 추가적인 상태 확인 로직을 여기에 구현할 수 있음
|
724 |
+
logger.debug("✅ 인덱스 상태 확인 완료")
|
725 |
+
except Exception as e:
|
726 |
+
logger.error(f"❌ 백그라운드 인덱스 체크 중 오류: {str(e)}")
|
727 |
+
|
728 |
+
# ✅ 유사 단어 검색 API
|
729 |
+
@app.post("/api/similar_words")
|
730 |
+
async def similar_words(word: str, top_k: int = 10):
|
731 |
+
"""Word2Vec 모델을 사용한 유사 단어 검색 API (비동기 지원)"""
|
732 |
+
try:
|
733 |
+
if word2vec_model is None:
|
734 |
+
return {"error": "Word2Vec 모델이 로드되지 않았습니다."}
|
735 |
+
|
736 |
+
loop = asyncio.get_event_loop()
|
737 |
+
|
738 |
+
def _get_similar():
|
739 |
+
if word not in word2vec_model:
|
740 |
+
return []
|
741 |
+
|
742 |
+
similar = word2vec_model.most_similar(word, topn=top_k)
|
743 |
+
return [{"word": w, "similarity": float(s)} for w, s in similar]
|
744 |
+
|
745 |
+
result = await loop.run_in_executor(thread_pool, _get_similar)
|
746 |
+
|
747 |
+
if not result:
|
748 |
+
return {"word": word, "similar_words": [], "message": "단어가 모델에 없습니다."}
|
749 |
+
|
750 |
+
return {"word": word, "similar_words": result}
|
751 |
+
except Exception as e:
|
752 |
+
logger.error(f"❌ 유사 단어 검색 중 오류: {str(e)}")
|
753 |
+
raise HTTPException(status_code=500, detail=f"유사 단어 검색 오류: {str(e)}")
|
754 |
+
|
755 |
# ✅ FAISS 인덱스 갱신 API (명시적으로 요청할 때만 실행)
|
756 |
@app.post("/api/update_index")
|
757 |
async def update_index(background_tasks: BackgroundTasks):
|
|
|
776 |
except Exception as e:
|
777 |
logger.error(f"❌ 백그라운드 인덱스 재구축 중 오류: {str(e)}")
|
778 |
|
779 |
+
# ✅ 인덱스 디버깅 API
|
780 |
+
@app.get("/api/debug_index")
|
781 |
+
async def debug_index(query: str, top_k: int = 20):
|
782 |
+
"""인덱스 디버깅을 위한 API (비동기 지원)"""
|
783 |
+
try:
|
784 |
+
await check_faiss_index()
|
785 |
+
|
786 |
+
loop = asyncio.get_event_loop()
|
787 |
+
|
788 |
+
# 원본 벡터 생성 (비동기)
|
789 |
+
def _get_vector():
|
790 |
+
vector = embedding_model.encode(query, convert_to_numpy=True).astype("float32")
|
791 |
+
norm = np.linalg.norm(vector)
|
792 |
+
normalized_vector = vector / norm
|
793 |
+
return normalized_vector, norm
|
794 |
+
|
795 |
+
normalized_vector, norm = await loop.run_in_executor(thread_pool, _get_vector)
|
796 |
+
|
797 |
+
# 원본 쿼리로 검색 (비동기)
|
798 |
+
def _search():
|
799 |
+
return faiss_index.search(np.array([normalized_vector]), top_k)
|
800 |
+
|
801 |
+
distances, indices = await loop.run_in_executor(thread_pool, _search)
|
802 |
+
|
803 |
+
# 결과 매핑
|
804 |
+
results = []
|
805 |
+
for i, (idx, dist) in enumerate(zip(indices[0], distances[0])):
|
806 |
+
if idx < len(indexed_items):
|
807 |
+
item_name = indexed_items[idx]
|
808 |
+
results.append({
|
809 |
+
"rank": i + 1,
|
810 |
+
"index": int(idx),
|
811 |
+
"item_name": item_name,
|
812 |
+
"distance/score": float(dist)
|
813 |
+
})
|
814 |
+
|
815 |
+
# 데이터셋에 해당 단어가 있는지 확인 (비동기)
|
816 |
+
def _find_matches():
|
817 |
+
contains = [item for item in indexed_items if query.lower() in item.lower()][:5]
|
818 |
+
exact = [item for item in indexed_items if query.lower() == item.lower()]
|
819 |
+
return contains, exact
|
820 |
+
|
821 |
+
contains_query, exact_matches = await loop.run_in_executor(thread_pool, _find_matches)
|
822 |
+
|
823 |
+
return {
|
824 |
+
"query": query,
|
825 |
+
"vector_norm": float(norm),
|
826 |
+
"contains_query": contains_query,
|
827 |
+
"exact_matches": exact_matches,
|
828 |
+
"results": results
|
829 |
+
}
|
830 |
+
except Exception as e:
|
831 |
+
logger.error(f"❌ 인덱스 디버깅 중 오류: {str(e)}")
|
832 |
+
raise HTTPException(status_code=500, detail=f"인덱스 디버깅 오류: {str(e)}")
|
833 |
+
|
834 |
+
# ✅ 문자열 포함 검색 API
|
835 |
+
@app.get("/api/text_search")
|
836 |
+
async def text_search(query: str, top_k: int = 10):
|
837 |
+
"""단순 텍스트 포함 검색 API (비동기 지원)"""
|
838 |
+
try:
|
839 |
+
loop = asyncio.get_event_loop()
|
840 |
+
|
841 |
+
# 비동기 검색 함수
|
842 |
+
def _text_search():
|
843 |
+
# 단순 텍스트 포함 검색
|
844 |
+
matched_items = []
|
845 |
+
for idx, item_name in enumerate(indexed_items):
|
846 |
+
if query.lower() in item_name.lower():
|
847 |
+
try:
|
848 |
+
item_seq = active_sale_items.loc[active_sale_items["ITEMNAME"] == item_name, "ITEMSEQ"].values[0]
|
849 |
+
matched_items.append({"ITEMSEQ": item_seq, "ITEMNAME": item_name, "match_type": "contains"})
|
850 |
+
except (IndexError, KeyError):
|
851 |
+
continue
|
852 |
+
|
853 |
+
# 정확히 일치하는 항목을 앞으로
|
854 |
+
exact_matches = []
|
855 |
+
partial_matches = []
|
856 |
+
|
857 |
+
for item in matched_items:
|
858 |
+
if query.lower() == item["ITEMNAME"].lower():
|
859 |
+
item["match_type"] = "exact"
|
860 |
+
exact_matches.append(item)
|
861 |
+
else:
|
862 |
+
partial_matches.append(item)
|
863 |
+
|
864 |
+
# 결합 및 제한
|
865 |
+
return exact_matches + partial_matches
|
866 |
+
|
867 |
+
# 비동기적으로 검색 실행
|
868 |
+
results = await loop.run_in_executor(thread_pool, _text_search)
|
869 |
+
|
870 |
+
logger.info(f"🔍 텍스트 검색 결과: {len(results)}개 찾음, 쿼리: '{query}'")
|
871 |
+
|
872 |
+
return {
|
873 |
+
"query": query,
|
874 |
+
"total_matches": len(results),
|
875 |
+
"results": results[:top_k]
|
876 |
+
}
|
877 |
+
except Exception as e:
|
878 |
+
logger.error(f"❌ 텍스트 검색 중 오류: {str(e)}")
|
879 |
+
raise HTTPException(status_code=500, detail=f"텍스트 검색 오류: {str(e)}")
|
880 |
+
|
881 |
# ✅ FastAPI 실행
|
882 |
if __name__ == "__main__":
|
883 |
# 서버 시작 시 저장된 인덱스 로드 시도
|