Implementing a Simple RDF Datastore With Partial SPARQL Support in Racket

This chapter explains a Racket implementation of a simple RDF (Resource Description Framework) datastore with partial SPARQL (SPARQL Protocol and RDF Query Language) support. We’ll cover the core RDF data structures, query parsing and execution, helper functions, and the main function with example queries. The file rdf_sparql.rkt can be found online at https://github.com/mark-watson/Racket-AI-book/source-code/simple_RDF_SPARQL.

Before looking at the code we look at sample use and output. The function test function demonstrates the usage of the RDF datastore and SPARQL query execution:

 1 (define (test)
 2   (set! rdf-store '())
 3 
 4   (add-triple "John" "age" "30")
 5   (add-triple "John" "likes" "pizza")
 6   (add-triple "Mary" "age" "25")
 7   (add-triple "Mary" "likes" "sushi")
 8   (add-triple "Bob" "age" "35")
 9   (add-triple "Bob" "likes" "burger")
10 
11   (print-all-triples)
12 
13   (define (print-query-results query-string)
14     (printf "Query: ~a\n" query-string)
15     (let ([results (execute-sparql-query query-string)])
16       (printf "Final Results:\n")
17       (if (null? results)
18           (printf "  No results\n")
19           (for ([result results])
20             (printf "  ~a\n"
21                     (string-join
22                      (map (lambda (pair)
23                             (format "~a: ~a" (car pair) (cdr pair)))
24                           result)
25                      ", "))))
26       (printf "\n")))
27 
28   (print-query-results "select * where { ?name age ?age . ?name likes ?food }")
29   (print-query-results "select ?s ?o where { ?s likes ?o }")
30   (print-query-results "select * where { ?name age ?age . ?name likes pizza }"))

This function test:

  1. Initializes the RDF store with sample data.
  2. Prints all triples in the datastore.
  3. Defines a print-query-results function to execute and display query results.
  4. Executes three example SPARQL queries:
    • Query all name-age-food combinations.
    • Query all subject-object pairs for the “likes” predicate.
    • Query all people who like pizza and their ages.

Function test generates this output:

 1 All triples in the datastore:
 2 Bob likes burger
 3 Bob age 35
 4 Mary likes sushi
 5 Mary age 25
 6 John likes pizza
 7 John age 30
 8 
 9 Query: select * where { ?name age ?age . ?name likes ?food }
10 Final Results:
11   ?age: 35, ?name: Bob, ?food: burger
12   ?age: 25, ?name: Mary, ?food: sushi
13   ?age: 30, ?name: John, ?food: pizza
14 
15 Query: select ?s ?o where { ?s likes ?o }
16 Final Results:
17   ?s: Bob, ?o: burger
18   ?s: Mary, ?o: sushi
19   ?s: John, ?o: pizza
20 
21 Query: select * where { ?name age ?age . ?name likes pizza }
22 Final Results:
23   ?age: 30, ?name: John

1. Core RDF Data Structures and Basic Operations

There are two parts to this example in file rdf_sparql.rkt: a simple unindexed RDF datastore and a partial SPARQL query implementation that supports compound where clause matches like: select * where { ?name age ?age . ?name likes pizza }.

1.1 RDF Triple Structure

The foundation of our RDF datastore is the triple structure:

1 (struct triple (subject predicate object) #:transparent)

This structure represents an RDF triple, consisting of a subject, predicate, and object. The #:transparent keyword makes the structure’s fields accessible for easier debugging and printing.

1.2 RDF Datastore

The RDF datastore is implemented as a simple list:

1 (define rdf-store '())

1.3 Basic Operations

Two fundamental operations are defined for the datastore:

  1. Adding a triple:
1 (define (add-triple subject predicate object)
2   (set! rdf-store (cons (triple subject predicate object) rdf-store)))
  1. Removing a triple:
1 (define (remove-triple subject predicate object)
2   (set! rdf-store
3         (filter (lambda (t)
4                   (not (and (equal? (triple-subject t) subject)
5                             (equal? (triple-predicate t) predicate)
6                             (equal? (triple-object t) object))))
7                 rdf-store)))

2. Query Parsing and Execution

2.1 SPARQL Query Structure

A simple SPARQL query is represented by the sparql-query structure:

1 (struct sparql-query (select-vars where-patterns) #:transparent)

2.2 Query Parsing

The parse-sparql-query function takes a query string and converts it into a sparql-query structure:

 1 (define (parse-sparql-query query-string)
 2   (define tokens (filter (lambda (token) (not (member token '("{" "}") string=?)))
 3                          (split-string query-string)))
 4   (define select-index (index-of tokens "select" string-ci=?))
 5   (define where-index (index-of tokens "where" string-ci=?))
 6   (define (sublist lst start end)
 7     (take (drop lst start) (- end start)))
 8   (define select-vars (sublist tokens (add1 select-index) where-index))
 9   (define where-clause (drop tokens (add1 where-index)))
10   (define where-patterns (parse-where-patterns where-clause))
11   (sparql-query select-vars where-patterns))

2.3 Query Execution

The main query execution function is execute-sparql-query:

1 (define (execute-sparql-query query-string)
2   (let* ([query (parse-sparql-query query-string)]
3          [where-patterns (sparql-query-where-patterns query)]
4          [select-vars (sparql-query-select-vars query)]
5          [results (execute-where-patterns where-patterns)]
6          [projected-results (project-results results select-vars)])
7     projected-results))

This function parses the query, executes the WHERE patterns, and projects the results based on the SELECT variables.

3. Helper Functions and Utilities

Several helper functions are implemented to support query execution:

  1. variable?: Checks if a string is a SPARQL variable (starts with ‘?’).
  2. triple-to-binding: Converts a triple to a binding based on a pattern.
  3. query-triples: Filters triples based on a given pattern.
  4. apply-bindings: Applies bindings to a pattern.
  5. merge-bindings: Merges two sets of bindings.
  6. project-results: Projects the final results based on the SELECT variables.
 1 (define (variable? str)
 2   (and (string? str) (> (string-length str) 0) (char=? (string-ref str 0) #\?)))
 3 
 4 (define (triple-to-binding t [pattern #f])
 5   (define binding '())
 6   (when (and pattern (variable? (first pattern)))
 7     (set! binding (cons (cons (first pattern) (triple-subject t)) binding)))
 8   (when (and pattern (variable? (second pattern)))
 9     (set! binding (cons (cons (second pattern) (triple-predicate t)) binding)))
10   (when (and pattern (variable? (third pattern)))
11     (set! binding (cons (cons (third pattern) (triple-object t)) binding)))
12   binding)
13 
14 (define (query-triples subject predicate object)
15   (filter
16    (lambda (t)
17     (and
18       (or (not subject) (variable? subject) (equal? (triple-subject t) subject))
19       (or (not predicate) (variable? predicate)
20           (equal? (triple-predicate t) predicate))
21       (or (not object) (variable? object) (equal? (triple-object t) object))))
22    rdf-store))
23 
24 (define (apply-bindings pattern bindings)
25   (map (lambda (item)
26          (if (variable? item)
27              (or (dict-ref bindings item #f) item)
28              item))
29        pattern))
30 
31 (define (merge-bindings binding1 binding2)
32   (append binding1 binding2))
33 
34 (define (project-results results select-vars)
35   (if (equal? select-vars '("*"))
36       (map remove-duplicate-bindings results)
37       (map (lambda (result)
38              (remove-duplicate-bindings
39               (map (lambda (var)
40                      (cons var (dict-ref result var #f)))
41                    select-vars)))
42            results)))

Conclusion

This implementation provides a basic framework for an RDF datastore with partial SPARQL support in Racket. While it lacks many features of a full-fledged RDF database and SPARQL engine, it demonstrates the core concepts and can serve as a starting point for more complex implementations. The code is simple and can be fun experimenting with.