에피소버 CMS 및 상업용 검색 플러그인
.NET 6+를 지원하는 버전을 사용할 수 있습니다. 자세한 내용은 Kristian Borg에 문의하십시오.
우선 인덱스를 만들어야합니다. 임베디드 메뉴 검색 엔진 -> 관리 및 상태를 통해 관리 페이지로 이동 한 다음 Create indices 버튼을 클릭하십시오.
이렇게하면 사이트에서 언어 당 하나의 색인이 생성됩니다. Commerce Addon이 설치된 경우 카탈로그 콘텐츠에 대한 추가 지수도 작성됩니다.

사이트의 각 활성 언어에 대해 별도의 인덱스가 생성됩니다. 나중에 더 많은 언어를 추가하면이 프로세스를 반복해야합니다.
Singleton Epinova.ElasticSearch.Core.Conventions.Indexing 으로 프로그래밍 방식으로 설정을 구성 할 수 있습니다. 일반적으로 초기화 가능한 모듈 또는 Application_start ()에서 appdomain 당 한 번만 수행하십시오.
샘플 구성 클래스 :
public static class SearchConfig
{
public static void Init ( )
{
Epinova . ElasticSearch . Core . Conventions . Indexing . Instance
. ExcludeType < ErrorPage > ( )
. ExcludeType < StartPage > ( )
. ExcludeRoot ( 42 )
. IncludeFileType ( "pdf" )
. IncludeFileType ( "docx" )
. ForType < ArticlePage > ( ) . IncludeProperty ( x => x . Changed )
. ForType < ArticlePage > ( ) . IncludeField ( x => x . GetFoo ( ) )
. ForType < ArticlePage > ( ) . IncludeField ( "TenYearsAgo" , x => DateTime . Now . AddYears ( - 10 ) )
. ForType < ArticlePage > ( ) . EnableSuggestions ( x => x . Title )
. ForType < TagPage > ( ) . EnableSuggestions ( )
. ForType < ArticlePage > ( ) . EnableHighlighting ( x => x . MyField )
. ForType < ArticlePage > ( ) . StemField ( x => x . MyField ) ;
}
}다른 옵션에 대한 설명 :
ExcludeType :이 유형은 색인화되지 않습니다. 기본 클래스와 인터페이스도 지원됩니다. ExcludeFromSearchAttribute 사용하여 유형을 장식하여 동일하게 달성 할 수 있습니다.
ExcludeRoot :이 노드와 그 어린이는 색인화되지 않습니다. 일반적으로 에피소버 페이지 트리의 노드 ID.
IncludeFileType : 색인화되어야하는 파일 유형의 확장자를 정의합니다. 구성의 <files> 노드도 참조하십시오.
ForType : 특정 유형의 인덱싱 규칙을위한 시작점. 그 자체로는 아무것도 없지만 다음 옵션에 대한 액세스를 제공합니다.
IncludeField : 색인화 할 사용자 정의 속성을 정의합니다. 예를 들어 외부 데이터를 가져 오거나 복잡한 집계를 수행하는 확장 방법.
EnableSuggestions : Lambda 표현식을 통해 모든 속성 또는 선택한 것의자가 공습 데이터를 제공합니다.
IncludeProperty : [검색 가능]로 속성을 장식하는 것과 동일한 효과. 모델 소스를 제어하지 않으면 사용할 수 있습니다.
EnableHighlighting : 추가 필드를 추가하여 강조 표시 할 수 있습니다.
StemField : 현재 언어로 속성을 분석해야한다고 정의합니다. MainIntro , MainBody 및 Description 이라는 속성은 항상 분석됩니다.
이 모듈은 에피소버와 동일한 규칙을 따르려고 시도합니다. 이는 [Searchable(false)] 로 명시 적으로 장식되지 않는 한 유형 string 및 XhtmlString 의 모든 속성이 색인화됩니다. 추가 속성은 [Searchable] 또는 위의 규칙으로 장식하여 색인을 색인화 할 수 있습니다. 인덱스 내용으로 [Searchable] 로 ContentArea 장식하십시오.
SearchResult result = service
. Search ( "bacon" )
. GetResults ( ) ; SearchResult result = service
. Search < ArticlePage > ( "bacon" )
. GetResults ( ) ; SearchResult result = service
. Search < ArticlePage > ( "bacon" )
. InField ( x => x . MainIntro )
. InField ( x => x . MainBody )
. InField ( x => x . MyCustomMethod ( ) )
. GetResults ( ) ;문구로 시작하는 인덱스 단어를 검색합니다.
string [ ] suggestions = service . GetSuggestions ( "baco" ) ;기본 길이 자동 (권장) :
SearchResult result = service
. Search < ArticlePage > ( "bacon" )
. Fuzzy ( )
. GetResults ( ) ;특정 길이 :
SearchResult result = service
. Search < ArticlePage > ( "bacon" )
. Fuzzy ( 4 )
. GetResults ( ) ;Stemming과 같은 기능을 지원하지 않으므로 조심스럽게 사용하십시오.
SearchResult result = service
. WildcardSearch < ArticlePage > ( "*bacon*" )
. GetResults ( ) ; SearchResult result = service
. WildcardSearch < ArticlePage > ( "me?t" )
. GetResults ( ) ; SearchResult result = service
. SimpleQuerystringSearch < ArticlePage > ( "(bacon | ham) melt -cheese" )
. GetResults ( ) ;제한된 운영자 :
SearchResult result = service
. SimpleQuerystringSearch < ArticlePage > ( " " bacon melt " sandwi*" ,
defaultOperator : Operator . And ,
allowedOperators :
SimpleQuerystringOperators . Prefix |
SimpleQuerystringOperators . Phrase ) ;
)
. GetResults ( ) ;구문은 https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-query.html을 참조하십시오.
Epinova.ElasticSearch.Core.Models.Properties.GeoPoint 의 속성을 사용하면 지리 기반 필터링을 수행 할 수 있습니다.
예제 모델 :
public class OfficePage : StandardPage
{
[ Display ]
public virtual double Lat { get ; set ; }
[ Display ]
public virtual double Lon { get ; set ; }
// Helper property, you could always roll an editor for this
public GeoPoint Location => new GeoPoint ( Lat , Lon ) ;
} 왼쪽 상단과 오른쪽 모서리를 기반으로 정사각형 영역 내부의 지점을 찾으십시오.
var topLeft = ( 59.9277542 , 10.7190847 ) ;
var bottomRight = ( 59.8881646 , 10.7983952 ) ;
SearchResult result = service
. Get < OfficePage > ( )
. FilterGeoBoundingBox ( x => x . Location , , topLeft , bottomRight )
. GetResults ( ) ; 중심점과 반경의 거리가 주어진 원 안에 점을 찾으십시오.
var center = ( 59.9277542 , 10.7190847 ) ;
var radius = "10km" ;
SearchResult result = service
. Get < OfficePage > ( )
. FilterGeoDistance ( x => x . Location , radius , center )
. GetResults ( ) ; 임의의 포인트가있는 다각형 내부의 점을 찾으십시오. 다각형 지점은 예를 들어 도시, 국가 또는 기타 유형의 지역의 윤곽이 될 수 있습니다.
var polygons = new [ ]
{
( 59.9702837 , 10.6149134 ) ,
( 59.9459601 , 11.0231964 ) ,
( 59.7789455 , 10.604809 )
} ;
SearchResult result = service
. Get < OfficePage > ( )
. FilterGeoPolygon ( x => x . Location , polygons )
. GetResults ( ) ;제공된 문서 ID와 유사한 콘텐츠를 찾으십시오
SearchResult result = service
. MoreLikeThis ( "42" )
. GetResults ( ) ;상업:
SearchResult result = service
. MoreLikeThis ( "123__CatalogContent" )
. GetResults ( ) ;선택적 매개 변수 :
minimumTermFrequency 입력 문서에서 용어가 무시되는 최소 기간 주파수. 기본값은 1입니다.
maxQueryTerms 선택할 최대 쿼리 용어 수입니다. 이 값을 높이면 쿼리 실행 속도를 희생하면서 정확도가 높아집니다. 기본값은 25입니다.
minimumDocFrequency 입력 문서에서 무시 될 최소 문서 주파수. 기본값은 3입니다.
minimumWordLength 단어 길이 용어가 무시할 최소 단어 길이. 기본값은 3입니다.
간단한 기계 장치:

