Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Working example of sort by distance with latitude/longitude and maxDistance? #230

Closed
carcinocron opened this issue Feb 17, 2018 · 6 comments

Comments

@carcinocron
Copy link

I'm trying to convert some non-feathers mongoose code to feathers and it looks like there are ten ways to do it and I guess I'm not familiar enough with them to translate them to feathers. An example would be really helpful.

@daffl
Copy link
Member

daffl commented Feb 17, 2018

Feathers doesn't really add a lot of magic over Mongoose. How does the Mongoose code look like?

@carcinocron
Copy link
Author

This code is inherited from a previous developer.

exports.getEventSearchByQuery = (req, res) => {

  let q = req.query.query ? JSON.parse(req.query.query) : {};
  let filterByLocation = false;

  let andQuery = [];
  let aggregationQueries = [];

  const toMeters = (distanceInMile) => {
    return distanceInMile * 1609.34;  // return distance in meters
  }

  if (_.has(q, 'near') && q.near) {
    filterByLocation = true;
    let maxDistance = toMeters( +q.near.maxDistance || 10 );
    let longitude = +q.near.loc[0] || -55.5806770;
    let latitude = +q.near.loc[1] || 30.7211210;

    let locationAggregation = { "$geoNear": {
        "near": {
            "type": "Point",
            "coordinates": [ longitude, latitude ]
        },
        "spherical": true,
        "maxDistance": maxDistance,
        "distanceField": "location.distance",
    }};
    aggregationQueries.push( locationAggregation );
  }

  if (_.has(q, 'text') && q.text) {
    if ( !filterByLocation ) {
      // if there is no location filter..
      aggregationQueries.push({ $match: { $text: { $search: q.text }}}, {$sort: { score: { $meta: "textScore" }}});
    } else {
      // with location filter..
      let keywords = q.text.split(" ");
      let keyworkdsLen = keywords.length;
      if (keyworkdsLen > 0){
        let regexes = [];
        for (var i = 0; i < keyworkdsLen; i++) {
          regexes[i] = new RegExp(keywords[i], "i");
        }
        andQuery.push({ $or : [ { name: {$in: regexes} },
                              { description: {$in: regexes} },
                              { keywords: {$in: keywords} }]
                    });
      }
    }
  }

  if (_.has(q, 'category') && q.category) {
    // Increment popularity.
    let categories = q.category;
    categories.forEach((category)=>{
      Category.incrementPopularity(category);
    });
    andQuery.push({"category._id": { $in: categories }});
  }

  if (_.has(q, 'type') && q.type) {
    andQuery.push({"type": { $in: q.type }});
  }

  if (_.has(q, 'status') && q.status) {
    andQuery.push({"status": { $in: q.status }});
  }

  if (_.has(q, 'method') && q.method) {
    andQuery.push({"method": { $in: q.method }});
  }

  if (andQuery.length > 0) {
    // Filter events that are not public.
    andQuery.push(publicEvents);
    andQuery.push(nonDeletedEvents);

    aggregationQueries.push({ "$match" : { "$and" : andQuery }});

    // TODO: make sort dynamic
    aggregationQueries.push({ "$sort" : { "dates.end_at": 1 }});
  }
  if ( aggregationQueries.length > 0 ){

    Event.aggregate(aggregationQueries).allowDiskUse(true).exec(responseHandler.handleMany.bind(null, 'events', res));
  } else {
    Event.find(publicEvents, responseHandler.handleMany.bind(null, 'events', res));
  }
}

@jamesjnadeau
Copy link
Contributor

My 2 cents: looks like some liberal application of before hooks here might help you wrangle this code. You can always extend the mongoose service to fit your needs if you get stuck. Another option for the aggregate's would be to run the query yourself and set hook.result to the value you'd like to return. Hope this helps, break it down into pieces and you'll get there. Good luck!

@daffl
Copy link
Member

daffl commented Feb 17, 2018

