AWS 기술 블로그
PostgreSQL의 고급 검색 기능을 사용한 구직 검색 엔진 구축
이 글은 AWS Database Blog에 게시된 Building a job search engine with PostgreSQL’s advanced search features by Ezat Karimi를 한국어로 번역 및 편집하였습니다.
오늘날의 고용 환경에서 구직 플랫폼은 고용주와 잠재적 후보자를 연결하는 데 중요한 역할을 합니다. 이러한 플랫폼 뒤에는 방대한 양의 정형 및 비정형 데이터를 처리하고 분석하여 관련성 높은 결과를 제공하는 복잡한 검색 엔진이 있습니다. 이러한 시스템을 구축하려면 복잡한 쿼리, 전체 텍스트 검색(Full-text search), 의미 이해를 위한 의미론적 검색(Semantic search), 위치 기반 추천을 위한 지리공간 기능을 처리할 수 있는 데이터베이스 기술이 필요합니다.
PostgreSQL은 포괄적인 검색 기능을 제공하여 구직 플랫폼 구현에 이상적인 솔루션으로 자리매김했습니다. PostgreSQL은 단일 데이터베이스 시스템 내에서 전체 텍스트 검색, 벡터 유사성 검색, 지리공간 검색(Geospatial search)을 독창적으로 결합합니다. 이러한 기능의 융합을 통해 개발자는 여러 전문 시스템을 관리하지 않고도 풍부한 검색 환경을 구축할 수 있습니다.
이 글에서는 PostgreSQL의 검색 기능을 활용하여 효과적인 구직 검색 엔진을 구축하는 방법을 살펴봅니다. 각 검색 기능을 자세히 살펴보고, PostgreSQL에서 이러한 기능을 어떻게 결합할 수 있는지 논의하며, 검색 엔진 확장에 따른 성능 최적화 전략을 제시합니다.
또한, 이 글에서 제시하는 기술이 전자상거래 상품 검색 및 콘텐츠 추천과 같은 다양한 분야의 검색 기반 애플리케이션에 어떻게 광범위하게 적용될 수 있는지에 대해서도 살펴보겠습니다.
현대 구직 검색 엔진의 구조
PostgreSQL의 구체적인 검색 기능을 살펴보기 전에, 현대 구직 플랫폼의 기본 구성 요소와 요구 사항을 이해하는 것이 중요합니다. 구직 검색 엔진은 다음과 같은 두 가지 핵심 요소로 구성됩니다.
데이터 저장소 – 모든 구직 플랫폼의 기반은 다양한 출처의 구인 공고를 저장하는 데이터베이스입니다. 이 데이터베이스는 웹 크롤러, 고용주의 공고 게시, 구인 게시판, 채용 플랫폼과의 통합을 통해 지속적으로 업데이트됩니다. 또한, 이 저장소에는 구직자의 프로필과 이력서도 저장됩니다.
검색 엔진 – 검색 엔진은 양방향 검색을 지원합니다. 구직자와 고용주가 기회를 찾고, 쿼리를 처리하고, 정형화된 데이터(직책, 위치, 급여 범위 등)와 비정형화된 콘텐츠(직무 설명, 구직자 이력서 등)를 분석 및 결합할 수 있습니다. 고급 검색 엔진은 단순 키워드 매칭을 넘어 맥락을 이해하고, 동의어를 처리하고, 관련 개념을 인식하고, 위치 기반 제약을 고려합니다.
효과적인 구직 엔진에는 다음과 같은 기능이 필요합니다.
- 전체 텍스트 검색(Full-text search) – 이 기능은 직책, 기술 및 조직 이름에 대한 정확한 어휘 매칭을 제공합니다. 정확한 구문 매칭과 부분 매칭에 대한 오타 허용 퍼지(fuzzy searches) 검색을 지원합니다. Full-text search는 사용자가 특정 검색 기준을 명확하게 표현할 수 있을 때 효과적이지만, 문맥적 이해는 부족합니다.
- 의미론적 검색(Semantic search) – 벡터 기반 유사성 검색은 직무 설명과 지원 자격을 문자 그대로의 용어로 해석하는 것을 넘어, 중요한 문맥적 이해를 제공합니다. 이는 키워드 매칭으로는 놓치기 쉬운 미묘한 전문적 관계와 암묵적인 요구 사항 및 자격을 포착하여 후보자와 직책이 더욱 지능적으로 매칭될 수 있도록 합니다.
- 지리공간 검색(Geospatial search) – 위치 정보는 지리적 고려 사항을 통합하여 결과를 개선하므로, 사용자가 특정 거리, 통근 시간 기준, 또는 지역 경계 내에서 기회를 찾을 수 있도록 합니다. 이를 통해 전문 자격과 실제 고용 시장 현실을 연결합니다.
이처럼 상호 보완적인 검색 기술을 통합함으로써, 구직 엔진은 정확한 용어, 문맥적 의미 및 지리적 고려 사항을 동시에 평가하는 정교한 쿼리를 처리하여 점점 더 복잡해지는 고용 환경에서 더 관련성이 높은 검색 결과를 제공할 수 있습니다.
종합적인 검색 솔루션으로서의 PostgreSQL
PostgreSQL은 강력한 데이터 저장소이자 고급 검색 엔진이라는 두 가지 목적을 모두 달성합니다. PostgreSQL은 내장된 기능과 확장 기능을 통해 단일 시스템 내에서 세 가지 필수 검색 차원을 모두 처리할 수 있습니다.
- tsvector, tsquery, GIN 인덱스와 같은 내장 타입을 사용하는 전체 텍스트 검색(Full-text search)
- pgvector 확장을 통해 의미론적 일치를 위한 검색을 수행하는 벡터 유사성 검색(Vector similarity search)
- GiST 인덱스를 사용하는 PostGIS 확장을 통한 지리공간 쿼리(Geospatial queries)
구직 검색 엔진은 PostgreSQL을 사용하여 구인 공고와 구직자 프로필을 저장하고, 수백만 개의 이력서와 구직 공고에 대한 실시간 전체 텍스트 검색 및 의미론적 검색을 제공하며, 지정된 지리적 반경 내에서 일치하는 구인 공고를 찾습니다. 이 통합된 접근 방식은 아키텍처를 간소화하고 운영 복잡성을 줄이며, 하이브리드 검색 전략을 가능하게 합니다.
다음과 같이 “job” 테이블과 “resume” 테이블로 구성된 구직 검색 엔진의 간단한 데이터 모델을 고려해 보겠습니다. 이 테이블에는 벡터 데이터, 벡터 임베딩, 그리고 구직자 위치와 지원자에 대한 지리 정보를 저장하기 위한 tsvector, vector, geometry 유형의 열이 있습니다.
CREATE TABLE job (
job_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
company TEXT,
title TEXT NOT NULL,
description TEXT,
title_tsv TSVECTOR -- Computed full-text search vectors
GENERATED ALWAYS AS (to_tsvector('english', title)) STORED,
description_tsv TSVECTOR
GENERATED ALWAYS AS (to_tsvector('english', description)) STORED,
semantic_vector VECTOR(3)
GENERATED ALWAYS AS (
embedding_function(title || ' ' || description) -- replace with actual embedding generation
) STORED,
skills_vector VECTOR(3) -- Computed skills vector
GENERATED ALWAYS AS (
embedding_function(array_to_string(skills, ' '))
) STORED,
geom GEOMETRY(Point, 4326),
location TEXT,
salary_range INT4RANGE,
experience_level TEXT,
job_location GEOMETRY(Point, 4326),
skills TEXT[]
);
CREATE TABLE resume (
candidate_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
candidate_name TEXT,
raw_resume TEXT,
resume_tsv TSVECTOR -- Computed search vectors
GENERATED ALWAYS AS (to_tsvector('english', raw_resume)) STORED,
skills_vector VECTOR(3) -- Computed semantic and skills vectors
GENERATED ALWAYS AS (
-- embedding_function(array_to_string(skills, ' '))
) STORED,
geom GEOMETRY(Point, 4326),
location TEXT,
skills TEXT[],
job_history TEXT[],
education_levels TEXT[]
);
이제 구직 검색 엔진이 PostgreSQL을 통해 다양한 검색 기술을 어떻게 구현할 수 있는지 살펴보겠습니다.
PostgreSQL의 전체 텍스트 검색
PostgreSQL의 전체 텍스트 검색 기능은 특정 키워드, 구문 및 요구 사항을 기반으로 구인 공고와 구직자 프로필을 매칭하는 견고한 기반을 제공합니다. 엔진은 구인 공고 및 이력서를 수집할 때, 토큰화 기법을 사용하여 사전 정의된 언어 사전을 사용하여 문서를 어휘소(lexemes)로 나눕니다. 이러한 사전은 텍스트 정규화, 불용어(stop-words) 제거, 어간 추출을 통해 단어를 어근으로 환원하는 과정을 안내합니다. 이렇게 표준화된 어휘소는 역색인(inverted index)에 매핑되어 빠른 검색을 위한 효율적인 구조를 생성합니다. 구직자가 검색어를 입력하면 엔진은 해당 검색어 토큰을 색인된 직무 설명 토큰과 매칭합니다. 시스템은 용어 빈도와 어휘 일치를 기반으로 결과의 순위를 매겨 관련 구인 공고의 목록을 제공합니다. 다음 다이어그램은 이 과정을 설명합니다.
PostgreSQL에서의 전체 텍스트 검색
PostgreSQL 전체 텍스트 검색은 다음과 같은 요소들로 구성됩니다.
- 사전 – PostgreSQL은 사전을 통해 언어별 어휘소(lexeme) 분석, 어간 분석 및 불용어(stop-word) 제거를 지원합니다. 이러한 사전은 원시 텍스트를 표준화된 어휘(단어의 어근 형태)로 변환하여 ‘working’, ‘worked’, ‘works’ 등의 변형어가 모두 ‘work’ 검색과 일치하도록 합니다. PostgreSQL은 다양한 언어에 대한 기본 사전을 제공하며, 특수 용어에 대한 사용자 정의 사전도 지원합니다.
- 텍스트 처리 – to_tsvector 함수는 문서(직무 설명 또는 이력서 등)를 위치 및 선택적 가중치와 함께 정규화된 어휘소를 저장하는 특수 tsvector 형식으로 변환합니다. 마찬가지로 to_tsquery 함수는 검색 쿼리를 이러한 문서 벡터와의 일치에 최적화된 형식으로 처리합니다.
- 일치 연산자 – 일치 연산자(@@)는 문서 벡터와 쿼리 간의 유사성을 평가하여 일치하는 항목이 있으면 true를 반환합니다.
- 순위 함수 – ts_rank 및 ts_rank_cd와 같은 함수는 용어 빈도 및 문서 구조와 같은 요소를 기반으로 일치 항목의 관련성을 판단하여 결과를 관련성 순으로 정렬할 수 있습니다.
다음은 ‘JavaScript’와 ‘React’ 또는 ‘Angular’ 관련 기술을 가진 구직자를 찾지만, ‘WordPress’를 언급한 구직자는 제외하는 예시입니다.
WITH resume AS(SELECT * FROM (VALUES
('John','react,javascript, wordpress'),
('Mary','angular, javascript')
) resume(candidate_name, skills))
SELECT candidate_name
FROM resume
WHERE TO_TSVECTOR(resume.skills) @@ to_tsquery('english', 'javascript & (react | angular) & !wordpress');
candidate_name
------------------
Mary
고급 전체 텍스트 검색 기능
PostgreSQL은 보다 복잡한 전체 텍스트 검색을 위해, 다음과 같은 몇 가지 고급 기능을 제공합니다.
근접성 검색(Proximity search) – 문서에서 서로 가까이 나타나는 단어를 찾습니다.
SELECT * FROM resume WHERE resume_tsv @@ to_tsquery('software <-> engineering');
이는 ‘software engineering’과 일치하지만 용어가 인접하지 않은 ‘software testing engineering’과는 일치하지 않습니다.
단순 순위 지정(Simple ranking) – 가장 관련성이 높은 결과가 더 높은 순위를 차지하도록 합니다. ts_rank는 단어의 빈도를 고려합니다. 텍스트와 일치하는 토큰이 많을수록 순위가 높아집니다. 직무 능력 요건에서 ‘Amazon’이라는 단어의 빈도를 기준으로 이력서 순위를 매길 수 있습니다.
WITH resume AS(SELECT * FROM (VALUES
('some-company:software engineering'),
('Amazon: software engineering')
) resume(skills))
SELECT resume.skills, TS_RANK_CD(TO_TSVECTOR(resume.skills), TO_TSQUERY('Amazon')) AS rank
FROM resume
ORDER BY rank DESC;
skills rank
---------------------------------------------------
Amazon: software engineering 0.1
some-company:software engineering 0.0
가중치 기반 순위(Weighted ranking) – 문서의 각 부분에 서로 다른 중요도를 할당합니다.
- A (가장 중요): 가장 높은 가중치
- B (높은 중요도): 두 번째로 높음
- C (중간 중요도): 세 번째 수준
- D (가장 낮은 중요도): 기본 가중치
다음은 예시입니다.
-- Create a function to generate weighted search vector
-- This function assigns weight "A" to column "title", and weight "B" to column "skills"
CREATE FUNCTION job_search_vector(title TEXT, skills TEXT)
RETURNS tsvector AS $$
BEGIN
RETURN setweight(to_tsvector('english', title), 'A') || setweight(to_tsvector('english', skills), 'B');
END
$$ LANGUAGE plpgsql;
WITH job AS(SELECT * FROM (VALUES
(1,'programmer','java, python, junit'),
(2,'QA','python, junit')
) job(id,title, skills))
SELECT job.id, job.title, job.skills,
ts_rank(job_search_vector(job.title, job.skills), to_tsquery('QA & python & junit & java')) AS rank
FROM job
ORDER BY rank DESC;
job_id title skills rank
-------------------------------------------------------------
2 QA python, junit 0.915068
1 programmer java, python, junit 0.77922493
퍼지 매칭(Fuzzy matching) – pg_trgm 확장은 전체 텍스트 검색을 보완하여 유사성 기반 매칭을 지원합니다. 이 기능은 사용자가 기술 용어나 직책을 잘못 입력할 수 있는 구직 플랫폼에서 매우 유용합니다.
SET pg_trgm.similarity_threshold = 0.4;
WITH job AS(SELECT * FROM (VALUES
(1,' programmer','java, python, junit'),
(2,'QA','python, junit')
) job(id,title, skills))
SELECT * FROM job WHERE job.title % 'programer';
job_id title skills
-------------------------------------------------------
1 programmer java, python, junit
성능을 위한 인덱싱
PostgreSQL은 전체 텍스트 검색 성능 최적화를 위한 특수 인덱스 유형을 제공합니다.
GIN (Generalized Inverted Index) – 업데이트 속도보다 검색 속도가 우선시되는 정적 텍스트 데이터에 적합합니다. GIN 인덱스는 tsvector 열과 함께 사용할 때 효과적이며 대부분의 구직 검색 시나리오에서 선호되는 인덱스입니다.
GiST (Generalized Search Tree) – 검색과 업데이트 성능 간의 균형이 잘 잡혀 있어 공간 소모가 적지만, 복잡한 쿼리의 경우 속도가 느릴 수 있습니다. GiST 인덱스는 업데이트가 빈번한 애플리케이션에 더 적합합니다.
pgvector를 사용한 의미론적 검색
전체 텍스트 검색은 정확히 일치하는 항목을 찾는 데 탁월하지만 의미와 맥락에 대한 이해는 부족합니다. 예를 들어 전체 텍스트 검색에서는 ‘소프트웨어 엔지니어’와 ‘개발자’가 유사한 역할을 나타낸다는 사실이나 ‘클라우드 아키텍처’가 ‘AWS 전문 지식’과 관련이 있다는 점을 자연스럽게 이해하지 못합니다. 바로 이것이 벡터 임베딩을 통한 의미론적 검색이 중요한 이유입니다.
벡터 임베딩에 대한 이해
벡터 임베딩은 텍스트를 고차원 공간의 점으로 표현하며, 이러한 점 간의 기하학적 관계는 의미론적 관계를 나타냅니다. 이 벡터 공간에서는 공통된 용어를 공유하지 않더라도 유사한 개념이 서로 더 가깝게(인접하게) 나타납니다. pgvector 확장은 PostgreSQL에 벡터 데이터 유형과 연산을 추가하여 이러한 임베딩을 데이터베이스에 직접 저장하고 효율적인 유사성 검색을 수행할 수 있도록 합니다.
의미론적 검색 구현
다음 다이어그램은 PostgreSQL에서 의미론적 검색이 어떻게 구현되는지 보여줍니다.
PostgreSQL에서의 의미론적 검색
벡터 검색을 구현하는 단계는 다음과 같습니다.
- 임베딩 생성 – 직무 설명과 구직자 이력서를 벡터 임베딩으로 변환합니다. 여기에는 일반적으로 Amazon Bedrock과 같은 서비스를 통해 제공되는 머신 러닝 모델을 사용합니다. 다음은 채용 공고를 위해 생성된 임베딩의 예입니다.
벡터 임베딩
- 벡터 저장 – pgvector의 벡터 데이터 타입을 사용하여 PostgreSQL에 임베딩을 저장합니다.
- 유사성 검색 – 다음 그림과 같이 벡터 연산자를 통해 벡터 임베딩 간의 거리를 계산하여 유사한 항목을 찾을 수 있습니다.
벡터 유사성 검색
다음은 구직자의 기술 세트 임베딩과 채용 공고에 대한 임베딩 간의 거리를 계산하는 쿼리입니다.
SELECT * FROM (
WITH job AS(SELECT * FROM (VALUES
(1, 'Data Scientist', 'python, machine learning, sql, statistics, deep learning', '[0.9, 0.7, 0.2]'::vector(3)),
(2, 'Frontend Developer', 'javascript, react, css, html, redux', '[0.2, 0.1, 0.9]'::vector(3)),
(3, 'Backend Engineer', 'java, spring, microservices, sql, api design', '[0.5, 0.9, 0.3]'::vector(3)),
(4, 'DevOps Engineer', 'kubernetes, docker, aws, terraform, ci/cd', '[0.6, 0.8, 0.4]'::vector(3))
) job(job_id, title, skills, skill_vector)
),
resume AS(SELECT * FROM (VALUES
(1, 'Mary', 'java, spring boot, microservices, postgresql, rest api', '[0.45, 0.95, 0.25]'::vector(3)),
(2, 'Jean', 'docker, kubernetes, aws, jenkins, terraform', '[0.65, 0.75, 0.35]'::vector(3)),
(3, 'Bill', 'javascript, python, react, express, mongodb, node.js', '[0.35, 0.6, 0.65]'::vector(3))
) resume(candidate_id, name, skills, skill_vector)
)
SELECT job.title, resume.name,
1 - (job.skill_vector resume.skill_vector) AS similarity_score -- Cosine similarity from distance
FROM job CROSS JOIN resume
) scores
WHERE similarity_score > 0.9
ORDER BY similarity_score DESC;
title name similarity_score
---------------------------------------------------
DevOps Engineer Jean 0.9133974713
Backend Engineer Mary 0.9133974391
<->
연산자는 벡터 간의 거리를 계산합니다. 거리가 짧을수록 유사도가 높습니다.
벡터 검색 성능 최적화
데이터셋의 크기가 커질수록 벡터 검색의 성능이 중요해집니다. PostgreSQL은 벡터 유사성 검색을 위한 특수 인덱스 타입을 제공합니다.
IVFFlat index – 더 효율적인 검색을 위해 벡터 공간을 작은 파티션으로 나눕니다.
CREATE INDEX ON resume USING ivfflat (skills_vector vector_l2_ops) WITH (lists = 100);
HNSW index – Hierarchical Navigable Small World graph index는 훨씬 더 빠른 근사 최근접 이웃 검색을 제공합니다.
CREATE INDEX ON resume USING hnsw (skills_vector vector_cosine_ops) WITH (m=16, ef_construction=64);
이러한 인덱스는 정확도를 다소 희생하더라도 검색 성능을 크게 향상시키므로, 1초 미만의 응답 시간이 중요한 대규모 구직 플랫폼에 적합합니다.
PostGIS를 사용한 지리공간 검색
위치는 구직 활동에 있어 중요한 요소입니다. 구직자는 일반적으로 통근할 수 있는 거리에 있는 일자리를 찾고, 고용주는 지역 인재 풀을 공략합니다. PostgreSQL의 PostGIS 확장 기능은 위치 기반 구직 기능을 구현하기 위한 지리공간 기능을 제공합니다.
지리공간 검색 구현
다음은 PostgreSQL의 지리공간 검색 아키텍처와 구현 단계를 보여줍니다.
PostgreSQL에서의 지리공간 검색
- PostGIS를 설치하고 활성화하세요.
CREATE EXTENSION postgis;
- geometry 또는 geography 열을 추가합니다.
-- The SRID 4326 refers to the WGS84 coordinate system used by GPS and most mapping applications.
ALTER TABLE job ADD COLUMN location GEOMETRY(Point, 4326);
ALTER TABLE resume ADD COLUMN location GEOMETRY(Point, 4326);
- GiST 인덱스를 사용한 인덱싱.
CREATE INDEX job_location_idx ON job USING GIST (location);
CREATE INDEX resume_location_idx ON resume USING GIST (location);
- ST_DWithin과 같은 지리공간 함수를 사용하여 지리공간 쿼리를 수행하고 필요에 따라 ST_Distance를 사용하여 거리를 기준으로 위치를 정렬합니다. 다음 쿼리는 이러한 예시를 보여줍니다.
WITH job AS (SELECT * FROM (VALUES
(1, 'Data Scientist', 'python, machine learning, sql, statistics, deep learning', ST_SetSRID(ST_MakePoint(-122.4194, 37.7749), 4326)), -- San Francisco
(2, 'Frontend Developer', 'javascript, react, css, html, redux', ST_SetSRID(ST_MakePoint(-74.0060, 40.7128), 4326)), -- New York
(3, 'Backend Engineer', 'java, spring, microservices, sql, api design', ST_SetSRID(ST_MakePoint(-118.2437, 34.0522), 4326)) -- Los Angeles
) job(job_id, title, skills, location)
),
resume AS( SELECT * FROM (VALUES
(1, 'John ', 'python, machine learning, tensorflow, statistics, pandas', ST_SetSRID(ST_MakePoint(-122.3321, 37.8272), 4326)), -- Berkeley
(3, 'Mary', 'java, spring boot, microservices, postgresql, rest api', ST_SetSRID(ST_MakePoint(-118.4912, 34.0195), 4326)), -- Santa Monica
(10, 'Riley', 'javascript, python, node.js, react, graphql, mongodb', ST_SetSRID(ST_MakePoint(-84.3880, 33.7490), 4326)) -- Atlanta
) resume(candidate_id, name, skills, location)
),
-- Calculate distances and rank candidates by proximity
ranked_candidates AS (
SELECT j.title AS job_title,
CASE j.job_id
WHEN 1 THEN 'San Francisco'
WHEN 2 THEN 'New York'
WHEN 3 THEN 'Los Angeles'
WHEN 4 THEN 'Chicago'
WHEN 5 THEN 'Dallas'
END AS job_location,
r.name AS candidate_name,
-- Calculate distance in kilometers
ST_Distance(j.location::geography, r.location::geography) / 1000 AS distance_km,
-- Rank candidates for each job by distance
ROW_NUMBER() OVER (PARTITION BY j.job_id ORDER BY ST_Distance(j.location::geography, r.location::geography)) AS rank
FROM job j
CROSS JOIN resume r
)
-- Select only the closest candidate for each job
SELECT job_title, job_location, candidate_name, ROUND(distance_km::numeric, 1) AS distance_km,
CASE -- Provide context about the proximity
WHEN distance_km < 5 THEN 'Walking distance'
WHEN distance_km < 15 THEN 'Short commute'
WHEN distance_km < 30 THEN 'Moderate commute'
ELSE 'Remote work only'
END AS commute_assessment
FROM ranked_candidates
WHERE TO_TSVECTOR ('english', job_title) @@ TO_TSQUERY ('english', 'engineer') -- select engineers only
AND rank = 1
ORDER BY job_title;
job_title job_location candidate_name distance_km commute_assessment
------------------------------------------------------------------------------------------
Backend Engineer Los Angeles Mary 23.1 Moderate commute
검색 기법 결합
각 검색 기법마다 고유한 장점이 있지만, 하이브리드 검색은 각 방법의 장점을 활용하여 다양한 사용 사례에서 더욱 관련성 높은 결과를 제공합니다. 전체 텍스트 검색과 의미론적 검색을 결합한 하이브리드 검색은 특정 용어와 전체적인 의미가 모두 중요한 복잡한 쿼리에 특히 효과적입니다. 전체 텍스트 검색을 통해 사용자 선호도를 일치시키고 유사성 검색을 통해 관련 콘텐츠에 대한 추천을 확장하면, 사용자에게 사용 가능한 옵션에 대한 더욱 포괄적인 정보를 제공하는 데 효과적입니다. 특정 기술이나 직책을 매칭(전체 텍스트 검색)하는 동시에, 유사성 검색을 통해 관련 직무나 활용 가능한 기술을 파악할 수 있습니다. 정확한 키워드 매칭으로는 사용자 의도를 완전히 파악하기 어려운 쿼리의 경우 유사성 검색을 통해 관련 결과를 찾을 수 있고, 전체 텍스트 검색을 통해 정확히 일치하는 항목이 누락되지 않도록 보장합니다. 위치를 기반으로 하는 구직 검색을 지원하기 위해서는, 지리적 맥락 검색을 전체 텍스트 검색 또는 의미론적 검색과 결합하여 검색 결과를 더욱 향상시킬 수 있습니다.
PostgreSQL의 하이브리드 검색(hybrid search)에서는 다양한 검색 기법의 자체적인 관련성 알고리즘을 사용하여 독립적으로 결과에 대한 순위를 매깁니다. 상호 순위 융합(RRF) 알고리즘은 이러한 다양한 순위를 의미 있게 결합하기 위해 각 결과에 통합 점수를 할당하는 특정 공식을 사용하여 순위를 병합합니다.
다음은 거리와 기술 일치를 고려하여 엔지니어링 직책에 가장 적합한 후보자를 보여주는 하이브리드 검색의 예입니다.
WITH job AS (SELECT * FROM (VALUES
(1, 'Data Scientist', 'python, machine learning, sql, statistics, deep learning',
'[0.9, 0.7, 0.2]'::vector(3),
ST_SetSRID(ST_MakePoint(-122.4194, 37.7749), 4326)), -- San Francisco
(2, 'Frontend Developer', 'javascript, react, css, html, redux',
'[0.2, 0.1, 0.9]'::vector(3),
ST_SetSRID(ST_MakePoint(-74.0060, 40.7128), 4326)), -- New York
(3, 'Backend Engineer', 'java, spring, microservices, sql, api design',
'[0.5, 0.9, 0.3]'::vector(3),
ST_SetSRID(ST_MakePoint(-118.2437, 34.0522), 4326)), -- Los Angeles
(4, 'DevOps Engineer', 'kubernetes, docker, aws, terraform, ci/cd',
'[0.6, 0.8, 0.4]'::vector(3),
ST_SetSRID(ST_MakePoint(-87.6298, 41.8781), 4326)), -- Chicago
(5, 'Full Stack Developer', 'javascript, python, react, node.js, mongodb',
'[0.4, 0.5, 0.7]'::vector(3),
ST_SetSRID(ST_MakePoint(-96.7970, 32.7767), 4326)) -- Dallas
) job(job_id, title, skills, skill_vector, location)
),
resume AS(SELECT * FROM (VALUES
(1, 'John', 'python, machine learning, tensorflow, statistics, pandas', '[0.85, 0.6, 0.25]'::vector(3),
ST_SetSRID(ST_MakePoint(-122.3321, 37.8272), 4326)), -- Berkeley (near SF)
(2, 'Sam', 'javascript, react, css, html, typescript, sass', '[0.15, 0.2, 0.95]'::vector(3),
ST_SetSRID(ST_MakePoint(-73.9352, 40.7306), 4326)), -- Queens, NY
(3, 'Mary', 'java, spring boot, microservices, postgresql, rest api', '[0.45, 0.95, 0.25]'::vector(3),
ST_SetSRID(ST_MakePoint(-118.4912, 34.0195), 4326)), -- Santa Monica (near LA)
(4, 'Jean', 'docker, kubernetes, aws, jenkins, terraform', '[0.65, 0.75, 0.35]'::vector(3),
ST_SetSRID(ST_MakePoint(-87.9065, 41.9742), 4326)), -- Evanston (near Chicago)
(5, 'Bill', 'javascript, python, react, express, mongodb, node.js', '[0.35, 0.6, 0.65]'::vector(3),
ST_SetSRID(ST_MakePoint(-84.3880, 33.7490), 4326)) -- Atlanta (far from Dallas)
) resume(candidate_id, name, skills, skill_vector, location)
),
all_matches AS ( -- Calculate all matches with scores
SELECT job.job_id, job.title AS job_title, job.skills AS job_skills, resume.candidate_id, resume.name AS candidate_name, resume.skills AS candidate_skills,
cosine_similarity(job.skill_vector, resume.skill_vector) AS skill_similarity,
ST_Distance(job.location::geography, resume.location::geography) / 1000 AS distance_km,
-- Location score (inversely related to distance)
GREATEST(0, 1 - (ST_Distance(job.location::geography, resume.location::geography) / 1000 / 500)) AS location_score,
(cosine_similarity(job.skill_vector, resume.skill_vector) * 0.7) + -- Combined score (70% skill, 30% location)
(GREATEST(0, 1 - (ST_Distance(job.location::geography, resume.location::geography) / 1000 / 500)) * 0.3) AS combined_score
FROM job CROSS JOIN resume
),
ranked_matches AS ( -- Rank candidates for each job
SELECT job_id, job_title, job_skills, candidate_id, candidate_name, candidate_skills, skill_similarity, distance_km, location_score, combined_score,
-- Rank within each job_id group by combined score
ROW_NUMBER() OVER (PARTITION BY job_id ORDER BY combined_score DESC) AS rank
FROM all_matches
)
SELECT job_title, candidate_name,
ROUND(skill_similarity::numeric, 2) AS skill_score, ROUND(distance_km::numeric, 1) AS distance_km,
ROUND(location_score::numeric, 2) AS location_score, ROUND(combined_score::numeric, 2) AS combined_score,
CASE -- Detailed match assessment
WHEN skill_similarity >= 0.9 AND distance_km = 0.8 AND distance_km = 0.7 AND distance_km = 0.6 OR distance_km <= 300 THEN 'Potential Match'
ELSE 'Low Match'
END AS match_quality
FROM ranked_matches
WHERE TO_TSVECTOR ('english', job_title) @@ TO_TSQUERY ('english', 'engineer')
AND rank = 1
ORDER BY job_id;
job_title candidate_name skill_score distance_km location_score combined_score match_quality
-------------------------------------------------------------------------------------------------------------------
Backend Engineer Mary 1.0 23.1 0.95 0.98 Perfect Match
DevOps Engineer Jean 1.0 25.3 0.95 0.98 Perfect Match
성능 및 확장 고려 사항
구직 검색 플랫폼이 데이터 양, 사용자 기반 및 쿼리 복잡성 측면에서 성장함에 따라 성능 최적화의 중요성이 점점 더 커지고 있습니다. PostgreSQL은 검색 엔진이 대규모 환경에서도 응답성을 유지할 수 있도록 다양한 메커니즘을 제공합니다.
구직 애플리케이션은 다음과 같은 몇 가지 구체적인 성능 문제에 직면해 있습니다.
- 계산 복잡성 – 여러 기법을 결합한 하이브리드 검색 쿼리는 특히 벡터 유사도 계산이나 지리공간 거리 측정과 같은 복잡한 연산이 포함되는 경우, 리소스를 많이 사용할 수 있습니다.
- 인덱싱 오버헤드 – 다양한 검색 기법에 대한 특수 인덱스를 유지하면, 스토리지 요구 사항이 증가하고 쓰기 작업이 느려질 수 있습니다.
- 결과 병합 – 서로 다른 검색 알고리즘의 결과를 결합하려면 복잡한 조인 연산과 점수 계산이 필요한 경우가 많습니다.
- 동시 쿼리 부하 – 인기 있는 구직 플랫폼은 특히 사용량이 가장 많은 시간대에는 많은 검색 요청을 동시에 처리해야 합니다.
PostgreSQL은 성능 문제를 해결하기 위한 여러 기능을 제공합니다.
병렬 쿼리 실행 – 쿼리 워크로드를 여러 CPU 코어에 분산합니다.
-- Enable parallel query with 4 workers
SET max_parallel_workers_per_gather = 4;
-- Execute a complex search query with parallel processing
SELECT * FROM job WHERE <complex_search_condition>;
쿼리 파이프라이닝 – 여러 쿼리 단계를 동시에 처리합니다.
-- Use Common Table Expressions (CTEs) for pipelining
WITH text_matches AS (
SELECT * FROM job WHERE title_tsv @@ to_tsquery('data & science')
)
SELECT * FROM text_matches WHERE skills_vector '[0.2, 0.7, 0.1]'::vector < 0.5;
구체화된 뷰 – 공통 검색 작업을 미리 계산합니다.
-- Create a materialized view for frequently used search results
CREATE MATERIALIZED VIEW popular_tech_jobs AS
SELECT * FROM job WHERE title_tsv @@ to_tsquery('software | developer | engineer')
AND salary_range && numrange(100000, 150000);
적절한 인덱싱 – 각 검색 차원에 적합한 인덱스 유형을 선택합니다.
-- Full-text search index
CREATE INDEX ON job USING GIN (title_tsv);
-- Vector search index
CREATE INDEX ON job USING ivfflat (skills_vector vector_cosine_ops) WITH (lists = 100);
-- Geospatial index
CREATE INDEX ON job USING GIST (location);
테이블 파티셔닝 – 논리적 구분에 따라 큰 테이블을 관리하기 쉬운 청크로 나누어, 검색에 불필요한 데이터를 제거합니다.
-- Partition job listings by posting date
CREATE TABLE job (
job_id BIGINT,
title TEXT,
description TEXT,
posting_date DATE,
-- other columns
) PARTITION BY RANGE (posting_date);
다른 애플리케이션은 어떨까요?
이 글에서는 구직 플랫폼에 초점을 맞추지만, 논의되는 아키텍처는 다양한 애플리케이션에 적용할 수 있습니다. 다음 표는 몇 가지 예를 보여줍니다.
Application | Full Text Search | Vector Search | Geospatial Search | How |
전자상거래 상품 검색 | 제품명, 설명 및 사양 | 제품 임베딩 기반 ‘유사 상품’ 추천 | 지역별 재고 및 예상 배송 시간 검색 | 고객이 특정 요구 사항에 맞는 상품을 찾는 동시에, 해당 지역에서 구매 가능한 상품을 기준으로 필터링하여 관심을 가질 만한 관련 상품을 찾을 수 있도록 지원합니다. |
부동산 플랫폼 | 부동산 특징, 편의 시설 및 설명 | 전반적으로 유사한 특징을 가진 부동산 찾기 | 동네 분석 및 관심 지점과의 거리 검색 | 주택 구매자가 명확한 기준에 맞는 부동산을 찾는 동시에, 고려하지 않았지만 자신의 라이프스타일 선호도에 맞는 지역을 발견할 수 있도록 지원합니다. |
콘텐츠 추천 시스템 | 주제별 기사 또는 동영상 | 임베딩을 기반으로 유사한 주제를 갖는 콘텐츠 찾기 | 지역별 관련 뉴스 및 이벤트 검색 | 정확한 콘텐츠 검색뿐 아니라, 사용자의 위치 및 관심사와 맥락적으로 관련된 우연한 추천도 가능하게 합니다. |
여행 및 접객 | 숙박 시설 편의 시설 및 특징 | “이곳과 비슷한 장소” 추천 | 관광 명소, 교통편, 액티비티와의 근접성 검색 | 여행객이 자신의 특정 요구 사항을 충족하는 숙박 시설을 찾는 동시에, 처음에는 고려하지 않았을 수 있는 지역의 옵션을 발견하도록 돕습니다. |
의료 서비스 제공자 매칭 | 의료 전문 분야 및 치료 | 유사한 진료 패턴과 환자 후기를 보유한 의료 서비스 제공자 찾기 | 근접성 및 접근성 검색 | 진료 방식과 편리한 위치 등의 요소를 고려하여, 환자가 자신의 특정 의료 요구에 맞는 의료 제공자를 찾을 수 있도록 돕습니다. |
결론
전체 텍스트 검색, 벡터 유사성 검색, 지리공간 기능이 결합된 PostgreSQL은 정교한 검색 애플리케이션을 구축할 수 있는 다용도 플랫폼입니다. PostgreSQL은 이러한 검색 기술을 단일 데이터베이스 시스템에 통합함으로써, 개발자가 여러 전문적인 시스템을 관리하지 않고도 복잡한 다차원 검색 환경을 구현할 수 있도록 지원합니다.
구직 플랫폼의 경우, 이 통합 접근 방식을 사용하면 명시적 기술 및 요구 사항(전체 텍스트 검색), 개념적으로 관련된 경험 및 자격(벡터 검색), 실질적인 위치 고려 사항(지리공간 검색)을 기반으로 채용공고에 후보자를 매칭할 수 있습니다. 이처럼 여러 기능을 조합하여, 단일 검색 기법만으로 제공할 수 있는 것보다 더 관련성이 높은 검색 결과를 얻을 수 있습니다.