Developer forum

Forum » Development » Calculate value on Query Time for Lucene

Calculate value on Query Time for Lucene

Nuno Aguiar Dynamicweb Employee
Nuno Aguiar
Reply

Hi,

 

I implemented a "Dealer Locator" where I sort a Query Publisher list by distance. To do this I have to get all results, and then using js calculate the distance from the location the user picked and each dealer (Users in DW).

 

This all works, but I am getting 600 results when I need just 3, but I can't know which of the them until I calculate the distance. I would like to try to submit the lat and long and have the distance be calculated server side and only return 3 results, which will certainly be more efficient.

 

That said, I am looking for some guindance on how I can implement this on query-time using the Query Publisher. Is this even possible in Dynamicweb, and if so how?

 

Best Regards,

Nuno Aguiar


Replies

 
Nicolai Pedersen
Reply

Hi Nuno

I would approach this somewhat different. Given the users location input, you can very simply define a 'square' with the users location in the center of that square - i.e. define a square of 10km x 10km. That would give you 4 corners of lat/lon pairs that can be used to query the index using these 4 cornes as bounds for the items returned. Not as precise as a circle, but would do the trick, and you already sort the results by distance anyways.

You can find the math here - see section 3: http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates

 
Nuno Aguiar Dynamicweb Employee
Nuno Aguiar
Reply

Hi Nicolai,

 

That does not seem to be a valid alternative for me, because the user is not inputing a radius. We need to get the Top 3 closest results, which could be 2 km away, or 200km away, so providing a large enough square defeats the purpose because I'm also getting too much results.

 

I explored a bit with the AfterQuery notification and I was able to calculate the distances, however tying that back to filter/sort the query is proving difficult with my C# skills. Because we're getting a dynamic collection of objects in QueryResult. Here's what I have so far

 

public override void OnNotify(string notification, Dynamicweb.Extensibility.Notifications.NotificationArgs args)
{
    if (!(args is Dynamicweb.Indexing.Notifications.Query.AfterQueryArgs afterQueryArgs)
        || afterQueryArgs.Query.Name != Constants.QueryPublisher.QueryName
        || !HasLatAndLong())
    {
        return;
    }

    var distanceToCenter = new Dictionary<string, double>(); // store index and calculated distance

    CalculateDistance(afterQueryArgs.Result.QueryResult, distanceToCenter);
    var sortedAndFilteredResults = distanceToCenter.OrderBy(k => k.Value).Take(3); // take only 3 closest results

    var count = 0;
    
    foreach (var obj in afterQueryArgs.Result.QueryResult)
    {
        // need to iterate though the QueryResults and remove the indexes not within "distanceToCenter"
    }
}

 

I tried to calculate the distance and update a document property so I would not need the distanceToCenter dictionary, but I could not do that either. I used reflection for it, but got a null reference.

obj.GetType().GetProperty(Constants.QueryPublisher.DistanceToCenter)?.SetValue(obj, "1000.00");

 

In case you have questions, the method CalculateDistance simply populates the dictionary with the calculated distance.

 

Any thoughts how I could achieve that?

 

Best Regards,

Nuno Aguiar

 
Nicolai Pedersen
Reply

I am not sure you can fiddle with the collection... Anyhow - here is a stub from user management:

foreach (object queryResult in queryServiceResponse.QueryResult)
                {
                    Dictionary<string, object> resultDictionary = queryResult as Dictionary<string, object>;

                    if (resultDictionary != null)
                    {
                        object value = null;

                        if (resultDictionary.TryGetValue("UserID", out value))
                        {
                            int userId = Converter.ToInt32(value);
                            if (userId > 0 && !users.ContainsKey(userId))
                            {
                                userIds.Add(userId);
                            }
                        }
                    }
                }
 
Nuno Aguiar Dynamicweb Employee
Nuno Aguiar
Reply

Hi Nicolai,

 

It did, thanks. Scott Sargent was also key helping debug this. The hardest thing was that Dynamicweb is generating a collection of objects that "happen to be" a Dictionary<string, object>. We also had to cast each object to a dictionary to work with it. Once we did that (and other normal debuging headaches) we got it to work.

I wonder why isn't Dynamicweb generating a collection of dictionaries in the first place? we'd avoid all of those casts.

 

Anyway, now we can calculate distances based on a provided lat-lon and then return only the closest ones back. In case it helps anyone with either the AfterQuery or this logic, here's the code

 

using System;
using System.Web;
using System.Collections.Generic;
using System.Linq;

namespace Dna.Winnebago.IndexBuilderExtender
{
    [Dynamicweb.Extensibility.Notifications.Subscribe(Dynamicweb.Indexing.Notifications.Query.AfterQuery)]
    public class AfterQuery : Dynamicweb.Extensibility.Notifications.NotificationSubscriber
    {
        public override void OnNotify(string notification, Dynamicweb.Extensibility.Notifications.NotificationArgs args)
        {
            if (!(args is Dynamicweb.Indexing.Notifications.Query.AfterQueryArgs afterQueryArgs)
                || afterQueryArgs.Query.Name != Constants.QueryPublisher.QueryName
                || !HasLatAndLong())
            {
                return;
            }

            CalculateDistance(afterQueryArgs.Result.QueryResult);
            afterQueryArgs.Result.QueryResult = Sort(afterQueryArgs.Result.QueryResult, Constants.QueryPublisher.DistanceToCenter + " asc");
            afterQueryArgs.Result = FilterResults(afterQueryArgs.Result);
        }