Keep in mind that Feathers services are not much more than normal Express middleware (except that you don't have to do the annoying request response handling).

The pre-built adapters are just convenience wrappers for the 95% case, for more complex aggregates it often makes more sense to create a new custom service (although as @jamesjnadeau pointed out, it may also be possible with hooks).

Your existing code can be pretty much 1 to 1 copied and turned into a service class:

const { promisify } = require('utils');

class FindEvents {
  async find(params) {
    let q = params.query.query ? JSON.parse(params.query.query) : {};
    let filterByLocation = false;

    let andQuery = [];
    let aggregationQueries = [];

    const toMeters = (distanceInMile) => {
      return distanceInMile * 1609.34;  // return distance in meters
    }

    if (_.has(q, 'near') && q.near) {
      filterByLocation = true;
      let maxDistance = toMeters( +q.near.maxDistance || 10 );
      let longitude = +q.near.loc[0] || -55.5806770;
      let latitude = +q.near.loc[1] || 30.7211210;

      let locationAggregation = { "$geoNear": {
          "near": {
              "type": "Point",
              "coordinates": [ longitude, latitude ]
          },
          "spherical": true,
          "maxDistance": maxDistance,
          "distanceField": "location.distance",
      }};
      aggregationQueries.push( locationAggregation );
    }

    if (_.has(q, 'text') && q.text) {
      if ( !filterByLocation ) {
        // if there is no location filter..
        aggregationQueries.push({ $match: { $text: { $search: q.text }}}, {$sort: { score: { $meta: "textScore" }}});
      } else {
        // with location filter..
        let keywords = q.text.split(" ");
        let keyworkdsLen = keywords.length;
        if (keyworkdsLen > 0){
          let regexes = [];
          for (var i = 0; i < keyworkdsLen; i++) {
            regexes[i] = new RegExp(keywords[i], "i");
          }
          andQuery.push({ $or : [ { name: {$in: regexes} },
                                { description: {$in: regexes} },
                                { keywords: {$in: keywords} }]
                      });
        }
      }
    }

    if (_.has(q, 'category') && q.category) {
      // Increment popularity.
      let categories = q.category;
      categories.forEach((category)=>{
        Category.incrementPopularity(category);
      });
      andQuery.push({"category._id": { $in: categories }});
    }

    if (_.has(q, 'type') && q.type) {
      andQuery.push({"type": { $in: q.type }});
    }

    if (_.has(q, 'status') && q.status) {
      andQuery.push({"status": { $in: q.status }});
    }

    if (_.has(q, 'method') && q.method) {
      andQuery.push({"method": { $in: q.method }});
    }

    if (andQuery.length > 0) {
      // Filter events that are not public.
      andQuery.push(publicEvents);
      andQuery.push(nonDeletedEvents);

      aggregationQueries.push({ "$match" : { "$and" : andQuery }});

      // TODO: make sort dynamic
      aggregationQueries.push({ "$sort" : { "dates.end_at": 1 }});
    }

    if ( aggregationQueries.length > 0 ) {
      // Get the Mongoose model
      const Event = this.app.service('events').Model;

      return promisify(Event.aggregate(aggregationQueries).allowDiskUse(true).exec)();
    } else {
      return this.app.service('events').find({
        query: publicEvents
      });
    }
  }

  setup(app) {
    // Keep a reference to `app` so we can get the model
    this.app = app;
  }
}

@ranhsd
Copy link

ranhsd commented Apr 16, 2018

Hi @daffl ,
maybe it is possible to introduce the $near parameters so when it will be sent in the query params the mongoose adapter will filter according to it. I was thinking about something like:

http://localhost:3030/places?location[$geoNear]={"near" : { "type" : "Point","coordinates" : [{-79.3968307,43.6656976}] },"maxDistance" :10}

or we can do:

http://localhost:3030/places?location[$geoNear]={"near" : { "type" : "Point", "coordinates" :{"long" -79.3968307,"lat":43.6656976},"maxDistance" : 10}

And in the mongoose service we will need to add an extra condition for

`
if filters.$geoNear {

}
`

and this will use the following function from Mongoose:

http://mongoosejs.com/docs/api.html#query_Query-nearSphere

What do you think ?

@ranhsd
Copy link

ranhsd commented Apr 17, 2018

Hi @daffl I managed to do it with hooks

@daffl daffl closed this as completed Apr 20, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants