개요
기준 Postgres 기준으로 ts_vector, ts_query 를 활용하여 Full Text Search 을 실행할 수 있음.
함께 읽어보면 좋은 내용 - RDB 의 FTS(Full Text Search) 이해하기
배경
SpringBoot와 JPA 를 활용하여 개발하는 경우가 다수이다.
흔히 Repository 를 활용한 영속성을 활용한다. spring-boot-data-commons. 일단 해당 모듈에서는 FTS 를 지원하는 메서드가 일체 존재하지 않는다. 그렇기 때문에 @Query 와 같이 직접 쿼리를 작성해야 한다.
@Query 를 작성하지 않고 개발하는 방식도 있는데, 그 방법도 약간의 문제점이 발견되었다.
실제 코드로 문제를 이해해보자.
문제1 Tokenizer 한계
테스트를 위해 다음과 같은 클래스를 Json 으로 만들어 tokenizer 가 Parsing 하도록 설정하여 ts_vector('english', feature) 실행
@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Feature {
private String path;
private String method;
private List<String> tags;
private String operationId;
private String summary;
private String description;
}
List.of(new ApiDocument.Feature(
"/api/example/foo",
"GET",
List.of("exampleTag1", "exampleTag2"),
"getExampleOperation",
"Example Summary",
"Example feature description"
)),
Example Dummy Data 를 활용해 Parsing 한 결과(to_tsvector)
'/api/example/foo':6 'descript':3 'exampl':1,7 'exampletag1':9 'exampletag2':10 'featur':2 'get':4 'getexampleoper':5 'summari':8
그럼 이것이 왜 한계일까?
흔히 우리가 Full Text Search 을 할 때 to_tsquery('keyword') 이렇게 작성하는데, 이 때 api path 를 검색하기 위해 to_tsquery('foo') 작성하게 되면 /api/example/foo 을 찾을 수 없다.
/api:* 라고 검색하면 prefix 기능을 활용하여 찾을 수 있지만, 찾고자한다면 /api/example/foo 전체를 입력해야만 한다.
Full Text Search 의 의미 자체가 Text 에 맞는 단어를 찾는 것이기 때문이며 이를 해결하기 위해 정규표현식(RegExp) 또는 LIKE 연산자 또는 tri-gram matching 을 활용해야 하는데, 그렇다면 성능에 대한 고민을 다시해볼 수 밖에 없게 된다.
[참고자료]
1. how-to-make-middle-and-suffix-matching-using-full-text-search
2. ElasticSearch 에서 스탠다드 Tokenizer로 '/componets/schema/User'를 나눈 것. PostreSQL 에서는 위에서 설명한 것과 같이 이렇게 나누지 못한다.
문제2 다이나믹한 작업시 JPQL 충돌
앞에서 언급한 @Query 방식을 활용할 경우 온전히 기능을 다 사용하기 어려울 수 있다.
@Query(value = "SELECT d.* FROM documents d JOIN documents_feature f ON d.document_id = f.document_id WHERE f.search_vector @@ to_tsquery(:keyword);", nativeQuery = true)
Documents findDocumentsByFeaturesKeyword(@Param("keyword") String keyword);
이렇게 단순한 것 까지는 문제가 되지 않는데, to_tsquery 는 FTS 중심적인 역할로 몇가지 활용가능한 연산자가 있다.
and($), OR(|), NOT(!), 프리픽스(:*) / 구문 검색(↔) 여기서 프리픽스를 의외로 많이 사용될 것 같은데, 위 JPQL 에서는 충돌이 발생하여 파라미터를 추가로 입력해야 한다는 예외를 발생시킨다.
이 부분을 해결하기 위해 따로 @Query 를 작성하지 않는 방식으로 구현하면 문제가 해결될 수 있을까?
일단은 해결가능할 것으로 보인다. 단, 약간의 시행착오가 또 발생한다. 아래와 같이 sql 을 작성했는데, 이것이 올바르게 작성된 syntax 인지 시행착오가 필요하다.(삽질이 필요하다..)
@Override
public List<Documents> findDocumentsByPrefixKeyword(String keyword) {
String sql = """
SELECT d.*
FROM documents d
JOIN documents_feature f
ON d.document_id = f.document_id
WHERE f.search_vector @@ to_tsquery('""" + keyword
+ """
:*');""";
return entityManager.createNativeQuery(sql, Documents.class).getResultList();
}
[참고자료]
엘라스틱서치에서는 다음과 같은 방식으로 처리한다.
ApiDocuments entity = new ApiDocuments(null, "foo", "bar");
ApiDocuments save = sut.save(entity);
assertThat(save.getId()).isNotNull();
IndexRequest<ApiDocuments> request = IndexRequest.of(i -> i
.index("api-documents") // 인덱스 이름
.id(save.getId())
.document(save)
);
elasticsearchClient.index(request);
차이점이라한다면 엘라스틱서치는 체이닝 함수로 되어 있다는 점. 이것의 장점은 앞서 PostgresSQL 의 겪었던 시행착오를 줄여주는 효과가 있을 수 있다고 생각한다.
문제3 한글 형태소 분석기 설치
이건 주관적인 문제점일 수 있지만, 그럼에도 시간을 꽤 잡아먹었던 부분이라 이야기해본다.
결론부터 이야기하면 MacOS 의 도커를 활용한 PostgresSQL 개발 환경에서는 한글 형태소 분석기를 확장 형태로 설치하는게 쉽지 않다.
우리는 서비스 기능의 일부로 검색시 한글을 지원해야 하지만, PostgresSQL 의 FTS 는기본적으로 한글을 지원하지 않는다. (형태소 분석기가 필요한 이유는 조사어구를 제거하기 위해서) 이를 해결하기 위해서는 PostgresSQL 에서 확장 모듈 형태로 지원하는데,
이때 docker 가 아닌 local 에 설치된 환경에서 여러 절차를 통해 한글 분석기를 설치할 수 있다. local 에서는 어떻게 설치할 수 있다고 가정하더라도- docker 를 활용한 애플리케이션 구현하는 곳에서는 이것을 설치하는게 쉽지 않아 보인다.
예를들어 테스트 코드를 활용하여 testContainer 사용하는 환경이라면?
결론
하면서 느낀점은 PostgresSQL 로 검색을 시늉낼 수는 있지만, 조금만 복잡도가 높아졌을 때 자바 문법과 PostgresSQL 문법을 고민하면서 시행착오를 겪어야만 했다. 거기에 더불어 진짜 검색을 잘 되도록 만들기 위해서는 to_tsquery 뿐만 아니라, like, pg_tram 을 활용하여 한번 쿼리를 보낼때 이 3가지를 같이 조회해서 노출시켜야만 우리가 원하는 결과물을 만들 수 있지 않을까 싶다.
참고
- pg_tram 은 3글자부터 인덱스 스캔을 활용하기 때문에 효율이 좋고, 2글자 검색시 풀스캔을 한다. 이를 해결하기 위해 pg_bitram (2글자) 가 있지만, 이또한 2글자 이상 검색시 효율이 좋지 못하다는 단점이 있다.
'아직 카테고리 미정' 카테고리의 다른 글
RDB 의 FTS(Full Text Search) 이해하기 (1) | 2024.09.16 |
---|---|
성능테스트 k6 결과 내역을 이해해보자. (0) | 2021.07.09 |
Mock 객체란 무엇일까? 왜 써야될까? (0) | 2021.03.11 |
TDD 좀 더 잘하기 (0) | 2021.03.11 |
테스트 주도 개발 입문해보기 (0) | 2021.03.09 |
댓글