전 세계적으로 유형을 제외하고 싶지 않다면 쿼리의 맥락에서만 가능합니다.
SearchResult result = service
. Search < ArticlePage > ( "bacon" )
. Exclude < SaladPage > ( )
. GetResults ( ) ; 쿼리 타임에서 노드 제외 :
SearchResult result = service
. Search < ArticlePage > ( "bacon" )
. Exclude ( 42 )
. Exclude ( contentInstance )
. Exclude ( contentReference )
. GetResults ( ) ; 쿼리는 CurrentCulture 기본값으로 사용합니다. 이것은 재정의 될 수 있습니다.
SearchResult result = service
. Search < ArticlePage > ( "bacon" )
. Language ( CultureInfo . GetCultureInfo ( "en" ) )
. GetResults ( ) ; 페이지 매김에 From() 및 Size() 의 메소드를 사용하거나 별명 Skip() 및 Take() :
SearchResult result = service
. Search < CoursePage > ( "foo" )
. From ( 10 )
. Size ( 20 )
. GetResults ( ) ; var query = service
. Search < CoursePage > ( "foo" )
. FacetsFor ( x => x . DepartmentID ) ; 측면은 전체 결과가 아닌 현재 적용된 필터에서 생성됩니다.
var query = service
. Search < CoursePage > ( "foo" )
. FacetsFor ( x => x . DepartmentID , usePostFilter : false ) ; 하나의 가치 :
string selectedFilter = Request . Querstring [ "filter" ] ;
query = query . Filter ( p => p . DepartmentID , selectedFilter ) ;확장 방법 별 하나의 값 :
string selectedFilter = Request . Querstring [ "filter" ] ;
query = query . Filter ( p => p . GetDepartmentID ( ) , selectedFilter ) ;다중 값 :
string [ ] selectedFilters = Request . Querstring [ "filters" ] ;
query = query . Filter ( p => p . DepartmentID , selectedFilters ) ;모든 필터를 필터링하기 위해 사용 및 연산자 :
string [ ] selectedFilters = Request . Querstring [ "filters" ] ;
query = query . Filter ( p => p . DepartmentID , selectedFilters , Operator . And ) ; SearchResult results = query . GetResults ( ) ; foreach ( FacetEntry facet in results . Facets )
{
// facet.Key = name of the property, ie. DepartmentID
// facet.Count = number of unique values for this facet
foreach ( FacetHit hit in facet . Hits )
{
// hit.Key = facet value
// hit.Count = number with this value
}
} 다중 필터를 그룹화하여보다 복잡한 쿼리를 형성 할 수 있습니다.
아래 예에서는 다음이 일치하는 데 충실해야합니다.
Sizes "xs" 또는 "xl" 입니다.
ProductCategory 는 "pants" 입니다.
Brand 는 "levis" 또는 "diesel" 입니다.
query = query
. FilterGroup ( group => group
. Or ( page => page . Sizes ( ) , new [ ] { "xs" , "xl" } )
. And ( page => page . ProductCategory , "pants" )
. Or ( page => page . Brand , new [ ] { "levis" , "diesel" } )
) ; 특정 값을 필터링 하려면 FilterMustNot 사용하십시오
var query = service
. Search < PageData > ( "foo" )
. FilterMustNot ( x => x . Title , "bar" ) ; 현재 사용자 ACL을 필터링하려면 FilterByACL 사용하십시오
var query = service
. Search < PageData > ( "foo" )
. FilterByACL ( ) ; EPiServer.Security.PrincipalInfo.Current 기본적으로 사용되지만 필요한 경우 사용자 정의 PrincipalInfo 공급할 수 있습니다.
주어진 값 범위 내에서 검색하려면 Range 기능을 사용하십시오.
지원되는 유형은 DateTime , double , decimal 및 long 입니다 ( int , byte 의 암시 적 변환 포함).
SearchResult result = service
. Search ( "bacon" )
. Range ( x => x . StartPublish , DateTime . Now . Date , DateTime . Now . Date . AddDays ( 2 ) )
. GetResults ( ) ; SearchResult result = service
. Search ( "bacon" )
. Range ( x => x . MyNumber , 10 , 20 )
. GetResults ( ) ;덜 논쟁은 선택 사항입니다.
SearchResult result = service
. Search ( "bacon" )
. Range ( x => x . MyNumber , 10 ) // Returns anything above 10
. GetResults ( ) ; 다른 간격 내에서 간격을 검색하려면 속성이 Epinova.ElasticSearch.Core.Models.Properties.IntegerRange 유형이어야합니다.
현재 int 유일하게 지원되는 유형입니다.
SearchResult result = service
. Search ( "bacon" )
. Range ( x => x . MyRange , 10 , 20 )
. GetResults ( ) ; public class ArticlePage : StandardPage
{
[Display]
public virtual int From { get; set; }
[Display]
public virtual int To { get; set; }
public IntegerRange MyRange => new IntegerRange(From, To);
}
속성이 유형의 IDictionary<string, object> 이고 [Searchable] 으로 표시되면 Elasticsearch에서 object 로 인덱싱됩니다.
이는 PIM 시스템과 같이 인덱싱 해야하는 동적 키 값 데이터가있는 시나리오에 유용합니다.
표준 속성 접근 :
public class ProductPage
{
[Searchable]
public IDictionary<string, object> Metadata { get; set; }
}
데이터 자체는 반환되지 않지만 쿼리하고 패싯을 만들 수 있습니다.
SearchResult result = service
. Search < ProductPage > ( "bacon" )
. InField ( x => x . Metadata + ".SomeKey" )
. FacetsFor ( x => x . Metadata + ".SomeKey" )
. GetResults ( ) ;사용자 정의 속성 :
public static class SearchConfig
{
public static void Init ( )
{
Epinova . ElasticSearch . Core . Conventions . Indexing . Instance
. ForType < ProductPage > ( ) . IncludeField ( "Metadata" , x => x . GetPimDataDictionary ( ) ) ;
}
}이 접근법은 데이터를 반환합니다.
SearchResult result = service
. Search < ProductPage > ( "bacon" )
. GetResults ( ) ;
var hit = result . Hits . First ( ) ;
var dict = hit . Custom [ "Metadata" ] as IDictionary < string , object > Boost 속성으로 장식하여 속성을 향상시킬 수 있습니다.
[ Boost ( 13 ) ]
public string Title { get ; set ; }… 또는 쿼리 시간에 :
SearchResult result = service
. Search ( "bacon" )
. Boost ( x => x . MyProp , 3 )
. GetResults ( ) ;유형은 양수 또는 부정적인 부스트를 제공 할 수 있습니다.
SearchResult result = service
. Search ( "bacon" )
. Boost < ArticlePage > ( 2 )
. GetResults ( ) ; SearchResult result = service
. Search ( "bacon" )
. Boost ( typeof ( ArticlePage ) , 2 )
. GetResults ( ) ; SearchResult result = service
. Search ( "bacon" )
. Boost < ArticlePage > ( - 3 )
. GetResults ( ) ;페이지 트리의 위치에 따라 히트를 향상시킬 수도 있습니다.
SearchResult result = service
. Search ( "bacon" )
. BoostByAncestor ( new ContentReference ( 42 ) , 2 )
. GetResults ( ) ; Decay 함수를 사용하여 값에 따라 날짜 속성이 더 낮게 점수를받을 수 있습니다. 이렇게하면 오래된 기사를 통해 새로운 기사를 홍보 할 수 있습니다.
SearchResult result = service
. Search ( "bacon" )
. Decay ( x => x . StartPublish , TimeSpan . FromDays ( 7 ) )
. GetResults ( ) ;날짜 간격 (두 번째 인수)이 발생할 때마다 점수에서 0.5 포인트가 빼게됩니다. 위의 예에서, 0.5 포인트는 7 일 후에, 14 일 후에 1 점 등을 빼게됩니다.
쿼리에서 .UseBestBets() 사용하여 선택한 콘텐츠를 정상보다 높은 점수를받습니다.
SearchResult result = service
. Search ( "bacon" )
. UseBestBets ( )
. GetResults ( ) ;가장 좋은 방법은 임베디드 메뉴 검색 엔진을 통해 관리 할 수 있습니다.

