Example Application: iOS and macOS Versions of my KnowledgeBookNavigator

I used many of the techniques discussed in this book, the Swift language, and the SwiftUI user interface framework to develop Swift version of my Knowledge Graph Navigator application for macOS. I originally wrote this as an example program in Common Lisp for another book project.

The GitHub repository for the KGN example is https://github.com/mark-watson/KGN. I copied the code from my stand-alone Swift libraries to this example to make it self contained. The easiest way to browse the source code is to open this project in Xcode.

I submitted the KGN app that we discuss in this chapter to Apple’s store and is available as a macOS app. If you load this project into Xcode, you can also build and run the iOS and iPadOS targets.

You will need to have read through the last chapter on semantic web and linked data technologies to understand this example because quite a lot of the code has embedded SPARQL queries to get information from DBPedia.org.

The other major part of this app is a slightly modified version of Apple’s question answering (QA) example using the BERT model in CoreML. Apple’s code is in the subdirectory AppleBERT. Please read the README file for this project and follow the directions for downloading and using Apple’s model and vocabulary file.

Screen Shots of macOS Application

In the first screenshot seen below, I had entered query text that included “Steve Jobs” and the popup list selector is used to let the user select which “Steve Jobs” entity from DBPedia that they want to use.

Entered query and KGN is asking user to disambiguate which "Steve Jobs" they want information for
Entered query and KGN is asking user to disambiguate which “Steve Jobs” they want information for
Showing results
Showing results

The previous screenshot shows the results to the query displayed as English text.

Notice the app prompt “Behind the scenes SPARQL queries” near the bottom of the app window. If you click on this field then the SPARQL queries used to answer the question are shown, as on the next screenshot:

Showing SPARQL queries used to gather data
Showing SPARQL queries used to gather data

Application Code Listings

I will list some of the code for this example application and I suggest that you, dear reader, also open this project in Xcode in order to navigate the sample code and more carefully read through it.

SPARQL

I introduced you to the use of SPARQL in the last chapter. This library can be used by adding a reference to the Project.swift file for this project. You can also clone the GitHub repository https://github.com/mark-watson/Nlp_swift to have the source code for local viewing and modification and I have copied the code into the KGN project.

The file SparqlQuery.swift is shown here:

 1 import Foundation
 2 
 3 public func sparqlDbPedia(query: String) -> Array<Dictionary<String,String>> {
 4     return SparqlEndpointHelpter(query: query,
 5         endPointUri: "https://dbpedia.org/sparql?query=") }
 6 
 7 public func sparqlWikidata(query: String) -> Array<Dictionary<String,String>> {
 8     return SparqlEndpointHelpter(query: query,
 9         endPointUri:
10           "https://query.wikidata.org/bigdata/namespace/wdq/sparql?query=") }
11 
12 public func SparqlEndpointHelpter(query: String,
13                                   endPointUri: String) ->
14                             Array<Dictionary<String,String>> {
15     var ret = Set<Dictionary<String,String>>();
16     var content = "{}"
17 
18     let maybeString = cacheLookupQuery7(key: query)
19     if maybeString?.count ?? 0 > 0 {
20         content = maybeString ?? ""
21     } else {
22         let requestUrl = URL(string: String(endPointUri + query.addingPercentEncodin\
23 g(withAllowedCharacters:
24           .urlHostAllowed)!) + "&format=json")!
25         do { content = try String(contentsOf: requestUrl) }
26           catch let error { print(error) }
27     }
28     let json = try? JSONSerialization.jsonObject(with: Data(content.utf8),
29                                                  options: [])
30     if let json2 = json as! Optional<Dictionary<String, Any?>> {
31         if let head = json2["head"] as? Dictionary<String, Any> {
32             if let xvars = head["vars"] as! NSArray? {
33                 if let results = json2["results"] as? Dictionary<String, Any> {
34                     if let bindings = results["bindings"] as! NSArray? {
35                         if bindings.count > 0 {
36                             for i in 0...(bindings.count-1) {
37                                 if let first_binding =
38                                 bindings[i] as? Dictionary<String,
39                                 Dictionary<String,String>> {
40                                     var ret2 = Dictionary<String,String>();
41                                     for key in xvars {
42                                         let key2 : String = key as! String
43                                         if let vals = (first_binding[key2]) {
44                                             let vv : String = vals["value"] ?? "err2"
45                                             ret2[key2] = vv } }
46                                     if ret2.count > 0 {
47                                         ret.insert(ret2)
48                                     }}}}}}}}}
49     return Array(ret) }

