Day 22 challenge
Goal: master DQL querying and multi-language client integration with DgraphTheme: context engineering week - advanced graph query masteryTime investment: ~30 minutes
What you’ll accomplish today
- Master DQL syntax for complex graph queries
- Use Ratel (Dgraph’s UI) to explore the news knowledge graph
- Learn multi-hop graph traversal and aggregation techniques
- Integrate Dgraph with agents using Python, JavaScript, and Go clients
- Build sophisticated graph-powered agent workflows
This requires access to a Dgraph instance (free Cloud instance available) and
familiarity with basic programming concepts. Be sure to complete Day 21 first.
You’ll work with multiple client libraries.
Step 1: DQL fundamentals
DQL (Dgraph Query Language) is designed specifically for graph traversal and analysis:Basic DQL syntax
Copy
Ask AI
{
articles(func: type(Article)) {
Article.title
Article.abstract
Article.uri
}
}
Key DQL concepts
- Functions: Entry points for queries (
type(),eq(),allofterms(), etc.) - Predicates: Properties to retrieve or traverse
- Variables: Store intermediate results (
var(func: ...)) - Filters: Refine results at any level(
@filter()) - Aggregations: Calculate values across sets (
count,sum,avg)
DQL vs. other query languages
DQL advantages:- Native graph traversal with unlimited depth
- Variables for complex multi-stage queries
- Built-in aggregation and filtering at any level
- Optimized for distributed graph operations
DQL thinking Unlike SQL joins, DQL follows relationships naturally. Think
about traversing paths through connected data rather than combining tables.
Step 2: Exploring with Ratel
Ratel is Dgraph’s built-in UI for query development and visualization. We’ll use Ratel to execute queries and explore the results. Follow the steps described in Day 21 to connect Ratel to your Hypermode Graph.Filtering and ordering
You can filter results using the@filter directive:
Copy
Ask AI
{
articles(func: type(Article)) @filter(has(Article.abstract)) {
Article.title
Article.abstract
}
}
orderasc or orderdesc parameter:
Copy
Ask AI
{
articles(func: type(Article), orderasc: Article.title) {
Article.title
Article.abstract
}
}
@index to Article.title to enable fast
sorting:
Copy
Ask AI
<Article.title>: string @index(exact) .
Date filtering
Your schema includesArticle.published as a date field. To filter by date:
Copy
Ask AI
{
recent_articles(func: type(Article)) @filter(ge(Article.published, "2025-01-01T00:00:00Z")) {
Article.title
Article.published
}
}
datetime index for faster date-based queries:
Copy
Ask AI
<Article.published>: datetime @index(hour) .
Nested traversals
Follow relationships between entities with nested queries:Copy
Ask AI
{
topics(func: type(Topic)) {
Topic.name
~Article.topic { # Traverse reverse edge to articles
Article.title
Article.abstract
}
}
}
Copy
Ask AI
{
articles(func: type(Article)) {
Article.title
Article.topic {
Topic.name
}
Article.org {
Organization.name
}
}
}
Full-text search
The schema has a full-text index onTopic.name, enabling text search:
Copy
Ask AI
{
topics(func: anyoftext(Topic.name, "technology AI")) {
Topic.name
~Article.topic {
Article.title
}
}
}
Copy
Ask AI
<Article.title>: string @index(fulltext) .
<Article.abstract>: string @index(fulltext, term) .
Geospatial queries
Your schema hasGeo.location as a geo field, enabling location-based
queries:
Copy
Ask AI
{
nearby_locations(func: near(Geo.location, [-74.0060, 40.7128], 50000)) {
Geo.name
Geo.location
~Article.geo {
Article.title
}
}
}
Vector similarity search
The schema includesArticle.embedding with an HNSW vector index, allowing
semantic searches:
Copy
Ask AI
query vector_search($embedding: string, $limit: int) {
articles(func: similar_to(Article.embedding, $limit, $embedding)) {
uid
Article.title
Article.abstract
score
}
}
Advanced queries: Combining multiple filters
Combine multiple filters for complex queries:Copy
Ask AI
{
tech_articles_2025(func: type(Article)) @filter(
anyoftext(Article.abstract, "technology AI") AND
ge(Article.published, "2025-01-01") AND
has(Article.geo)
) {
Article.title
Article.abstract
Article.published
Article.geo {
Geo.name
Geo.location
}
Article.topic {
Topic.name
}
}
}
Additional schema improvements
To enable more advanced queries, consider these improvements:-
Add indexes to organization and author names for searching:
CopyAsk AI
<Organization.name>: string @index(exact, term) . <Author.name>: string @index(exact, term) . -
Add count indexing to quickly count relationships:
CopyAsk AI
<Article.topic>: [uid] @count @reverse . <Author.article>: [uid] @count @reverse . -
Add unique ID constraints for article URIs:
CopyAsk AI
<Article.uri>: string @index(exact) @upsert . -
Add date partitioning for more efficient date range queries:
CopyAsk AI
<Article.published>: datetime @index(year, month, day, hour) .
Client directives
DQL offers several client-side directives that modify query behavior without affecting the underlying data.The @cascade directive
The @cascade directive filters out nodes where any of the requested fields are
null or empty:
Copy
Ask AI
{
articles(func: type(Article)) @cascade {
Article.title
Article.abstract
Article.topic {
Topic.name
}
}
}
The @facets directive
While not currently configured in your schema, facets let you add metadata to
edges. To add and query facets, you’d update your schema like this:
Copy
Ask AI
<Article.topic>: [uid] @reverse @facets(relevance: float) .
Copy
Ask AI
{
articles(func: type(Article)) {
Article.title
Article.topic @facets(relevance) {
Topic.name
}
}
}
The @filter directive (with multiple conditions)
Combine multiple filter conditions using logical operators:
Copy
Ask AI
{
articles(func: type(Article)) @filter(has(Article.abstract) AND (anyoftext(Article.abstract, "climate") OR anyoftext(Article.abstract, "weather"))) {
Article.title
Article.abstract
}
}
The @recurse directive
For recursive traversals (useful if your graph has hierarchical relationships):
Copy
Ask AI
{
topics(func: type(Topic)) {
Topic.name
subtopics @recurse(depth: 3) {
name
subtopics
}
}
}
subtopics predicate to
your schema.
Aggregation queries
DQL provides functions for aggregating data:Basic count
Copy
Ask AI
{
total_articles(func: type(Article)) {
count(uid)
}
}
Count with grouping
Copy
Ask AI
{
topics(func: type(Topic)) {
Topic.name
article_count: count(~Article.topic)
}
}
Multiple aggregations
Copy
Ask AI
{
articles(func: type(Article)) {
// trunk-ignore(vale/error)
topic_stats: Article.topic {
# Requires @index(exact) on Topic.name
topic_min: min(Topic.name)
topic_max: max(Topic.name)
topic_count: count(uid)
}
}
}
Value-based aggregations
For numeric fields with appropriate indexes (not in your current schema):Copy
Ask AI
{
# This would require adding a numeric wordCount field with an @index(int)
article_stats(func: type(Article)) {
min_words: min(Article.wordCount)
max_words: max(Article.wordCount)
avg_words: avg(Article.wordCount)
sum_words: sum(Article.wordCount)
}
}
Grouping with @groupby
Group and aggregate data (requires adding @index directives to the fields used
in @groupby):
Copy
Ask AI
{
articles(func: type(Article)) @groupby(Article.published) {
month: min(Article.published)
count: count(uid)
}
}
<Article.published>: datetime @index(month) in the
schema.
Date-based aggregations
Copy
Ask AI
{
publications_by_month(func: type(Article)) {
count: count(uid)
month: datetrunc(Article.published, "month")
} @groupby(month)
}
datetime index on Article.published.
Combined advanced example
This example combines multiple directives and aggregations:Copy
Ask AI
{
topic_statistics(func: type(Topic)) @filter(has(~Article.topic)) {
Topic.name
articles: ~Article.topic @cascade {
count: count(uid)
recent_count: count(uid) @filter(ge(Article.published, "2025-01-01T00:00:00Z"))
oldest: min(Article.published)
newest: max(Article.published)
}
}
}
Step 4: Client integrations
Dgraph provides clients for multiple programming languages, including Python, Go, and JavaScript. You can use these clients to connect to your Dgraph instance and perform operations like queries, mutations, and transactions.Setup and basic connection
- Python
- JavaScript
- Go
Copy
Ask AI
import pydgraph
import grpc
import json
from datetime import datetime
# Create Dgraph client
def create_client():
stub = pydgraph.DgraphClientStub('localhost:9080')
client =pydgraph.open("dgraph://<YOUR_HYPERMODE_GRAPH_CONNECTION_STRING>")
return client
# Example agent integration class
class NewsGraphAgent:
def __init__(self):
self.client = create_client()
def search_articles_by_topic(self, topic_name, limit=10):
"""Search articles using full-text search on topic names"""
query = f"""
{{
topics(func: anyoftext(Topic.name, "{topic_name}")) {{
Topic.name
articles: ~Article.topic (first: {limit}) {{
uid
Article.title
Article.abstract
Article.published
Article.uri
}}
}}
}}
"""
txn = self.client.txn()
try:
response = txn.query(query)
return json.loads(response.json)
finally:
txn.discard()
def search_articles_by_content(self, search_terms, limit=10):
"""Full-text search across article titles and abstracts"""
query = f"""
{{
articles(func: anyoftext(Article.title, "{search_terms}"), first: {limit}) {{
uid
Article.title
Article.abstract
Article.published
Article.topic {{
Topic.name
}}
Article.org {{
Organization.name
}}
}}
}}
"""
txn = self.client.txn()
try:
response = txn.query(query)
return json.loads(response.json)
finally:
txn.discard()
def get_recent_articles(self, days_back=30, limit=20):
"""Get articles published within the last N days"""
from datetime import datetime, timedelta
cutoff_date = (datetime.now() - timedelta(days=days_back)).isoformat() + "Z"
query = f"""
{{
recent_articles(func: type(Article)) @filter(ge(Article.published, "{cutoff_date}"))
(orderdesc: Article.published, first: {limit}) {{
uid
Article.title
Article.abstract
Article.published
Article.topic {{
Topic.name
}}
Article.org {{
Organization.name
}}
Article.geo {{
Geo.name
Geo.location
}}
}}
}}
"""
txn = self.client.txn()
try:
response = txn.query(query)
return json.loads(response.json)
finally:
txn.discard()
def search_articles_near_location(self, latitude, longitude, radius_meters=50000, limit=10):
"""Find articles associated with locations near given coordinates"""
query = f"""
{{
nearby_locations(func: near(Geo.location, [{longitude}, {latitude}], {radius_meters})) {{
Geo.name
Geo.location
articles: ~Article.geo (first: {limit}) {{
uid
Article.title
Article.abstract
Article.published
Article.topic {{
Topic.name
}}
}}
}}
}}
"""
txn = self.client.txn()
try:
response = txn.query(query)
return json.loads(response.json)
finally:
txn.discard()
def vector_similarity_search(self, embedding_vector, limit=5):
"""Perform semantic search using article embeddings"""
query = """
query vector_search($embedding: string, $limit: int) {
articles(func: similar_to(Article.embedding, $limit, $embedding)) {
uid
Article.title
Article.abstract
Article.published
score
Article.topic {
Topic.name
}
Article.org {
Organization.name
}
}
}
"""
variables = {
"$embedding": json.dumps(embedding_vector),
"$limit": str(limit)
}
txn = self.client.txn()
try:
response = txn.query(query, variables=variables)
return json.loads(response.json)
finally:
txn.discard()
def get_topic_statistics(self):
"""Get comprehensive statistics for each topic"""
query = """
{
topic_statistics(func: type(Topic)) @filter(has(~Article.topic)) {
Topic.name
total_articles: count(~Article.topic)
recent_articles: count(~Article.topic @filter(ge(Article.published, "2025-01-01T00:00:00Z")))
oldest_article: min(val(~Article.topic)) {
Article.published
}
newest_article: max(val(~Article.topic)) {
Article.published
}
}
}
"""
txn = self.client.txn()
try:
response = txn.query(query)
return json.loads(response.json)
finally:
txn.discard()
def complex_filtered_search(self, content_terms, topic_terms=None, since_date="2025-01-01", has_location=False):
"""Advanced search combining multiple filters and conditions"""
location_filter = "AND has(Article.geo)" if has_location else ""
topic_filter = f'AND anyoftext(Article.topic, "{topic_terms}")' if topic_terms else ""
query = f"""
{{
filtered_articles(func: type(Article)) @filter(
anyoftext(Article.abstract, "{content_terms}") AND
ge(Article.published, "{since_date}T00:00:00Z")
{topic_filter}
{location_filter}
) @cascade {{
uid
Article.title
Article.abstract
Article.published
Article.topic {{
Topic.name
}}
Article.geo {{
Geo.name
Geo.location
}}
Article.org {{
Organization.name
}}
}}
}}
"""
txn = self.client.txn()
try:
response = txn.query(query)
return json.loads(response.json)
finally:
txn.discard()
def analyze_publication_trends(self):
"""Analyze publication patterns over time using groupby"""
query = """
{
publication_trends(func: type(Article)) @groupby(Article.published) {
month: datetrunc(Article.published, "month")
article_count: count(uid)
}
}
"""
txn = self.client.txn()
try:
response = txn.query(query)
data = json.loads(response.json)
return self._process_publication_trends(data)
finally:
txn.discard()
def get_normalized_article_data(self, limit=10):
"""Get flattened article data using @normalize"""
query = f"""
{{
articles(func: type(Article), first: {limit}) @normalize {{
title: Article.title
abstract: Article.abstract
published: Article.published
uri: Article.uri
topics: Article.topic {{
name: Topic.name
}}
organizations: Article.org {{
name: Organization.name
}}
location: Article.geo {{
name: Geo.name
}}
}}
}}
"""
txn = self.client.txn()
try:
response = txn.query(query)
return json.loads(response.json)
finally:
txn.discard()
def _process_publication_trends(self, data):
"""Process publication trend data into a more usable format"""
trends = data.get('publication_trends', [])
processed_trends = []
for trend in trends:
processed_trends.append({
'month': trend.get('month'),
'article_count': trend.get('article_count', 0)
})
# Sort by month
processed_trends.sort(key=lambda x: x['month'] if x['month'] else '')
return {
'trends': processed_trends,
'total_months': len(processed_trends),
'peak_month': max(processed_trends, key=lambda x: x['article_count']) if processed_trends else None
}
Copy
Ask AI
const dgraph = require("dgraph-js")
const grpc = require("grpc")
const { URL } = require('url')
class NewsGraphJS {
constructor(connectionString = "dgraph://<YOUR_DGRAPH_CONNECTION_STRING_HERE>") {
/**
* Initialize with Dgraph connection string.
* Examples:
* - dgraph://localhost:9080 (local development)
* - dgraph://dgraph.example.com:443?ssl=true (production with SSL)
*/
this.connectionString = connectionString
this.client = this._createClient(connectionString)
}
_createClient(connectionString) {
const dgraphClient = await dgraph.open(
'dgraph://<YOUR_CONNECTION_STRING>',
)
return dgraphClient
}
async searchArticlesByTopic(topicName, limit = 10) {
/**
* Search articles using full-text search on topic names
*/
const query = `
{
topics(func: anyoftext(Topic.name, "${topicName}")) {
Topic.name
articles: ~Article.topic (first: ${limit}) {
uid
Article.title
Article.abstract
Article.published
Article.uri
}
}
}
`
const txn = this.client.newTxn()
try {
const response = await txn.query(query)
return response.getJson()
} finally {
await txn.discard()
}
}
async searchArticlesByContent(searchTerms, limit = 10) {
/**
* Full-text search across article titles and abstracts
*/
const query = `
{
articles(func: anyoftext(Article.title, "${searchTerms}"), first: ${limit}) {
uid
Article.title
Article.abstract
Article.published
Article.topic {
Topic.name
}
Article.org {
Organization.name
}
}
}
`
const txn = this.client.newTxn()
try {
const response = await txn.query(query)
return response.getJson()
} finally {
await txn.discard()
}
}
async getRecentArticles(daysBack = 30, limit = 20) {
/**
* Get articles published within the last N days
*/
const cutoffDate = new Date(Date.now() - daysBack * 24 * 60 * 60 * 1000).toISOString()
const query = `
{
recent_articles(func: type(Article)) @filter(ge(Article.published, "${cutoffDate}"))
(orderdesc: Article.published, first: ${limit}) {
uid
Article.title
Article.abstract
Article.published
Article.topic {
Topic.name
}
Article.org {
Organization.name
}
Article.geo {
Geo.name
Geo.location
}
}
}
`
const txn = this.client.newTxn()
try {
const response = await txn.query(query)
return response.getJson()
} finally {
await txn.discard()
}
}
async searchArticlesNearLocation(latitude, longitude, radiusMeters = 50000, limit = 10) {
/**
* Find articles associated with locations near given coordinates
*/
const query = `
{
nearby_locations(func: near(Geo.location, [${longitude}, ${latitude}], ${radiusMeters})) {
Geo.name
Geo.location
articles: ~Article.geo (first: ${limit}) {
uid
Article.title
Article.abstract
Article.published
Article.topic {
Topic.name
}
}
}
}
`
const txn = this.client.newTxn()
try {
const response = await txn.query(query)
return response.getJson()
} finally {
await txn.discard()
}
}
async vectorSimilaritySearch(embeddingVector, limit = 5) {
/**
* Perform semantic search using article embeddings
*/
const query = `
query vector_search($embedding: string, $limit: int) {
articles(func: similar_to(Article.embedding, $limit, $embedding)) {
uid
Article.title
Article.abstract
Article.published
score
Article.topic {
Topic.name
}
Article.org {
Organization.name
}
}
}
`
const variables = {
$embedding: JSON.stringify(embeddingVector),
$limit: limit.toString()
}
const txn = this.client.newTxn()
try {
const response = await txn.queryWithVars(query, variables)
return response.getJson()
} finally {
await txn.discard()
}
}
async getTopicStatistics() {
/**
* Get comprehensive statistics for each topic
*/
const query = `
{
topic_statistics(func: type(Topic)) @filter(has(~Article.topic)) {
Topic.name
total_articles: count(~Article.topic)
recent_articles: count(~Article.topic @filter(ge(Article.published, "2025-01-01T00:00:00Z")))
oldest_article: min(val(~Article.topic)) {
Article.published
}
newest_article: max(val(~Article.topic)) {
Article.published
}
}
}
`
const txn = this.client.newTxn()
try {
const response = await txn.query(query)
return response.getJson()
} finally {
await txn.discard()
}
}
async complexFilteredSearch(contentTerms, topicTerms = null, sinceDate = "2025-01-01", hasLocation = false) {
/**
* Advanced search combining multiple filters and conditions
*/
const locationFilter = hasLocation ? "AND has(Article.geo)" : ""
const topicFilter = topicTerms ? `AND anyoftext(Article.topic, "${topicTerms}")` : ""
const query = `
{
filtered_articles(func: type(Article)) @filter(
anyoftext(Article.abstract, "${contentTerms}") AND
ge(Article.published, "${sinceDate}T00:00:00Z")
${topicFilter}
${locationFilter}
) @cascade {
uid
Article.title
Article.abstract
Article.published
Article.topic {
Topic.name
}
Article.geo {
Geo.name
Geo.location
}
Article.org {
Organization.name
}
}
}
`
const txn = this.client.newTxn()
try {
const response = await txn.query(query)
return response.getJson()
} finally {
await txn.discard()
}
}
async analyzePublicationTrends() {
/**
* Analyze publication patterns over time using groupby
*/
const query = `
{
publication_trends(func: type(Article)) @groupby(Article.published) {
month: datetrunc(Article.published, "month")
article_count: count(uid)
}
}
`
const txn = this.client.newTxn()
try {
const response = await txn.query(query)
const data = response.getJson()
return this._processPublicationTrends(data)
} finally {
await txn.discard()
}
}
async getNormalizedArticleData(limit = 10) {
/**
* Get flattened article data using @normalize
*/
const query = `
{
articles(func: type(Article), first: ${limit}) @normalize {
title: Article.title
abstract: Article.abstract
published: Article.published
uri: Article.uri
topics: Article.topic {
name: Topic.name
}
organizations: Article.org {
name: Organization.name
}
location: Article.geo {
name: Geo.name
}
}
}
`
const txn = this.client.newTxn()
try {
const response = await txn.query(query)
return response.getJson()
} finally {
await txn.discard()
}
}
async testConnection() {
/**
* Test the Dgraph connection
*/
try {
const query = "{ test(func: type(Article), first: 1) { uid } }"
const txn = this.client.newTxn()
try {
const response = await txn.query(query)
return {
status: "connected",
connectionString: this.connectionString,
response: "OK"
}
} finally {
await txn.discard()
}
} catch (error) {
return {
status: "error",
connectionString: this.connectionString,
error: error.message
}
}
}
_processPublicationTrends(data) {
/**
* Process publication trend data into a more usable format
*/
const trends = data.publication_trends || []
const processedTrends = trends.map(trend => ({
month: trend.month,
article_count: trend.article_count || 0
}))
// Sort by month
processedTrends.sort((a, b) => {
if (!a.month) return 1
if (!b.month) return -1
return a.month.localeCompare(b.month)
})
const peakMonth = processedTrends.length > 0
? processedTrends.reduce((max, trend) =>
trend.article_count > max.article_count ? trend : max
)
: null
return {
trends: processedTrends,
total_months: processedTrends.length,
peak_month: peakMonth
}
}
// Legacy methods for backward compatibility
async searchCompanyNews(companyName, daysBack = 30) {
/**
* Legacy method: search for organization-related articles
*/
const query = `
{
company_news(func: anyoftext(Organization.name, "${companyName}")) {
Organization.name
recent_mentions: ~Article.org @filter(gt(Article.published, "2024-01-01")) (first: 20) {
Article.title
Article.published
Article.abstract
Article.topic {
Topic.name
}
}
}
}
`
const txn = this.client.newTxn()
try {
const response = await txn.query(query)
return response.getJson()
} finally {
await txn.discard()
}
}
async analyzeCompetitiveLandscape(companies) {
/**
* Legacy method: analyze competitive mentions across companies
*/
const query = `
{
var(func: anyoftext(Organization.name, "${companies.join(" ")}")) {
competitor_articles as ~Article.org {
other_competitors: Article.org @filter(anyoftext(Organization.name, "${companies.join(" ")}"))
}
}
competitive_analysis(func: uid(competitor_articles)) {
Article.title
Article.published
Article.abstract
companies_mentioned: Article.org @filter(anyoftext(Organization.name, "${companies.join(" ")}")) {
Organization.name
}
}
}
`
const txn = this.client.newTxn()
try {
const response = await txn.query(query)
return this.processCompetitiveData(response.getJson())
} finally {
await txn.discard()
}
}
processCompetitiveData(data) {
/**
* Process competitive analysis data
*/
const articles = data.competitive_analysis || []
const companyMentions = {}
articles.forEach((article) => {
article.companies_mentioned?.forEach((company) => {
if (!companyMentions[company["Organization.name"]]) {
companyMentions[company["Organization.name"]] = {
total_mentions: 0,
articles: [],
}
}
companyMentions[company["Organization.name"]].total_mentions++
companyMentions[company["Organization.name"]].articles.push(article)
})
})
return {
analysis_date: new Date().toISOString(),
companies_analyzed: Object.keys(companyMentions),
competitive_insights: companyMentions,
total_articles_analyzed: articles.length,
}
}
}
// Usage examples with different connection strings
async function exampleUsage() {
try {
// Local development
const localAgent = new NewsGraphJS("dgraph://localhost:9080")
// Production with SSL
const prodAgent = new NewsGraphJS("dgraph://dgraph.example.com:443?ssl=true")
// Test connections
console.log("Local connection:", await localAgent.testConnection())
console.log("Production connection:", await prodAgent.testConnection())
// Use the agent
const recentArticles = await localAgent.getRecentArticles(7, 5)
console.log("Recent articles:", JSON.stringify(recentArticles, null, 2))
// Search by topic
const techArticles = await localAgent.searchArticlesByTopic("technology")
console.log("Tech articles:", JSON.stringify(techArticles, null, 2))
// Get topic statistics
const topicStats = await localAgent.getTopicStatistics()
console.log("Topic statistics:", JSON.stringify(topicStats, null, 2))
} catch (error) {
console.error("Error:", error.message)
}
}
module.exports = { NewsGraphJS, exampleUsage }
Copy
Ask AI
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/url"
"strconv"
"strings"
"time"
"github.com/dgraph-io/dgo/v2"
"github.com/dgraph-io/dgo/v2/protos/api"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
type NewsGraphGo struct {
client *dgo.Dgraph
connectionString string
}
// NewNewsGraphClient creates a new Dgraph client with connection string support
func NewNewsGraphClient(connectionString string) (*NewsGraphGo, error) {
if connectionString == "" {
connectionString = "dgraph://localhost:9080"
}
client, err := dgo.Open(connectionString)
if err != nil {
return nil, fmt.Errorf("failed to create Dgraph client: %v", err)
}
return &NewsGraphGo{
client: client,
connectionString: connectionString,
}, nil
}
// SearchArticlesByTopic searches articles using full-text search on topic names
func (ng *NewsGraphGo) SearchArticlesByTopic(topicName string, limit int) (*TopicSearchResult, error) {
query := fmt.Sprintf(`
{
topics(func: anyoftext(Topic.name, "%s")) {
Topic.name
articles: ~Article.topic (first: %d) {
uid
Article.title
Article.abstract
Article.published
Article.uri
}
}
}
`, topicName, limit)
txn := ng.client.NewTxn()
defer txn.Discard(context.Background())
response, err := txn.Query(context.Background(), query)
if err != nil {
return nil, err
}
var result TopicSearchResult
err = json.Unmarshal(response.Json, &result)
return &result, err
}
// SearchArticlesByContent performs full-text search across article titles and abstracts
func (ng *NewsGraphGo) SearchArticlesByContent(searchTerms string, limit int) (*ArticleSearchResult, error) {
query := fmt.Sprintf(`
{
articles(func: anyoftext(Article.title, "%s"), first: %d) {
uid
Article.title
Article.abstract
Article.published
Article.topic {
Topic.name
}
Article.org {
Organization.name
}
}
}
`, searchTerms, limit)
txn := ng.client.NewTxn()
defer txn.Discard(context.Background())
response, err := txn.Query(context.Background(), query)
if err != nil {
return nil, err
}
var result ArticleSearchResult
err = json.Unmarshal(response.Json, &result)
return &result, err
}
// GetRecentArticles gets articles published within the last N days
func (ng *NewsGraphGo) GetRecentArticles(daysBack, limit int) (*ArticleSearchResult, error) {
cutoffDate := time.Now().AddDate(0, 0, -daysBack).Format(time.RFC3339)
query := fmt.Sprintf(`
{
recent_articles(func: type(Article)) @filter(ge(Article.published, "%s"))
(orderdesc: Article.published, first: %d) {
uid
Article.title
Article.abstract
Article.published
Article.topic {
Topic.name
}
Article.org {
Organization.name
}
Article.geo {
Geo.name
Geo.location
}
}
}
`, cutoffDate, limit)
txn := ng.client.NewTxn()
defer txn.Discard(context.Background())
response, err := txn.Query(context.Background(), query)
if err != nil {
return nil, err
}
var result ArticleSearchResult
err = json.Unmarshal(response.Json, &result)
return &result, err
}
// SearchArticlesNearLocation finds articles associated with locations near given coordinates
func (ng *NewsGraphGo) SearchArticlesNearLocation(latitude, longitude float64, radiusMeters, limit int) (*LocationSearchResult, error) {
query := fmt.Sprintf(`
{
nearby_locations(func: near(Geo.location, [%f, %f], %d)) {
Geo.name
Geo.location
articles: ~Article.geo (first: %d) {
uid
Article.title
Article.abstract
Article.published
Article.topic {
Topic.name
}
}
}
}
`, longitude, latitude, radiusMeters, limit)
txn := ng.client.NewTxn()
defer txn.Discard(context.Background())
response, err := txn.Query(context.Background(), query)
if err != nil {
return nil, err
}
var result LocationSearchResult
err = json.Unmarshal(response.Json, &result)
return &result, err
}
// VectorSimilaritySearch performs semantic search using article embeddings
func (ng *NewsGraphGo) VectorSimilaritySearch(embeddingVector []float64, limit int) (*VectorSearchResult, error) {
// Convert embedding vector to string format
embeddingJSON, err := json.Marshal(embeddingVector)
if err != nil {
return nil, fmt.Errorf("failed to marshal embedding vector: %v", err)
}
query := `
query vector_search($embedding: string, $limit: int) {
articles(func: similar_to(Article.embedding, $limit, $embedding)) {
uid
Article.title
Article.abstract
Article.published
score
Article.topic {
Topic.name
}
Article.org {
Organization.name
}
}
}
`
variables := map[string]string{
"$embedding": string(embeddingJSON),
"$limit": strconv.Itoa(limit),
}
txn := ng.client.NewTxn()
defer txn.Discard(context.Background())
response, err := txn.QueryWithVars(context.Background(), query, variables)
if err != nil {
return nil, err
}
var result VectorSearchResult
err = json.Unmarshal(response.Json, &result)
return &result, err
}
// GetTopicStatistics gets comprehensive statistics for each topic
func (ng *NewsGraphGo) GetTopicStatistics() (*TopicStatsResult, error) {
query := `
{
topic_statistics(func: type(Topic)) @filter(has(~Article.topic)) {
Topic.name
total_articles: count(~Article.topic)
recent_articles: count(~Article.topic @filter(ge(Article.published, "2025-01-01T00:00:00Z")))
oldest_article: min(val(~Article.topic)) {
Article.published
}
newest_article: max(val(~Article.topic)) {
Article.published
}
}
}
`
txn := ng.client.NewTxn()
defer txn.Discard(context.Background())
response, err := txn.Query(context.Background(), query)
if err != nil {
return nil, err
}
var result TopicStatsResult
err = json.Unmarshal(response.Json, &result)
return &result, err
}
// ComplexFilteredSearch performs advanced search combining multiple filters and conditions
func (ng *NewsGraphGo) ComplexFilteredSearch(contentTerms string, topicTerms *string, sinceDate string, hasLocation bool) (*ArticleSearchResult, error) {
locationFilter := ""
if hasLocation {
locationFilter = "AND has(Article.geo)"
}
topicFilter := ""
if topicTerms != nil {
topicFilter = fmt.Sprintf(`AND anyoftext(Article.topic, "%s")`, *topicTerms)
}
query := fmt.Sprintf(`
{
filtered_articles(func: type(Article)) @filter(
anyoftext(Article.abstract, "%s") AND
ge(Article.published, "%sT00:00:00Z")
%s
%s
) @cascade {
uid
Article.title
Article.abstract
Article.published
Article.topic {
Topic.name
}
Article.geo {
Geo.name
Geo.location
}
Article.org {
Organization.name
}
}
}
`, contentTerms, sinceDate, topicFilter, locationFilter)
txn := ng.client.NewTxn()
defer txn.Discard(context.Background())
response, err := txn.Query(context.Background(), query)
if err != nil {
return nil, err
}
var result ArticleSearchResult
err = json.Unmarshal(response.Json, &result)
return &result, err
}
// AnalyzePublicationTrends analyzes publication patterns over time using groupby
func (ng *NewsGraphGo) AnalyzePublicationTrends() (*PublicationTrendsResult, error) {
query := `
{
publication_trends(func: type(Article)) @groupby(Article.published) {
month: datetrunc(Article.published, "month")
article_count: count(uid)
}
}
`
txn := ng.client.NewTxn()
defer txn.Discard(context.Background())
response, err := txn.Query(context.Background(), query)
if err != nil {
return nil, err
}
var rawResult map[string]interface{}
err = json.Unmarshal(response.Json, &rawResult)
if err != nil {
return nil, err
}
return ng.processPublicationTrends(rawResult), nil
}
// GetNormalizedArticleData gets flattened article data using @normalize
func (ng *NewsGraphGo) GetNormalizedArticleData(limit int) (*NormalizedArticleResult, error) {
query := fmt.Sprintf(`
{
articles(func: type(Article), first: %d) @normalize {
title: Article.title
abstract: Article.abstract
published: Article.published
uri: Article.uri
topics: Article.topic {
name: Topic.name
}
organizations: Article.org {
name: Organization.name
}
location: Article.geo {
name: Geo.name
}
}
}
`, limit)
txn := ng.client.NewTxn()
defer txn.Discard(context.Background())
response, err := txn.Query(context.Background(), query)
if err != nil {
return nil, err
}
var result NormalizedArticleResult
err = json.Unmarshal(response.Json, &result)
return &result, err
}
// TestConnection tests the Dgraph connection
func (ng *NewsGraphGo) TestConnection() *ConnectionStatus {
query := "{ test(func: type(Article), first: 1) { uid } }"
txn := ng.client.NewTxn()
defer txn.Discard(context.Background())
_, err := txn.Query(context.Background(), query)
if err != nil {
return &ConnectionStatus{
Status: "error",
ConnectionString: ng.connectionString,
Error: err.Error(),
}
}
return &ConnectionStatus{
Status: "connected",
ConnectionString: ng.connectionString,
Response: "OK",
}
}
// Helper method to process publication trends
func (ng *NewsGraphGo) processPublicationTrends(data map[string]interface{}) *PublicationTrendsResult {
trendsInterface, ok := data["publication_trends"].([]interface{})
if !ok {
return &PublicationTrendsResult{
Trends: []TrendData{},
TotalMonths: 0,
}
}
var trends []TrendData
var peakMonth *TrendData
for _, trendInterface := range trendsInterface {
trendMap, ok := trendInterface.(map[string]interface{})
if !ok {
continue
}
trend := TrendData{
Month: getString(trendMap, "month"),
ArticleCount: getInt(trendMap, "article_count"),
}
trends = append(trends, trend)
if peakMonth == nil || trend.ArticleCount > peakMonth.ArticleCount {
peakMonth = &trend
}
}
return &PublicationTrendsResult{
Trends: trends,
TotalMonths: len(trends),
PeakMonth: peakMonth,
}
}
// Legacy methods for backward compatibility
func (ng *NewsGraphGo) SearchCompanyNews(companyName string, daysBack int) (*CompanyNewsResult, error) {
query := fmt.Sprintf(`
{
company_news(func: anyoftext(Organization.name, "%s")) {
Organization.name
recent_mentions: ~Article.org @filter(gt(Article.published, "2024-01-01")) (first: 20) {
Article.title
Article.published
Article.abstract
Article.topic {
Topic.name
}
}
}
}
`, companyName)
txn := ng.client.NewTxn()
defer txn.Discard(context.Background())
response, err := txn.Query(context.Background(), query)
if err != nil {
return nil, err
}
var result CompanyNewsResult
err = json.Unmarshal(response.Json, &result)
return &result, err
}
func (ng *NewsGraphGo) AnalyzeCompetitiveLandscape(companies []string) (*CompetitiveAnalysisResult, error) {
companiesStr := strings.Join(companies, " ")
query := fmt.Sprintf(`
{
var(func: anyoftext(Organization.name, "%s")) {
competitor_articles as ~Article.org {
other_competitors: Article.org @filter(anyoftext(Organization.name, "%s"))
}
}
competitive_analysis(func: uid(competitor_articles)) {
Article.title
Article.published
Article.abstract
companies_mentioned: Article.org @filter(anyoftext(Organization.name, "%s")) {
Organization.name
}
}
}
`, companiesStr, companiesStr, companiesStr)
txn := ng.client.NewTxn()
defer txn.Discard(context.Background())
response, err := txn.Query(context.Background(), query)
if err != nil {
return nil, err
}
var rawResult map[string]interface{}
err = json.Unmarshal(response.Json, &rawResult)
if err != nil {
return nil, err
}
return ng.processCompetitiveData(rawResult, companies), nil
}
func (ng *NewsGraphGo) processCompetitiveData(data map[string]interface{}, companies []string) *CompetitiveAnalysisResult {
articlesInterface, ok := data["competitive_analysis"].([]interface{})
if !ok {
articlesInterface = []interface{}{}
}
companyMentions := make(map[string]*CompanyMention)
for _, articleInterface := range articlesInterface {
articleMap, ok := articleInterface.(map[string]interface{})
if !ok {
continue
}
companiesInterface, ok := articleMap["companies_mentioned"].([]interface{})
if !ok {
continue
}
for _, companyInterface := range companiesInterface {
companyMap, ok := companyInterface.(map[string]interface{})
if !ok {
continue
}
companyName := getString(companyMap, "Organization.name")
if companyName == "" {
continue
}
if _, exists := companyMentions[companyName]; !exists {
companyMentions[companyName] = &CompanyMention{
TotalMentions: 0,
Articles: []map[string]interface{}{},
}
}
companyMentions[companyName].TotalMentions++
companyMentions[companyName].Articles = append(
companyMentions[companyName].Articles,
articleMap,
)
}
}
var companiesAnalyzed []string
for company := range companyMentions {
companiesAnalyzed = append(companiesAnalyzed, company)
}
return &CompetitiveAnalysisResult{
AnalysisDate: time.Now().Format(time.RFC3339),
CompaniesAnalyzed: companiesAnalyzed,
CompetitiveInsights: companyMentions,
TotalArticlesAnalyzed: len(articlesInterface),
}
}
// Helper functions
func getString(m map[string]interface{}, key string) string {
if val, ok := m[key].(string); ok {
return val
}
return ""
}
func getInt(m map[string]interface{}, key string) int {
if val, ok := m[key].(float64); ok {
return int(val)
}
return 0
}
// Type definitions
type Article struct {
UID string `json:"uid"`
Title string `json:"Article.title"`
Abstract string `json:"Article.abstract"`
Published string `json:"Article.published"`
URI string `json:"Article.uri"`
Topic []Topic `json:"Article.topic"`
Org []Organization `json:"Article.org"`
Geo []Geo `json:"Article.geo"`
}
type Topic struct {
Name string `json:"Topic.name"`
}
type Organization struct {
Name string `json:"Organization.name"`
}
type Geo struct {
Name string `json:"Geo.name"`
Location string `json:"Geo.location"`
}
type TopicSearchResult struct {
Topics []struct {
Name string `json:"Topic.name"`
Articles []Article `json:"articles"`
} `json:"topics"`
}
type ArticleSearchResult struct {
Articles []Article `json:"articles,omitempty"`
RecentArticles []Article `json:"recent_articles,omitempty"`
FilteredArticles []Article `json:"filtered_articles,omitempty"`
}
type LocationSearchResult struct {
NearbyLocations []struct {
Name string `json:"Geo.name"`
Location string `json:"Geo.location"`
Articles []Article `json:"articles"`
} `json:"nearby_locations"`
}
type VectorSearchResult struct {
Articles []struct {
Article
Score float64 `json:"score"`
} `json:"articles"`
}
type TopicStatsResult struct {
TopicStatistics []struct {
Name string `json:"Topic.name"`
TotalArticles int `json:"total_articles"`
RecentArticles int `json:"recent_articles"`
OldestArticle []struct {
Published string `json:"Article.published"`
} `json:"oldest_article"`
NewestArticle []struct {
Published string `json:"Article.published"`
} `json:"newest_article"`
} `json:"topic_statistics"`
}
type PublicationTrendsResult struct {
Trends []TrendData `json:"trends"`
TotalMonths int `json:"total_months"`
PeakMonth *TrendData `json:"peak_month"`
}
type TrendData struct {
Month string `json:"month"`
ArticleCount int `json:"article_count"`
}
type NormalizedArticleResult struct {
Articles []struct {
Title string `json:"title"`
Abstract string `json:"abstract"`
Published string `json:"published"`
URI string `json:"uri"`
Topics []struct {
Name string `json:"name"`
} `json:"topics"`
Organizations []struct {
Name string `json:"name"`
} `json:"organizations"`
Location []struct {
Name string `json:"name"`
} `json:"location"`
} `json:"articles"`
}
type ConnectionStatus struct {
Status string `json:"status"`
ConnectionString string `json:"connection_string"`
Response string `json:"response,omitempty"`
Error string `json:"error,omitempty"`
}
type CompanyNewsResult struct {
CompanyNews []struct {
Name string `json:"Organization.name"`
RecentMentions []Article `json:"recent_mentions"`
} `json:"company_news"`
}
type CompetitiveAnalysisResult struct {
AnalysisDate string `json:"analysis_date"`
CompaniesAnalyzed []string `json:"companies_analyzed"`
CompetitiveInsights map[string]*CompanyMention `json:"competitive_insights"`
TotalArticlesAnalyzed int `json:"total_articles_analyzed"`
}
type CompanyMention struct {
TotalMentions int `json:"total_mentions"`
Articles []map[string]interface{} `json:"articles"`
}
// Example usage
func ExampleUsage() {
// Local development
localAgent, err := NewNewsGraphClient("dgraph://localhost:9080")
if err != nil {
log.Fatal("Failed to create local client:", err)
}
// Production with SSL
prodAgent, err := NewNewsGraphClient("dgraph://dgraph.example.com:443?ssl=true")
if err != nil {
log.Fatal("Failed to create production client:", err)
}
// Test connections
fmt.Println("Local connection:", localAgent.TestConnection())
fmt.Println("Production connection:", prodAgent.TestConnection())
// Use the agent
recentArticles, err := localAgent.GetRecentArticles(7, 5)
if err != nil {
log.Printf("Error getting recent articles: %v", err)
} else {
fmt.Printf("Recent articles: %+v\n", recentArticles)
}
// Search by topic
techArticles, err := localAgent.SearchArticlesByTopic("technology", 10)
if err != nil {
log.Printf("Error searching by topic: %v", err)
} else {
fmt.Printf("Tech articles: %+v\n", techArticles)
}
// Get topic statistics
topicStats, err := localAgent.GetTopicStatistics()
if err != nil {
log.Printf("Error getting topic statistics: %v", err)
} else {
fmt.Printf("Topic statistics: %+v\n", topicStats)
}
}
func main() {
ExampleUsage()
}
What you’ve accomplished
In 30 minutes, you’ve mastered advanced graph querying: DQL mastery: learned sophisticated query patterns for graph traversal and analysis Ratel exploration: used visual tools to understand graph structure and optimize queries Multi-language integration: implemented Dgraph clients in Python, JavaScript, and Go Agent integration: connected graph reasoning capabilities to intelligent agents Advanced patterns: built complex analysis workflows using graph-native operationsThe power of graph querying
DQL enables reasoning that traditional databases can’t: Traditional queries: “What articles mention OpenAI?” Graph-powered queries: “What entities are connected to OpenAI through 2-3 degrees of separation, how has this network evolved over time, and what does the pattern suggest about competitive positioning?” This completes your mastery of context engineering fundamentals.Week 4 Complete
You’ve mastered context engineering - from prompts to sophisticated graph
reasoning. Ready for production deployment in Week 5!
Pro tip for today
Build a comprehensive graph analysis workflow:Copy
Ask AI
Create a complete analysis workflow that:
1. Takes a business question (e.g., "How is the AI industry competitive landscape evolving?")
2. Extracts relevant entities and relationships from the question
3. Designs appropriate DQL queries to explore the graph
4. Analyzes patterns across multiple dimensions (temporal, network, sentiment)
5. Synthesizes insights that answer the original business question
6. Explains the graph reasoning behind each conclusion
Show me both the technical implementation and the business insights it reveals.
Time to complete: ~30 minutes Skills learned DQL mastery, Ratel exploration, multi-language client integration, graph-powered agent reasoning, advanced analysis workflows Week 4 complete: context engineering mastery achieved!
Remember Graph querying is about following the connections that reveal
hidden insights. The most valuable discoveries often come from relationships
that weren’t obvious until you traversed the graph.