NestClient를 활용하여 검색 필터 기능,인덱싱 기능을 살펴봅시다.

전체 인덱싱

        public async Task<int> ReindexAll()
        {
            await _elasticClient.DeleteByQueryAsync<SearchGoods>(q => q.MatchAll());
            var allGoods = await FindAll();
            foreach (var item in allGoods)
            {
                // 키워드 검색대상 추가
                item.terms = $"{item.nameKr} {item.category1} {item.category2} {item.category3} {item.terms}";
                await _elasticClient.IndexDocumentAsync(item);
            }            
            return allGoods.Count;
        }


업데이트

검색 문서 업데이트는 크게 2가지 방법이 있다.

  • 문서 1개를 모두수정 또는 없을시 신규생성(Upset)
  • 문서 1개의 특정 속성만 수정

문서 1개 전체 수정

SearchGoods item= new SearchGoods
{
    something1="",
    something2="",
}

//수정 대상을 찾기위해, 검색문서의 고유 ID를 먼저 Search해야한다.
//고유 ID를 별도로 관리하여,이 과정을 더 효율적으로 만들수는 있다.
var searchResponse = await _elasticClient.SearchAsync<SearchGoods>(sd => sd
    .Index(_indexName)                
    .Size(1)
    .Query(q => q
        .Match(m => m.Field("id").Query(item.no.ToString())
        )));
var hitIds = searchResponse.HitsMetadata.Hits.Select(h => h.Id);

await _elasticClient.UpdateAsync<SearchGoods>(DocumentPath<SearchGoods>.Id(hitIds.First()), descriptor => descriptor
        .Index(_indexName)
        //.DocAsUpsert()    --이옵션을 주면, 업데이트대상 문서가 없을시 신규로 만든다.
        .Doc(item)
    );    

문서 1개를 전체 수정또는 신규 생성할때 유용합니다.  하지만 이 방식을 문서업데이트 전체 전략에 사용하지 못하는 경우도 있습니다.

다음과 같은 수정 시나리오를 구상해봅시다.

상품의 가격만 바꾸고 싶을 뿐인데, 부분적으로 증가된 사용자의 View카운팅까지 알아야 하며 모를경우 0으로 초기화를 시키게됩니다.

이러한 업데이트는 특정 문서의 속성만 변경하는 업데이트 방식이 필요하게됩니다.

특정 필드데이터만 수정

        public async Task<int> UpdateItem(SearchGoods item)
        {
            await _elasticClient.UpdateAsync<SearchGoods>(item.no, u => u.Doc(item));

            await _elasticClient.UpdateByQueryAsync<SearchGoods>(u => u
            .Query(q => q
            .Match(m => m
                .Field(p => p.goodsNo)
                .Query(item.goodsNo)
            ))
            .Script($"ctx._source.price = {item.price}")
            .Conflicts(Conflicts.Proceed)
            .Refresh(true));

            await _searchRepository.SaveChangesAsync();
            return 0;
        }

전통적인 특정 조건의 유니크값을 검색한후, 특정 필드의 속성만 변경하는 방법입니다. 


검색 기능

public async Task<SearchResult> FindByFilter(SearchFilter filterOpt)
        {
            var result = new SearchResult();
            int page = filterOpt.paging == null ? 0 : filterOpt.paging.page;
            int limit = Math.Min(100,filterOpt.paging == null ? 10 : filterOpt.paging.limit);

            //Paging
            var searchDes = new SearchDescriptor<SearchGoods>()
                .From(page)
                .Size(limit);

            //Sort
            var sortDescriptor = new SortDescriptor<SearchGoods> ();
			
			..... Sort정의            

            searchDes = searchDes.Sort( s=>sortDescriptor);

            //Filter
            var filters = new List<Func<QueryContainerDescriptor<SearchGoods>, QueryContainer>>();
			..... Filter정의

            searchDes = searchDes.Query(q=>q                
                .Bool(bq => bq.Filter(filters)));

            var engine_result = await _elasticClient.SearchAsync<SearchGoods>(searchDes);
            result.list = engine_result.Documents.ToList<SearchGoods>();
            result.total = (int)engine_result.Total;
            result.size = result.list.Count;           
        }

기본적으로 페이징+소팅+각종 필터의 결합된 조건으로 검색처리가 가능합니다.

다양한 검색 조건이 결합될수 있으며, 조건의 조합에 따른 쿼리문을 작성하는것은 개발구현 복잡성을 높일수 있으며

DSL은 이러한 로직을 간단하게 만들어 줍니다.


정렬 조건 추가

           if(!String.IsNullOrWhiteSpace(filterOpt.sort?.price))
            {
                if (filterOpt.sort.price == "desc")
                {
                    sortDescriptor.Field(f => f.price, Nest.SortOrder.Descending);
                }
                else if(filterOpt.sort.price == "asc")
                {
                    sortDescriptor.Field(f => f.price, Nest.SortOrder.Ascending);
                }
            }


필터 조건 추가

            if ( !String.IsNullOrWhiteSpace(filterOpt.filters?.category1) )
            {
                filters.Add(fq => fq.Match(t => t.Field(f => f.category1).Query(filterOpt.filters.category1)));                
            }


범위 필터 조건추가

            //Price
            if (filterOpt.filters?.minPrice > 0 && filterOpt.filters?.maxPrice > 0)
            {
                filters.Add(fq => fq.Range(c => c
                    .Name("named_query")
                    .Boost(1.1)
                    .Field(p => p.price)
                    .GreaterThanOrEquals(filterOpt.filters.minPrice)
                    .LessThanOrEquals(filterOpt.filters.maxPrice)
                    .Relation(RangeRelation.Within)
                ));
            }


풀 Text 키워드 검색(terms)

            //Query
            if (!String.IsNullOrWhiteSpace(filterOpt.keyword))
            {
                var keywords = filterOpt.keyword.Split(' ');
                filters.Add(fq => fq.Terms(t => t.Field(f => f.terms).Terms(keywords)));
            }

검색 키워드를 화이트 스페이스 단위로 분리하여, Fulltext Multi 검색을 시도하게 됩니다.

형태소 분석기를 활용하고 싶으면 Split가 아닌, 내재된 형태속 분석기 모듈을 사용할수 있습니다.

검색 문서 실시간 이벤트 적용 

기본 상품정보의 갱신은 거의 없다고 가정하고 View수는 이벤트를 통해 특정 특정기간 증분수만큼

증가가된다고 가정해봅시다. 이경우 유입이벤트 발생시마다 기존문서를 찾은후 기존 원자성을 유지한후

View수를 1 올리는 것은 전통적이고 쉬운방식이지만, Nosql에서는 비효율적인 업데이트 방식입니다.

엘라서틱 에서는 파티션(데이터가 분산저장)된 데이터를 샤드(파티션된것을 모아주는 기법)를 통해 고성능 집계처리를 할수 있으며

증가분에대한 데이터를 Insert하는 이벤트 기법으로 View에대한 총합및 시간에따른 View변경 그래프를 그려낼수 있습니다.


 

이와 관련된 부분은 다음과 같은 주제로 다시 정리될 예정입니다.

  • 전체 데이터 갱신시,인덱스된 데이터를 Swap하여 무중단으로 API를 제공하는방법
  • 원본데이터와 이벤트 집계용 데이터를 하나의 문서로 제공하고 필터및 집계하는 방법


추가 참고 Link



  • No labels