The file QueryCache.swift contains code written by Khoa Pham (MIT License) that can be found in the GitHub repository https://github.com/onmyway133/EasyStash. This file is used to cache SPARQL queries and the results. In testing this application I noticed that there were many repeated queries to DBPedia so I decided to cache results. Here is the simple API I added on top of Khoa Pham’s code:

 1 //  Created by khoa on 27/05/2019.
 2 //  Copyright © 2019 Khoa Pham. All rights reserved. MIT License.
 3 //  https://github.com/onmyway133/EasyStash
 4 //
 5 
 6 import Foundation
 7 
 8 //      Mark's simple wrapper:
 9 
10 var storage: Storage? = nil
11 
12 public func cacheStoreQuery(key: String, value: String) {
13     do { try storage?.save(object: value, forKey: key) } catch {}
14 }
15 public func cacheLookupQuery7(key: String) -> String? {
16     // optional DEBUG code: clear cache
17     //do { try storage?.removeAll() } catch { print("ERROR CLEARING CACHE") }
18     do {
19         return try storage?.load(forKey: key, as: String.self)
20     } catch { return "" }
21 }
22 
23 // remaining code not shown for brevity.

The code in file GenerateSparql.swift is used to generate queries for DBPedia. The line-wrapping for embedded SPARQL queries in the next code section is difficult to read so you may want to open the source file in Xcode. Please note that the KGN application prints out the SPARQL queries used to fetch information from DBPedia. The embedded SPARQL query templates used here have variable slots that filled in at runtime to customize the queries.

  1 //
  2 //  GenerateSparql.swift
  3 //  KGNbeta1
  4 //
  5 //  Created by Mark Watson on 2/28/20.
  6 //  Copyright © 2021 Mark Watson. All rights reserved.
  7 //
  8 
  9 import Foundation
 10 
 11 public func uri_to_display_text(uri: String)
 12                                      -> String {
 13     return uri.replacingOccurrences(of: "http://dbpedia.org/resource/Category/",
 14         with: "").
 15       replacingOccurrences(of: "http://dbpedia.org/resource/",
 16         with: "").
 17          replacingOccurrences(of: "_", with: " ")
 18 }
 19 
 20 public func get_SPARQL_for_finding_URIs_for_PERSON_NAME(nameString: String)
 21                                               -> String {
 22     return
 23         "# SPARQL to find all URIs for name: " +
 24         nameString + "\nSELECT DISTINCT ?person_uri ?comment {\n" +
 25         "  ?person_uri <http://xmlns.com/foaf/0.1/name> \"" +
 26         nameString + "\"@en .\n" +
 27         "  OPTIONAL { ?person_uri <http://www.w3.org/2000/01/rdf-schema#comment>\n" +
 28         "     ?comment . FILTER (lang(?comment) = 'en') } .\n" +
 29         "} LIMIT 10\n"
 30 }
 31 
 32 public func get_SPARQL_for_PERSON_URI(aURI: String) -> String {
 33     return
 34         "# <" + aURI + ">\nSELECT DISTINCT ?comment (GROUP_CONCAT(DISTINCT ?birthpla\
 35 ce; SEPARATOR=' | ') AS ?birthplace)\n  (GROUP_CONCAT(DISTINCT ?almamater; SEPARATOR\
 36 =' | ') AS ?almamater) (GROUP_CONCAT(DISTINCT ?spouse; SEPARATOR=' | ') AS ?spouse) \
 37 {\n" +
 38         "  <" + aURI + "> <http://www.w3.org/2000/01/rdf-schema#comment>  ?comment .\
 39  FILTER  (lang(?comment) = 'en') .\n" +
 40         "  OPTIONAL { <" + aURI + "> <http://dbpedia.org/ontology/birthPlace> ?birth\
 41 place } .\n" +
 42         "  OPTIONAL { <" + aURI + "> <http://dbpedia.org/ontology/almaMater> ?almama\
 43 ter } .\n" +
 44         "  OPTIONAL { <" + aURI + "> <http://dbpedia.org/ontology/spouse> ?spouse } \
 45 .\n" +
 46         "} LIMIT 5\n"
 47 }
 48 
 49 public func get_display_text_for_PERSON_URI(personURI: String) -> [String] {
 50     var ret: String = "\(uri_to_display_text(uri: personURI))\n\n"
 51     let person_details_sparql = get_SPARQL_for_PERSON_URI(aURI: personURI)
 52     let person_details = sparqlDbPedia(query: person_details_sparql)
 53     
 54     for pd in person_details {
 55         //let comment = pd["comment"]
 56         ret.append("\(pd["comment"] ?? "")\n\n")
 57         let subject_uris = pd["subject_uris"]
 58         let uri_list: [String] = subject_uris?.components(separatedBy: " | ") ?? []
 59         //ret.append("<ul>\n")
 60         for u in uri_list {
 61             let subject = uri_to_display_text(uri: u)
 62             ret.append("\(subject)\n") }
 63         //ret.append("</ul>\n")
 64         if let spouse = pd["spouse"] {
 65             if spouse.count > 0 {
 66                 ret.append("Spouse: \(uri_to_display_text(uri: spouse))\n") } }
 67         if let almamater = pd["almamater"] {
 68             if almamater.count > 0 {
 69                 ret.append("Almamater: \(uri_to_display_text(uri: almamater))\n") } }
 70         if let birthplace = pd["birthplace"] {
 71             if birthplace.count > 0 {
 72                 ret.append("Birthplace: \(uri_to_display_text(uri: birthplace))\n") \
 73 } }
 74     }
 75     return ["# SPARQL for a specific person:\n" + person_details_sparql, ret]
 76 }
 77 
 78 //     "  ?place_uri <http://xmlns.com/foaf/0.1/name> \"" + placeString + "\"@en .\n\
 79 " +
 80 
 81 public func get_SPARQL_for_finding_URIs_for_PLACE_NAME(placeString: String)
 82                                                -> String {
 83     return
 84         "# " + placeString + "\nSELECT DISTINCT ?place_uri ?comment {\n" +
 85         "  ?place_uri rdfs:label \"" + placeString + "\"@en .\n" +
 86         "  ?place_uri <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://sche\
 87 ma.org/Place> .\n" +
 88         "  OPTIONAL { ?place_uri <http://www.w3.org/2000/01/rdf-schema#comment>\n" +
 89         "     ?comment . FILTER (lang(?comment) = 'en') } .\n" +
 90         "} LIMIT 10\n"
 91 }
 92 
 93 public func get_SPARQL_for_PLACE_URI(aURI: String) -> String {
 94     return
 95         "# <" + aURI + ">\nSELECT DISTINCT ?comment (GROUP_CONCAT(DISTINCT ?subject_\
 96 uris; SEPARATOR=' | ') AS ?subject_uris) {\n" +
 97         "  <" + aURI + "> <http://www.w3.org/2000/01/rdf-schema#comment>  ?comment .\
 98  FILTER  (lang(?comment) = 'en') .\n" +
 99         "  OPTIONAL { <" + aURI + "> <http://purl.org/dc/terms/subject> ?subject_uri\
100 s } .\n" +
101         "} LIMIT 5\n"
102 }
103 
104 public func get_HTML_for_place_URI(placeURI: String) -> String {
105     var ret: String = "<h2>" + placeURI + "</h2>\n"
106     let place_details_sparql = get_SPARQL_for_PLACE_URI(aURI: placeURI)
107     let place_details = sparqlDbPedia(query: place_details_sparql)
108     
109     for pd in place_details {
110         //let comment = pd["comment"]
111         ret.append("<p><strong>\(pd["comment"] ?? "")</strong></p>\n")
112         let subject_uris = pd["subject_uris"]
113         let uri_list: [String] = subject_uris?.components(separatedBy: " | ") ?? []
114         ret.append("<ul>\n")
115         for u in uri_list {
116             let subject = u.replacingOccurrences(of: "http://dbpedia.org/resource/Ca\
117 tegory:", with: "").replacingOccurrences(of: "_", with: " ").replacingOccurrences(of\
118 : "-", with: " ")
119             ret.append("  <li>\(subject)</li>\n")
120         }
121         ret.append("</ul>\n")
122     }
123     return ret
124 }
125 
126 public func get_SPARQL_for_finding_URIs_for_ORGANIZATION_NAME(orgString: String) -> \
127 String {
128     return
129         "# " + orgString + "\nSELECT DISTINCT ?org_uri ?comment {\n" +
130         "  ?org_uri rdfs:label \"" + orgString + "\"@en .\n" +
131         "  ?org_uri <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema\
132 .org/Organization> .\n" +
133         "  OPTIONAL { ?org_uri <http://www.w3.org/2000/01/rdf-schema#comment>\n" +
134         "     ?comment . FILTER (lang(?comment) = 'en') } .\n" +
135         "} LIMIT 2\n"
136 }

