반응형

하이브리드 검색

의미 기반(Vector) 검색은 '뜻이 비슷한 것'은 잘 찾음. 

  • 하지만 에러 코드(ORA-01555), 특정 제품명, 고유명사처럼 '이 단어가 정확히 들어있어야 하는' 검색은 의미 기반 검색만으로는 놓치기 쉬움
  • 벡터는 '느낌'은 비슷해도 '정확한 문자열'을 보장하지 않기 때문에 전통적인 키워드 검색(Oracle Text)과 의미 기반 검색을 동시에 쓰고 결과를 합치는 것이 하이브리드 검색

결과를 어떻게 '합치나'(융합, Fusion)?

  • RSF (Relevance Score Fusion) 
    • 두 검색 각각의 점수 자체를 가중치로 '키워드 점수 90점 + 벡터 점수 70점'처럼 실제 점수 크기가 반영.
  • RRF (Reciprocal Rank Fusion) 
    • 점수 크기 대신 순위(1등, 2등, 3등...)만 보고 합침. 점수 스케일이 서로 다른 두 검색을 억지로 정규화할 필요가 없어서 안정적.
  • WRRF (Weighted RRF) 
    • RRF와 같은데, '키워드 쪽 순위를 더 중요하게' 같은 가중치를 추가로 줄 수 있는 버전

그리고 결과를 합칠 때 UNION(둘 중 하나라도 맞으면 포함) 방식으로 할지, INTERSECT(키워드+의미 둘 다 맞아야 포함) 방식으로 할지도 정할 수 있음.

[oracle@rhel10nm ~]$ --#권한 준비 + Vectorizer Preference 생성
[oracle@rhel10nm ~]$ id
uid=5001(oracle) gid=500(dba) groups=500(dba) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
[oracle@rhel10nm ~]$
[oracle@rhel10nm ~]$ env | grep SID
ORACLE_SID=CDB1
[oracle@rhel10nm ~]$ sqlplus "/as sysdba"
SQL> show con_name
CON_NAME
------------------------------
CDB$ROOT
SQL>
SQL> ALTER SESSION SET CONTAINER = PDB1;
Session altered.
SQL> show con_name
CON_NAME
------------------------------
PDB1
SQL> show user
USER is "SYS"
SQL>
SQL> GRANT CTXAPP TO VECLAB;
Grant succeeded.
SQL> GRANT CREATE SEQUENCE, CREATE TRIGGER, CREATE JOB TO VECLAB;
Grant succeeded.
SQL> CONNECT veclab/VecLab_2026#@//localhost:1521/pdb1
Connected.
SQL>
SQL> show user
USER is "VECLAB"
SQL>
SQL> --Vectorizer preference
SQL> BEGIN
  2  DBMS_VECTOR_CHAIN.CREATE_PREFERENCE(
  3  'lab_hybrid_vectorizer',
  4  DBMS_VECTOR_CHAIN.VECTORIZER,
  5  JSON('{
  6  "vector_idxtype": "hnsw",
  7  "model": "ALL_MINILM_L12_V2",
  8  "by": "words",
  9  "max": 30,
 10  "overlap": 8
 11  }')
 12  );
 13  END;
 14  /
PL/SQL procedure successfully completed.
SQL> --결과 확인
SQL> col PRE_NAME for a25
SQL> col PRE_OBJECT for a20
SQL> SELECT pre_name, pre_object FROM ctx_preferences WHERE pre_name = 'LAB_HYBRID_VECTORIZER';
PRE_NAME                  PRE_OBJECT
------------------------- --------------------
LAB_HYBRID_VECTORIZER     BASIC_VECTORIZER
SQL>
SQL> --Hybrid Vector Index 생성
SQL> CREATE HYBRID VECTOR INDEX lab_docs_hybrid_idx
  2  ON lab_raw_docs(doc_text)
  3  PARAMETERS('VECTORIZER lab_hybrid_vectorizer');