        private bool HasLatAndLong()
        {
            var latitude = HttpContext.Current.Request[Constants.QueryString.Latitude];
            var longitude = HttpContext.Current.Request[Constants.QueryString.Longitude];

            return !string.IsNullOrEmpty(latitude) && !string.IsNullOrEmpty(longitude);
        }

        private double GetCenterLatOrLong(string queryStringParameter)
        {
            var parameter = HttpContext.Current.Request[queryStringParameter];
            if (double.TryParse(parameter, out var degrees))
            {
                return degrees;
            }
            return 0;
        }

        private void CalculateDistance(IEnumerable<object> dealers)
        {
            var centerPointLat = GetCenterLatOrLong(Constants.QueryString.Latitude);
            var centerPointLon = GetCenterLatOrLong(Constants.QueryString.Longitude);

            foreach (var obj in dealers)
            {
                var dealer = (Dictionary<string, object>) obj;

                double distanceBetweenPlaces = 1000000000;
                if (HasLatAndLong(dealer))
                { 
                    var docLat = double.Parse(dealer[Constants.User.Latitude].ToString());
                    var docLon = double.Parse(dealer[Constants.User.Longitude].ToString());

                    distanceBetweenPlaces = Helpers.DistanceBetweenPlaces(centerPointLon, centerPointLat, docLon, docLat);
                }
                SetDistance(dealer, distanceBetweenPlaces);
            }
        }
        
        private bool HasLatAndLong(Dictionary<string, object> doc)
        {
            if (doc == null) return false;

            var latitude = doc.ContainsKey(Constants.User.Latitude) ? doc[Constants.User.Latitude]?.ToString() : "";
            var longitude = doc.ContainsKey(Constants.User.Longitude) ? doc[Constants.User.Longitude]?.ToString() : "";

            return !string.IsNullOrEmpty(latitude) && !string.IsNullOrEmpty(longitude) && latitude != "0" && longitude != "0";
        }

        private void SetDistance(Dictionary<string, object> dealer, double distanceBetweenPlaces)
        {
            if (!dealer.ContainsKey(Constants.QueryPublisher.DistanceToCenter))
            {
                dealer.Add(Constants.QueryPublisher.DistanceToCenter, distanceBetweenPlaces);
            }
            else
            {
                dealer[Constants.QueryPublisher.DistanceToCenter] = distanceBetweenPlaces;
            }
        }

        private static IEnumerable<object> Sort(IEnumerable<object> data, string orderByString)
        {
            var dealers = new List<Dictionary<string, object>>();
            data.ToList().ForEach(o => dealers.Add(o as Dictionary<string, object>));
            var orderBy = orderByString
                .Split(',')
                .Select(s => s.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))
                .Select(a => new { Field = a[0], Descending = "desc".Equals(a[1], StringComparison.InvariantCultureIgnoreCase) })
                .ToList();

            if (orderBy.Count == 0)
                return data;

            // First one is OrderBy or OrderByDescending.
            IOrderedEnumerable<Dictionary<string, object>> ordered = orderBy[0].Descending ? dealers.OrderByDescending(d => d[orderBy[0].Field]) : dealers.OrderBy(d => d[orderBy[0].Field]);
            for (int i = 1; i < orderBy.Count; i++)
            {
                // Rest are ThenBy or ThenByDescending.
                var orderClause = orderBy[i];
                ordered = orderBy[i].Descending ? ordered.ThenByDescending(d => d[orderClause.Field]) : ordered.ThenBy(d => d[orderClause.Field]);
            }
            return ordered;
        }

        private Dynamicweb.Indexing.Querying.IQueryResult FilterResults(Dynamicweb.Indexing.Querying.IQueryResult dealers)
        {
            var maxResults = GetMaxResultsParameter() != 0 ? GetMaxResultsParameter() : 3;

            var filteredResults = dealers;
            filteredResults.QueryResult = filteredResults.QueryResult.Take(maxResults);
            filteredResults.TotalCount = filteredResults.QueryResult.Count();
            filteredResults.Count = filteredResults.QueryResult.Count();

            return filteredResults;
        }

        private int GetMaxResultsParameter()
        {
            var maxResultsParameter = HttpContext.Current.Request["MaxResults"];
            if (string.IsNullOrEmpty(maxResultsParameter))
            {
                maxResultsParameter = "0";
            }
            return int.Parse(maxResultsParameter);
        }
    }
}

 

Best Regards,

Nuno Aguiar

 
Adrian Ursu Dynamicweb Employee
Adrian Ursu
Reply

Hi guys.

Looks pretty interesting.
Can this approach be applied also for adding live price information to the results of a Query?

That would help with Sort by Price or filter by Price range.

Thank you,
Adrian

 
Nuno Aguiar Dynamicweb Employee
Nuno Aguiar
Reply

Hi Adrian,

 

For a small enough product catalogue, maybe, otherwise I'll say no.

 

This is a performance killer because I need to load ALL users (in your case would be all produtcs), to then do the proper distance calculations (in your case find all customer specific prices) and then manually sort and get the paged the results (in my case I just need the Top 2 or 5 - I don't have to worry about paging).

This works for me, because I have a very small set of data (up to 400 users and I don't have to rely on a 3rd party to do the distance calculation (in your case get customer live pricing for all products from an ERP).

 

That said, it's a solution for a specific problem, in my opinion.

 

Best Regards,

Nuno Aguiar

 

You must be logged in to post in the forum