The file AppSparql contains more utility functions for getting entity and relationship data from DBPedia:

  1 //  AppSparql.swift
  2 //  Created by ML Watson on 7/18/21.
  3 
  4 import Foundation
  5 
  6 let detailSparql = """
  7 PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
  8 select ?entity ?label ?description ?comment where {
  9     ?entity rdfs:label "<name>"@en .
 10     ?entity schema:description ?description . filter (lang(?description) = 'en') . f\
 11 ilter(!regex(?description,"Wikimedia disambiguation page")) .
 12  } limit 5000
 13 """
 14 
 15 let personSparql = """
 16   select ?uri ?comment {
 17       ?uri <http://xmlns.com/foaf/0.1/name> "<name>"@en .
 18       ?uri <http://www.w3.org/2000/01/rdf-schema#comment>  ?comment .
 19           FILTER  (lang(?comment) = 'en') .
 20   }
 21 """
 22 
 23 
 24 let personDetailSparql = """
 25 SELECT DISTINCT ?label ?comment
 26                      
 27      (GROUP_CONCAT (DISTINCT ?birthplace; SEPARATOR=' | ') AS ?birthplace)
 28      (GROUP_CONCAT (DISTINCT ?almamater; SEPARATOR=' | ') AS ?almamater)
 29      (GROUP_CONCAT (DISTINCT ?spouse; SEPARATOR=' | ') AS ?spouse) {
 30        <name> <http://www.w3.org/2000/01/rdf-schema#comment>  ?comment .
 31        FILTER  (lang(?comment) = 'en') .
 32      OPTIONAL { <name> <http://dbpedia.org/ontology/birthPlace> ?birthplace } .
 33      OPTIONAL { <name> <http://dbpedia.org/ontology/almaMater> ?almamater } .
 34      OPTIONAL { <name> <http://dbpedia.org/ontology/spouse> ?spouse } .
 35      OPTIONAL { <name>  <http://www.w3.org/2000/01/rdf-schema#label> ?label .
 36         FILTER  (lang(?label) = 'en') }
 37 } LIMIT 10
 38 """
 39 
 40 let placeSparql = """
 41 SELECT DISTINCT ?uri ?comment WHERE {
 42    ?uri rdfs:label "<name>"@en .
 43    ?uri <http://www.w3.org/2000/01/rdf-schema#comment>  ?comment .
 44    FILTER (lang(?comment) = 'en') .
 45    ?place <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Place\
 46 > .
 47 } LIMIT 80
 48 """
 49 
 50 let organizationSparql = """
 51 SELECT DISTINCT ?uri ?comment WHERE {
 52    ?uri rdfs:label "<name>"@en .
 53    ?uri <http://www.w3.org/2000/01/rdf-schema#comment>  ?comment .
 54    FILTER (lang(?comment) = 'en') .
 55    ?uri <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Organiz\
 56 ation> .
 57 } LIMIT 80
 58 """
 59 
 60 func entityDetail(name: String) -> [Dictionary<String,String>] {
 61     var ret: [Dictionary<String,String>] = []
 62     let sparql = detailSparql.replacingOccurrences(of: "<name>", with: name)
 63     print(sparql)
 64     let r = sparqlDbPedia(query: sparql)
 65     r.forEach { result in
 66         print(result)
 67         ret.append(result)
 68     }
 69     return ret
 70 }
 71 
 72 func personDetail(name: String) -> [Dictionary<String,String>] {
 73     var ret: [Dictionary<String,String>] = []
 74     let sparql = personSparql.replacingOccurrences(of: "<name>", with: name)
 75     print(sparql)
 76     let r = sparqlDbPedia(query: sparql)
 77     r.forEach { result in
 78         print(result)
 79         ret.append(result)
 80     }
 81     return ret
 82 }
 83 
 84 func placeDetail(name: String) -> [Dictionary<String,String>] {
 85     var ret: [Dictionary<String,String>] = []
 86     let sparql = placeSparql.replacingOccurrences(of: "<name>", with: name)
 87     print(sparql)
 88     let r = sparqlDbPedia(query: sparql)
 89     r.forEach { result in
 90         print(result)
 91         ret.append(result)
 92     }
 93     return ret
 94 }
 95 
 96 func organizationDetail(name: String) -> [Dictionary<String,String>] {
 97     var ret: [Dictionary<String,String>] = []
 98     let sparql = organizationSparql.replacingOccurrences(of: "<name>", with: name)
 99     print(sparql)
100     let r = sparqlDbPedia(query: sparql)
101     r.forEach { result in
102         print(result)
103         ret.append(result)
104     }
105     return ret
106 }
107 
108 public func processEntities(inputString: String) -> [(name: String, type: String, ur\
109 i: String, comment: String)] {
110     let entities = getEntities(text: inputString)
111     var augmentedEntities: [(name: String, type: String, uri: String, comment: Strin\
112 g)] = []
113     for (entityName, entityType) in entities {
114         print("** entityName:", entityName, "entityType:", entityType)
115         if entityType == "PersonalName" {
116             let data = personDetail(name: entityName)
117             for d in data {
118                 augmentedEntities.append((name: entityName, type: entityType,
119                     uri: "<" + d["uri"]! + ">", comment: "<" + d["comment"]! + ">"))
120             }
121         }
122         if entityType == "OrganizationName" {
123             let data = organizationDetail(name: entityName)
124             for d in data {
125                 augmentedEntities.append((name: entityName, type: entityType,
126                     uri: "<" + d["uri"]! + ">", comment: "<" + d["comment"]! + ">"))
127             }
128         }
129         if entityType == "PlaceName" {
130             let data = placeDetail(name: entityName)
131             for d in data {
132                 augmentedEntities.append((name: entityName, type: entityType,
133                     uri: "<" + d["uri"]! + ">", comment: "<" + d["comment"]! + ">"))
134             }
135         }
136     }
137     return augmentedEntities
138 }
139 
140 
141 extension Array where Element: Hashable {
142     func uniqueValuesHelper() -> [Element] {
143         var addedDict = [Element: Bool]()
144         return filter { addedDict.updateValue(true, forKey: $0) == nil }
145     }
146     mutating func uniqueValues() {
147         self = self.uniqueValuesHelper()
148     }
149 }
150 
151 
152 func getAllRelationships(inputString: String) -> [String] {
153     let augmentedEntities = processEntities(inputString: inputString)
154     var relationshipTriples: [String] = []
155     for ae1 in augmentedEntities {
156         for ae2 in augmentedEntities {
157             if ae1 != ae2 {
158                 let er1 = dbpediaGetRelationships(entity1Uri: ae1.uri,
159                                                   entity2Uri: ae2.uri)
160                 relationshipTriples.append(contentsOf: er1)
161                 let er2 = dbpediaGetRelationships(entity1Uri: ae2.uri,
162                                                   entity2Uri: ae1.uri)
163                 relationshipTriples.append(contentsOf: er2)
164             }
165         }
166     }
167     relationshipTriples.uniqueValues()
168     relationshipTriples.sort()
169     return relationshipTriples
170 }

