本文介绍如何使用ElasticsearchCRUD在Elasticsearch中创建父,子和孙子文档。 如果创建相互关联的文档,那么文件全部保存到Elasticsearch中的同一个分片很重要。 搜索性能更好,如果可以为搜索定义特定的分片。
当创建父文档和子文档关系时,父定义对于子文档是足够的。 这样可确保将子文档保存到同一分片中。 一旦使用了孙子文档,就需要一个路由定义,否则孙子文档不会总是被保存到同一个分片中,创建子文档的所有优点都将丢失。
步骤1:定义文档模型
在本应用程序中使用了LeagueCup
,Team
和Player
类。 LeagueCup
类是父类。 它有一个Team
类的列表。 Team
类有一个子Player
类的列表。 我们希望将所有文档保存在同一个索引中,并确保将子文件和孙子节文档保存到同一个分片中。 子文档需要Key
属性定义,以便ElasticsearchCrud知道哪个属性被用作_id
定义。
public class LeagueCup
{
public long Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public List<Team> Teams { get; set; }
}
public class Team
{
[Key]
public long Id { get; set; }
public string Name { get; set; }
public string Stadium { get; set; }
public List<Player> Players { get; set; }
}
public class Player
{
[Key]
public long Id { get; set; }
public string Name { get; set; }
public int Goals { get; set; }
public int Assists { get; set; }
public string Position { get; set; }
public int Age { get; set; }
}
步骤2:使用正确的映射创建索引
要使用映射创建索引,需要更改ElasticsearchCRUD中上下文的默认配置。 ElasticsearchSerializerConfiguration
Config
包含所有必需的配置。 我们希望将每个子文档保存为单独的映射或索引类型,并且还可以处理每种类型的所有子文档。 使用UserDefinedRouting
,路由也被强制用于子文档。 这不是默认的,因为如果没有使用孙子文档,这不是必需的。 Elasticsearch中的默认配置将完整的子树保存为嵌套项,处理所有子项,并且不添加路由。
不同类型也需要映射定义。 默认情况下,每种类型将被保存到自己的索引中。 这是改变,所以关系中的所有类型都保存到相同的索引:leagues。
private static readonly IElasticsearchMappingResolver ElasticsearchMappingResolver = new ElasticsearchMappingResolver();
private const bool SaveChildObjectsAsWellAsParent = true;
private const bool ProcessChildDocumentsAsSeparateChildIndex = true;
private const bool UserDefinedRouting = true;
private static readonly ElasticsearchSerializerConfiguration Config = new ElasticsearchSerializerConfiguration(ElasticsearchMappingResolver, SaveChildObjectsAsWellAsParent,
ProcessChildDocumentsAsSeparateChildIndex, UserDefinedRouting);
private const string ConnectionString = "http://localhost:9200";
static void Main(string[] args)
{
//定义类型的映射,以便所有使用与父级相同的索引
ElasticsearchMappingResolver.AddElasticSearchMappingForEntityType(typeof(LeagueCup), MappingUtils.GetElasticsearchMapping("leagues"));
ElasticsearchMappingResolver.AddElasticSearchMappingForEntityType(typeof(Team), MappingUtils.GetElasticsearchMapping("leagues"));
ElasticsearchMappingResolver.AddElasticSearchMappingForEntityType(typeof(Player), MappingUtils.GetElasticsearchMapping("leagues"));
CreateIndexWithRouting();
}
CreateIndexWithRouting
方法创建一个新的索引,具有三种类型的映射。 context.CreateIndex()在三个不同的PUT请求中执行,每个类型一个。
private static void CreateIndexWithRouting()
{
//使用路由作为子父关系。 如果您使用孙子文档,则需要这样做。
//如果路由确保孙子文档保存到与父文档相同的分片。
// --------------
//如果仅使用父文档和子文档,则不需要路由。 子文档被保存
//使用父定义与父文档相同的分片。
// --------------
//可以使用配置参数定义路由定义:ElasticsearchSerializerConfiguration中的UserDefinedRouting
//var config = new ElasticsearchSerializerConfiguration(ElasticsearchMappingResolver, SaveChildObjectsAsWellAsParent,
// ProcessChildDocumentsAsSeparateChildIndex, UserDefinedRouting);
using (var context = new ElasticsearchContext(ConnectionString, Config))
{
context.TraceProvider = new ConsoleTraceProvider();
//在Elasticsearch中创建索引
//这创建了一个索引`leagues`和3种类型,leaguecup, team, player
var ret = context.CreateIndex<LeagueCup>();
}
}
具有父映射的创建索引如下发送:
PUT http://localhost:9200/leagues/ HTTP/1.1
Content-Type: application/json
Host: localhost:9200
Content-Length: 192
Expect: 100-continue
Connection: Keep-Alive
{
"settings": {
"number_of_shards":5,
"number_of_replicas":1
},
"mappings": {
"leaguecup": {
"properties": {
"id":{ "type" : "long" },
"name":{ "type" : "string" },
"description":{ "type" : "string" } }
}
}
}
发送第一个子类PUT请求如下所示。 路由仅使用必需属性定义。 不需要其他选项,因为如果使用属性,以下请求将发送到Elasticsearch,然后重新路由,这会导致性能损失。
PUT http://localhost:9200/leagues/team/_mappings HTTP/1.1
Content-Type: application/json
Host: localhost:9200
Content-Length: 174
Expect: 100-continue
{
"team": {
"_parent": {
"type":"leaguecup"
},
"_routing": {
"required":"true"
},
"properties": {
"id": { "type" : "long" },
"name":{ "type" : "string" },
"stadium":{ "type" : "string" }
}
}
}
孙子类映射PUT请求发送如下:
PUT http://localhost:9200/leagues/player/_mappings HTTP/1.1
Content-Type: application/json
Host: localhost:9200
Content-Length: 265
Expect: 100-continue
{
"player": {
"_parent":{"type":"team"},
"_routing":{"required":"true"},
"properties":{"id":{ "type" : "long" },
"name":{ "type" : "string" },
"goals":{ "type" : "integer" },
"assists":{ "type" : "integer" },
"position":{ "type" : "string" },
"age":{ "type" : "integer" }
}
}
}
步骤3:添加LeagueCup文档
现在索引和类型映射存在,可以添加一个新的LeagueCup文件。
private static long CreateNewLeague()
{
var swissCup = new LeagueCup {Description = "Nataional Cup Switzerland", Id = 1, Name = "Swiss Cup"};
using (var context = new ElasticsearchContext(ConnectionString, Config))
{
context.TraceProvider = new ConsoleTraceProvider();
context.AddUpdateDocument(swissCup, swissCup.Id);
context.SaveChanges();
}
return swissCup.Id;
}
添加文档请求作为批量请求的一部分发送。 ElasticsearchCRUD在bulk请求中发送所有添加,更新和删除请求。 然后可以将不同的请求优化为单个请求。 context.SaveChanges()
发送所有待处理的请求。
POST http://localhost:9200/_bulk HTTP/1.1
Content-Type: application/json
Host: localhost:9200
Content-Length: 131
Expect: 100-continue
{"index":{"_index":"leagues","_type":"leaguecup","_id":"1"}}
{"id":1,"name":"Swiss Cup","description":"Nataional Cup Switzerland"}
步骤4:添加Team文档
team 请求使用父类LeagueCup
的父ID发送。
/// <summary>
/// parentId是父对象的id
/// Elasticsearch需要路由标识,强制所有子对象都保存到同一个分片。 这对性能有好处。
/// 因为这是一个第一级的子级,所以routingId和parentId是一样的。
/// </summary>
private static long AddTeamToCup(long leagueId)
{
var youngBoys = new Team {Id=2,Name="Young Boys", Stadium="Wankdorf Bern"};
using (var context = new ElasticsearchContext(ConnectionString, Config))
{
context.TraceProvider = new ConsoleTraceProvider();
context.AddUpdateDocument(youngBoys, youngBoys.Id, new RoutingDefinition { ParentId = leagueId, RoutingId = leagueId });
context.SaveChanges();
}
return youngBoys.Id;
}
此请求使用父Id以及路由Id。 因为文档是一级的子级,所以两个id是相同的。
POST http://localhost:9200/_bulk HTTP/1.1
Content-Type: application/json
Host: localhost:9200
Content-Length: 136
Expect: 100-continue
{"index":{"_index":"leagues","_type":"team","_id":"2","_parent":1,"_routing":1}}
{"id":2,"name":"Young Boys","stadium":"Wankdorf Bern"}
步骤5:添加Player文档
然后可以将player
添加到team
父级的索引中,并将路由添加到leagueCup
顶级父级。
static void AddPlayerToTeam(long teamId, long leagueId)
{
var yvonMvogo = new Player { Id = 3, Name = "Yvon Mvogo", Age = 20, Goals = 0, Assists = 0, Position = "Goalkeeper" };
using (var context = new ElasticsearchContext(ConnectionString, Config))
{
context.TraceProvider = new ConsoleTraceProvider();
context.AddUpdateDocument(yvonMvogo, yvonMvogo.Id, new RoutingDefinition { ParentId = teamId, RoutingId = leagueId });
context.SaveChanges();
PUT请求再次以bulk 请求发送。 这当然可以与以前的请求一起发送,但是demo目的是单独发送的。
POST http://localhost:9200/_bulk HTTP/1.1
Content-Type: application/json
Host: localhost:9200
Content-Length: 167
Expect: 100-continue
{"index":{"_index":"leagues","_type":"player","_id":"3","_parent":2,"_routing":1}}
{"id":3,"name":"Yvon Mvogo","goals":0,"assists":0,"position":"Goalkeeper","age":20}
现在索引中存在3个文档,可以从搜索引擎中选择文档。 player
文档的GET
请求需要父Id和路由Id。
private static Player GetPlayer(long playerId, long leagueId, long teamId)
{
Player player;
using (var context = new ElasticsearchContext(ConnectionString, Config))
{
context.TraceProvider = new ConsoleTraceProvider();
player = context.GetDocument<Player>(playerId, new RoutingDefinition { ParentId = teamId, RoutingId = leagueId });
}
return player;
}
GetPlayer
请求如下发送:
GET http://localhost:9200/leagues/player/3?parent=2&routing=1 HTTP/1.1
Host: localhost:9200
响应:
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Content-Length: 167
{
"_index":"leagues",
"_type":"player",
"_id":"3","_version":1,
"found":true,"
_source": {
"id":3,
"name":
"Yvon Mvogo",
"goals":0,
"assists":0,
"position":"Goalkeeper",
"age":20
}
}
结论:
在Elasticsearch中定义和使用子文档和孙子文档非常简单。 如果要优化搜索性能,则需要将文档保存到同一个分片。 这是通过路由实现的。 如果只使用父文件和子文档,则只需要父Id。 如果同时更新和添加所有树结构,也可以使用嵌套文档。 所有数据结构都有优缺点。 应根据您的要求选择正确的。