@@ -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+ 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
548710Add Elasticsearch health checks:
0 commit comments