AppleBERT

The files in the directory AppleBERT were copied from Apple’s example https://developer.apple.com/documentation/coreml/model_integration_samples/finding_answers_to_questions_in_a_text_document with a few changes to get returned results in a convenient format for this application. Apple’s BERT documentation is excellent and you should review it.

Relationships

The file Relationships.swift fetches relationship data for pairs of DBPedia entities. Note that the first SPARQL template has variable slots <e1> and <e2> that are replaced at runtime with URIs representing the entities that we are searching for relationships between these two entities:

 1 // relationships between DBPedia entities
 2 
 3 let relSparql =  """
 4 SELECT DISTINCT ?p {<e1> ?p <e2> .FILTER (!regex(str(?p), 'wikiPage', 'i'))} LIMIT 5
 5 """
 6 
 7 public func dbpediaGetRelationships(entity1Uri: String, entity2Uri: String)
 8                                       -> [String] {
 9     var ret: [String] = []
10     let sparql1 = relSparql.replacingOccurrences(of: "<e1>",
11       with: entity1Uri).replacingOccurrences(of: "<e2>",
12         with: entity2Uri)
13     let r1 = sparqlDbPedia(query: sparql1)
14     r1.forEach { result in
15         if let relName = result["p"] {
16             let rdfStatement = entity1Uri + " <" + relName + "> " + entity2Uri + " ."
17             print(rdfStatement)
18             ret.append(rdfStatement)
19         }
20     }
21     let sparql2 = relSparql.replacingOccurrences(of: "<e1>",
22         with: entity2Uri).replacingOccurrences(of: "<e2>",
23             with: entity1Uri)
24     let r2 = sparqlDbPedia(query: sparql2)
25     r2.forEach { result in
26         if let relName = result["p"] {
27             let rdfStatement = entity2Uri + " <" + relName + "> " + entity1Uri + " ."
28             print(rdfStatement)
29             ret.append(rdfStatement)
30         }
31     }
32     return Array(Set(ret))
33 }
34 
35 public func uriToPrintName(_ uri: String) -> String {
36     let slashIndex = uri.lastIndex(of: "/")
37     if slashIndex == nil { return uri }
38     var s = uri[slashIndex!...]
39     s = s.dropFirst()
40     if s.count > 0 { s.removeLast() }
41     return String(s).replacingOccurrences(of: "_", with: " ")
42 }
43 
44 public func relationshipsoEnglish(rs: [String]) -> String {
45     var lines: [String] = []
46     for r in rs {
47         let triples = r.split(separator: " ", maxSplits: 3,
48             omittingEmptySubsequences: true)
49         if triples.count > 2 {
50             lines.append(uriToPrintName(String(triples[0])) + " " +
51               uriToPrintName(String(triples[1])) + " " +
52                 uriToPrintName(String(triples[2])))
53         } else {
54             lines.append(r)
55         }
56     }
57     let linesNoDuplicates = Set(lines)
58     return linesNoDuplicates.joined(separator: "\n")
59 }