Index created.
SQL> --인덱스 생성에 시간이 좀 걸림(5개 문서를 내부적으로 청크+임베딩+키워드 색인까지 다 하는 과정)
SQL> 
SQL> COLUMN index_name FORMAT A25
SQL> SELECT index_name, index_type FROM user_indexes WHERE index_name = 'LAB_DOCS_HYBRID_IDX';
INDEX_NAME                INDEX_TYPE
------------------------- ---------------------------
LAB_DOCS_HYBRID_IDX       DOMAIN
SQL> --인덱스가 DOMAIN 타입 생성 확인
SQL>
SQL> --하이브리드 검색 실행
SQL> set echo on
SQL> SET LONG 100000
SQL> SET LONGCHUNKSIZE 100000
SQL> SET PAGESIZE 1000
SQL> SET LINESIZE 200
SQL>
SQL> SELECT JSON_SERIALIZE(
  2    DBMS_HYBRID_VECTOR.SEARCH(
  3      JSON('{
  4        "hybrid_index_name": "LAB_DOCS_HYBRID_IDX",
  5        "search_scorer": "rsf",
  6        "search_fusion": "UNION",
  7        "vector": {
  8          "search_text": "recovering from data loss",
  9          "search_mode": "DOCUMENT",
 10          "score_weight": 1,
 11          "rank_penalty": 5
 12        },
 13        "text": {
 14          "contains": "RMAN",
 15          "score_weight": 10,
 16          "rank_penalty": 1
 17        },
 18        "return": {
 19          "values": ["rowid", "score", "vector_score", "text_score"],
 20          "topN": 5
 21        }
 22      }')
 23    ) RETURNING CLOB PRETTY
 24  ) AS result
 25  FROM dual;
RESULT
---------------------------------------------------------------
[
  {
    "rowid" : "AAARluAAAAAAAN+AAC",
    "score" : 14.58,
    "vector_score" : 60.38,
    "text_score" : 10
  },
  {
    "rowid" : "AAARluAAAAAAAN+AAA",
    "score" : 5.46,
    "vector_score" : 60.1,
    "text_score" : 0
  },
  {
    "rowid" : "AAARluAAAAAAAN+AAD",
    "score" : 5.25,
    "vector_score" : 57.71,
    "text_score" : 0
  },
  {
    "rowid" : "AAARluAAAAAAAN+AAB",
    "score" : 5.2,
    "vector_score" : 57.18,
    "text_score" : 0
  },
  {
    "rowid" : "AAARluAAAAAAAN+AAE",
    "score" : 5.15,
    "vector_score" : 56.66,
    "text_score" : 0
  }
]
SQL> --에러 없이 JSON 텍스트가 출력 되어야 함
SQL>
SQL> --결과를 읽기 좋은 테이블로 변환
SQL> SET LINESIZE 200
SQL> COLUMN doc_title FORMAT A25
SQL>
SQL> SELECT dt.doc_id, dt.doc_title,
  2         jt.score, jt.vector_score, jt.text_score
  3  FROM JSON_TABLE(
  4          DBMS_HYBRID_VECTOR.SEARCH(
  5            JSON('{
  6              "hybrid_index_name": "LAB_DOCS_HYBRID_IDX",
  7              "search_scorer": "rsf",
  8              "search_fusion": "UNION",
  9              "vector": {
 10                "search_text": "recovering from data loss",
 11                "search_mode": "DOCUMENT",
 12                "score_weight": 1,
 13                "rank_penalty": 5
 14              },
 15              "text": {
 16                "contains": "RMAN",
 17                "score_weight": 10,
 18                "rank_penalty": 1
 19              },
 20              "return": {
 21                "values": ["rowid", "score", "vector_score", "text_score"],
 22                "topN": 5
 23              }
 24            }')
 25          ),
 26          '$[*]' COLUMNS (
 27              row_id       VARCHAR2(20) PATH '$.rowid',
 28              score        NUMBER       PATH '$.score',
 29              vector_score NUMBER       PATH '$.vector_score',
 30              text_score   NUMBER       PATH '$.text_score'
 31          )
 32       ) jt,
 33       lab_raw_docs dt
 34  WHERE dt.rowid = CHARTOROWID(jt.row_id)
 35  ORDER BY jt.score DESC;

    DOC_ID DOC_TITLE                      SCORE VECTOR_SCORE TEXT_SCORE
---------- ------------------------- ---------- ------------ ----------
         3 RMAN Guide                     14.58        60.38         10
         1 Oracle DB Guide                 5.46         60.1          0
         4 ASM Guide                       5.25        57.71          0
         2 RAC Guide                        5.2        57.18          0
         5 GoldenGate Guide                5.15        56.66          0

SQL> --1등이 doc_title = 'RMAN Guide', text_score=10(키워드 "RMAN" 실제로 포함)과 vector_score(의미상 "data loss recovery"와 유사) 둘 다 만족해서 압도적으로 높은 score를 받은 것임
SQL> --나머지 4개 문서(Data Guard, RAC, ASM, GoldenGate)는 text_score=0(RMAN이란 단어가 없음)이지만, 의미상으로는 어느 정도 관련 있어서 UNION 방식이라 결과에 포함된 것.
SQL>
SQL> --INTERSECT로 바꿔서 비교
SQL> col DOC_TITLE for a20
SQL> SELECT dt.doc_id, dt.doc_title,
  2         jt.score, jt.vector_score, jt.text_score
  3  FROM JSON_TABLE(
  4          DBMS_HYBRID_VECTOR.SEARCH(
  5            JSON('{
  6              "hybrid_index_name": "LAB_DOCS_HYBRID_IDX",
  7              "search_scorer": "rsf",
  8              "search_fusion": "INTERSECT",
  9              "vector": {
 10                "search_text": "recovering from data loss",
 11                "search_mode": "DOCUMENT",
 12                "score_weight": 1,
 13                "rank_penalty": 5
 14              },
 15              "text": {
 16                "contains": "RMAN",
 17                "score_weight": 10,
 18                "rank_penalty": 1
 19              },
 20              "return": {
 21                "values": ["rowid", "score", "vector_score", "text_score"],
 22                "topN": 5
 23              }
 24            }')
 25          ),
 26          '$[*]' COLUMNS (
 27              row_id       VARCHAR2(20) PATH '$.rowid',
 28              score        NUMBER       PATH '$.score',
 29              vector_score NUMBER       PATH '$.vector_score',
 30              text_score   NUMBER       PATH '$.text_score'
 31          )
 32       ) jt,
 33       lab_raw_docs dt
 34  WHERE dt.rowid = CHARTOROWID(jt.row_id)
 35  ORDER BY jt.score DESC;
    DOC_ID DOC_TITLE                 SCORE VECTOR_SCORE TEXT_SCORE
---------- -------------------- ---------- ------------ ----------
         3 RMAN Guide                14.58        60.38         10
SQL> --RMAN Guide 한 건만 출력되어야 함 / INTERSECT는 '키워드도 있고 + 의미도 맞아야' 하기 때문에, RMAN이란 단어가 없는 나머지 4개는 출력되지 않음
SQL>
SQL> --search_scorer 비교 (rsf vs rrf vs wrrf)
SQL> --같은 UNION 검색을 search_scorer만 바꿔서 수행
SQL> col DOC_TITLE for a20
SQL> -- RRF: rank-based fusion
SQL> SELECT dt.doc_id, dt.doc_title,
  2         jt.score, jt.vector_score, jt.text_score
  3  FROM JSON_TABLE(
  4          DBMS_HYBRID_VECTOR.SEARCH(
  5            JSON('{
  6              "hybrid_index_name": "LAB_DOCS_HYBRID_IDX",
  7              "search_scorer": "rrf",
  8              "search_fusion": "UNION",
  9              "vector": {
 10                "search_text": "recovering from data loss",
 11                "search_mode": "DOCUMENT",
 12                "score_weight": 1,
 13                "rank_penalty": 5
 14              },
 15              "text": {
 16                "contains": "RMAN",
 17                "score_weight": 10,
 18                "rank_penalty": 1
 19              },
 20              "return": {
 21                "values": ["rowid", "score", "vector_score", "text_score"],
 22                "topN": 5
 23              }
 24            }')
 25          ),
 26          '$[*]' COLUMNS (
 27              row_id       VARCHAR2(20) PATH '$.rowid',
 28              score        NUMBER       PATH '$.score',
 29              vector_score NUMBER       PATH '$.vector_score',
 30              text_score   NUMBER       PATH '$.text_score'
 31          )
 32       ) jt,
 33       lab_raw_docs dt
 34  WHERE dt.rowid = CHARTOROWID(jt.row_id)
 35  ORDER BY jt.score DESC;
    DOC_ID DOC_TITLE                 SCORE VECTOR_SCORE TEXT_SCORE
---------- -------------------- ---------- ------------ ----------
         3 RMAN Guide                  100        60.38         10
         1 Oracle DB Guide           28.57         60.1          0
         4 ASM Guide                 25.89        57.71          0
         2 RAC Guide                 23.81        57.18          0
         5 GoldenGate Guide          22.14        56.66          0
