apollo-universal-starter-kit icon indicating copy to clipboard operation
apollo-universal-starter-kit copied to clipboard

Site wide search solution

Open sakhmedbayev opened this issue 7 years ago • 5 comments

This is not an issue rather an enhancement proposal. It would be nice if the kit features some kind of site search solution. I am currently researching over Algolia. I will see if I will manage to implement something that is worthy of a PR.

sakhmedbayev avatar Dec 14 '17 05:12 sakhmedbayev

Search is a very expensive operation especially when it is triggered sitewide. We must carefully plan how and what to show in the results.

mairh avatar Dec 14 '17 09:12 mairh

If u use Dgraph as standard, u can take advantage of the search system built into their database. It has all sorts of indexing and etc. And it is easy to reconcile with this kit. You can index gigantic texts (Like blog) completely and it handles this with extreme ease.

MichelDiz avatar Dec 18 '17 20:12 MichelDiz

This might help...

export default function filterBuilder(queryBuilder, args) {
  let { filters } = args;

  console.log("FILTERS", filters)

  // add filter conditions
  if (filters) {

    let first = true;
    for (let filter of filters) {

      // Pre Filters Recursion
      if (filter.prefilters) {
        if (first) {
          first = false
          queryBuilder.where( function() {
            filterBuilder(this, { filters: filter.prefilters })
          })
        } else {
          if (filter.filterBool === 'and') {
            queryBuilder.andWhere( function() {
              filterBuilder(this, { filters: filter.prefilters })
            })
          } else if (filter.filterBool === 'or') {
            queryBuilder.orWhere( function() {
              filterBuilder(this, { filters: filter.prefilters })
            })
          } else {
            // Default to OR
            queryBuilder.orWhere( function() {
              filterBuilder(this, { filters: filter.prefilters })
            })
          }
        }
      }


      // This Filter Visitation
      if (filter.field) {
        let column = filter.field;
        if (filter.table) {
          column = filter.table + "." + column
        }
        column = decamelize(column)

        let compare = '='
        if (filter.compare) {
          compare = filter.compare
        }

        let value = filter.value ? filter.value : filter.values
        if(!value) {
          value = filter.timeValue ? filter.timeValue : filter.timeValues
          if(!value) {
            value = filter.intValue ? filter.intValue : filter.intValues
          }
          if(!value) {
            value = filter.floatValue ? filter.floatValue : filter.floatValues
          }
          if(!value) {
            value = filter.boolValue ? filter.boolValue : filter.boolValues
          }
        }

        if (first) {
          first = false
          queryBuilder.where(column, compare, value);
        } else {
          if (filter.bool === 'and') {
            queryBuilder.andWhere(column, compare, value);
          } else if (filter.bool === 'or') {
            queryBuilder.orWhere(column, compare, value);
          } else {
            // Default to OR
            queryBuilder.orWhere(column, compare, value);
          }
        }
      }


      // Post Filters Recursion
      if (filter.postfilters) {
        if (first) {
          first = false
          queryBuilder.where( function() {
            filterBuilder(this, { filters: filter.postfilters })
          })
        } else {
          if (filter.filterBool === 'and') {
            queryBuilder.andWhere( function() {
              filterBuilder(this, { filters: filter.postfilters })
            })
          } else if (filter.filterBool === 'or') {
            queryBuilder.orWhere( function() {
              filterBuilder(this, { filters: filter.postfilters })
            })
          } else {
            // Default to OR
            queryBuilder.orWhere( function() {
              filterBuilder(this, { filters: filter.postfilters })
            })
          }
        }
      }


    }
  }

  return queryBuilder
}
export default class User {
  async list(args, trx) {
    try {

      let queryBuilder = knex
        .select(...selectFields)
        .from('users AS u')
        .leftJoin('user_profile AS p', 'p.user_id', 'u.id');

      // add filter conditions
      queryBuilder = filterBuilder(queryBuilder, args)

      // paging and ordering
      queryBuilder = paging(queryBuilder, args)
      queryBuilder = ordering(queryBuilder, args)


      if (trx) {
        queryBuilder.transacting(trx);
      }

      let rows = await queryBuilder;
      return camelizeKeys(rows);
    } catch (e) {
      log.error('Error in User.list', e);
      throw e;
    }
  }

  ...
}
input FilterInput {
  ### Whoa deja vue thinking about subfilters while looking at...
  #
  # http://knexjs.org/#Builder-where -- Grouped Chain
  # and the "filterBuilder" I was working on

  # This should happen first
  prefilters: [FilterInput]

  # search by username, email, or any column
  type: String
  bool: String
  table: String
  field: String
  compare: String
  value: String
  values: [String]

  intValue: Int
  intValues: [Int]
  floatValue: Float
  floatValues: [Float]
  boolValue: Boolean
  boolValues: [Boolean]

  # This should happen last
  postfilters: [FilterInput]

}
{
  users(limit:10 filters:[
    {
      
    	prefilters:[
        {
          table:"u"
          field:"email"
          compare:"like"
          value:"%admin%"
        },
      	{
          bool: "or"
          table:"p"
          field:"displayName"
          compare:"like"
          value:"%Boss%"
        }
      ]
    },
    {
      bool:"and"
      table:"u"
      field:"createdAt"
      compare:"between"
      values: ["2017-12-21 05:00:00","2017-12-21 05:00:54"]
    }
  ]){
    email
    createdAt
    profile{
      displayName
    }
  }
}
export default function paging(queryBuilder, args) {
  const { offset, limit } = args;

  if (offset) {
    queryBuilder.offset(offset);
  }

  if (limit) {
    queryBuilder.limit(limit);
  }

  return queryBuilder
}
import { decamelize } from 'humps';

export default function ordering(queryBuilder, args) {
  let { orderBys } = args;

  // add order by
  if (orderBys) {
    for (let orderBy of orderBys) {
      if (orderBy && orderBy.column) {
        let column = orderBy.column;
        if (orderBy.table) {
          column = orderBy.table + "." + column
        }
        column = decamelize(column)

        let order = 'asc';
        if (orderBy.order) {
          order = orderBy.order;
        }
        queryBuilder.orderBy(column, order);
      }
    }
  }

  return queryBuilder;
}

verdverm avatar Dec 23 '17 03:12 verdverm

Thank you for sharing @verdverm. Is there any way I can look the final product of this implementation?

sakhmedbayev avatar Dec 23 '17 11:12 sakhmedbayev

I'd say it's more of an adapter for resolvers. The filterBuilder above is a more general filtering scheme based on the way the orderBy was set up. If you look at the current User.list resolver, this is basically the same code. I just went nuts on the filter to make it more flexible and pulled the code out into functions we can use elsewhere.

As of now, you can use the filter adapter to combine filters in interesting ways, depending on the context the user is searching from. A site wide search could have its own resolver, which uses the several filtered loaders. Then you have to organize and rank results somehow. That's the Google secret sauce.

As far as something you can just use out of the box, there probably isn't something yet. I've been working from the graphiql interface, so the frontend of my auth-upgrades branch hasn't caught up. I'm starting to break up my work so that we can merge it in smaller chunks. The above is one of those pieces. (A set of sql helpers) I imagine we will have a similar adapter for other storage engines. It would be cool to work towards some search input type that can be used against multiple storage engines. Something like this: https://github.com/GraphQLGuide/all-the-databases/

verdverm avatar Dec 23 '17 19:12 verdverm