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 main demonstrates the usage of the RDF datastore and SPARQL query execution:

 1 (define (main)
 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 }"))
31 
32 ;; Run the main function
33 (main)

This function main:

  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

First, we need to split the query string into tokens, ignoring the curly braces { and }. We define a helper split-string:

1 (define (split-string string [delimiter " "])
2   (string-split string delimiter))

The parse-where-patterns helper parses the WHERE patterns, separating them by periods:

 1 (define (parse-where-patterns where-clause)
 2   (let loop ([tokens where-clause]
 3              [current-pattern '()]
 4              [patterns '()])
 5     (cond
 6       [(null? tokens)
 7        (if (null? current-pattern)
 8            (reverse patterns)
 9            (reverse (cons (reverse current-pattern) patterns)))]
10       [(string=? (car tokens) ".")
11        (loop (cdr tokens)
12              '()
13              (if (null? current-pattern)
14                  patterns
15                  (cons (reverse current-pattern) patterns)))]
16       [else
17        (loop (cdr tokens)
18              (cons (car tokens) current-pattern)
19              patterns)])))

The main 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

Query execution works recursively. execute-where-patterns initiates execution by finding bindings for the first pattern in the WHERE clause. Subsequent patterns are matched using execute-where-patterns-with-bindings, combining existing variable bindings with new ones:

 1 ;; Execute WHERE patterns with bindings
 2 (define (execute-where-patterns-with-bindings patterns bindings)
 3   (if (null? patterns)
 4       (list bindings)
 5       (let* ([pattern (first patterns)]
 6              [remaining-patterns (rest patterns)]
 7              [bound-pattern (apply-bindings pattern bindings)]
 8              [matching-triples (apply query-triples bound-pattern)])
 9         (let ([new-bindings (map (lambda (t)
10                                    (merge-bindings bindings (triple-to-binding t pat\
11 tern)))
12                                  matching-triples)])
13           (if (null? remaining-patterns)
14               new-bindings
15               (append-map (lambda (binding)
16                             (execute-where-patterns-with-bindings remaining-patterns\
17  binding))
18                           new-bindings))))))
19 
20 (define (execute-where-patterns patterns)
21   (if (null? patterns)
22       (list '())
23       (let* ([pattern (first patterns)]
24              [remaining-patterns (rest patterns)]
25              [matching-triples (apply query-triples pattern)])
26         (let ([bindings (map (lambda (t) (triple-to-binding t pattern)) matching-tri\
27 ples)])
28           (if (null? remaining-patterns)
29               bindings
30               (append-map (lambda (binding)
31                             (let ([results (execute-where-patterns-with-bindings rem\
32 aining-patterns binding)])
33                               (map (lambda (result)
34                                      (merge-bindings binding result))
35                                    results)))
36                           bindings))))))

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.
  7. remove-duplicate-bindings: Removes duplicate bindings for the same variable.
  8. print-all-triples: Prints all triples in the store.
 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)))
43 
44 (define (remove-duplicate-bindings bindings)
45   (remove-duplicates bindings #:key car))
46 
47 (define (print-all-triples)
48   (printf "All triples in the datastore:\n")
49   (for ([t rdf-store])
50     (printf "~a ~a ~a\n"
51             (triple-subject t)
52             (triple-predicate t)
53             (triple-object t)))
54   (printf "\n"))

The following diagram shows the high-level architecture of the RDF datastore and SPARQL query engine implemented in this chapter:

Architecture diagram
Architecture diagram

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.