전체 데이터 흐름

  1. 데이터 생성/수정
    1. 사용자가 판매글을 작성하거나 수정하면, 먼저 RDBMS 에 데이터가 저장/수정됨.
    2. 트랜잭션 내에서 해당 TransactionFeed 엔티티를 TransactionFeedDocument로 변환하고, 변환된 TransactionFeedDocument를 ELS 인덱스에 저장(색인)함.
  2. 데이터 삭제
    1. 사용자가 판매글을 삭제하면, RDBMS의 isDeleted 플래그를 true로 업데이트함.
    2. 변경된 상태를 TransactionFeedDocument에 반영하여 ELS 인덱스에도 업데이트.
  3. 데이터 조회
    1. 사용자가 키워드 검색 또는 필터링을 요청하면 searchFeeds 호출
    2. 사용자의 요청(키워드, 필터 조건, 정렬)을 바탕으로 ELS Bool Query를 동적으로 생성.
    3. 생성된 쿼리를 ELS로 전송하여 TransactionFeedDocument를 검색.
    4. 검색 결과를 GetFeedSummaryResponseDto로 변환하여 사용자에게 반환.

분석기(Analyzer) 설정

  1. 색인 분석기 (index_analyzer)
    1. 문서를 ELS에 저장할 때 사용.
    2. 구성
      • tokenizer: nori_tokenizer
      • filter:
        • lowercase: 영문 소문자 변환.
        • nori_part_of_speech: 불필요한 품사(조사, 어미 등)를 제거함. 예를 들어 '아이폰은' -> '아이폰'으로 색인하여, '아이폰'이라는 검색어로 쉽게 찾을 수 있게 함.
        • nori_readingform: 한자 한글 변환.
  2. 검색 분석기 (search_analyzer)
    1. 사용자의 검색어를 분석할 때 사용.
    2. 구성
      • tokenizer: nori_tokenizer
      • filter:
        • lowercase: 영문 소문자 변환.
        • nori_readingform: 한자 한글 변환.
        • synonym_graph_filter: 동의어를 확장함. 예를 들어 사용자가 '스크'로 검색하면, 'SKT', 'skt', '에스케이티' 등의 동의어가 모두 포함되어 검색됨.

인덱싱 시에는 품사를 제거하여 관련없는 문서가 검색될 확률을 낮추도록 함. 검색 시에는 동의어를 확장하도록 하여, 검색 재현율을 높이도록 함.

검색 기능 코드 설명

키워드 분석 및 필터 변환

// TransactionFeedServiceImpl.java 中 searchFeeds 메서드 일부

// ... 키워드를 공백 기준으로 분리 ...
String[] words = keyword.trim().split("\\\\s+");

for (String word : words) {
    String lowerWord = word.toLowerCase();

    // 1. 통신사 동의어 확인
    if (TELECOM_SYNONYM_MAP.containsKey(lowerWord)) {
        Long telecomId = TELECOM_SYNONYM_MAP.get(lowerWord);
        // boolQueryBuilder에 telecomCompanyId 필터 추가
        continue;
    }

    // 2. 데이터 크기 확인
    Long parsedAmount = parseDataAmount(word);
    if (parsedAmount != null) {
        // boolQueryBuilder에 salesDataAmount 필터 추가
        continue;
    }

    // 3. 남은 단어는 텍스트 검색용으로 수집
    textKeywords.add(word);
}

// 남은 텍스트 키워드가 있으면 multi-match 검색 실행
if (!textKeywords.isEmpty()) {
    String searchText = String.join(" ", textKeywords);
    boolQueryBuilder.filter(f -> f.multiMatch(mm -> mm
            .query(searchText)
            .fields("title", "content", "nickname",
                    "title.prefix", "content.prefix", "nickname.prefix")
    ));
}

동적 필터 적용