.Track() 함수를 사용하여 간단한 통계를 수집 할 수 있습니다. 이것은 용어가 쿼리 된 횟수와 히트를 반환했는지 여부를 추적합니다.
SearchResult result = service
. Search ( "bacon" )
. Track ( )
. GetResults ( ) ; 
참고 : Connection String이 이름이없는 경우 EPiServerDB 라는 경우 trackingConnectionStringName -Configuration에서 이름을 제공해야합니다.
Stemming은 기본적으로 유형 XhtmlString 의 모든 속성 또는 MainIntro 또는 MainBody 라는 속성에 적용됩니다.
언어는 컨텐츠 언어를 기반으로합니다. 유형 string 의 다른 속성은 Stem -attribute로 장식하여 줄기 될 수 있습니다.
[ Stem ]
public string Title { get ; set ; } 점수 나 분석없이 특정 유형의 내용을 나열하려면 Get 기능을 사용하십시오. 간단한 목록을 위해 SortBy 와 함께 사용할 수 있습니다.
SearchResult result = service
. StartFrom ( somePageLink )
. Get < ArticlePage > ( )
. GetResults ( ) ; 정렬은 일반적으로 각 경기의 점수를 기반으로 Elasticsearch에서 수행됩니다. 수동 정렬은 스코어링이 관련이없는 시나리오에서만 사용해야합니다. 예를 들어 앞에서 언급 한 Get 기능을 사용할 때.
SearchResult result = service
. Get < ArticlePage > ( )
. SortBy ( p => p . StartPublish )
. ThenBy ( p => p . Foo )
. ThenBy ( p => p . Bar )
. GetResults ( ) ; Geopoint를 정렬 할 때 더욱 의무적 인 주장이 있습니다. compareTo . 항목은 이러한 좌표와 비교되며 결과 거리는 정렬 값으로 사용됩니다.
절대 제어를 위해 스크립트를 사용하여 문서를 정렬 할 수 있습니다. 이것은 성능에 영향을 줄 수 있습니다.
예제 정렬 특정 타임 스탬프를 기반으로합니다.
var timestamp = new DateTimeOffset ( new DateTime ( 2019 , 1 , 1 ) ) . ToUnixTimeMilliseconds ( ) ;
var script = $ "doc['StartPublish'].date.getMillis() > { timestamp } ? 0 : 1" ;
SearchResult result = service
. Get < ArticlePage > ( )
. SortByScript ( script , true , "number" )
. GetResults ( ) ;스크립팅 구문은 https://www.elastic.co/guide/en/elasticsearch/painless/current/index.html을 참조하십시오.
철자가 틀린 단어를 검색 할 때 인덱스에서 발견 된 유사한 단어를 제안 할 수있는 지붕 널 필터가 포함되어 있습니다.
발견 된 모든 제안은 검색 결과의 DidYouMean 속성에 포함됩니다.
SearchResult result = service
. Search ( "alloi" )
. GetResults ( ) ;
string [ ] didYouMean = result . DidYouMean ; // [ "alloy", "all" ] 제안을위한 소스 역할을하는 속성에는 [DidYouMeanSource] 가 표시되어야합니다.
public class StandardPage : SitePageData
{
[ DidYouMeanSource ]
public virtual XhtmlString MainBody { get ; set ; }
} GetResults() 가 반환 한 결과는 Eppyerver에 대한 지식이 없습니다. 에피소버 컨텍스트에서 GetContentResults() 함수를 사용하십시오.
이렇게하면 필터 FilterAccess , FilterPublished 및 FilterTemplate 자동으로 적용됩니다.
IEnumerable < IContent > content = service
. Search < CoursePage > ( text )
. GetContentResults ( ) ; 편집 모드에서 검색 할 때 포함 된 제공 업체를 사용하려면 CMS-> admin-> 구성 -> 도구 설정 -> 검색 구성으로 이동하십시오.
제공자를 진드기로 틱하고 목록의 상단으로 드래그하십시오.
동의어는 메뉴 검색 엔진 -> 동의어에서 관리 할 수 있습니다.
게시, 이동 및 삭제와 같은 일반적인 작업을 수행 할 때는 컨텐츠가 자동으로 다시 표시됩니다.
모든 내용의 초기 인덱싱을 수행하려면 예정된 작업을 실행하십시오«Elasticsearch : Index CMS 컨텐츠»
RENINDEXING은 도구 메뉴를 통해 개별 컨텐츠에서 수동으로 트리거 될 수 있습니다.

