First steps
What is the BFS?
https://en.wikipedia.org/wiki/Breadth-first_search
Breadth-first search (BFS) is an algorithm for traversing or searching tree or graph data structures. It starts at the tree root (or some arbitrary node of a graph, sometimes referred to as a ‘search key’[1]), and explores all of the neighbor nodes at the present depth prior to moving on to the nodes at the next depth level.
![]()
Task
Get friends hierarchy based on friendships graph.
Implementation
Let’s start from Member model, which can have different friendships levels.
class Member < ApplicationRecord
has_friendship
def friends_of_friends
Member.joins(:friendships).where(:id => friendships.pluck(:friend_id))
end
end
This is the graph’s Node structure, it has link to the member_id and adjacents Set with uniq elements.
require "set"
module Structures
class Node
attr_accessor :member_id, :adjacents
def initialize(member_id)
@adjacents = Set.new
@member_id = member_id
end
def to_s
@member_id
end
end
end
Let’s define Graph structure with nodes array, where we can get node by ID or add edge.
module Structures
class Graph
attr_accessor :nodes
def initialize
@nodes = []
end
def get_node(id)
@nodes.select { |x| x.member_id == id }&.first
end
def add_edge(node_one, node_two)
node_one.adjacents << node_two
node_two.adjacents << node_one
end
end
end
Funniest part - let’s go through our graph and perform search!
module Structures
class GraphQuery
def initialize(graph, source_node)
@graph = graph
@node = source_node
@visited = []
@edge_to = {}
search(source_node)
end
def build_path_to(node)
return unless has_path_to?(node)
path = []
while(node != @node) do
path.unshift(node)
node = @edge_to[node]
end
path.unshift(@node)
end
private
def search(node)
queue = []
queue << node
@visited << node
while queue.any?
current_node = queue.shift
current_node.adjacents.each do |adjacent_node|
next if @visited.include?(adjacent_node)
queue << adjacent_node
@visited << adjacent_node
@edge_to[adjacent_node] = current_node
end
end
end
def has_path_to?(node)
@visited.include?(node)
end
end
end
This is the Search module, where we’re building Graph structure and iterating through all friendships. See code comments below.
module Search
class Query
include Dry::Transaction
step :find_headings
step :graph_scope
def self.index_query(id:, q:, &block)
new.call(id: id, q: q, &block)
end
def build_graph
@graph = Structures::Graph.new
data = HasFriendship::Friendship.pluck(:friendable_id, :friend_id)
data.each do |friends|
node = @graph.get_node(friends.first)
friend_node = @graph.get_node(friends.last)
if !node
node = Structures::Node.new(friends.first)
end
if !friend_node
friend_node = Structures::Node.new(friends.last)
end
@graph.nodes << node
@graph.nodes << friend_node
@graph.add_edge(node, friend_node)
end
@graph
end
def graph_scope(id:, result_pairs:)
graph = build_graph
# GET ROOT NODE
me = graph.get_node(id.to_i)
results = []
result_pairs.each_with_index do |result, index|
# GET FRIEND FROM GRAPH NODE
friend = graph.get_node(result['member_id'].to_i)
# BUILDING SHORTEST PATH TO THE FRIEND
path = Structures::GraphQuery.new(graph, me).build_path_to(friend)
graph_path = path.map(&:member_id)
mems = Member.where(id: graph_path).sort_by { |p| graph_path.index(p.id) }
# SOME CODE FOR RESULTS POPULATION
end
Success(result: results)
end
end
end
Final
Final result returns us shortest path between logged user and Ivan user through your closest friend.
{
"result": [
[
"Alan",
"Bart",
"Mark",
"Ivan"
]
]
}
.
Stay tuned!