/// <summary> /// 根据给定的两个节点,探索能够与这两个节点相连的中间结点集合。 /// 注意,在代码中请使用 ExploreInterceptionNodesAsync 的重载。 /// 不要直接调用此函数。 /// </summary> private async Task ExploreInterceptionNodesInternalAsync(IReadOnlyList <KgNode> nodes1, KgNode node2) { if (nodes1 == null) { throw new ArgumentNullException(nameof(nodes1)); } if (node2 == null) { throw new ArgumentNullException(nameof(node2)); } if (nodes1.Count == 0) { return; // Nothing to explore. } KgNode node1; var explore12DomainKey = new InterceptionFetchingDomainKey(node2.Id); // 注意, node1 和 node2 的方向是不能互换的, // 所以不要想着把对侧也标记为已经探索过了。 var nodes1ToExplore = nodes1 .Where(n => GetStatus(n.Id).TryMarkAsFetching(explore12DomainKey)) .ToArray(); Debug.Assert(nodes1ToExplore.IsDistinct()); if (nodes1ToExplore.Length == 0) { goto WAIT_FOR_EXPLORATIONS; } IReadOnlyCollection <PaperNode> papers1 = null; IReadOnlyCollection <AuthorNode> authors1 = null; IReadOnlyCollection <AffiliationNode> affiliations1 = null; IReadOnlyCollection <FieldOfStudyNode> foss1 = null; if (nodes1ToExplore.Length == 1) { node1 = nodes1ToExplore[0]; if (node1 is PaperNode) { papers1 = new[] { (PaperNode)node1 } } ; else if (node1 is AuthorNode) { authors1 = new[] { (AuthorNode)node1 } } ; else if (node1 is AffiliationNode) { affiliations1 = new[] { (AffiliationNode)node1 } } ; else if (node1 is FieldOfStudyNode) { foss1 = new[] { (FieldOfStudyNode)node1 } } ; } else { node1 = null; if (nodes1ToExplore[0] is PaperNode) { papers1 = nodes1ToExplore.CollectionCast <PaperNode>(); } else if (nodes1ToExplore[0] is AuthorNode) { authors1 = nodes1ToExplore.CollectionCast <AuthorNode>(); } else if (nodes1ToExplore[0] is AffiliationNode) { affiliations1 = nodes1ToExplore.CollectionCast <AffiliationNode>(); } else if (nodes1ToExplore[0] is FieldOfStudyNode) { foss1 = nodes1ToExplore.CollectionCast <FieldOfStudyNode>(); } else { throw new ArgumentException("集合包含元素的类型过于复杂。请尝试使用单元素集合多次调用。", nameof(node1)); } } if (papers1 != null) { // 需要调查 papers1 的 AuId JId CId FId 等联结关系。 await FetchPapersAsync(papers1); } var paper2 = node2 as PaperNode; if (paper2 != null) { Debug.Assert(GetStatus(paper2.Id) .GetFetchingStatus(NodeStatus.PaperFetching) == FetchingStatus.Fetched); } PaperNode[] papers1References = null; if (papers1 != null) { // 注意,参考文献(或是作者——尽管在这里不需要)很可能会重复。 papers1References = papers1.SelectMany(p1 => graph .AdjacentOutVertices(p1.Id)) .Distinct() .Select(id => nodes[id]) .OfType <PaperNode>() .ToArray(); } // searchConstraint : 建议尽量简短,因为 FromPapers1 的约束可能 // 会是 100 个条件的并。 Func <string, Task> FetchFromPapers1References = searchConstraint => { // 注意,我们是来做全局搜索的。像是 Id -> AuId -> Id // 这种探索在探索论文时应该已经解决了。 Debug.Assert(papers1 != null); Debug.Assert(papers1References != null); var tasks = papers1References .Partition(SEB.MaxChainedIdCount) .Select(nodes3 => FetchPapersAsync(nodes3, searchConstraint)); return(Task.WhenAll(tasks)); }; if (paper2 != null) { // 带有 作者/研究领域/会议/期刊 等属性限制, // 搜索 >引用< 中含有 paper2 的论文 Id。 // attributeConstraint 可以长一些。 if (papers1 != null) { // Id1 -> Id3 -> Id2 //Trace.WriteLine("Fetch From Papers1 " + papers1.Count); var citations = CitationCount(paper2); var papersToFetch = papers1References.Length; if (citations < papersToFetch) { // 从 Id2 方向向左探索 Id1 可能会好一些。 // 注意到 Left to Right 一次只能探索约 100 篇 // Right to Left 一次可以探索约 1000 篇,就是有点儿慢。 await FetchCitationsToPaperAsync(paper2, null, ConcurrentPagingMode.Pessimistic); } else { await FetchFromPapers1References(SEB.ReferenceIdContains(paper2.Id)); } } else if (authors1 != null) { // AA.AuId <-> Id -> Id await Task.WhenAll(authors1 .Select(n => n.Id) .Partition(SEB.MaxChainedAuIdCount) .Select(id1s => FetchCitationsToPaperAsync(paper2, SEB.AuthorIdIn(id1s), ConcurrentPagingMode.Optimistic))); } else if (affiliations1 != null) { // AA.AfId <-> AA.AuId <-> Id // 注意,要确认 AuId 是否位于 nodes1 列表中。 // 这里的 AuId 是 Id2 论文的作者列表。 await Task.WhenAll(graph.AdjacentOutVertices(node2.Id) .Where(id => nodes[id] is AuthorNode) .Select(id => FetchAuthorAffiliationRelations(id, affiliations1.Select(af => af.Id)))); } else if (foss1 != null) { // F.FId <-> Id -> Id await Task.WhenAll(foss1.Select(fos => fos.Id) .Partition(SEB.MaxChainedFIdCount) .Select(fids => FetchCitationsToPaperAsync(paper2, SEB.FieldOfStudyIdIn(fids), ConcurrentPagingMode.Pessimistic))); } else if (node1 is ConferenceNode) { // C.CId <-> Id -> Id await FetchCitationsToPaperAsync(paper2, SEB.ConferenceIdEquals(node1.Id), ConcurrentPagingMode.Optimistic); } else if (node1 is JournalNode) { // J.JId <-> Id -> Id await FetchCitationsToPaperAsync(paper2, SEB.JournalIdEquals(node1.Id), ConcurrentPagingMode.Optimistic); } else { Debug.WriteLine("Ignoreed: {0}-{1}", nodes1ToExplore[0], node2); } } else { Debug.Assert(node2 is AuthorNode); // 带有 研究领域/会议/期刊 等属性限制, // 搜索 author2 的论文 Id。 // attributeConstraint 可以长一些。 Func <string, Task> ExplorePapersOfAuthor2WithAttributes = async attributeConstraint => { // 如果作者的所有论文已经被探索过了, // 那么很幸运,不需要再探索了。 if (await GetStatus(node2.Id).UntilFetchedAsync(NodeStatus.AuthorPapersFetching)) { return; } await SearchClient.EvaluateAsync(SEB.And( attributeConstraint, SEB.AuthorIdContains(node2.Id)), Assumptions.AuthorMaxPapers, ConcurrentPagingMode.Optimistic, page => Task.WhenAll(page.Entities.Select(async entity => { RegisterNode(entity); await ExplorePaperAsync(entity); }))); }; if (papers1 != null) { // Id1 -> Id3 -> AA.AuId2 await FetchFromPapers1References(SEB.AuthorIdContains(node2.Id)); } else if (authors1 != null) { // AA.AuId1 <-> Id3 <-> AA.AuId2 // 探索 AA.AuId1 的所有论文。此处的探索还可以顺便确定 AuId1 的所有组织。 // 注意到每个作者都会写很多论文 // 不论如何,现在尝试从 Id1 向 Id2 探索。 // 我们需要列出 Id1 的所有文献,以获得其曾经位于的所有组织。 await FetchAuthorsPapersAsync(authors1); // AA.AuId1 <-> AA.AfId3 <-> AA.AuId2 // AA.AuId1 <-> AA.AfId3 已经在前面探索完毕。 // 只需探索 AA.AfId3 <-> AA.AuId2 。 await FetchAuthorAffiliationRelations(node2.Id, authors1 .SelectMany(au1 => graph.AdjacentOutVertices(au1.Id) .Where(id2 => nodes[id2] is AffiliationNode))); } else if (affiliations1 != null) { // 不可能和 Affiliations 扯上 2-hop 关系啦…… // AA.AfId - ? - AA.AuId } else if (foss1 != null) { // F.FId <-> Id <-> AA.AuId await Task.WhenAll(foss1.Select(fos => fos.Id) .Partition(SEB.MaxChainedFIdCount) .Select(fids => ExplorePapersOfAuthor2WithAttributes(SEB.FieldOfStudyIdIn(fids)))); } else if (node1 is ConferenceNode) { // C.CId <-> Id <-> AA.AuId await ExplorePapersOfAuthor2WithAttributes(SEB.ConferenceIdEquals(node1.Id)); } else if (node1 is JournalNode) { // J.JId <-> Id <-> AA.AuId await ExplorePapersOfAuthor2WithAttributes(SEB.JournalIdEquals(node1.Id)); } else { Debug.WriteLine("Ignoreed: {0}-{1}", nodes1ToExplore[0], node2); } } // 标记探索完毕 foreach (var n in nodes1ToExplore) { GetStatus(n.Id).MarkAsFetched(explore12DomainKey); } // 等待其他线程(如果有) WAIT_FOR_EXPLORATIONS: var waitResult = await Task.WhenAll(nodes1.Select(n => GetStatus(n.Id).UntilFetchedAsync(explore12DomainKey))); Debug.Assert(waitResult.All(r => r)); } }
/// <summary> /// 异步探索作者的所有论文,顺便探索他/她在发表这些论文时所位于的机构。 /// 如果其他线程正在探索此节点,则等待此节点探索完毕。 /// </summary> private async Task FetchAuthorsPapersAsync(IReadOnlyCollection <AuthorNode> authorNodes) { // 有些类似于 // Task LocalExploreAsync(IEnumerable<PaperNode> paperNodes); // 探索 author 的所有论文。此处的探索还可以顺便确定 author 的所有组织。 var nodesToExplore = authorNodes .Where(n => GetStatus(n.Id).TryMarkAsFetching(NodeStatus.AuthorPapersFetching)) .ToArray(); if (nodesToExplore.Length == 0) { goto WAIT_FOR_EXPLORATIONS; } // 随后,先把 authorNodes 注册一遍。 var fetchTasks = nodesToExplore.Select(n => n.Id) .Partition(SEB.MaxChainedAuIdCount) .Select(async ids => { // 一次探索若干作者。这意味着不同作者的文章是混在一起的。 // 假定 Partition 返回的是 IList / ICollection var idc = (ICollection <long>)ids; //var explorationResult = await SearchClient.EvaluateAsync(SEB.AuthorIdIn(idc), Assumptions.AuthorMaxPapers * idc.Count, ConcurrentPagingMode.Optimistic, async page => { foreach (var et in page.Entities) { // 此处还可以注册 paper 与其所有作者之间的关系。 // 这样做的好处是,万一 author1 和 author2 同时写了一篇论文。 // 在这里就可以发现了。 RegisterNode(et); var localExploreTask = ExplorePaperAsync(et); // 为检索结果里的所有作者注册所有可能的机构。 // 这里比较麻烦,因为一个作者可以属于多个机构,所以 // 不能使用 LocalExploreAsync (会认为已经探索过。) // 而需要手动建立节点关系。 foreach (var au in et.Authors) { if (au.AffiliationId == null) { continue; } var author = RegisterAuthorNode(au); var affiliation = RegisterAffiliationNode(au); RegisterEdge(author, affiliation); } await localExploreTask; } //return page.Entities.Count; }); // 实际情况应当是, SUM(er.Entities.Count) >> idc.Count //var pageSubtotal = explorationResult.Sum(); //if (pageSubtotal < idc.Count) // Logger.Magik.Warn(this, "批量查询实体 Id 时,返回结果数量不足。期望:>>{0},实际:{1}。", idc.Count, pageSubtotal); //return pageSubtotal; }); //var subtotal = await Task.WhenAll(fetchTasks); //var total = subtotal.Sum(); // 标记为“已经探索过”。 foreach (var an in nodesToExplore) { GetStatus(an.Id).MarkAsFetched(NodeStatus.AuthorPapersFetching); } WAIT_FOR_EXPLORATIONS: //确保返回前,所有 Fetching 的节点已经由此线程或其他线程处理完毕。 var waitResult = await Task.WhenAll(authorNodes.Select(n => GetStatus(n.Id).UntilFetchedAsync(NodeStatus.AuthorPapersFetching))); Debug.Assert(waitResult.All(r => r)); }