
지난 번 작성한 전체 유저 목록 조회 기능에 검색 기능을 추가했다.
검색 조건은 다음과 같았다.
입력은 하나였으며, 사용자 이름, 이메일, Role, 소속 이름 총 네 개의 항목에 대해 검색할 수 있어야 했다.
문제는 소속 이름(Firm Name)과 나머지 검색 항목이 서로 다른 테이블에서 관리된다는 점이었다.
이를 해결하기 위해 어떻게 검색 조건을 통합적으로 처리할 수 있을지 고민했다.
그 결과, ILIKE와 Full-Text Search를 병합해서 사용하는 것이 가장 효율적이라는 결론을 내렸다.
이 과정에서 사용된 기술과 결과를 정리한다.
검색 조건:
1. 사용자 이름 (username)
2. 이메일 (email)
3. 사용자 역할 (role_type)
4. 소속 이름 (firm_name)
이 네 가지를 하나의 검색어로 처리해야 한다.
username, email, role_type은 users 테이블에 포함되어 있지만,firm_name은 별도의 테이블(firm)에 존재한다.username ILIKE '%검색어%'CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_users_username_trgm ON users USING gin (username gin_trgm_ops);
CREATE INDEX idx_users_email_trgm ON users USING gin (email gin_trgm_ops);
CREATE INDEX idx_firm_name_trgm ON firm USING gin (firm_name gin_trgm_ops);
pg_trgm 확장:gin_trgm_ops:tsvector 컬럼 생성:
ALTER TABLE users ADD COLUMN search_vector tsvector;
ALTER TABLE firm ADD COLUMN search_vector tsvector;
기존 데이터 업데이트:
UPDATE users
SET search_vector =
setweight(to_tsvector('simple', username), 'A') ||
setweight(to_tsvector('simple', email), 'B') ||
setweight(to_tsvector('simple', CAST(role_type AS TEXT)), 'C');
UPDATE firm
SET search_vector = to_tsvector('simple', firm_name);
트리거 생성:
CREATE OR REPLACE FUNCTION update_users_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector =
setweight(to_tsvector('simple', NEW.username), 'A') ||
setweight(to_tsvector('simple', NEW.email), 'B') ||
setweight(to_tsvector('simple', CAST(NEW.role_type AS TEXT)), 'C');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_users_search_vector
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_users_search_vector();
GIN 인덱스 생성:
CREATE INDEX idx_users_search_vector ON users USING gin (search_vector);
CREATE INDEX idx_firm_search_vector ON firm USING gin (search_vector);
search_vector:GIN 인덱스:users 테이블: 46개의 사용자 데이터.ts_query = func.plainto_tsquery(text("'simple'"), condition)
query = query.outerjoin(Firm, UserModel.firm_id == Firm.id).filter(
or_(
UserModel.username.ilike(f"%{condition}%"), # ILIKE로 검색
UserModel.email.ilike(f"%{condition}%"), # ILIKE로 검색
cast(UserModel.role_type, String).ilike(f"%{condition}%"), # ENUM 처리
Firm.firm_name.ilike(f"%{condition}%"), # ILIKE로 검색
UserModel.search_vector.op('@@')(ts_query), # Full-Text Search
Firm.search_vector.op('@@')(ts_query) # Full-Text Search
)
)
성능 결과:
query = self.db.query(UserModel).outerjoin(Firm, UserModel.firm_id == Firm.id).filter(
or_(
UserModel.username.ilike(f"%{condition}%"),
UserModel.email.ilike(f"%{condition}%"),
cast(UserModel.role_type, String).ilike(f"%{condition}%"),
Firm.firm_name.ilike(f"%{condition}%")
)
)
성능 결과:
이번에는 ILIKE와 풀텍스트 검색(Full-Text Search)을 조합해 사용자 목록 조회 기능에 검색을 추가한 사례를 다뤘다. PostgreSQL의 다양한 기능을 활용하면서 효율적인 검색 방법을 고민했고, 작지만 의미 있는 성과를 확인할 수 있었다.
다만, 솔직히 말하자면 이 과정에서 몇 가지 고민이 들었다. 우선 테스트 데이터가 1000건으로 작다 보니, 실제 대규모 데이터 환경에서는 어떤 문제가 발생할지 명확히 예측하기 어렵다. 예를 들어, 데이터가 수백만 건으로 늘어나거나 삽입/업데이트 작업이 잦아진다면 현재 설계가 여전히 효과적일지 확신하기 어렵다. 또, GIN 인덱스의 유지 비용이나 JOIN 연산의 성능 문제를 어떻게 해결해야 할지도 아직은 막연하다.
이런 상황이 오면 어떤 대안이 적절할지, 그리고 그 대안을 어떻게 준비해야 할지 잘 모르겠다. 왜냐하면 아직 내 경험과 공부가 부족하기 때문이다. 하지만 분명한 것은, 이러한 고민을 마주할 때마다 배움과 경험을 쌓아가면서 조금씩 더 나아질 수 있을 거라는 믿음이다.
앞으로 데이터가 많아지고 복잡한 요구 사항이 추가되면, Redis 같은 캐싱 시스템이나 Elasticsearch 같은 전문 검색 엔진을 사용하는 방법도 고민하게 될 것이다. 그 시점이 오면, 그때의 나에게 더 나은 답을 기대해본다.
PostgreSQL에서 ILIKE는 대소문자를 구분하지 않고 패턴 매칭을 수행하는 연산자다.
예를 들어, ILIKE '%검색어%'와 같이 사용하면 특정 문자열을 포함하는 모든 레코드를 찾을 수 있다.
하지만 이 방식은 인덱스를 효율적으로 사용하지 못해 대량의 데이터에서 성능 저하를 초래할 수 있다.
반면, 풀텍스트 검색은 자연어 처리를 통해 문서 내의 단어를 기반으로 검색을 수행한다.
이를 위해 tsvector와 tsquery 데이터 타입을 사용하며, 인덱스를 통해 빠른 검색이 가능하다.

ARRAY, JSONB, tsvector, 그리고 pg_trgm과 함께 사용tsvector (Full-Text Search)pg_trgm (문자열 유사도)JSONB (JSON 데이터 검색)ARRAY기본 명령어
CREATE INDEX idx_users_search_vector ON users USING gin (search_vector);
CREATE INDEX idx_users_email_trgm ON users USING gin (email gin_trgm_ops);
USING gin: GIN 인덱스를 생성.gin_trgm_ops: pg_trgm 확장을 사용해 ILIKE와 같은 연산 최적화.활용 사례
CREATE INDEX idx_users_search_vector ON users USING gin (search_vector);ILIKE 최적화 (pg_trgm):CREATE INDEX idx_users_username_trgm ON users USING gin (username gin_trgm_ops);
Ref.
https://www.postgresql.org/docs/current/textsearch.html
https://www.slingacademy.com/article/using-like-and-ilike-in-postgresql/
https://www.postgresql.org/docs/current/functions-matching.html
https://www.postgresql.org/docs/current/datatype-textsearch.html
https://www.postgresql.org/docs/current/gin.html