diff --git a/_posts/2024-01-27-postgres-16-korean.md b/_posts/2024-01-27-postgres-16-korean.md
index d93bbf0e31..191e70c3b2 100644
--- a/_posts/2024-01-27-postgres-16-korean.md
+++ b/_posts/2024-01-27-postgres-16-korean.md
@@ -6,7 +6,7 @@ tags: ["extension", "korean", "collate", "rum", "gin"]
image: "https://dyclassroom.com/image/topic/postgresql/postgresql.jpg"
---
-> Postgresql 에서 한글 검색을 위한 gin 과 rum 인덱스를 살펴보고, 예제 데이터로 검색 사례들을 테스트 한다.
+> Postgresql 에서 한글 검색을 위한 gin 과 rum 인덱스를 살펴보고, 한글 텍스트 데이터로 검색 사례들을 테스트 합니다.
{: .prompt-tip }
- [PostgreSQL 15 한글 검색 설정](/posts/2023-05-14-postgres-15-korean/) : ko-x-icu, mecab
@@ -16,7 +16,7 @@ image: "https://dyclassroom.com/image/topic/postgresql/postgresql.jpg"
### 데이터베이스 설정
-> 기본 collate 는 'C.utf8' 이지만, 필요시 한글 텍스트 컬럼은 'ko-x-icu' 를 지정하면 된다.
+> 기본 collate 는 'C.utf8' 이지만, 한글 정렬 필요시 컬럼별로 'ko-x-icu' 를 지정하면 된다.
```sql
-- 데이터베이스 생성: tablespace, owner, collate
@@ -93,20 +93,20 @@ limit 5;
### [textsearch 함수 및 연산자](https://www.postgresql.org/docs/current/functions-textsearch.html)
```sql
-# '제주시' 검색
-> select content from {DB}
+-- '제주시' 검색
+select content from {DB}
where to_tsvector('korean', content)
@@ to_tsquery('korean','제주시')
limit 5;
-# not '제주' 검색
-> select content from {DB}
+-- not '제주' 검색
+select content from {DB}
where to_tsvector('korean', content)
@@ to_tsquery('korean','!제주')
limit 5;
-# '서귀포 & 모슬포' 검색
-> select content from {DB}
+-- '서귀포 & 모슬포' 검색
+select content from {DB}
where to_tsvector('korean', content)
@@ to_tsquery('korean','서귀포 & 모슬포')
order by content limit 5;
@@ -127,7 +127,7 @@ limit 5;
- `websearch_to_tsquery('english', '"fat rat" or cat dog') → 'fat' <-> 'rat' | 'cat' & 'dog'` :
웹검색처럼 텍스트를 tsquery 로 변환 (구절 검색, or/and 연산자)
- `plainto_tsquery('english', 'The Fat Rats') → 'fat' & 'rat'` :
- 텍스트를 term 들의 tsquery 로 변환
+ 텍스트를 '&'로 연결된 tsquery 로 변환
- `phraseto_tsquery('english', 'The Fat Rats') → 'fat' <-> 'rat'`
`phraseto_tsquery('english', 'The Cat and Rats') → 'cat' <2> 'rat'` :
텍스트 문장을 term 들과 연결관계를 포함된 tsquery 로 변환
@@ -136,13 +136,36 @@ limit 5;
우선순위 문자(A/B/C/D) 를 tsvector 에 부여
- `ts_headline('The fat cat ate the rat.', 'cat') → The fat cat ate the rat.` :
(tsvector 가 아니라) raw 문자열에서 매칭된 단어를 강조하여 출력
-- `ts_rank(to_tsvector('raining cats and dogs'), 'cat') → 0.06079271` :
- tf 기반 매칭 점수를 반환 (비교: `ts_rank_cd` 는 밀도 기반이라 더 정확하다)
- - [가중치 A/B/C/D 사용시 기본 가중치 사용](https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-RANKING) : `{A:1.0, B:0.4 C:0.2 D:0.1}`
- `tsvector_to_array ( tsvector ) → text[]` :
tsvector 로부터 token 배열을 출력
- `ts_stat('SELECT vector FROM apod')` :
문서(record)의 document statistics 출력 (word, 문서수, 총 TF)
+- `ts_rank(to_tsvector('raining cats and dogs'), 'cat') → 0.06079271` :
+ tf 기반 매칭 점수를 반환 (비교: `ts_rank_cd` 는 밀도 기반 랭킹을 사용)
+ - [가중치 A/B/C/D 사용시 기본 가중치 사용](https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-RANKING) : `{A:1.0, B:0.4 C:0.2 D:0.1}`
+
+> ts_rank 함수의 normalization 옵션 (flag bit)
+
+- 기본값 0 : 문서 길이를 무시
+- 1 : `1 + log(문서길이)` 로 나누기 (짧은 문서에서 우위)
+- 4 : 근접우선인 조화평균거리로 나누기 (ts_rank_cd 에서만 사용)
+- 8 : 문서의 유일 단어수로 나누기 (문서의 정보성)
+- 32 : `rank / (rank+1)` (백분율 환산)
+
+```sql
+SELECT title,
+ ts_rank_cd(textsearch, query, 32 /* rank/(rank+1) */ ) AS rank
+FROM apod, to_tsquery('neutrino|(dark & matter)') query
+WHERE query @@ textsearch
+ORDER BY rank DESC
+LIMIT 10;
+-- title | rank
+-- -----------------------------------------+-------------------
+-- Neutrinos in the Sun | 0.756097569485493
+-- The Sudbury Neutrino Detector | 0.705882361190954
+-- A MACHO View of Galactic Dark Matter | 0.668123210574724
+-- Hot Gas and Dark Matter | 0.65655958650282
+```
#### 단어별 [document statistics](https://www.postgresql.org/docs/current/textsearch-features.html#TEXTSEARCH-STATISTICS) 출력
@@ -154,9 +177,9 @@ limit 5;
```sql
SELECT * FROM ts_stat('SELECT gin_vec FROM public.test_gin')
-where length(word) > 2
-ORDER BY nentry DESC, ndoc DESC, word
-LIMIT 10;
+ where length(word) > 2
+ ORDER BY nentry DESC, ndoc DESC, word
+ LIMIT 10;
-- ------------------------
-- | word | ndoc | nentry |
-- ------------------------
@@ -273,7 +296,7 @@ explain (analyze)
-- | "title" | "rnk" |
-- --------------------
-- "고성림 서귀포해경서장 취임 ‘희망의 서귀포 바다 만들겠다’" 1.064899
--- "고성림 서귀포해양경찰서장 취임...‘안전하고 깨끗한 서귀포 바다 만들 것’" 1.0530303
+-- "고성림 서귀포해양경찰서장 취임...‘안전하고 깨끗한 서귀포...’" 1.0530303
-- "서귀포시, ‘제25회 서귀포 겨울바다 국제펭귄수영대회’ 개최" 0.6617919
-- "문화재청, 서귀포 문섬 바다 관광잠수함 운항 재허가 '불허'" 0.60041153
-- "제주 거점 여성 문학인 동백문학회, ‘동백문학 3호’ 발간" 0.05
@@ -315,7 +338,7 @@ insert into rum_test
) t(c1);
-- 20만건 생성
-$ pgbench -M prepared -n -r -P 1 -f ./test.sql -c 50 -j 50 -t 200000
+-- $ pgbench -M prepared -n -r -P 1 -f ./test.sql -c 50 -j 50 -t 200000
-- 색인 생성
set maintenance_work_mem ='64GB';
@@ -330,10 +353,14 @@ explain analyze
------------------------------------------
QUERY PLAN
------------------------------------------
- Limit (cost=18988.45..19088.30 rows=100 width=1391) (actual time=58.912..59.165 rows=100 loops=1)
- -> Index Scan using rumidx on rum_test (cost=16.00..99620.35 rows=99749 width=1391) (actual time=16.426..57.892 rows=19100 loops=1)
- Index Cond: (c1 @@ '''1'' | ''2'''::tsquery)
- Order By: (c1 <=> '''1'' | ''2'''::tsquery)
+ Limit
+ (cost=18988.45..19088.30 rows=100 width=1391)
+ (actual time=58.912..59.165 rows=100 loops=1)
+ -> Index Scan using rumidx on rum_test
+ (cost=16.00..99620.35 rows=99749 width=1391)
+ (actual time=16.426..57.892 rows=19100 loops=1)
+ Index Cond: (c1 @@ '''1'' | ''2'''::tsquery)
+ Order By: (c1 <=> '''1'' | ''2'''::tsquery)
Planning time: 0.133 ms
Execution time: 59.220 ms
(6 rows)
@@ -364,6 +391,11 @@ sudo -u postgres psql -d {DB} -c `create extension rum`;
### rum 검색 테스트 (예제)
+RUM 은 ts_rank 와 ts_tank_cd 을 결합한 새로운 ranking 함수를 사용한다. ([출처](https://pgconf.ru/media/2017/04/03/20170316H3_Korotkov-rum.pdf))
+
+- ts_rank 는 논리 연산자를 지원하지 않고
+- ts_rank_cd 는 OR 쿼리에서 잘 작동하지 않는다.
+
> 더 많은 내용은 RUM 깃허브 [readme 문서](https://github.com/postgrespro/rum/blob/master/README.md) 참조
- text 컬럼 't' 를 tsvector 로 색인
diff --git a/_posts/2024-02-01-postgres-pgvector-day1.md b/_posts/2024-02-01-postgres-pgvector-day1.md
new file mode 100644
index 0000000000..5eb9eaad04
--- /dev/null
+++ b/_posts/2024-02-01-postgres-pgvector-day1.md
@@ -0,0 +1,323 @@
+---
+date: 2024-02-01 00:00:00 +0900
+title: PostgreSQL pgvector - 1일차
+categories: ["database", "postgres"]
+tags: ["pg16", "korean", "pgvector", "1st-day"]
+image: "https://dyclassroom.com/image/topic/postgresql/postgresql.jpg"
+---
+
+> PostgreSQL 에서 한글 검색을 위한 pgvector 사용법을 알아봅니다. 우선은 pgvector 설정부터 시작합니다.
+{: .prompt-tip }
+
+## 1. [pgvector](https://github.com/pgvector/pgvector) 설정
+
+### pgvector 설치
+
+```bash
+git clone --branch v0.6.0 https://github.com/pgvector/pgvector.git
+cd pgvector
+
+sudo make
+sudo make install
+
+sudo -u postgres psql -d {DB} -c 'CREATE EXTENSION vector'
+```
+
+> [supabase 의 pgvector](https://supabase.com/docs/guides/database/extensions/pgvector) 는 `create extension` 만 하면 된다.
+
+```bash
+# 로컬 docker 인스턴스 접속
+psql -h localhost -p 54322 -d postgres -U postgres
+```
+
+### pgvector 쿼리
+
+```sql
+CREATE TABLE items (
+ id bigserial PRIMARY KEY,
+ embedding vector(3)
+ );
+-- 또는 ALTER TABLE items ADD COLUMN embedding vector(3);
+
+-- upsert vectors
+INSERT INTO items (id, embedding) VALUES
+ (1, '[1,2,3]'),
+ (2, '[4,5,6]'),
+ (3, '[6,3,1]')
+ ON CONFLICT (id) DO
+ UPDATE SET embedding = EXCLUDED.embedding;
+
+-- --------------------------------------------------
+-- query : distacne, inner_product, cosine_similarity
+-- --------------------------------------------------
+
+SELECT id, embedding,
+ embedding <-> '[3,1,2]' as dist
+ FROM items
+ ORDER BY dist;
+-- "id" | "embedding" | "dist"
+-- -------------------------------
+-- 1 "[1,2,3]" 2.449489742783178
+-- 3 "[6,3,1]" 3.7416573867739413
+-- 2 "[4,5,6]" 5.744562646538029
+
+SELECT embedding,
+ (embedding <#> '[3,1,2]') * -1 AS inner_product
+ FROM items;
+-- "embedding" | "inner_product"
+-- -----------------------------
+-- "[1,2,3]" 11
+-- "[4,5,6]" 29
+-- "[6,3,1]" 23
+
+SELECT embedding,
+ 1 - (embedding <=> '[3,1,2]') AS cosine_similarity
+ FROM items;
+-- "embedding" | "cosine_similarity"
+-- ---------------------------------
+-- "[1,2,3]" 0.7857142857142857
+-- "[4,5,6]" 0.8832601106161003
+-- "[6,3,1]" 0.9063269671749657
+
+-- --------------------------------------------------
+-- Aggregation : avg
+-- --------------------------------------------------
+
+SELECT AVG(embedding) FROM items;
+-- "[3.6666667,3.3333333,3.3333333]"
+```
+
+### [HNSW](https://github.com/pgvector/pgvector/blob/master/README.md#hnsw) 인덱스
+
+Approximate NN(근사적인 근접 이웃) 탐색을 위해 사용되는 그래프 기반 인덱스이다.
+
+- 거리 (L2 distance) `vector_l2_ops`
+- 내적 (Inner product) `vector_ip_ops`
+- 코사인 (Cosine distance) `vector_cosine_ops`
+
+```sql
+SET maintenance_work_mem = '8GB';
+SET max_parallel_maintenance_workers = 7; -- speed up build (default=2)
+SET max_parallel_workers_per_gather = 4; -- speed up query
+
+CREATE INDEX ON items USING hnsw (embedding vector_l2_ops);
+CREATE INDEX ON items USING hnsw (embedding vector_ip_ops);
+CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops);
+```
+
+> HNSW vs IVFFlat [인덱스 비교](https://tembo.io/blog/vector-indexes-in-pgvector#picking-the-right-index-for-your-use-case)
+
+- 인덱스 크기와 빌드 시간이 중요하면 IVFFlat 를 선택 => 대규모 정적 데이터
+ - lists 크기의 클러스터를 중심점을 기준으로 vector 탐색
+- 검색 속도와 업데이트 반영이 중요하다면 HNSW 를 선택 => 소규모 동적 데이터
+ - multi-layer 그래프 기반으로 가까운 node(vector) 를 탐색
+
+| 비교 | IVFFlat | HNSW |
+| :--- | :--- | :-- |
+| Build Time (in seconds) | 128 | 4,065 |
+| Size (in MB) | 257 | 729 |
+| Speed (in QPS) | 2.6 | 40.5 |
+| 업데이트 이후 리콜 영향 | Significant | Negligible |
+
+
+## 2. openai 임베딩으로 vector 변환
+
+### foot 리뷰 데이터
+
+- 1천건
+- CSV 다운로드 : [fine_food_reviews_1k.csv](https://github.com/openai/openai-cookbook/blob/main/examples/data/fine_food_reviews_1k.csv)
+ - embedding 포함 CSV : [fine_food_reviews_with_embeddings_1k.csv](https://github.com/openai/openai-cookbook/blob/main/examples/data/fine_food_reviews_with_embeddings_1k.csv)
+
+```sql
+-- drop table if exists foodreview1k;
+create table foodreview1k (
+ id bigserial primary key,
+ regdt bigint, /* to_timestamp() at time zone 'Asia/Seoul', */
+ productid text,
+ userid text,
+ score smallint,
+ summary text,
+ body text
+)
+
+-- psql 에서 실행
+\copy foodreview1k(id, regdt, productid, userid, score, summary, body) from '$HOME/Downloads/fine_food_reviews_1k.csv' delimiter ',' csv header;
+
+-- supabase : Row Level Security
+alter table foodreview1k enable row level security;
+-- supabase : policy
+create policy "foodreview1k are viewable by everyone"
+on foodreview1k for select
+to authenticated, anon
+using ( true );
+
+-------------------------------------------
+
+drop table if exists foodreview1k_embedding;
+create table foodreview1k_embedding (
+ id bigserial primary key,
+ productid text,
+ userid text,
+ score smallint,
+ summary text,
+ body text,
+ combined text,
+ n_tokens int,
+ embedding vector
+)
+
+-- psql 에서 실행
+\copy foodreview1k_embedding(id, productid, userid, score, summary, body, combined, n_tokens, embedding) from '/Volumes/SSD2T_WORK/Users/bgmin/Downloads/fine_food_reviews_with_embeddings_1k.csv' delimiter ',' csv header;
+
+-- supabase : Row Level Security
+alter table foodreview1k_embedding enable row level security;
+-- supabase : policy
+create policy "foodreview1k_embedding are viewable by everyone"
+on foodreview1k_embedding for select
+to authenticated, anon
+using ( true );
+
+-- TEST
+select id, data.embedding <-> query.embedding as dist, summary
+from foodreview1k_embedding data,
+(select embedding from foodreview1k_embedding where id=0) query
+order by dist
+limit 5;
+-- 0 0 "where does one start... with a treat like this"
+-- 726 0.915856064307327 "Perfect treat"
+-- 359 0.924484765683111 "Wonderful!"
+-- 59 0.950686317853922 "Yummy & Great Small Gift"
+-- 907 0.953927412158517 "NTune@60"
+```
+
+### openai api 로 임베딩하기
+
+- 0번 리뷰 문서의 summary(제목) 만으로 임베딩 벡터 변환 요청
+- postgresql 에서 api 를 요청해서 임베딩 벡터를 가져오기
+ - json 의 텍스트 결과에서 new-lines, spaces 등을 제거
+ - vector 타입으로 변환
+- foodreview1k_embedding 의 임베딩 벡터와의 거리 기준 상위 5개 문서 가져오기
+ - 앞에서 자신의 임베딩 벡터로 뽑은 상위 5개 문서와 비교하기
+
+#### [pgsql-http](https://github.com/pramsey/pgsql-http) : HTTP Client
+
+postgresql 에서 OPENAI API 를 간단히 사용하기 위해 extension 을 설치한다.
+
+- http : http_get, http_post, http_put, http_patch, http_delete
+- [http_request](https://github.com/pramsey/pgsql-http/blob/master/http--1.6.sql)
+ - method
+ - uri
+ - headers : http_header[]
+ - content_type
+ - content
+
+```sql
+-- supabase
+create extension http;
+
+SELECT urlencode('my special string''s & things?');
+-- "my+special+string%27s+%26+things%3F"
+
+SELECT content::json->>'origin' as origin
+ FROM http_get('http://httpbun.com/ip');
+-- {자신의 WAN IP}
+
+SELECT content::json->>'Authorization'
+ FROM http((
+ 'GET',
+ 'http://httpbun.com/headers',
+ ARRAY[http_header('Authorization','Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9')],
+ NULL,
+ NULL
+ )::http_request);
+-- "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"
+
+
+SELECT (unnest(headers)).*
+ FROM http_get('http://httpbun.com/');
+```
+
+#### openai API 사용법
+
+- API_KEY 가 필요하다.
+
+```bash
+curl https://api.openai.com/v1/embeddings \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer $OPENAI_API_KEY" \
+ -d '{
+ "input": "Your text string goes here",
+ "model": "text-embedding-3-small"
+ }'
+```
+
+#### 쿼리 임베딩 벡터로 거리 기준 5개 문서 출력
+
+```sql
+create table foodreview1k_query(
+ id bigserial primary key,
+ summary text,
+ embedding vector
+);
+
+-- openapi 로부터 embedding 결과를 받아 테이블에 저장
+insert into foodreview1k_query
+with
+ query_text as (
+ select 0 as id
+ ,'where does one start...and stop... with a treat like this' as summary
+ ),
+ query_embedding as (
+ select content::json->'data'->0->>'embedding' as embedding
+ FROM http((
+ 'POST',
+ 'https://api.openai.com/v1/embeddings',
+ ARRAY[http_header('Authorization','Bearer $OPENAI_API_KEY')],
+ 'application/json',
+ '{
+ "input": "Your text string goes here",
+ "model": "text-embedding-3-small"
+ }'
+ )::http_request)
+ )
+ select regexp_replace(embedding,'[\r\n\t ]', '', 'g')::vector as qvector
+ from query_embedding;
+
+-- 리뷰 테이블에 쿼리 벡터로 유사 문서 검색
+select id, data.embedding <-> query.embedding as dist, summary
+from foodreview1k_embedding data,
+(select embedding from foodreview1k_query where id=0) query
+order by dist
+limit 5;
+-- 0 0.832656749864305 "where does one start... with a treat like this"
+-- 726 1.06856213209488 "Perfect treat"
+-- 390 1.12354179478311 "Good Girl Treats (GGTs)"
+-- 269 1.12738237577285 "Fantastic -- But ..."
+-- 194 1.13635072197582 "More Good Stuff"
+```
+
+> (이전 원본 embedding 쿼리 결과와) 0번 리뷰의 summary 벡터를 쿼리한 결과 비교
+
+| rank | 원본 벡터 | | summary 쿼리 | |
+| | idx | dist | idx | dist |
+| :--- | :--- | :--- | :--- | :--- |
+| 1 | 0 | 0 | 0 | 0.832656749864305 |
+| 2 | 726 | 0.915856064307327 | 726 | 1.06856213209488 |
+| 3 | 359 | 0.924484765683111 | 390 | 1.12354179478311 |
+| 4 | 59 | 0.950686317853922 | 269 | 1.12738237577285 |
+| 5 | 907 | 0.953927412158517 | 194 | 1.13635072197582 |
+
+
+## 9. Review
+
+- supabase 의 pg 에는 여러 확장 기능들이 준비되어 있어 편리하다.
+ - Row Level Security 와 policy 도 사용해 보았다.
+- 영문 리뷰 데이터를 대상으로 임베딩과 pg 벡터 연산자를 사용해보았다.
+ - 일단 생각대로 잘 되었다. 한글 데이터에서는 어떻게 될지 모르겠지만.
+- 여기까지 하고 다음 문서에서 계속하자.
+
+
+
+
+> **끝!** 읽어주셔서 감사합니다.
+{: .prompt-info }