SQL> -- WRRF: rank-based fusion
SQL> SELECT dt.doc_id, dt.doc_title,
  2         jt.score, jt.vector_score, jt.text_score
  3  FROM JSON_TABLE(
  4          DBMS_HYBRID_VECTOR.SEARCH(
  5            JSON('{
  6              "hybrid_index_name": "LAB_DOCS_HYBRID_IDX",
  7              "search_scorer": "wrrf",
  8              "search_fusion": "UNION",
  9              "vector": {
 10                "search_text": "recovering from data loss",
 11                "search_mode": "DOCUMENT",
 12                "score_weight": 1,
 13                "rank_penalty": 5
 14              },
 15              "text": {
 16                "contains": "RMAN",
 17                "score_weight": 10,
 18                "rank_penalty": 1
 19              },
 20              "return": {
 21                "values": ["rowid", "score", "vector_score", "text_score"],
 22                "topN": 5
 23              }
 24            }')
 25          ),
 26          '$[*]' COLUMNS (
 27              row_id       VARCHAR2(20) PATH '$.rowid',
 28              score        NUMBER       PATH '$.score',
 29              vector_score NUMBER       PATH '$.vector_score',
 30              text_score   NUMBER       PATH '$.text_score'
 31          )
 32       ) jt,
 33       lab_raw_docs dt
 34  WHERE dt.rowid = CHARTOROWID(jt.row_id)
 35  ORDER BY jt.score DESC;
    DOC_ID DOC_TITLE                 SCORE VECTOR_SCORE TEXT_SCORE
---------- -------------------- ---------- ------------ ----------
         3 RMAN Guide                  100        60.38         10
         1 Oracle DB Guide           11.98         60.1          0
         4 ASM Guide                 11.64        57.71          0
         2 RAC Guide                 11.37        57.18          0
         5 GoldenGate Guide          11.15        56.66          0
SQL> --RMAN Guide가 1등이라는 순위 자체는 세 방식(rsf/rrf/wrrf) 모두 동일
SQL> --다만 score 컬럼의 절대값과, 2~5등 사이의 점수 격차가 방식마다 달라 짐
SQL> exit
[oracle@rhel10nm ~]$

 

Score 1위(RMAN) 2위 3위 4위 5위 2~5위간 점수폭
RSF 14.58 5.46 5.25 5.2 5.15 0.31
RRF 100 28.57 25.89 23.81 22.14 6.43
WRRF 100 11.98 11.64 11.37 11.15 0.82

 

세 방식 모두 순위(등수) 자체는 완전히 동일하며 text_score가 0으로 동률인 2~5위는 결국 vector_score 순서(60.1 > 57.71 > 57.18 > 56.66)를 그대로 따랐기 때문 임.

다만 점수 간 '격차'의 표현 방식이 확연히 다름

  • RSF는 원래 점수(가중합)를 그대로 반영해서, 5개 문서의 vector_score 자체가 원래 비슷비슷했던 것(56~60점대)이 그대로 드러나 2~5위 격차가 거의 없음.
  • RRF는 등수 기반이라 1위와 100점으로 정규화되고, 나머지는 '몇 등이냐'로 계산되면서 RSF보다 격차가 더 벌어 짐.
  • WRRF는 RRF와 같은 등수 기반인데, text_score 쪽에 준 높은 가중치(score_weight:10)가 '키워드 없는 나머지'를 더 강하게 한 덩어리로 해서, 2~5위끼리는 오히려 RRF보다 더 좁게 뭉처 짐.

원본 점수 스케일을 그대로 믿고 싶으면 RSF, 검색엔진마다 점수 스케일이 달라 신뢰하기 어려우면 RRF, 거기에 '그래도 키워드 쪽에 더 무게를 싣고 싶다'면 WRRF.

 

항목 확인 결과
Vectorizer Preference + Hybrid Vector Index 원본 텍스트 컬럼에 직접 생성 완료
UNION 검색 키워드 없어도 의미 유사하면 포함 확인
INTERSECT 검색 키워드+의미 둘 다 맞는 것만 남는 것 확인
RSF/RRF/WRRF 비교 순위는 동일, 점수 격차 표현 방식 차이 확인

 

반응형

+ Recent posts