sysgears / apollo-universal-starter-kit

Apollo Universal Starter Kit is a SEO-friendly, fully-configured, modular starter application that helps developers to streamline web, server, and mobile development with cutting-edge technologies and ultimate code reuse.
https://apollokit.org
MIT License
1.68k stars 323 forks source link

Site wide search solution #569

Open sakhmedbayev opened 6 years ago

sakhmedbayev commented 6 years ago

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.

mairh commented 6 years ago

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

MichelDiz commented 6 years ago

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.

verdverm commented 6 years ago

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;
}
sakhmedbayev commented 6 years ago

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

verdverm commented 6 years ago

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/