NLP

The file NlpWhiteboard provides high level NLP utility functions for the application:

 1 //
 2 //  NlpWhiteboard.swift
 3 //  KGN
 4 //
 5 //  Copyright © 2021 Mark Watson. All rights reserved.
 6 //
 7 
 8 public struct NlpWhiteboard {
 9 
10     var originalText: String = ""
11     var people: [String] = []
12     var places: [String] = []
13     var organizations: [String] = []
14     var sparql: String = ""
15 
16     init() { }
17 
18     mutating func set_text(originalText: String) {
19         self.originalText = originalText
20         let (people, places, organizations) = getAllEntities(text:  originalText)
21         self.people = people; self.places = places; self.organizations = organizatio\
22 ns
23     }
24     
25     mutating func query_to_choices(behindTheScenesSparqlText: inout String)
26           -> [[[String]]] { // return inner: [comment, uri]
27         var ret: Set<[[String]]> = []
28         if people.count > 0 {
29             for i in 0...(people.count - 1) {
30                 self.sparql =
31                   get_SPARQL_for_finding_URIs_for_PERSON_NAME(nameString: people[i])
32                 behindTheScenesSparqlText += self.sparql
33                 let results = sparqlDbPedia(query: self.sparql)
34                 if results.count > 0 {
35                     ret.insert( results.map { [($0["comment"]
36                                                 ?? ""),
37                                                 ($0["person_uri"] ?? "")] })
38                 }
39             }
40         }
41         if organizations.count > 0 {
42             for i in 0...(organizations.count - 1) {
43                 self.sparql = get_SPARQL_for_finding_URIs_for_ORGANIZATION_NAME(
44                     orgString: organizations[i])
45                 behindTheScenesSparqlText += self.sparql
46                 let results = sparqlDbPedia(query: self.sparql)
47                 if results.count > 0 {
48                     ret.insert(results.map { [($0["comment"] ??
49                       ""), ($0["org_uri"] ?? "")] })
50                 }
51             }
52         }
53         if places.count > 0 {
54             for i in 0...(places.count - 1) {
55                 self.sparql = get_SPARQL_for_finding_URIs_for_PLACE_NAME(
56                     placeString: places[i])
57                 behindTheScenesSparqlText += self.sparql
58                 let results = sparqlDbPedia(query: self.sparql)
59                 if results.count > 0 {
60                     ret.insert( results.map { [($0["comment"] ??
61                       ""), ($0["place_uri"] ?? "")] })
62                 }
63             }
64         }
65         //print("\n\n+++++++ ret:\n", ret, "\n\n")
66         return Array(ret)
67     }
68 }

