Hibernate Search запрашивает все точки пересечения сущностей

Я работаю над приложением, в котором есть рестораны, каждый из которых имеет покрытие доставки, и мне нужно ответить за пользователя, какие рестораны могут доставить его/ее текущее местоположение.

Я получаю тривиальное решение с помощью hibernate-spatial, но когда я перехожу в hibernate-search для полнотекстового поиска в сочетании с географическим (и из-за масштабируемости), я еще не нашел решения. Некоторые идеи/предложения/примеры?

Например, в Hibernate Spatial запрос выглядит так:

SELECT r FROM Restaurant r WHERE within(:point, r.coverage)

где, очевидно, охват — это охват ресторана.

Я думаю, что решение справиться с этим с помощью hibernate-search заключается в добавлении фильтра, но все образцы, которые я нашел, не были чем-то похожим.


person Ignacio    schedule 11.09.2016    source источник


Ответы (1)


Я нашел решение после нескольких незначительных головных болей.

Я реализую мост для геометрического поля:

import com.spatial4j.core.context.jts.JtsSpatialContext;
import com.spatial4j.core.shape.jts.JtsGeometry;
import com.vividsolutions.jts.geom.Geometry;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.spatial.SpatialStrategy;
import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy;
import org.apache.lucene.spatial.prefix.tree.GeohashPrefixTree;
import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree;
import org.apache.lucene.spatial.serialized.SerializedDVStrategy;
import org.hibernate.search.bridge.FieldBridge;
import org.hibernate.search.bridge.LuceneOptions;

/**
 * @see https://docs.jboss.org/hibernate/stable/search/reference/en-US/html/ch04.html#section-custom-bridges
 * @see http://www.gossamer-threads.com/lists/lucene/java-user/254444
 */
public class GeometryBridge implements FieldBridge {

    // http://unterbahn.com/2009/11/metric-dimensions-of-geohash-partitions-at-the-equator/
    public static final int GEOHASH_MAX_LEVELS = 9; // 4.78m

    public void set(String name, Object value, Document document, LuceneOptions luceneOptions) {
        JtsSpatialContext spatialContext = JtsSpatialContext.GEO;
        SpatialPrefixTree grid = new GeohashPrefixTree(spatialContext, 22);

        // Preparing the tree strategy field
        SpatialStrategy treeStrategy = new RecursivePrefixTreeStrategy(grid, name);
        for (IndexableField field: treeStrategy.createIndexableFields(
            new JtsGeometry((Geometry) value, spatialContext, false, true))) {
            document.add(field);
        }

        // Preparing the verify strategy field
        SerializedDVStrategy verifyStrategy = new SerializedDVStrategy(spatialContext, "serialized_" + name);
        for (IndexableField field: verifyStrategy.createIndexableFields(
            new JtsGeometry((Geometry) value, spatialContext, false, true))) {
            document.add(field);
        }
    }

}

Позже настраиваемый фильтр для фильтрации запроса:

import com.spatial4j.core.context.jts.JtsSpatialContext;
import com.spatial4j.core.shape.Point;
import org.apache.lucene.search.ConstantScoreQuery;
import org.apache.lucene.search.Filter;
import org.apache.lucene.search.FilteredQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryWrapperFilter;
import org.apache.lucene.spatial.SpatialStrategy;
import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy;
import org.apache.lucene.spatial.prefix.tree.GeohashPrefixTree;
import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.query.SpatialOperation;
import org.apache.lucene.spatial.serialized.SerializedDVStrategy;
import org.hibernate.search.annotations.Factory;
import org.hibernate.search.filter.impl.CachingWrapperFilter;

/**
 * @see https://vimeo.com/106843184
 * @see http://www.slideshare.net/lucenerevolution/lucene-solr-4-spatial-extended-deep-dive
 */
public class CoverageFilterFactory {

    public static final String NAME = "coverage";

    private String field;
    private double latitude;
    private double longitude;

    public void setField(String field) {
        this.field = field;
    }

    public void setLatitude(double latitude) {
        this.latitude = latitude;
    }

    public void setLongitude(double longitude) {
        this.longitude = longitude;
    }

