Статьи

Геопространственная (дистанционная) огранка с использованием граней динамического диапазона Lucene

В Lucene недавно было сделано несколько незаметных улучшений, которые в совокупности сделали неожиданно простым добавление геопространственной дистанции в любое поисковое приложение Lucene, например:

1
2
3
< 1 km (147)
< 2 km (579)
< 5 km (2775)

Такие дистанционные грани, которые позволяют пользователю быстро фильтровать свои результаты поиска по тем, которые находятся близко к их местоположению, стали особенно важными в последнее время, поскольку в настоящее время большинство запросов выполняется с мобильных смартфонов.

В прошлом это было сложно реализовать, потому что это очень динамично и дорого: количество фасетов зависит от местоположения каждого пользователя и поэтому не может быть кэшировано и разделено между пользователями, а базовая математика для пространственного расстояния сложна.

Но несколько недавних улучшений Lucene теперь делают это удивительно простым!

Во-первых, огранка динамического диапазона Lucene была обобщена для принятия любого значения ValueSource , а не только числового поля значений документа из индекса. Благодаря недавно добавленному модулю выражений это означает, что вы можете предлагать фасеты динамического диапазона, вычисленные из произвольного выражения JavaScript , поскольку выражение компилируется на лету в ValueSource с использованием пользовательских сгенерированных байт-кодов Java с ASM . Аспекты диапазона Lucene теперь также быстрее, используя деревья сегментов, чтобы быстро назначить каждое значение соответствующим диапазонам .

Во-вторых, функция расстояния Haversine была добавлена в модуль выражений. Реализация использует впечатляюще быстрое приближение к обычно дорогостоящим тригонометрическим функциям, частично отобранным из проекта Java Optimized Development Kit , не жертвуя слишком большой точностью. Маловероятно, что аппроксимации когда-либо будут иметь значение на практике, и существует открытая проблема для дальнейшего улучшения аппроксимации.

Внезапно, вооружившись этими улучшениями, если вы индексируете широту и долготу как DoubleDocValuesField в каждом документе и знаете местоположение пользователя по широте / долготе для каждого запроса, вы можете легко вычислить количество фасетов и предложить детализацию по любому набору выбранных расстояний. ,

Сначала индексируйте ваши документы с помощью полей широты и долготы:

1
2
3
4
Document doc = new Document();
doc.add(new DoubleField("latitude", 40.759011, Field.Store.NO));
doc.add(new DoubleField("longitude", -73.9844722, Field.Store.NO));
writer.addDocument(doc);

Во время поиска получите ValueSource путем создания динамического выражения, которое вызывает функцию Haversine:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
private ValueSource getDistanceValueSource() {
  Expression distance;
  try {
    distance = JavascriptCompiler.compile(
                 "haversin(40.7143528,-74.0059731,latitude,longitude)");
  } catch (ParseException pe) {
    // Should not happen
    throw new RuntimeException(pe);
  }
  SimpleBindings bindings = new SimpleBindings();
  bindings.add(new SortField("latitude", SortField.Type.DOUBLE));
  bindings.add(new SortField("longitude", SortField.Type.DOUBLE));
 
  return distance.getValueSource(bindings);
}

Вместо вышеуказанной широты / долготы, вы должны указать местоположение пользователя.

Используя этот ValueSource , рассчитайте динамический подсчет фасетов следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
FacetsCollector fc = new FacetsCollector();
 
searcher.search(new MatchAllDocsQuery(), fc);
 
Facets facets = new DoubleRangeFacetCounts(
                    "field",
                    getDistanceValueSource(), fc,
                    ONE_KM,
                    TWO_KM,
                    FIVE_KM,
                    TEN_KM);
 
return facets.getTopChildren(10, "field");

Обычно вы используете «реальный» запрос вместо MatchAllDocsQuery просмотра на верхнем уровне. Наконец, когда пользователь выберет расстояние для детализации, используйте метод Range.getFilter и добавьте его в DrillDownQuery используя ConstantScoreQuery :

01
02
03
04
05
06
07
08
09
10
public TopDocs drillDown(DoubleRange range) throws IOException {
  // Passing no baseQuery means we drill down on all
  // documents ("browse only"):
  DrillDownQuery q = new DrillDownQuery(null);
 
  q.add("field", new ConstantScoreQuery(
                     range.getFilter(getDistanceValueSource())));
 
  return searcher.search(q, 10);
}

Смотрите полный исходный код здесь , из модуля lucene/demo .

Когда я впервые протестировал этот пример, произошла забавная ошибка , а затем были пересмотрены API-интерфейсы фасетов , так что вам нужно дождаться выпуска Lucene 4.7 или просто использовать текущие исходники 4.x , чтобы получить этот пример. за работой.

Хотя этот пример прост и работает правильно, возможны некоторые явные улучшения производительности, такие как использование ограничивающего прямоугольника в качестве быстрого соответствия, чтобы избежать вычисления Haversine для попаданий, которые явно находятся за пределами диапазона возможных углублений (исправлений) добро пожаловать!). Тем не менее, это хороший шаг вперед для огранки Lucene, и удивительно, что геопространственное огранение расстояния с Lucene может быть настолько простым.

Справка: геопространственная (дистанционная) грань с использованием граней динамического диапазона Lucene от нашего партнера по JCG Майкла Мак Кэндлеса из блога Changing Bits .