aikobay commited on
Commit
b8c7ffb
·
verified ·
1 Parent(s): cc53f17

Update searchAsyncSingle.py

Browse files
Files changed (1) hide show
  1. 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) * 1.5) # 계수를 4에서 1.5로 감소
359
- nlist = max(16, min(nlist, 256)) # 최대값을 1024에서 256으로 감소
360
 
361
  # 양자화 파라미터 - 차원 수에 맞게 조정
362
- M = min(32, dimension // 4) # 서브벡터 수
363
  nbits = 8 # 비트 수
364
 
365
  # 고속 IVF 인덱스 생성
@@ -378,7 +378,7 @@ async def rebuild_faiss_index():
378
 
379
  # 검색 품질 향상을 위한 설정
380
  # nprobe = 몇 개의 클러스터를 검색할지 (높을수록 정확도 ↑, 속도 ↓)
381
- index.nprobe = min(8, max(4, nlist // 16)) # 적은 클러스터 탐색 (nlist의 6.25%)
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 = 1): # top_n 2에서 1로 감소
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
- # 성능 중심 설정 (KeyBERT 파라미터 최적화)
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.3] # 임계값 상향 (0.2 → 0.3)
441
  return [k[0] for k in filtered]
442
  except Exception as e:
443
  logger.error(f"❌ 키워드 추출 오류: {str(e)}")
444
- # 단어 분리로 폴백 - 첫 단어만 사용
445
- return [words[0]] if words else [query]
446
-
447
 
448
 
449
  # ✅ 최적화된 키워드 확장 함수
450
- async def expand_keywords_with_word2vec(keywords: list, max_new=1): # max_new 2에서 1로 감소
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
- keyword = keywords[0]
466
-
467
- # 단일 단어인 경우
468
- if keyword in word2vec_model:
469
- # 유사도가 높은 단어만 선택 (임계값 적용)
470
- similar_words = word2vec_model.most_similar(keyword, topn=max_new)
471
- for word, score in similar_words:
472
- if score > 0.8: # 높은 유사도 임계값 적용 (0.7 → 0.8)
473
- expanded.add(word)
474
- # 복합어 처리 (첫 단어만)
475
- elif len(keyword.split()) > 1:
476
- word = keyword.split()[0]
477
- if word in word2vec_model and len(word) > 1:
478
- similar = word2vec_model.most_similar(word, topn=1)
479
- if similar and similar[0][1] > 0.85: # 임계값 더 높임 (0.8 → 0.85)
480
- expanded.add(similar[0][0])
481
-
482
- # 결과 변환 - 최대 3개로 제한 (이전 5개)
483
  result = list(expanded)
484
- if len(result) > 3:
485
- return keywords + result[len(keywords):3]
 
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 = 3, keywords=None, expanded_keywords=None): # top_k를 5에서 3으로 감소
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, batch_size=batch_size)
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
- # IVF 인덱스의 nprobe 감소 ( 적은 클러스터 검색)
554
- original_nprobe = faiss_index.nprobe
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
- # 키워드 벡터 배치 검색 - 키워드 벡터가 적으면 생략 (1개 이하)
570
- if keyword_vectors.shape[0] > 1:
571
- # IVF 인덱스의 nprobe 감소 (더 적은 클러스터 검색)
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.3) # 순위별 가중치 감소 더 빠르게 (0.2 → 0.3)
587
- weight = 0.5 * rank_weight # 가중치 감소 (0.6 → 0.5)
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.4] # 최소 점수 필터링 상향 (0.3 → 0.4)
602
 
603
  # 점수 기준 내림차순 정렬
604
  sorted_items = sorted(filtered_items, key=lambda x: x[1], reverse=True)
605
 
606
- # 최종 결과 변환 - 상위 N개만
607
  recommendations = []
608
- top_items = sorted_items[:top_k] # top_k개만 처리
609
-
610
- # 결과가 충분하면 상품 정보 조회
611
- if top_items:
612
- item_indices = [idx for idx, _ in top_items]
613
- item_names = [indexed_items[idx] for idx in item_indices if idx < len(indexed_items)]
614
-
615
- # 메모리 내 조회 최적화 - 한 번에 조회
616
- items_df = active_sale_items[active_sale_items["ITEMNAME"].isin(item_names)]
617
-
618
- # 결과 매핑
619
- for idx, score in top_items:
620
- if idx < len(indexed_items):
621
- item_name = indexed_items[idx]
622
- mask = items_df["ITEMNAME"] == item_name
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) == 0: # 결과가 없을 때만 직접 매칭 수행
638
- direct_matches = await find_direct_matches(query, min(2, top_k)) # 최대 2개만 가져오기
 
 
639
  if direct_matches:
640
  recommendations.extend(direct_matches)
641
 
642
- # 처리 시간 로깅 필요한 경우만
643
  elapsed = time.time() - start_time
644
- if elapsed > 0.5: # 임계값 하향 (1.0 → 0.5)
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), 10) # 1~10 범위로 제한 (이전 20)
712
-
713
- # 병렬 프로세싱을 위한 동시 실행 - 단순 쿼리는 키워드 처리 간소화
714
- if len(search_query) <= 5: # 짧은 쿼리는 모든 키워드 처리 건너뛰기 (길이 5 이하)
715
- keywords = [search_query.split()[0] if search_query.split() else search_query]
716
- expanded_keywords = keywords # 확장 없이 동일하게 사용
717
- else:
718
- keywords = await extract_keywords(search_query)
719
- if request.use_expansion:
720
- expanded_keywords = await expand_keywords_with_word2vec(keywords, max_new=1)
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
- # 응답 시간 측정 (0.5초 이상만 로깅)
741
  elapsed = time.time() - start_time
742
- if elapsed > 0.5:
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
  # 서버 시작 시 저장된 인덱스 로드 시도