    @Factory
    public Filter getFilter() {
        JtsSpatialContext spatialContext = JtsSpatialContext.GEO;
        Point point = spatialContext.makePoint(latitude, longitude);
        SpatialArgs spatialArgs = new SpatialArgs(SpatialOperation.Intersects, point);

        SpatialPrefixTree grid = new GeohashPrefixTree(spatialContext, GeometryBridge.GEOHASH_MAX_LEVELS);
        SpatialStrategy treeStrategy = new RecursivePrefixTreeStrategy(grid, field);

        SerializedDVStrategy verifyStrategy = new SerializedDVStrategy(spatialContext, "serialized_" + field);

        Query treeQuery = new ConstantScoreQuery(treeStrategy.makeFilter(spatialArgs));
        Query combinedQuery = new FilteredQuery(treeQuery,
            verifyStrategy.makeFilter(spatialArgs),
            FilteredQuery.QUERY_FIRST_FILTER_STRATEGY);

        return new CachingWrapperFilter(new QueryWrapperFilter(combinedQuery));
    }

}

И, наконец, вот код пользовательского репозитория гибернации:

import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import javax.persistence.EntityManager;
import org.apache.lucene.search.Sort;
import org.hibernate.search.annotations.Spatial;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.FullTextQuery;
import org.hibernate.search.jpa.Search;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.hibernate.search.query.dsl.Unit;
import org.hibernate.search.spatial.DistanceSortField;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;

public class CompanyRepositoryImpl implements CompanyRepositoryCustom {

    @Autowired
    private EntityManager entityManager;

    @Override
    public Page<SearchResult> search(Locale locale, double latitude, double longitude,
        Order.DeliveryMode deliveryMode, String text, Pageable pageRequest) {
        FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager);

        String analyzerName;
        switch (locale.getLanguage()) {
            case "es": analyzerName = SPANISH_NAME_FIELD_DISCRIMINATOR; break;
            case "en": analyzerName = ENGLISH_NAME_FIELD_DISCRIMINATOR; break;
            default: throw new RuntimeException("Unexpected language found");
        }

        QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory()
            .buildQueryBuilder()
            .forEntity(Company.class)
                .overridesForField(Company_.name.getName(), analyzerName)
                .overridesForField(Company_.groups.getName() + "." + Group_.name.getName(), analyzerName)
                .overridesForField(Company_.products.getName() + "." + Product_.name.getName(), analyzerName)
            .get();
        org.apache.lucene.search.Query luceneQuery = queryBuilder
            .spatial()
                .within(30000, Unit.KM)
                    .ofLatitude(latitude)
                    .andLongitude(longitude)
            .createQuery();
        // Adding text if corresponds
        if (text != null) {
            luceneQuery = queryBuilder
                .bool()
                    .must(luceneQuery)
                    .must(queryBuilder
                        .keyword()
                            .onField(Company_.name.getName())
                                .boostedTo(2f)
                            .andField(Company_.headline.getName())
                            .andField(Company_.groups.getName() + "." + Group_.name.getName())
                            .andField(Company_.products.getName() + "." + Product_.name.getName())
                            .matching(text)
                        .createQuery()
                    )
                .createQuery();
        }

        // wrap Lucene query in a javax.persistence.Query
        FullTextQuery fullTextQuery = fullTextEntityManager.createFullTextQuery(luceneQuery);
        fullTextQuery.setProjection(FullTextQuery.SPATIAL_DISTANCE, FullTextQuery.THIS);
        fullTextQuery.setSpatialParameters(latitude, longitude, Spatial.COORDINATES_DEFAULT_FIELD);

        // Applying filters
        if (deliveryMode != null) {
            fullTextQuery.enableFullTextFilter(DeliveryModeFilterFactory.NAME).setParameter("deliveryMode", deliveryMode);
            if (deliveryMode == Order.DeliveryMode.DELIVERY) {
                fullTextQuery.enableFullTextFilter(CoverageFilterFactory.NAME)
                    .setParameter("field", "coverage")
                    .setParameter("latitude", latitude)
                    .setParameter("longitude", longitude);
            }
        }

        // Sorting the results
        Sort distanceSort = new Sort(new DistanceSortField(latitude, longitude, Spatial.COORDINATES_DEFAULT_FIELD));
        fullTextQuery.setSort(distanceSort);

        // Execute Search
        // Caution: The number of results might be slightly different from getResultList().size() because getResultList() may be not in sync with the database at the time of query.
        long total = fullTextQuery.getResultSize();
        List<Object[]> result = fullTextQuery
            .setMaxResults(pageRequest.getPageSize())
            .setFirstResult(pageRequest.getOffset())
            .getResultList();

        // Transforming the results
        List<SearchResult> content = result.stream()
            .map(o -> new SearchResult((double) o[0], (Company) o[1]))
            .collect(Collectors.toList());

        return new PageImpl<>(content, pageRequest, total);
    }

}
person Ignacio    schedule 12.09.2016