… 또는 페이지 트리의 컨텍스트 메뉴를 통해 :

GetResults() 가 반환 한 결과는 Eppyerver에 대한 지식이 없습니다. Episerver Commerce 컨텍스트에서 GetCatalogResults() 함수를 사용하십시오. 올바른 색인을 자동으로 선택하고 필터 필터를 FilterAccess , FilterPublished 및 FilterTemplate 적용합니다.
IEnumerable < ProductContent > content = service
. Search < ProductContent > ( text )
. GetCatalogResults ( ) ; 게시, 이동 및 삭제와 같은 일반적인 작업을 수행 할 때는 컨텐츠가 자동으로 다시 표시됩니다.
모든 컨텐츠의 초기 인덱싱을 수행하려면 예정된 작업을 실행하십시오«Elasticsearch : Index Commerce Content»
메뉴 검색 엔진 -> 관리 및 상태를 통해 Normal과 Tri -Gram 토큰 화기 (최소 = 3, Max = 3, Tokens = 3, Digit, Char, Elastic 권장 사항) 사이를 전환 할 수 있습니다.
Highlight() 함수를 사용하여 텍스트에서 일치가 발생한 곳에서 150 자 발췌문을 얻으십시오.
SearchResult result = service
. Search ( "bacon" )
. Highlight ( )
. GetContentResults ( ) ; MainIntro , MainBody , Attachment 및 Description 이라는 속성에서 기본적으로 강조 표시됩니다.
기본 마커는 <mark> 입니다
다음 구성 옵션 으로이 동작을 사용자 정의 할 수 있습니다.
Indexing . Instance . ForType < ArticlePage > ( ) . EnableHighlighting ( x => x . MyField ) ;
Indexing . Instance . SetHighlightFragmentSize ( 42 ) ;
Indexing . Instance . SetHighlightTag ( "blink" ) ; 결과는 각 히트의 Highlight 속성에서 찾을 수 있습니다.
Bulk 기능을 사용하여 사용자 정의 컨텐츠를 색인하십시오.
예:
var obj1 = new ComplexType { StringProperty = "this is myobj 1" } ;
var obj2 = new ComplexType { StringProperty = "this is myobj 2" } ;
var obj3 = new ComplexType { StringProperty = "this is myobj 3" } ;
ICoreIndexer indexer = ServiceLocator . Current . GetInstance < ICoreIndexer > ( ) ; // Can also be injected
BulkBatchResult bulk = indexer . Bulk ( new [ ]
{
new BulkOperation ( obj1 , "no" ) ,
new BulkOperation ( obj2 , "no" ) ,
new BulkOperation ( obj3 , "no" )
} ) ;
var results = _service
. Search < ComplexType > ( "myobj" )
. InField ( x => x . StringProperty )
. GetCustomResults ( ) ; 예를 들어 충돌을 피하기 위해 사용자 정의 색인을 선호하는 경우 색인 및 검색시 제공해야합니다.
var obj1 = new ComplexType { StringProperty = "this is myobj 1" } ;
var obj2 = new ComplexType { StringProperty = "this is myobj 2" } ;
var obj3 = new ComplexType { StringProperty = "this is myobj 3" } ;
ICoreIndexer indexer = ServiceLocator . Current . GetInstance < ICoreIndexer > ( ) ; // Can also be injected
string indexName = "my-uber-custom-name-no" ;
BulkBatchResult bulk = indexer . Bulk ( new [ ]
{
new BulkOperation ( obj1 , "no" , index : indexName ) ,
new BulkOperation ( obj2 , "no" , index : indexName ) ,
new BulkOperation ( obj3 , "no" , index : indexName )
} ) ;
var results = _service
. UseIndex ( indexName )
. Search < ComplexType > ( "myobj" )
. InField ( x => x . StringProperty )
. GetCustomResults ( ) ; epinova.elasticsearch는 표준 HttpClient 사용하여 Elasticsearch를 호출합니다. 때로는 메시지를 다르게 처리하는 것이 필요합니다. 예를 들어 클라우드 서비스 요청 서명.
여러 httpmessagehandlers를 원한다면 설정하기 전에 체인을하는 것이 좋습니다.
예를 들어:
MessageHandler . Instance . SetMessageHandler ( new AWSHandler ( ) ) ; GetCustomResults GetContentResults 와 달리 강력하게 입력 된 오브젝트를 반환하며 ID 만 반환합니다.
사용자 정의 객체에는 Id 속성이 필요하지 않거나 (또는 BulkOperation CTOR의 해당 인수) 버전 작성 및 업데이트/삭제를 제어하려는 경우 권장됩니다.