mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-17 09:11:03 +02:00
feat: preserve RDF language tags and datatypes through ingestion and query (#1047)
Language tags and XSD datatypes on RDF literals were being silently dropped at multiple points in the pipeline, making SPARQL FILTER(LANG()) queries and multilingual datasets non-functional. Ingestion fixes: - load_knowledge.py and load_turtle.py now extract .language and .datatype from rdflib Literal objects instead of discarding them - Triple dataclass gains optional o_datatype and o_language fields - _string_to_term() emits "dt" and "ln" in the wire format - AsyncBulkClient uses the same term serialization as BulkClient Query fix: - TriplesClient.query_gen() preserves Term objects directly instead of converting through Uri/Literal (which are str subclasses with no room for language metadata), enabling SPARQL LANG() filters to work
This commit is contained in:
parent
f9f5a2318f
commit
0c36dda443
6 changed files with 53 additions and 20 deletions
|
|
@ -4,6 +4,7 @@ import websockets
|
||||||
from typing import Optional, AsyncIterator, Dict, Any, Iterator
|
from typing import Optional, AsyncIterator, Dict, Any, Iterator
|
||||||
|
|
||||||
from . types import Triple
|
from . types import Triple
|
||||||
|
from . bulk_client import _string_to_term
|
||||||
|
|
||||||
|
|
||||||
class AsyncBulkClient:
|
class AsyncBulkClient:
|
||||||
|
|
@ -45,9 +46,13 @@ class AsyncBulkClient:
|
||||||
async with websockets.connect(ws_url, ping_interval=20, ping_timeout=self.timeout) as websocket:
|
async with websockets.connect(ws_url, ping_interval=20, ping_timeout=self.timeout) as websocket:
|
||||||
async for triple in triples:
|
async for triple in triples:
|
||||||
message = {
|
message = {
|
||||||
"s": triple.s,
|
"s": _string_to_term(triple.s),
|
||||||
"p": triple.p,
|
"p": _string_to_term(triple.p),
|
||||||
"o": triple.o
|
"o": _string_to_term(
|
||||||
|
triple.o,
|
||||||
|
datatype=triple.o_datatype,
|
||||||
|
language=triple.o_language,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
await websocket.send(json.dumps(message))
|
await websocket.send(json.dumps(message))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,19 @@ from . types import Triple
|
||||||
from . exceptions import ProtocolException
|
from . exceptions import ProtocolException
|
||||||
|
|
||||||
|
|
||||||
def _string_to_term(value: str) -> Dict[str, Any]:
|
def _string_to_term(
|
||||||
|
value: str, datatype: str = "", language: str = ""
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""Convert a string value to Term format for the gateway."""
|
"""Convert a string value to Term format for the gateway."""
|
||||||
# Treat URIs as IRI type, otherwise as literal
|
|
||||||
if value.startswith("http://") or value.startswith("https://") or "://" in value:
|
if value.startswith("http://") or value.startswith("https://") or "://" in value:
|
||||||
return {"t": "i", "i": value}
|
return {"t": "i", "i": value}
|
||||||
else:
|
else:
|
||||||
return {"t": "l", "v": value}
|
result: Dict[str, Any] = {"t": "l", "v": value}
|
||||||
|
if datatype:
|
||||||
|
result["dt"] = datatype
|
||||||
|
if language:
|
||||||
|
result["ln"] = language
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class BulkClient:
|
class BulkClient:
|
||||||
|
|
@ -141,7 +147,11 @@ class BulkClient:
|
||||||
batch.append({
|
batch.append({
|
||||||
"s": _string_to_term(triple.s),
|
"s": _string_to_term(triple.s),
|
||||||
"p": _string_to_term(triple.p),
|
"p": _string_to_term(triple.p),
|
||||||
"o": _string_to_term(triple.o)
|
"o": _string_to_term(
|
||||||
|
triple.o,
|
||||||
|
datatype=triple.o_datatype,
|
||||||
|
language=triple.o_language,
|
||||||
|
),
|
||||||
})
|
})
|
||||||
if len(batch) >= batch_size:
|
if len(batch) >= batch_size:
|
||||||
message = {
|
message = {
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,14 @@ class Triple:
|
||||||
s: Subject (entity URI or value)
|
s: Subject (entity URI or value)
|
||||||
p: Predicate (relationship URI)
|
p: Predicate (relationship URI)
|
||||||
o: Object (entity URI, literal value, or typed value)
|
o: Object (entity URI, literal value, or typed value)
|
||||||
|
o_datatype: XSD datatype URI for literal objects (e.g. "xsd:string")
|
||||||
|
o_language: Language tag for literal objects (e.g. "en", "lt")
|
||||||
"""
|
"""
|
||||||
s : str
|
s : str
|
||||||
p : str
|
p : str
|
||||||
o : str
|
o : str
|
||||||
|
o_datatype : str = ""
|
||||||
|
o_language : str = ""
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class ConfigKey:
|
class ConfigKey:
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ class TriplesClient(RequestResponse):
|
||||||
raise RuntimeError(resp.error.message)
|
raise RuntimeError(resp.error.message)
|
||||||
|
|
||||||
batch = [
|
batch = [
|
||||||
Triple(to_value(v.s), to_value(v.p), to_value(v.o))
|
Triple(v.s, v.p, v.o)
|
||||||
for v in resp.triples
|
for v in resp.triples
|
||||||
]
|
]
|
||||||
await queue.put(batch)
|
await queue.put(batch)
|
||||||
|
|
|
||||||
|
|
@ -44,15 +44,21 @@ class KnowledgeLoader:
|
||||||
for e in g:
|
for e in g:
|
||||||
s_value = str(e[0])
|
s_value = str(e[0])
|
||||||
p_value = str(e[1])
|
p_value = str(e[1])
|
||||||
|
o_value = str(e[2])
|
||||||
|
|
||||||
if isinstance(e[2], rdflib.term.URIRef):
|
o_datatype = ""
|
||||||
o_value = str(e[2])
|
o_language = ""
|
||||||
o_is_uri = True
|
|
||||||
else:
|
|
||||||
o_value = str(e[2])
|
|
||||||
o_is_uri = False
|
|
||||||
|
|
||||||
yield Triple(s=s_value, p=p_value, o=o_value)
|
if isinstance(e[2], rdflib.term.Literal):
|
||||||
|
if e[2].language:
|
||||||
|
o_language = str(e[2].language)
|
||||||
|
if e[2].datatype:
|
||||||
|
o_datatype = str(e[2].datatype)
|
||||||
|
|
||||||
|
yield Triple(
|
||||||
|
s=s_value, p=p_value, o=o_value,
|
||||||
|
o_datatype=o_datatype, o_language=o_language,
|
||||||
|
)
|
||||||
|
|
||||||
def load_entity_contexts_from_file(self, file) -> Iterator[Tuple[str, str]]:
|
def load_entity_contexts_from_file(self, file) -> Iterator[Tuple[str, str]]:
|
||||||
"""Generator that yields (entity, context) tuples from a Turtle file"""
|
"""Generator that yields (entity, context) tuples from a Turtle file"""
|
||||||
|
|
|
||||||
|
|
@ -44,13 +44,21 @@ class Loader:
|
||||||
for e in g:
|
for e in g:
|
||||||
s_value = str(e[0])
|
s_value = str(e[0])
|
||||||
p_value = str(e[1])
|
p_value = str(e[1])
|
||||||
|
o_value = str(e[2])
|
||||||
|
|
||||||
if isinstance(e[2], rdflib.term.URIRef):
|
o_datatype = ""
|
||||||
o_value = str(e[2])
|
o_language = ""
|
||||||
else:
|
|
||||||
o_value = str(e[2])
|
|
||||||
|
|
||||||
yield Triple(s=s_value, p=p_value, o=o_value)
|
if isinstance(e[2], rdflib.term.Literal):
|
||||||
|
if e[2].language:
|
||||||
|
o_language = str(e[2].language)
|
||||||
|
if e[2].datatype:
|
||||||
|
o_datatype = str(e[2].datatype)
|
||||||
|
|
||||||
|
yield Triple(
|
||||||
|
s=s_value, p=p_value, o=o_value,
|
||||||
|
o_datatype=o_datatype, o_language=o_language,
|
||||||
|
)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Load triples using Python API"""
|
"""Load triples using Python API"""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue