blob: d20f4ad18a46c275d31a1fd80217ca7f474ce6c4 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Add NuGet References:
// Lucene.Net.Analysis.Common
// Lucene.Net.Expressions
// Lucene.Net.Facet
using J2N;
using Lucene.Net.Analysis.Core;
using Lucene.Net.Documents;
using Lucene.Net.Expressions;
using Lucene.Net.Expressions.JS;
using Lucene.Net.Facet;
using Lucene.Net.Facet.Range;
using Lucene.Net.Facet.Taxonomy;
using Lucene.Net.Index;
using Lucene.Net.Queries;
using Lucene.Net.Queries.Function;
using Lucene.Net.Search;
using Lucene.Net.Store;
using Lucene.Net.Util;
using System;
using System.Diagnostics;
using System.Globalization;
namespace Lucene.Net.Demo.Facet
{
/// <summary>
/// Shows simple usage of dynamic range faceting, using the
/// expressions module to calculate distance.
/// </summary>
public sealed class DistanceFacetsExample : IDisposable
{
/// <summary>
/// Using a constant for all functionality related to a specific index
/// is the best strategy. This allows you to upgrade Lucene.Net first
/// and plan the upgrade of the index binary format for a later time.
/// Once the index is upgraded, you simply need to update the constant
/// version and redeploy your application.
/// </summary>
private const LuceneVersion EXAMPLE_VERSION = LuceneVersion.LUCENE_48;
internal static readonly DoubleRange ONE_KM = new DoubleRange("< 1 km", 0.0, true, 1.0, false);
internal static readonly DoubleRange TWO_KM = new DoubleRange("< 2 km", 0.0, true, 2.0, false);
internal static readonly DoubleRange FIVE_KM = new DoubleRange("< 5 km", 0.0, true, 5.0, false);
internal static readonly DoubleRange TEN_KM = new DoubleRange("< 10 km", 0.0, true, 10.0, false);
private readonly Directory indexDir = new RAMDirectory();
private IndexSearcher searcher;
private readonly FacetsConfig config = new FacetsConfig();
/// <summary>The "home" latitude.</summary>
public readonly static double ORIGIN_LATITUDE = 40.7143528;
/// <summary>The "home" longitude.</summary>
public readonly static double ORIGIN_LONGITUDE = -74.0059731;
/// <summary>
/// Radius of the Earth in KM
/// <para/>
/// NOTE: this is approximate, because the earth is a bit
/// wider at the equator than the poles. See
/// http://en.wikipedia.org/wiki/Earth_radius
/// </summary>
public readonly static double EARTH_RADIUS_KM = 6371.01;
/// <summary>Build the example index.</summary>
public void Index()
{
using IndexWriter writer = new IndexWriter(indexDir, new IndexWriterConfig(EXAMPLE_VERSION,
new WhitespaceAnalyzer(EXAMPLE_VERSION)));
// TODO: we could index in radians instead ... saves all the conversions in GetBoundingBoxFilter
// Add documents with latitude/longitude location:
writer.AddDocument(new Document
{
new DoubleField("latitude", 40.759011, Field.Store.NO),
new DoubleField("longitude", -73.9844722, Field.Store.NO)
});
writer.AddDocument(new Document
{
new DoubleField("latitude", 40.718266, Field.Store.NO),
new DoubleField("longitude", -74.007819, Field.Store.NO)
});
writer.AddDocument(new Document
{
new DoubleField("latitude", 40.7051157, Field.Store.NO),
new DoubleField("longitude", -74.0088305, Field.Store.NO)
});
// Open near-real-time searcher
searcher = new IndexSearcher(DirectoryReader.Open(writer, true));
}
private static ValueSource GetDistanceValueSource()
{
Expression distance = JavascriptCompiler.Compile(
string.Format(CultureInfo.InvariantCulture, "haversin({0:R},{1:R},latitude,longitude)", ORIGIN_LATITUDE, ORIGIN_LONGITUDE));
SimpleBindings bindings = new SimpleBindings();
bindings.Add(new SortField("latitude", SortFieldType.DOUBLE));
bindings.Add(new SortField("longitude", SortFieldType.DOUBLE));
return distance.GetValueSource(bindings);
}
/// <summary>
/// Given a latitude and longitude (in degrees) and the
/// maximum great circle (surface of the earth) distance,
/// returns a simple Filter bounding box to "fast match"
/// candidates.
/// </summary>
public static Filter GetBoundingBoxFilter(double originLat, double originLng, double maxDistanceKM)
{
// Basic bounding box geo math from
// http://JanMatuschek.de/LatitudeLongitudeBoundingCoordinates,
// licensed under creative commons 3.0:
// http://creativecommons.org/licenses/by/3.0
// TODO: maybe switch to recursive prefix tree instead
// (in lucene/spatial)? It should be more efficient
// since it's a 2D trie...
// Degrees -> Radians:
double originLatRadians = originLat.ToRadians();
double originLngRadians = originLng.ToRadians();
double angle = maxDistanceKM / (SloppyMath.EarthDiameter(originLat) / 2.0);
double minLat = originLatRadians - angle;
double maxLat = originLatRadians + angle;
double minLng;
double maxLng;
if (minLat > -90.ToRadians() && maxLat < 90.ToRadians())
{
double delta = Math.Asin(Math.Sin(angle) / Math.Cos(originLatRadians));
minLng = originLngRadians - delta;
if (minLng < -180.ToRadians())
{
minLng += 2 * Math.PI;
}
maxLng = originLngRadians + delta;
if (maxLng > 180.ToRadians())
{
maxLng -= 2 * Math.PI;
}
}
else
{
// The query includes a pole!
minLat = Math.Max(minLat, -90.ToRadians());
maxLat = Math.Min(maxLat, 90.ToRadians());
minLng = -180.ToRadians();
maxLng = 180.ToRadians();
}
BooleanFilter f = new BooleanFilter
{
// Add latitude range filter:
{
NumericRangeFilter.NewDoubleRange("latitude", minLat.ToDegrees(), maxLat.ToDegrees(), true, true),
Occur.MUST
}
};
// Add longitude range filter:
if (minLng > maxLng)
{
// The bounding box crosses the international date
// line:
BooleanFilter lonF = new BooleanFilter
{
{
NumericRangeFilter.NewDoubleRange("longitude", minLng.ToDegrees(), null, true, true),
Occur.SHOULD
},
{
NumericRangeFilter.NewDoubleRange("longitude", null, maxLng.ToDegrees(), true, true),
Occur.SHOULD
}
};
f.Add(lonF, Occur.MUST);
}
else
{
f.Add(NumericRangeFilter.NewDoubleRange("longitude", minLng.ToDegrees(), maxLng.ToDegrees(), true, true),
Occur.MUST);
}
return f;
}
/// <summary>User runs a query and counts facets.</summary>
public FacetResult Search()
{
FacetsCollector fc = new FacetsCollector();
searcher.Search(new MatchAllDocsQuery(), fc);
Facets facets = new DoubleRangeFacetCounts("field", GetDistanceValueSource(), fc,
GetBoundingBoxFilter(ORIGIN_LATITUDE, ORIGIN_LONGITUDE, 10.0),
ONE_KM,
TWO_KM,
FIVE_KM,
TEN_KM);
return facets.GetTopChildren(10, "field");
}
/// <summary>User drills down on the specified range.</summary>
public TopDocs DrillDown(DoubleRange range)
{
// Passing no baseQuery means we drill down on all
// documents ("browse only"):
DrillDownQuery q = new DrillDownQuery(null);
ValueSource vs = GetDistanceValueSource();
q.Add("field", range.GetFilter(GetBoundingBoxFilter(ORIGIN_LATITUDE, ORIGIN_LONGITUDE, range.Max), vs));
DrillSideways ds = new SearchDrillSideways(searcher, config, vs);
return ds.Search(q, 10).Hits;
}
private class SearchDrillSideways : DrillSideways
{
private readonly ValueSource vs;
public SearchDrillSideways(IndexSearcher indexSearcher, FacetsConfig facetsConfig, ValueSource valueSource)
: base(indexSearcher, facetsConfig, (TaxonomyReader)null)
{
this.vs = valueSource;
}
protected override Facets BuildFacetsResult(FacetsCollector drillDowns, FacetsCollector[] drillSideways, string[] drillSidewaysDims)
{
Debug.Assert(drillSideways.Length == 1);
return new DoubleRangeFacetCounts("field", vs, drillSideways[0], ONE_KM, TWO_KM, FIVE_KM, TEN_KM);
}
}
public void Dispose()
{
searcher?.IndexReader?.Dispose();
indexDir?.Dispose();
}
/// <summary>Runs the search and drill-down examples and prints the results.</summary>
public static void Main(string[] args)
{
using DistanceFacetsExample example = new DistanceFacetsExample();
example.Index();
Console.WriteLine("Distance facet counting example:");
Console.WriteLine("-----------------------");
Console.WriteLine(example.Search());
Console.WriteLine("\n");
Console.WriteLine("Distance facet drill-down example (field/< 2 km):");
Console.WriteLine("---------------------------------------------");
TopDocs hits = example.DrillDown(TWO_KM);
Console.WriteLine(hits.TotalHits + " totalHits");
}
}
}