The file NLPutils.swift provides lower level NLP utilities:

  1 //  NLPutils.swift
  2 //  KGN
  3 //
  4 //  Copyright © 2021 Mark Watson. All rights reserved.
  5 //
  6 
  7 import Foundation
  8 import NaturalLanguage
  9 
 10 public func getPersonDescription(personName: String) -> [String] {
 11     let sparql = get_SPARQL_for_finding_URIs_for_PERSON_NAME(nameString: personName)
 12     let results = sparqlDbPedia(query: sparql)
 13     return [sparql, results.map {
 14       ($0["comment"] ?? $0["abstract"] ?? "") }.joined(separator: " . ")]
 15 }
 16 
 17 
 18 public func getPlaceDescription(placeName: String) -> [String] {
 19     let sparql = get_SPARQL_for_finding_URIs_for_PLACE_NAME(placeString: placeName)
 20     let results = sparqlDbPedia(query: sparql)
 21     return [sparql, results.map { ($0["comment"] ??
 22         $0["abstract"] ?? "") }.joined(separator: " . ")]
 23 }
 24 
 25 public func getOrganizationDescription(organizationName: String) -> [String] {
 26     let sparql = get_SPARQL_for_finding_URIs_for_ORGANIZATION_NAME(
 27         orgString: organizationName)
 28     let results = sparqlDbPedia(query: sparql)
 29     print("=== getOrganizationDescription results =\n", results)
 30     return [sparql, results.map { ($0["comment"] ?? $0["abstract"] ?? "") }
 31         .joined(separator: " . ")]
 32 }
 33 
 34 let tokenizer = NLTokenizer(unit: .word)
 35 let tagger = NSLinguisticTagger(tagSchemes:[.tokenType, .language, .lexicalClass,
 36   .nameType, .lemma], options: 0)
 37 let options: NSLinguisticTagger.Options =
 38     [.omitPunctuation, .omitWhitespace, .joinNames]
 39 
 40 let tokenizerOptions: NSLinguisticTagger.Options =
 41     [.omitPunctuation, .omitWhitespace, .joinNames]
 42 
 43 public func getEntities(text: String) -> [(String, String)] {
 44     var words: [(String, String)] = []
 45     tagger.string = text
 46     let range = NSRange(location: 0, length: text.utf16.count)
 47     tagger.enumerateTags(in: range, unit: .word,
 48         scheme: .nameType, options: options) { tag, tokenRange, stop in
 49         let word = (text as NSString).substring(with: tokenRange)
 50         let tagType = tag?.rawValue ?? "unkown"
 51         if tagType != "unkown" && tagType != "OtherWord" {
 52             words.append((word, tagType))
 53         }
 54     }
 55     return words
 56 }
 57 
 58 public func tokenizeText(text: String) -> [String] {
 59     var tokens: [String] = []
 60     tokenizer.string = text
 61     tokenizer.enumerateTokens(in: text.startIndex..<text.endIndex) { tokenRange, _ in
 62         tokens.append(String(text[tokenRange]))
 63         return true
 64     }
 65     return tokens
 66 }
 67 
 68 let entityTagger = NLTagger(tagSchemes: [.nameType])
 69 let entityOptions: NLTagger.Options = [.omitPunctuation, .omitWhitespace, .joinNames]
 70 let entityTagTypess: [NLTag] = [.personalName, .placeName, .organizationName]
 71 
 72 public func getAllEntities(text: String) -> ([String],[String],[String]) {
 73     var words: [(String, String)] = []
 74     var people: [String] = []
 75     var places: [String] = []
 76     var organizations: [String] = []
 77     entityTagger.string = text
 78     entityTagger.enumerateTags(in: text.startIndex..<text.endIndex, unit: .word,
 79         scheme: .nameType, options: entityOptions) { tag, tokenRange in
 80         if let tag = tag, entityTagTypess.contains(tag) {
 81             let word = String(text[tokenRange])
 82             if tag.rawValue == "PersonalName" {
 83                 people.append(word)
 84             } else if tag.rawValue == "PlaceName" {
 85                 places.append(word)
 86             } else if tag.rawValue == "OrganizationName" {
 87                 organizations.append(word)
 88             } else {
 89                 print("\nERROR: unkown entity type: |\(tag.rawValue)|")
 90             }
 91             words.append((word, tag.rawValue))
 92         }
 93         return true
 94     }
 95     return (people, places, organizations)
 96 }
 97 
 98 func splitLongStrings(_ s: String, limit: Int) -> String {
 99     var ret: [String] = []
100     let tokens = s.split(separator: " ")
101     var subLine = ""
102     for token in tokens {
103         if subLine.count > limit {
104             ret.append(subLine)
105             subLine = ""
106         } else {
107             subLine = subLine + " " + token
108         }
109     }
110     if subLine.count > 0 {
111         ret.append(subLine)
112     }
113     return ret.joined(separator: "\n")
114 }

Views

This is not a book about SwiftUI programming, and indeed I expect many of you dear readers know much more about UI development with SwiftUI than I do. I am not going to list the four view files:

  • MainView.swift
  • QueryView.swift
  • AboutView.swift
  • InfoView.swift

Main KGN

The top level app code in the file KGNApp.swift is fairly simple. I hardcoded the window size for macOS and the window sizes for running this example on iPadOS or iOS are commented out:

 1 import SwiftUI
 2 
 3 @main
 4 struct KGNApp: App {
 5     var body: some Scene {
 6         WindowGroup {
 7           MainView()
 8             .frame(width: 1200, height: 770)    // << here !!
 9             //.frame(width: 660, height: 770)    // << here !!
10             //..frame(width: 500, height: 800)    // << here !!
11         }
12     }
13 }

I was impressed by the SwiftUI framework. Applications are fairly portable across macOS, iOS, and iPadOS. I am not a UI developer by profession (as this application shows) but I enjoyed learning just enough about SwiftUI to write this example application.