docs: def _search(problem, fringe, graph_search=False, depth_limit=None,
              node_factory=SearchNode, graph_replace_when_better=False,
              viewer=None):
      '''
      Basic search algorithm, base of all the other search algorithms.
      '''
      if viewer:
          viewer.event('started')
  
      memory = set()
      initial_node = node_factory(state=problem.initial_state,
                                  problem=problem)
      fringe.append(initial_node)
  
      while fringe:
          if viewer:
              viewer.event('new_iteration', fringe.sorted())
  
          node = fringe.pop()
  
          if problem.is_goal(node.state):
              if viewer:
                  viewer.event('chosen_node', node, True)
                  viewer.event('finished', fringe.sorted(), node, 'goal found')
              return node
          else:
              if viewer:
                  viewer.event('chosen_node', node, False)
  
          memory.add(node.state)
  
          if depth_limit is None or node.depth < depth_limit:
              expanded = node.expand()
              if viewer:
                  viewer.event('expanded', [node], [expanded])
  
              for n in expanded:
                  if graph_search:
                      others = [x for x in fringe if x.state == n.state]
                      assert len(others) in (0, 1)
                      if n.state not in memory and len(others) == 0:
                          fringe.append(n)
                      elif graph_replace_when_better and len(others) > 0 and n < others[0]:
                          fringe.remove(others[0])
                          fringe.append(n)
                  else:
                      fringe.append(n)
  
      if viewer:
          viewer.event('finished', fringe.sorted(), None, 'goal not found')