Repository: kythe/kythe
Pull request #5917 (2023-11-03 21:21:13)

feat: include canonical node info for identifier service
Allow the user to request that only canonical nodes be returned. This is useful when there is more than one node for a given symbol. This happens in c++ where we have a node for the declaration and a node for the definition.


Patch:
diff --git a/kythe/go/serving/identifiers/identifiers.go b/kythe/go/serving/identifiers/identifiers.go
index 0c871a05d..b5b21c0ed 100644
--- a/kythe/go/serving/identifiers/identifiers.go
+++ b/kythe/go/serving/identifiers/identifiers.go
@@ -49,6 +49,11 @@ type Table struct {
 	table.Proto
 }
 
+type matchNode struct {
+	match           *ipb.FindReply_Match
+	canonicalTicket string
+}
+
 // Find implements the Service interface for Table
 func (it *Table) Find(ctx context.Context, req *ipb.FindRequest) (*ipb.FindReply, error) {
 	var (
@@ -58,25 +63,54 @@ func (it *Table) Find(ctx context.Context, req *ipb.FindRequest) (*ipb.FindReply
 		reply     ipb.FindReply
 	)
 
-	return &reply, it.LookupValues(ctx, []byte(qname), (*srvpb.IdentifierMatch)(nil), func(msg proto.Message) error {
+	// If clustering is done across language boundaries (e.g. proto), then it is possible the
+	// canonical node has a different qualified name (e.g. build_event_stream.BuildEvent vs
+	// build_event_stream::BuildEvent). If the user asked for the qualified name that isn't the
+	// canonical node, return all results since we won't be returning the canonical node.
+	allNodes := make(map[string]*matchNode)
+	var orderedTickets []string
+
+	if err := it.LookupValues(ctx, []byte(qname), (*srvpb.IdentifierMatch)(nil), func(msg proto.Message) error {
 		match := msg.(*srvpb.IdentifierMatch)
 		for _, node := range match.GetNode() {
 			if !validCorpusAndLang(corpora, languages, node) {
 				continue
 			}
 
-			matchNode := &ipb.FindReply_Match{
+			replyMatch := &ipb.FindReply_Match{
 				Ticket:        node.GetTicket(),
 				NodeKind:      node.GetNodeKind(),
 				NodeSubkind:   node.GetNodeSubkind(),
 				BaseName:      match.GetBaseName(),
 				QualifiedName: match.GetQualifiedName(),
 			}
-
-			reply.Matches = append(reply.GetMatches(), matchNode)
+			if n := allNodes[node.GetTicket()]; n != nil {
+				log.Errorf("Two matches found for the same ticket: %v, %v", n, node)
+			}
+			allNodes[node.GetTicket()] = &matchNode{
+				match:           replyMatch,
+				canonicalTicket: node.GetCanonicalNodeTicket(),
+			}
+			orderedTickets = append(orderedTickets, node.GetTicket())
 		}
 		return nil
-	})
+	}); err != nil {
+		return nil, err
+	}
+
+	for _, t := range orderedTickets {
+		ticketMatch := allNodes[t]
+		if req.GetPickCanonicalNodes() &&
+			ticketMatch.canonicalTicket != "" &&
+			ticketMatch.canonicalTicket != ticketMatch.match.GetTicket() &&
+			allNodes[ticketMatch.canonicalTicket] != nil {
+			continue
+		}
+
+		reply.Matches = append(reply.GetMatches(), ticketMatch.match)
+	}
+
+	return &reply, nil
 }
 
 func validCorpusAndLang(corpora, langs []string, node *srvpb.IdentifierMatch_Node) bool {
diff --git a/kythe/proto/identifier.proto b/kythe/proto/identifier.proto
index 700baef21..23ec45f88 100644
--- a/kythe/proto/identifier.proto
+++ b/kythe/proto/identifier.proto
@@ -35,6 +35,11 @@ message FindRequest {
 
   // Restricts the match to the given languages.
   repeated string languages = 3;
+
+  // When two or more nodes are present for the same logical symbol (e.g. the
+  // definition and declaration of a field in C++) and if a canonical node is
+  // known, only return the canonical node.
+  bool pick_canonical_nodes = 4;
 }
 
 message FindReply {
diff --git a/kythe/proto/identifier_go_proto/identifier.pb.go b/kythe/proto/identifier_go_proto/identifier.pb.go
index ff6d99b22..bf0e2c52e 100644
--- a/kythe/proto/identifier_go_proto/identifier.pb.go
+++ b/kythe/proto/identifier_go_proto/identifier.pb.go
@@ -25,9 +25,10 @@ type FindRequest struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Identifier string   `protobuf:"bytes,1,opt,name=identifier,proto3" json:"identifier,omitempty"`
-	Corpus     []string `protobuf:"bytes,2,rep,name=corpus,proto3" json:"corpus,omitempty"`
-	Languages  []string `protobuf:"bytes,3,rep,name=languages,proto3" json:"languages,omitempty"`
+	Identifier         string   `protobuf:"bytes,1,opt,name=identifier,proto3" json:"identifier,omitempty"`
+	Corpus             []string `protobuf:"bytes,2,rep,name=corpus,proto3" json:"corpus,omitempty"`
+	Languages          []string `protobuf:"bytes,3,rep,name=languages,proto3" json:"languages,omitempty"`
+	PickCanonicalNodes bool     `protobuf:"varint,4,opt,name=pick_canonical_nodes,json=pickCanonicalNodes,proto3" json:"pick_canonical_nodes,omitempty"`
 }
 
 func (x *FindRequest) Reset() {
@@ -83,6 +84,13 @@ func (x *FindRequest) GetLanguages() []string {
 	return nil
 }
 
+func (x *FindRequest) GetPickCanonicalNodes() bool {
+	if x != nil {
+		return x.PickCanonicalNodes
+	}
+	return false
+}
+
 type FindReply struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -214,38 +222,42 @@ var File_kythe_proto_identifier_proto protoreflect.FileDescriptor
 var file_kythe_proto_identifier_proto_rawDesc = []byte{
 	0x0a, 0x1c, 0x6b, 0x79, 0x74, 0x68, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x69, 0x64,
 	0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0b,
-	0x6b, 0x79, 0x74, 0x68, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x63, 0x0a, 0x0b, 0x46,
-	0x69, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x64,
-	0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a,
-	0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f,
-	0x72, 0x70, 0x75, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x63, 0x6f, 0x72, 0x70,
-	0x75, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x73, 0x18,
-	0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x73,
-	0x22, 0xe9, 0x01, 0x0a, 0x09, 0x46, 0x69, 0x6e, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x36,
-	0x0a, 0x07, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
-	0x1c, 0x2e, 0x6b, 0x79, 0x74, 0x68, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x69,
-	0x6e, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x2e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x07, 0x6d,
-	0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x1a, 0xa3, 0x01, 0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68,
-	0x12, 0x16, 0x0a, 0x06, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x06, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65,
-	0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x6f, 0x64,
-	0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x73, 0x75,
-	0x62, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x6f, 0x64,
-	0x65, 0x53, 0x75, 0x62, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x62, 0x61, 0x73, 0x65,
-	0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x62, 0x61, 0x73,
-	0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69,
-	0x65, 0x64, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x71,
-	0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x32, 0x4d, 0x0a, 0x11,
-	0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
-	0x65, 0x12, 0x38, 0x0a, 0x04, 0x46, 0x69, 0x6e, 0x64, 0x12, 0x18, 0x2e, 0x6b, 0x79, 0x74, 0x68,
-	0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75,
-	0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x6b, 0x79, 0x74, 0x68, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
-	0x6f, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x42, 0x4b, 0x0a, 0x1f, 0x63,
-	0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x64, 0x65, 0x76, 0x74, 0x6f, 0x6f,
-	0x6c, 0x73, 0x2e, 0x6b, 0x79, 0x74, 0x68, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x28,
-	0x6b, 0x79, 0x74, 0x68, 0x65, 0x2e, 0x69, 0x6f, 0x2f, 0x6b, 0x79, 0x74, 0x68, 0x65, 0x2f, 0x70,
-	0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x5f,
-	0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x6b, 0x79, 0x74, 0x68, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x95, 0x01, 0x0a, 0x0b,
+	0x46, 0x69, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x69,
+	0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x63,
+	0x6f, 0x72, 0x70, 0x75, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x63, 0x6f, 0x72,
+	0x70, 0x75, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x73,
+	0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65,
+	0x73, 0x12, 0x30, 0x0a, 0x14, 0x70, 0x69, 0x63, 0x6b, 0x5f, 0x63, 0x61, 0x6e, 0x6f, 0x6e, 0x69,
+	0x63, 0x61, 0x6c, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52,
+	0x12, 0x70, 0x69, 0x63, 0x6b, 0x43, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x4e, 0x6f,
+	0x64, 0x65, 0x73, 0x22, 0xe9, 0x01, 0x0a, 0x09, 0x46, 0x69, 0x6e, 0x64, 0x52, 0x65, 0x70, 0x6c,
+	0x79, 0x12, 0x36, 0x0a, 0x07, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
+	0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6b, 0x79, 0x74, 0x68, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x2e, 0x46, 0x69, 0x6e, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x2e, 0x4d, 0x61, 0x74, 0x63, 0x68,
+	0x52, 0x07, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x1a, 0xa3, 0x01, 0x0a, 0x05, 0x4d, 0x61,
+	0x74, 0x63, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6e,
+	0x6f, 0x64, 0x65, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
+	0x6e, 0x6f, 0x64, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x6f, 0x64, 0x65,
+	0x5f, 0x73, 0x75, 0x62, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b,
+	0x6e, 0x6f, 0x64, 0x65, 0x53, 0x75, 0x62, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x62,
+	0x61, 0x73, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
+	0x62, 0x61, 0x73, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x71, 0x75, 0x61, 0x6c,
+	0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x0d, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x32,
+	0x4d, 0x0a, 0x11, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x53, 0x65, 0x72,
+	0x76, 0x69, 0x63, 0x65, 0x12, 0x38, 0x0a, 0x04, 0x46, 0x69, 0x6e, 0x64, 0x12, 0x18, 0x2e, 0x6b,
+	0x79, 0x74, 0x68, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x6b, 0x79, 0x74, 0x68, 0x65, 0x2e, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x42, 0x4b,
+	0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x64, 0x65, 0x76,
+	0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x2e, 0x6b, 0x79, 0x74, 0x68, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x5a, 0x28, 0x6b, 0x79, 0x74, 0x68, 0x65, 0x2e, 0x69, 0x6f, 0x2f, 0x6b, 0x79, 0x74, 0x68,
+	0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69,
+	0x65, 0x72, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/kythe/proto/serving.proto b/kythe/proto/serving.proto
index 17f1ce63c..d6c8ca6e6 100644
--- a/kythe/proto/serving.proto
+++ b/kythe/proto/serving.proto
@@ -412,6 +412,11 @@ message IdentifierMatch {
     // Kythe ticket for the matched node.
     string ticket = 1;
 
+    // Ticket for the canonical node, if there is one. For example, if there is
+    // a definition node and a declaration node only one will be the canonical
+    // node.
+    string canonical_node_ticket = 4;
+
     // Kind of the node being referenced.
     string node_kind = 2;
 
diff --git a/kythe/proto/serving_go_proto/serving.pb.go b/kythe/proto/serving_go_proto/serving.pb.go
index f1501413a..4d1c1c598 100644
--- a/kythe/proto/serving_go_proto/serving.pb.go
+++ b/kythe/proto/serving_go_proto/serving.pb.go
@@ -2635,9 +2635,10 @@ type IdentifierMatch_Node struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Ticket      string `protobuf:"bytes,1,opt,name=ticket,proto3" json:"ticket,omitempty"`
-	NodeKind    string `protobuf:"bytes,2,opt,name=node_kind,json=nodeKind,proto3" json:"node_kind,omitempty"`
-	NodeSubkind string `protobuf:"bytes,3,opt,name=node_subkind,json=nodeSubkind,proto3" json:"node_subkind,omitempty"`
+	Ticket              string `protobuf:"bytes,1,opt,name=ticket,proto3" json:"ticket,omitempty"`
+	CanonicalNodeTicket string `protobuf:"bytes,4,opt,name=canonical_node_ticket,json=canonicalNodeTicket,proto3" json:"canonical_node_ticket,omitempty"`
+	NodeKind            string `protobuf:"bytes,2,opt,name=node_kind,json=nodeKind,proto3" json:"node_kind,omitempty"`
+	NodeSubkind         string `protobuf:"bytes,3,opt,name=node_subkind,json=nodeSubkind,proto3" json:"node_subkind,omitempty"`
 }
 
 func (x *IdentifierMatch_Node) Reset() {
@@ -2679,6 +2680,13 @@ func (x *IdentifierMatch_Node) GetTicket() string {
 	return ""
 }
 
+func (x *IdentifierMatch_Node) GetCanonicalNodeTicket() string {
+	if x != nil {
+		return x.CanonicalNodeTicket
+	}
+	return ""
+}
+
 func (x *IdentifierMatch_Node) GetNodeKind() string {
 	if x != nil {
 		return x.NodeKind
@@ -3101,7 +3109,7 @@ var file_kythe_proto_serving_proto_rawDesc = []byte{
 	0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6e, 0x6f, 0x64,
 	0x65, 0x12, 0x23, 0x0a, 0x0d, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x65, 0x64, 0x5f,
 	0x62, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65,
-	0x6e, 0x74, 0x65, 0x64, 0x42, 0x79, 0x22, 0xf4, 0x01, 0x0a, 0x0f, 0x49, 0x64, 0x65, 0x6e, 0x74,
+	0x6e, 0x74, 0x65, 0x64, 0x42, 0x79, 0x22, 0xa9, 0x02, 0x0a, 0x0f, 0x49, 0x64, 0x65, 0x6e, 0x74,
 	0x69, 0x66, 0x69, 0x65, 0x72, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x25, 0x0a, 0x0e, 0x71, 0x75,
 	0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
 	0x28, 0x09, 0x52, 0x0d, 0x71, 0x75, 0x61, 0x6c, 0x69, 0x66, 0x69, 0x65, 0x64, 0x4e, 0x61, 0x6d,
@@ -3110,55 +3118,58 @@ var file_kythe_proto_serving_proto_rawDesc = []byte{
 	0x0a, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6b,
 	0x79, 0x74, 0x68, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69,
 	0x6e, 0x67, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x4d, 0x61, 0x74,
-	0x63, 0x68, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x1a, 0x5e, 0x0a,
-	0x04, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1b, 0x0a,
-	0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x6f,
-	0x64, 0x65, 0x5f, 0x73, 0x75, 0x62, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x0b, 0x6e, 0x6f, 0x64, 0x65, 0x53, 0x75, 0x62, 0x6b, 0x69, 0x6e, 0x64, 0x22, 0x8e, 0x01,
-	0x0a, 0x09, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x74,
-	0x69, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x74, 0x69,
-	0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x37, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20,
-	0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x6b, 0x79, 0x74, 0x68, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
-	0x6f, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69,
-	0x76, 0x65, 0x73, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x2e,
-	0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57,
-	0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x41, 0x52, 0x45, 0x4e, 0x54, 0x53, 0x10, 0x01,
-	0x12, 0x0c, 0x0a, 0x08, 0x43, 0x48, 0x49, 0x4c, 0x44, 0x52, 0x45, 0x4e, 0x10, 0x02, 0x22, 0x8b,
-	0x01, 0x0a, 0x09, 0x43, 0x61, 0x6c, 0x6c, 0x67, 0x72, 0x61, 0x70, 0x68, 0x12, 0x18, 0x0a, 0x07,
-	0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x74,
-	0x69, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x37, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02,
-	0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x6b, 0x79, 0x74, 0x68, 0x65, 0x2e, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x43, 0x61, 0x6c, 0x6c, 0x67,
-	0x72, 0x61, 0x70, 0x68, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22,
-	0x2b, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f,
-	0x57, 0x4e, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x41, 0x4c, 0x4c, 0x45, 0x52, 0x10, 0x01,
-	0x12, 0x0a, 0x0a, 0x06, 0x43, 0x41, 0x4c, 0x4c, 0x45, 0x45, 0x10, 0x02, 0x22, 0xa2, 0x02, 0x0a,
-	0x04, 0x44, 0x69, 0x66, 0x66, 0x12, 0x23, 0x0a, 0x0b, 0x73, 0x70, 0x61, 0x6e, 0x5f, 0x6c, 0x65,
-	0x6e, 0x67, 0x74, 0x68, 0x18, 0x01, 0x20, 0x03, 0x28, 0x05, 0x42, 0x02, 0x10, 0x01, 0x52, 0x0a,
-	0x73, 0x70, 0x61, 0x6e, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x3f, 0x0a, 0x09, 0x73, 0x70,
-	0x61, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x1e, 0x2e,
-	0x6b, 0x79, 0x74, 0x68, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x73, 0x65, 0x72, 0x76,
-	0x69, 0x6e, 0x67, 0x2e, 0x44, 0x69, 0x66, 0x66, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x42, 0x02, 0x10,
-	0x01, 0x52, 0x08, 0x73, 0x70, 0x61, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x27, 0x0a, 0x0d, 0x73,
-	0x70, 0x61, 0x6e, 0x5f, 0x6e, 0x65, 0x77, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03,
-	0x28, 0x05, 0x42, 0x02, 0x10, 0x01, 0x52, 0x0c, 0x73, 0x70, 0x61, 0x6e, 0x4e, 0x65, 0x77, 0x6c,
-	0x69, 0x6e, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x12, 0x73, 0x70, 0x61, 0x6e, 0x5f, 0x66, 0x69, 0x72,
-	0x73, 0x74, 0x5f, 0x6e, 0x65, 0x77, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x04, 0x20, 0x03, 0x28, 0x05,
-	0x42, 0x02, 0x10, 0x01, 0x52, 0x10, 0x73, 0x70, 0x61, 0x6e, 0x46, 0x69, 0x72, 0x73, 0x74, 0x4e,
-	0x65, 0x77, 0x6c, 0x69, 0x6e, 0x65, 0x12, 0x2e, 0x0a, 0x11, 0x73, 0x70, 0x61, 0x6e, 0x5f, 0x6c,
-	0x61, 0x73, 0x74, 0x5f, 0x6e, 0x65, 0x77, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x05, 0x20, 0x03, 0x28,
-	0x05, 0x42, 0x02, 0x10, 0x01, 0x52, 0x0f, 0x73, 0x70, 0x61, 0x6e, 0x4c, 0x61, 0x73, 0x74, 0x4e,
-	0x65, 0x77, 0x6c, 0x69, 0x6e, 0x65, 0x22, 0x29, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09,
-	0x0a, 0x05, 0x45, 0x51, 0x55, 0x41, 0x4c, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x49, 0x4e, 0x53,
-	0x45, 0x52, 0x54, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10,
-	0x02, 0x42, 0x48, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
-	0x64, 0x65, 0x76, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x2e, 0x6b, 0x79, 0x74, 0x68, 0x65, 0x2e, 0x70,
-	0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x25, 0x6b, 0x79, 0x74, 0x68, 0x65, 0x2e, 0x69, 0x6f, 0x2f, 0x6b,
-	0x79, 0x74, 0x68, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69,
-	0x6e, 0x67, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x33,
+	0x63, 0x68, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x1a, 0x92, 0x01,
+	0x0a, 0x04, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x32,
+	0x0a, 0x15, 0x63, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x6e, 0x6f, 0x64, 0x65,
+	0x5f, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x63,
+	0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x4e, 0x6f, 0x64, 0x65, 0x54, 0x69, 0x63, 0x6b,
+	0x65, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18,
+	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12,
+	0x21, 0x0a, 0x0c, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x73, 0x75, 0x62, 0x6b, 0x69, 0x6e, 0x64, 0x18,
+	0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x6f, 0x64, 0x65, 0x53, 0x75, 0x62, 0x6b, 0x69,
+	0x6e, 0x64, 0x22, 0x8e, 0x01, 0x0a, 0x09, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x73,
+	0x12, 0x18, 0x0a, 0x07, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
+	0x09, 0x52, 0x07, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x37, 0x0a, 0x04, 0x74, 0x79,
+	0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x6b, 0x79, 0x74, 0x68, 0x65,
+	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x52,
+	0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x73, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74,
+	0x79, 0x70, 0x65, 0x22, 0x2e, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55,
+	0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x41, 0x52, 0x45,
+	0x4e, 0x54, 0x53, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x48, 0x49, 0x4c, 0x44, 0x52, 0x45,
+	0x4e, 0x10, 0x02, 0x22, 0x8b, 0x01, 0x0a, 0x09, 0x43, 0x61, 0x6c, 0x6c, 0x67, 0x72, 0x61, 0x70,
+	0x68, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03,
+	0x28, 0x09, 0x52, 0x07, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x37, 0x0a, 0x04, 0x74,
+	0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x6b, 0x79, 0x74, 0x68,
+	0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e,
+	0x43, 0x61, 0x6c, 0x6c, 0x67, 0x72, 0x61, 0x70, 0x68, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04,
+	0x74, 0x79, 0x70, 0x65, 0x22, 0x2b, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07,
+	0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x41, 0x4c,
+	0x4c, 0x45, 0x52, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x41, 0x4c, 0x4c, 0x45, 0x45, 0x10,
+	0x02, 0x22, 0xa2, 0x02, 0x0a, 0x04, 0x44, 0x69, 0x66, 0x66, 0x12, 0x23, 0x0a, 0x0b, 0x73, 0x70,
+	0x61, 0x6e, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x01, 0x20, 0x03, 0x28, 0x05, 0x42,
+	0x02, 0x10, 0x01, 0x52, 0x0a, 0x73, 0x70, 0x61, 0x6e, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12,
+	0x3f, 0x0a, 0x09, 0x73, 0x70, 0x61, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x03,
+	0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x6b, 0x79, 0x74, 0x68, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x69, 0x66, 0x66, 0x2e, 0x54, 0x79,
+	0x70, 0x65, 0x42, 0x02, 0x10, 0x01, 0x52, 0x08, 0x73, 0x70, 0x61, 0x6e, 0x54, 0x79, 0x70, 0x65,
+	0x12, 0x27, 0x0a, 0x0d, 0x73, 0x70, 0x61, 0x6e, 0x5f, 0x6e, 0x65, 0x77, 0x6c, 0x69, 0x6e, 0x65,
+	0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x05, 0x42, 0x02, 0x10, 0x01, 0x52, 0x0c, 0x73, 0x70, 0x61,
+	0x6e, 0x4e, 0x65, 0x77, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x12, 0x73, 0x70, 0x61,
+	0x6e, 0x5f, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x6e, 0x65, 0x77, 0x6c, 0x69, 0x6e, 0x65, 0x18,
+	0x04, 0x20, 0x03, 0x28, 0x05, 0x42, 0x02, 0x10, 0x01, 0x52, 0x10, 0x73, 0x70, 0x61, 0x6e, 0x46,
+	0x69, 0x72, 0x73, 0x74, 0x4e, 0x65, 0x77, 0x6c, 0x69, 0x6e, 0x65, 0x12, 0x2e, 0x0a, 0x11, 0x73,
+	0x70, 0x61, 0x6e, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6e, 0x65, 0x77, 0x6c, 0x69, 0x6e, 0x65,
+	0x18, 0x05, 0x20, 0x03, 0x28, 0x05, 0x42, 0x02, 0x10, 0x01, 0x52, 0x0f, 0x73, 0x70, 0x61, 0x6e,
+	0x4c, 0x61, 0x73, 0x74, 0x4e, 0x65, 0x77, 0x6c, 0x69, 0x6e, 0x65, 0x22, 0x29, 0x0a, 0x04, 0x54,
+	0x79, 0x70, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x51, 0x55, 0x41, 0x4c, 0x10, 0x00, 0x12, 0x0a,
+	0x0a, 0x06, 0x49, 0x4e, 0x53, 0x45, 0x52, 0x54, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x45,
+	0x4c, 0x45, 0x54, 0x45, 0x10, 0x02, 0x42, 0x48, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f,
+	0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x64, 0x65, 0x76, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x2e, 0x6b, 0x79,
+	0x74, 0x68, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x25, 0x6b, 0x79, 0x74, 0x68, 0x65,
+	0x2e, 0x69, 0x6f, 0x2f, 0x6b, 0x79, 0x74, 0x68, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f,
+	0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x5f, 0x67, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (


Test patch:
diff --git a/kythe/go/serving/identifiers/identifiers_test.go b/kythe/go/serving/identifiers/identifiers_test.go
index 277adae8f..8589e1ef7 100644
--- a/kythe/go/serving/identifiers/identifiers_test.go
+++ b/kythe/go/serving/identifiers/identifiers_test.go
@@ -30,81 +30,132 @@ import (
 )
 
 var matchTable = Table{testProtoTable{
-	"foo::bar": []proto.Message{&srvpb.IdentifierMatch{
-		Node: []*srvpb.IdentifierMatch_Node{
-			node("kythe://corpus?lang=c++", "record", "class"),
+	"foo::bar": []proto.Message{
+		&srvpb.IdentifierMatch{
+			Node: []*srvpb.IdentifierMatch_Node{
+				node("kythe://corpus?lang=c++", "", "record", "class"),
+			},
+			BaseName:      "bar",
+			QualifiedName: "foo::bar",
 		},
-		BaseName:      "bar",
-		QualifiedName: "foo::bar",
-	}, &srvpb.IdentifierMatch{
-		Node: []*srvpb.IdentifierMatch_Node{
-			node("kythe://corpus?lang=rust", "record", "struct"),
+		&srvpb.IdentifierMatch{
+			Node: []*srvpb.IdentifierMatch_Node{
+				node("kythe://corpus?lang=c++#decl", "kythe://corpus?lang=c++", "record", "class"),
+			},
+			BaseName:      "bar",
+			QualifiedName: "foo::bar",
 		},
-		BaseName:      "bar",
-		QualifiedName: "foo::bar",
-	}},
+		&srvpb.IdentifierMatch{
+			Node: []*srvpb.IdentifierMatch_Node{
+				node("kythe://corpus?lang=rust", "", "record", "struct"),
+			},
+			BaseName:      "bar",
+			QualifiedName: "foo::bar",
+		},
+	},
+
+	"foo.bar": []proto.Message{
+		&srvpb.IdentifierMatch{
+			Node: []*srvpb.IdentifierMatch_Node{
+				node("kythe://corpus?lang=java", "kythe://corpus?lang=c++", "record", "interface"),
+			},
+			BaseName:      "bar",
+			QualifiedName: "foo.bar",
+		},
+	},
 
-	"com.java.package.Interface": []proto.Message{&srvpb.IdentifierMatch{
-		Node: []*srvpb.IdentifierMatch_Node{
-			node("kythe://habeas?lang=java", "record", "interface"),
+	"com.java.package.Interface": []proto.Message{
+		&srvpb.IdentifierMatch{
+			Node: []*srvpb.IdentifierMatch_Node{
+				node("kythe://habeas?lang=java", "", "record", "interface"),
+			},
+			BaseName:      "Interface",
+			QualifiedName: "com.java.package.Interface",
 		},
-		BaseName:      "Interface",
-		QualifiedName: "com.java.package.Interface",
-	}},
+	},
 }}
 
 var tests = []testCase{
 	{
-		findRequest("foo::bar", nil, nil),
+		"Qualified name across languages",
+		findRequest("foo::bar", nil, nil, false),
+		[]*ipb.FindReply_Match{
+			match("kythe://corpus?lang=c++", "record", "class", "bar", "foo::bar"),
+			match("kythe://corpus?lang=c++#decl", "record", "class", "bar", "foo::bar"),
+			match("kythe://corpus?lang=rust", "record", "struct", "bar", "foo::bar"),
+		},
+	},
+	{
+		"Canonical node for Qualified name across languages",
+		findRequest("foo::bar", nil, nil, true),
 		[]*ipb.FindReply_Match{
 			match("kythe://corpus?lang=c++", "record", "class", "bar", "foo::bar"),
 			match("kythe://corpus?lang=rust", "record", "struct", "bar", "foo::bar"),
 		},
 	},
 	{
-		findRequest("foo::bar", nil, []string{"rust"}),
+		"Java qualified name",
+		findRequest("foo.bar", nil, nil, false),
+		[]*ipb.FindReply_Match{
+			match("kythe://corpus?lang=java", "record", "interface", "bar", "foo.bar"),
+		},
+	},
+	{
+		"Java qualified name with canonical node with a different qualified name",
+		findRequest("foo.bar", nil, nil, true),
+		[]*ipb.FindReply_Match{
+			match("kythe://corpus?lang=java", "record", "interface", "bar", "foo.bar"),
+		},
+	},
+	{
+		"Rust only",
+		findRequest("foo::bar", nil, []string{"rust"}, false),
 		[]*ipb.FindReply_Match{
 			match("kythe://corpus?lang=rust", "record", "struct", "bar", "foo::bar"),
 		},
 	},
 	{
-		findRequest("com.java.package.Interface", []string{"habeas"}, nil),
+		"Corpus filter matches",
+		findRequest("com.java.package.Interface", []string{"habeas"}, nil, false),
 		[]*ipb.FindReply_Match{
 			match("kythe://habeas?lang=java", "record", "interface", "Interface", "com.java.package.Interface"),
 		},
 	},
 	{
-		findRequest("com.java.package.Interface", []string{"corpus"}, nil),
+		"Corpus filter does not match",
+		findRequest("com.java.package.Interface", []string{"corpus"}, nil, false),
 		nil,
 	},
 }
 
 func TestFind(t *testing.T) {
 	for _, test := range tests {
-		reply, err := matchTable.Find(context.TODO(), &test.FindRequest)
+		reply, err := matchTable.Find(context.TODO(), test.Request)
 		if err != nil {
-			t.Errorf("unexpected error for request %v: %v", test.FindRequest, err)
+			t.Errorf("unexpected error for request %v: %v", test.Request, err)
 		}
 
 		if err := testutil.DeepEqual(test.Matches, reply.Matches); err != nil {
-			t.Error(err)
+			t.Errorf("Failed %s\n%v", test.Name, err)
 		}
 	}
 }
 
-func findRequest(qname string, corpora, langs []string) ipb.FindRequest {
-	return ipb.FindRequest{
-		Identifier: qname,
-		Corpus:     corpora,
-		Languages:  langs,
+func findRequest(qname string, corpora, langs []string, canonicalOnly bool) *ipb.FindRequest {
+	return &ipb.FindRequest{
+		Identifier:         qname,
+		PickCanonicalNodes: canonicalOnly,
+		Corpus:             corpora,
+		Languages:          langs,
 	}
 }
 
-func node(ticket, kind, subkind string) *srvpb.IdentifierMatch_Node {
+func node(ticket, canonicalNodeTicket, kind, subkind string) *srvpb.IdentifierMatch_Node {
 	return &srvpb.IdentifierMatch_Node{
-		Ticket:      ticket,
-		NodeKind:    kind,
-		NodeSubkind: subkind,
+		Ticket:              ticket,
+		CanonicalNodeTicket: canonicalNodeTicket,
+		NodeKind:            kind,
+		NodeSubkind:         subkind,
 	}
 }
 
@@ -119,7 +170,8 @@ func match(ticket, kind, subkind, bname, qname string) *ipb.FindReply_Match {
 }
 
 type testCase struct {
-	ipb.FindRequest
+	Name    string
+	Request *ipb.FindRequest
 	Matches []*ipb.FindReply_Match
 }