Search in tree structures (mongo, elasticsearch)

Hello
I have structure like this:
folder -> sub folder -> sub sub folder -> sub ...N... sub folder, where each folder has name field

The case is to search in this structure by name, i need to get matched folders and display this in tree view with all it’s parents (even if parent’s name doesn’t match the query) on client

Is there some efficient way to store and search in structures like this? available instruments are mongo and elasticsearch

My current approach with mongo:

# Folders doc format
# _id: String
# name: String
# parentId: String

searchFolders = (parameters) =>
  parameters = Object.assign({}, parameters)
  options = {sort: {name: 1}}
  parameters.parentId ?= null
  
  folders = Folders.find({}
    parentId: parameters.parentId
  }, options).fetch()

  filtered = []
  searchRegexp = Utils.createTextSearchRegexp(parameters.search)
  
  for folder in folders
    nameMatch = searchRegexp.test(folder.name)
    parameters.parentId = folder._id
  
    if nameMatch or (children = searchFolders(parameters, counts)).length
  
      if children?.length
        folder.children = children
      filtered.push(folder)
  
    if filtered.length is 20
      break
  
  filtered

On the server you can use mongo’s $graphLookup for that:

const rawFolders = Folders.rawCollection();
const wrappedAggregate = Meteor.wrapAsync(rawFolders.aggregate, rawFolders);
wrappedAggregate([
    { $match: { name: searchedName } },
    {
        $graphLookup: {
            from: 'folders',
            startWith: '$parentId',
            connectFromField: 'parentId',
            connectToField: '_id',
            as: 'ancestors',
            depthField: 'depth',
        },
    },
]);

This does a search for folders matching the name, and then adds a new field to each returned document called ancestors with the chain of folders that lead to it

EDIT: Added Meteor.wrapAsync to solution

3 Likes

Of course, to get this on the client, you either need to fetch the data with a Meteor method call or publish using a package like jcbernack:reactive-aggregate