@@ -11,10 +11,118 @@ export class PageRepository extends Repository<Page> implements OnModuleInit {
1111 CREATE EXTENSION IF NOT EXISTS vector
1212 ` ) ;
1313
14- // vector 컬럼 추가
14+ // vector 컬럼 추가 (존재 여부 확인 후 추가)
1515 await this . dataSource . query ( `
16- ALTER TABLE page ADD COLUMN embedding vector(384);
17- ` ) ;
16+ DO $$
17+ DECLARE
18+ column_exists BOOLEAN;
19+ index_exists BOOLEAN;
20+ BEGIN
21+ -- 컬럼 존재 여부 확인
22+ SELECT EXISTS (
23+ SELECT 1
24+ FROM pg_attribute
25+ WHERE attrelid = 'page'::regclass
26+ AND attname = 'embedding'
27+ ) INTO column_exists;
28+
29+ IF NOT column_exists THEN
30+ -- 컬럼이 없으면 추가
31+ EXECUTE 'ALTER TABLE page ADD COLUMN embedding vector(384)';
32+ END IF;
33+
34+ -- 인덱스 존재 여부 확인
35+ SELECT EXISTS (
36+ SELECT 1
37+ FROM pg_indexes
38+ WHERE tablename = 'page'
39+ AND indexname = 'page_embedding_hnsw_idx'
40+ ) INTO index_exists;
41+
42+ IF NOT index_exists THEN
43+ -- HNSW 인덱스 생성
44+ EXECUTE 'CREATE INDEX page_embedding_hnsw_idx ON page USING hnsw (embedding vector_ip_ops)';
45+ END IF;
46+
47+ -- GIN 인덱스 존재 여부 확인
48+ SELECT EXISTS (
49+ SELECT 1
50+ FROM pg_indexes
51+ WHERE tablename = 'page'
52+ AND indexname = 'page_fts_gin_idx'
53+ ) INTO index_exists;
54+
55+ IF NOT index_exists THEN
56+ -- GIN 인덱스 생성
57+ EXECUTE 'CREATE INDEX page_fts_gin_idx ON page USING gin(fts)';
58+ END IF;
59+ END $$;
60+
61+ create or replace function hybrid_search(
62+ query_text text,
63+ query_embedding vector(512),
64+ match_count int,
65+ full_text_weight float = 1,
66+ semantic_weight float = 1,
67+ rrf_k int = 50
68+ )
69+ returns setof page
70+ language sql
71+ as $$
72+ with full_text as (
73+ select
74+ id,
75+ -- Note: ts_rank_cd is not indexable but will only rank matches of the where clause
76+ -- which shouldn't be too big
77+ row_number() over(order by ts_rank_cd(fts, websearch_to_tsquery(query_text)) desc) as rank_ix
78+ from
79+ page
80+ where
81+ fts @@ websearch_to_tsquery(query_text)
82+ order by rank_ix
83+ limit least(match_count, 30) * 2
84+ ),
85+ semantic as (
86+ select
87+ id,
88+ row_number() over (order by embedding <#> query_embedding) as rank_ix
89+ from
90+ page
91+ order by rank_ix
92+ limit least(match_count, 30) * 2
93+ )
94+ select
95+ page.*
96+ from
97+ full_text
98+ full outer join semantic
99+ on full_text.id = semantic.id
100+ join page
101+ on coalesce(full_text.id, semantic.id) = page.id
102+ order by
103+ coalesce(1.0 / (rrf_k + full_text.rank_ix), 0.0) * full_text_weight +
104+ coalesce(1.0 / (rrf_k + semantic.rank_ix), 0.0) * semantic_weight
105+ desc
106+ limit
107+ least(match_count, 30)
108+ $$;
109+
110+
111+ ` ) ;
112+
113+ // fts 컬럼 추가 (존재 여부 확인 후 추가)
114+ await this . dataSource . query ( `
115+ DO $$
116+ BEGIN
117+ IF NOT EXISTS (SELECT 1 FROM pg_attribute
118+ WHERE attrelid = 'page'::regclass
119+ AND attname = 'fts')
120+ THEN
121+ ALTER TABLE page
122+ ADD COLUMN fts tsvector GENERATED ALWAYS AS (to_tsvector('english', document)) STORED;
123+ END IF;
124+ END $$;
125+ ` ) ;
18126 }
19127
20128 constructor ( @InjectDataSource ( ) private dataSource : DataSource ) {
0 commit comments