Skip to content

Commit 3009b85

Browse files
committed
docs: parent / child
1 parent 88054d3 commit 3009b85

File tree

1 file changed

+162
-0
lines changed

1 file changed

+162
-0
lines changed

docs/guide/elasticsearch-setup.md

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,168 @@ services.AddStartupAction("ConfigureElasticsearch", async sp =>
543543
});
544544
```
545545

546+
## Parent-Child Relationships
547+
548+
Elasticsearch supports parent-child relationships using join fields. This allows you to model hierarchical data where children are stored in the same index as parents but can be queried independently.
549+
550+
### Defining Parent-Child Documents
551+
552+
Implement `IParentChildDocument` for both parent and child entities:
553+
554+
```csharp
555+
using Foundatio.Repositories.Elasticsearch;
556+
using Foundatio.Repositories.Models;
557+
using Nest;
558+
559+
// Parent document
560+
public class Organization : IParentChildDocument, IHaveDates, ISupportSoftDeletes
561+
{
562+
public string Id { get; set; }
563+
564+
// IParentChildDocument - parent doesn't need a ParentId
565+
string IParentChildDocument.ParentId { get; set; }
566+
JoinField IParentChildDocument.Discriminator { get; set; }
567+
568+
public string Name { get; set; }
569+
public DateTime CreatedUtc { get; set; }
570+
public DateTime UpdatedUtc { get; set; }
571+
public bool IsDeleted { get; set; }
572+
}
573+
574+
// Child document
575+
public class Employee : IParentChildDocument, IHaveDates, ISupportSoftDeletes
576+
{
577+
public string Id { get; set; }
578+
579+
// Child must have ParentId
580+
public string ParentId { get; set; }
581+
JoinField IParentChildDocument.Discriminator { get; set; }
582+
583+
public string Name { get; set; }
584+
public string Email { get; set; }
585+
public DateTime CreatedUtc { get; set; }
586+
public DateTime UpdatedUtc { get; set; }
587+
public bool IsDeleted { get; set; }
588+
}
589+
```
590+
591+
### Configuring the Index
592+
593+
Create a single index with a join field mapping:
594+
595+
```csharp
596+
public sealed class OrganizationIndex : VersionedIndex
597+
{
598+
public OrganizationIndex(IElasticConfiguration configuration)
599+
: base(configuration, "organizations", version: 1) { }
600+
601+
public override CreateIndexDescriptor ConfigureIndex(CreateIndexDescriptor idx)
602+
{
603+
return base.ConfigureIndex(idx
604+
.Settings(s => s.NumberOfReplicas(0).NumberOfShards(1))
605+
.Map<IParentChildDocument>(m => m
606+
.AutoMap<Organization>()
607+
.AutoMap<Employee>()
608+
.Properties(p => p
609+
.SetupDefaults()
610+
.Keyword(k => k.Name(o => ((Organization)o).Name))
611+
.Keyword(k => k.Name(e => ((Employee)e).Email))
612+
// Configure the join field
613+
.Join(j => j
614+
.Name(n => n.Discriminator)
615+
.Relations(r => r.Join<Organization, Employee>())
616+
)
617+
)));
618+
}
619+
}
620+
```
621+
622+
### Creating Repositories
623+
624+
Create separate repositories for parent and child:
625+
626+
```csharp
627+
// Parent repository
628+
public class OrganizationRepository : ElasticRepositoryBase<Organization>
629+
{
630+
public OrganizationRepository(OrganizationIndex index) : base(index) { }
631+
}
632+
633+
// Child repository - must set HasParent and GetParentIdFunc
634+
public class EmployeeRepository : ElasticRepositoryBase<Employee>
635+
{
636+
public EmployeeRepository(OrganizationIndex index) : base(index)
637+
{
638+
HasParent = true;
639+
GetParentIdFunc = e => e.ParentId;
640+
641+
// Required for soft delete filtering on parent
642+
DocumentType = typeof(Employee);
643+
ParentDocumentType = typeof(Organization);
644+
}
645+
}
646+
```
647+
648+
### Working with Parent-Child Documents
649+
650+
```csharp
651+
// Add parent
652+
var org = await orgRepository.AddAsync(new Organization { Name = "Acme Corp" });
653+
654+
// Add child with parent reference
655+
var employee = await employeeRepository.AddAsync(new Employee
656+
{
657+
Name = "John Doe",
658+
Email = "[email protected]",
659+
ParentId = org.Id // Link to parent
660+
});
661+
662+
// Get child by ID (requires routing for efficiency)
663+
var emp = await employeeRepository.GetByIdAsync(new Id(employee.Id, org.Id));
664+
665+
// Or without routing (uses search fallback)
666+
var emp = await employeeRepository.GetByIdAsync(employee.Id);
667+
668+
// Query children by parent
669+
var employees = await employeeRepository.FindAsync(q => q.ParentId("organization", org.Id));
670+
```
671+
672+
### Parent-Child Soft Delete Behavior
673+
674+
When a parent is soft-deleted, children are automatically filtered from queries:
675+
676+
```csharp
677+
// Soft delete the parent
678+
org.IsDeleted = true;
679+
await orgRepository.SaveAsync(org);
680+
681+
// Children are now filtered (even though they're not deleted)
682+
var count = await employeeRepository.CountAsync(); // Returns 0
683+
684+
// Restore parent
685+
org.IsDeleted = false;
686+
await orgRepository.SaveAsync(org);
687+
688+
// Children are visible again
689+
var count = await employeeRepository.CountAsync(); // Returns children count
690+
```
691+
692+
### Querying with Parent Filters
693+
694+
```csharp
695+
// Find children where parent matches criteria
696+
var results = await employeeRepository.FindAsync(q => q
697+
.ParentQuery(pq => pq
698+
.DocumentType<Organization>()
699+
.FieldEquals(o => o.Name, "Acme Corp")));
700+
```
701+
702+
::: warning Routing Considerations
703+
- Child documents are routed to the same shard as their parent using `ParentId`
704+
- For best performance, always provide routing when getting children by ID: `new Id(childId, parentId)`
705+
- Without routing, the repository falls back to a search query which is slower
706+
:::
707+
546708
## Health Checks
547709

548710
Add Elasticsearch health checks:

0 commit comments

Comments
 (0)