mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Update location search to instantiate GeocodingService with query
This commit is contained in:
parent
b276828af3
commit
690f80766e
4 changed files with 61 additions and 170 deletions
|
|
@ -5,11 +5,12 @@ class Api::V1::LocationsController < ApiController
|
||||||
before_action :validate_suggestion_params, only: [:suggestions]
|
before_action :validate_suggestion_params, only: [:suggestions]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
if search_query.present? || coordinate_search?
|
if coordinate_search?
|
||||||
search_results = LocationSearch::PointFinder.new(current_api_user, search_params).call
|
search_results = LocationSearch::PointFinder.new(current_api_user, search_params).call
|
||||||
|
|
||||||
render json: Api::LocationSearchResultSerializer.new(search_results).call
|
render json: Api::LocationSearchResultSerializer.new(search_results).call
|
||||||
else
|
else
|
||||||
render json: { error: 'Search query parameter (q) or coordinates (lat, lon) are required' }, status: :bad_request
|
render json: { error: 'Coordinates (lat, lon) are required' }, status: :bad_request
|
||||||
end
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error "Location search error: #{e.message}"
|
Rails.logger.error "Location search error: #{e.message}"
|
||||||
|
|
@ -48,11 +49,8 @@ class Api::V1::LocationsController < ApiController
|
||||||
|
|
||||||
def search_params
|
def search_params
|
||||||
{
|
{
|
||||||
query: search_query,
|
|
||||||
latitude: params[:lat]&.to_f,
|
latitude: params[:lat]&.to_f,
|
||||||
longitude: params[:lon]&.to_f,
|
longitude: params[:lon]&.to_f,
|
||||||
name: params[:name],
|
|
||||||
address: params[:address],
|
|
||||||
limit: params[:limit]&.to_i || 50,
|
limit: params[:limit]&.to_i || 50,
|
||||||
date_from: parse_date(params[:date_from]),
|
date_from: parse_date(params[:date_from]),
|
||||||
date_to: parse_date(params[:date_to]),
|
date_to: parse_date(params[:date_to]),
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ module LocationSearch
|
||||||
results.each do |result|
|
results.each do |result|
|
||||||
# Check if there's already a result within 100m
|
# Check if there's already a result within 100m
|
||||||
duplicate = deduplicated.find do |existing|
|
duplicate = deduplicated.find do |existing|
|
||||||
distance = calculate_distance(
|
distance = calculate_distance_in_meters(
|
||||||
result[:lat], result[:lon],
|
result[:lat], result[:lon],
|
||||||
existing[:lat], existing[:lon]
|
existing[:lat], existing[:lon]
|
||||||
)
|
)
|
||||||
|
|
@ -91,22 +91,18 @@ module LocationSearch
|
||||||
deduplicated
|
deduplicated
|
||||||
end
|
end
|
||||||
|
|
||||||
def calculate_distance(lat1, lon1, lat2, lon2)
|
def calculate_distance_in_meters(lat1, lon1, lat2, lon2)
|
||||||
# Haversine formula for distance calculation in meters
|
# Use Geocoder's distance calculation (same as in Distanceable concern)
|
||||||
rad_per_deg = Math::PI / 180
|
distance_km = Geocoder::Calculations.distance_between(
|
||||||
rkm = 6_371_000 # Earth radius in meters
|
[lat1, lon1],
|
||||||
|
[lat2, lon2],
|
||||||
|
units: :km
|
||||||
|
)
|
||||||
|
|
||||||
dlat_rad = (lat2 - lat1) * rad_per_deg
|
# Convert to meters and handle potential nil/invalid results
|
||||||
dlon_rad = (lon2 - lon1) * rad_per_deg
|
return 0 unless distance_km.is_a?(Numeric) && distance_km.finite?
|
||||||
|
|
||||||
lat1_rad = lat1 * rad_per_deg
|
|
||||||
lat2_rad = lat2 * rad_per_deg
|
|
||||||
|
|
||||||
a = Math.sin(dlat_rad / 2)**2 + Math.cos(lat1_rad) * Math.cos(lat2_rad) * Math.sin(dlon_rad / 2)**2
|
|
||||||
c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
|
||||||
|
|
||||||
rkm * c
|
|
||||||
end
|
|
||||||
|
|
||||||
|
distance_km * 1000 # Convert km to meters
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,8 @@ module LocationSearch
|
||||||
class PointFinder
|
class PointFinder
|
||||||
def initialize(user, params = {})
|
def initialize(user, params = {})
|
||||||
@user = user
|
@user = user
|
||||||
@query = params[:query]
|
|
||||||
@latitude = params[:latitude]
|
@latitude = params[:latitude]
|
||||||
@longitude = params[:longitude]
|
@longitude = params[:longitude]
|
||||||
@name = params[:name] || 'Selected Location'
|
|
||||||
@address = params[:address] || ''
|
|
||||||
@limit = params[:limit] || 50
|
@limit = params[:limit] || 50
|
||||||
@date_from = params[:date_from]
|
@date_from = params[:date_from]
|
||||||
@date_to = params[:date_to]
|
@date_to = params[:date_to]
|
||||||
|
|
@ -18,8 +15,6 @@ module LocationSearch
|
||||||
def call
|
def call
|
||||||
if coordinate_search?
|
if coordinate_search?
|
||||||
return coordinate_based_search
|
return coordinate_based_search
|
||||||
elsif @query.present?
|
|
||||||
return text_based_search
|
|
||||||
else
|
else
|
||||||
return empty_result
|
return empty_result
|
||||||
end
|
end
|
||||||
|
|
@ -32,40 +27,15 @@ module LocationSearch
|
||||||
end
|
end
|
||||||
|
|
||||||
def coordinate_based_search
|
def coordinate_based_search
|
||||||
Rails.logger.info "LocationSearch: Coordinate-based search at [#{@latitude}, #{@longitude}] for '#{@name}'"
|
|
||||||
|
|
||||||
# Create a single location object with the provided coordinates
|
|
||||||
location = {
|
location = {
|
||||||
lat: @latitude,
|
lat: @latitude,
|
||||||
lon: @longitude,
|
lon: @longitude,
|
||||||
name: @name,
|
|
||||||
address: @address,
|
|
||||||
type: 'coordinate_search'
|
type: 'coordinate_search'
|
||||||
}
|
}
|
||||||
|
|
||||||
find_matching_points([location])
|
find_matching_points([location])
|
||||||
end
|
end
|
||||||
|
|
||||||
def text_based_search
|
|
||||||
return empty_result if @query.blank?
|
|
||||||
|
|
||||||
geocoded_locations = LocationSearch::GeocodingService.new(@query).search
|
|
||||||
|
|
||||||
# Debug: Log geocoding results
|
|
||||||
Rails.logger.info "LocationSearch: Geocoding '#{@query}' returned #{geocoded_locations.length} locations"
|
|
||||||
geocoded_locations.each_with_index do |loc, idx|
|
|
||||||
Rails.logger.info "LocationSearch: [#{idx}] #{loc[:name]} at [#{loc[:lat]}, #{loc[:lon]}] - #{loc[:address]}"
|
|
||||||
end
|
|
||||||
|
|
||||||
return empty_result if geocoded_locations.empty?
|
|
||||||
|
|
||||||
find_matching_points(geocoded_locations)
|
|
||||||
end
|
|
||||||
|
|
||||||
def geocoding_service
|
|
||||||
@geocoding_service ||= LocationSearch::GeocodingService.new(@query || '')
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_matching_points(geocoded_locations)
|
def find_matching_points(geocoded_locations)
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
|
|
@ -113,14 +83,9 @@ module LocationSearch
|
||||||
end
|
end
|
||||||
|
|
||||||
{
|
{
|
||||||
query: @query,
|
|
||||||
locations: results,
|
locations: results,
|
||||||
total_locations: results.length,
|
total_locations: results.length,
|
||||||
search_metadata: {
|
search_metadata: {}
|
||||||
geocoding_provider: geocoding_service.provider_name,
|
|
||||||
candidates_found: geocoded_locations.length,
|
|
||||||
search_time_ms: nil # TODO: implement timing
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -160,11 +125,9 @@ module LocationSearch
|
||||||
|
|
||||||
def empty_result
|
def empty_result
|
||||||
{
|
{
|
||||||
query: @query,
|
|
||||||
locations: [],
|
locations: [],
|
||||||
total_locations: 0,
|
total_locations: 0,
|
||||||
search_metadata: {
|
search_metadata: {
|
||||||
geocoding_provider: nil,
|
|
||||||
candidates_found: 0,
|
candidates_found: 0,
|
||||||
search_time_ms: 0
|
search_time_ms: 0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,22 +5,10 @@ require 'rails_helper'
|
||||||
RSpec.describe LocationSearch::PointFinder do
|
RSpec.describe LocationSearch::PointFinder do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:service) { described_class.new(user, search_params) }
|
let(:service) { described_class.new(user, search_params) }
|
||||||
let(:search_params) { { query: 'Kaufland' } }
|
let(:search_params) { { latitude: 52.5200, longitude: 13.4050 } }
|
||||||
|
|
||||||
describe '#call' do
|
describe '#call' do
|
||||||
context 'with valid search query' do
|
context 'with valid coordinates' do
|
||||||
let(:mock_geocoded_locations) do
|
|
||||||
[
|
|
||||||
{
|
|
||||||
lat: 52.5200,
|
|
||||||
lon: 13.4050,
|
|
||||||
name: 'Kaufland Mitte',
|
|
||||||
address: 'Alexanderplatz 1, Berlin',
|
|
||||||
type: 'shop'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:mock_matching_points) do
|
let(:mock_matching_points) do
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
@ -47,10 +35,6 @@ RSpec.describe LocationSearch::PointFinder do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(LocationSearch::GeocodingService).to receive(:new).and_return(
|
|
||||||
double('GeocodingService', search: mock_geocoded_locations, provider_name: 'Test Provider')
|
|
||||||
)
|
|
||||||
|
|
||||||
allow_any_instance_of(LocationSearch::SpatialMatcher)
|
allow_any_instance_of(LocationSearch::SpatialMatcher)
|
||||||
.to receive(:find_points_near).and_return(mock_matching_points)
|
.to receive(:find_points_near).and_return(mock_matching_points)
|
||||||
|
|
||||||
|
|
@ -61,75 +45,25 @@ RSpec.describe LocationSearch::PointFinder do
|
||||||
it 'returns search results with location data' do
|
it 'returns search results with location data' do
|
||||||
result = service.call
|
result = service.call
|
||||||
|
|
||||||
expect(result[:query]).to eq('Kaufland')
|
|
||||||
expect(result[:locations]).to be_an(Array)
|
expect(result[:locations]).to be_an(Array)
|
||||||
expect(result[:locations].first).to include(
|
expect(result[:locations].first).to include(
|
||||||
place_name: 'Kaufland Mitte',
|
|
||||||
coordinates: [52.5200, 13.4050],
|
coordinates: [52.5200, 13.4050],
|
||||||
address: 'Alexanderplatz 1, Berlin',
|
|
||||||
total_visits: 1
|
total_visits: 1
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'includes search metadata' do
|
it 'calls spatial matcher with correct coordinates and radius' do
|
||||||
result = service.call
|
|
||||||
|
|
||||||
expect(result[:search_metadata]).to include(
|
|
||||||
:geocoding_provider,
|
|
||||||
:candidates_found,
|
|
||||||
:search_time_ms
|
|
||||||
)
|
|
||||||
expect(result[:search_metadata][:candidates_found]).to eq(1)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'calls geocoding service with the query' do
|
|
||||||
expect(LocationSearch::GeocodingService)
|
|
||||||
.to receive(:new).with('Kaufland')
|
|
||||||
.and_return(double('GeocodingService', search: mock_geocoded_locations, provider_name: 'Test Provider'))
|
|
||||||
|
|
||||||
service.call
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'calls spatial matcher with correct parameters' do
|
|
||||||
expect_any_instance_of(LocationSearch::SpatialMatcher)
|
expect_any_instance_of(LocationSearch::SpatialMatcher)
|
||||||
.to receive(:find_points_near)
|
.to receive(:find_points_near)
|
||||||
.with(user, 52.5200, 13.4050, 75, { date_from: nil, date_to: nil })
|
.with(user, 52.5200, 13.4050, 500, { date_from: nil, date_to: nil })
|
||||||
|
|
||||||
service.call
|
service.call
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'determines appropriate search radius for shop type' do
|
context 'with custom radius override' do
|
||||||
expect_any_instance_of(LocationSearch::SpatialMatcher)
|
let(:search_params) { { latitude: 52.5200, longitude: 13.4050, radius_override: 150 } }
|
||||||
.to receive(:find_points_near)
|
|
||||||
.with(user, anything, anything, 75, anything)
|
|
||||||
|
|
||||||
service.call
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with different place types' do
|
|
||||||
it 'uses smaller radius for street addresses' do
|
|
||||||
mock_geocoded_locations[0][:type] = 'street'
|
|
||||||
|
|
||||||
expect_any_instance_of(LocationSearch::SpatialMatcher)
|
|
||||||
.to receive(:find_points_near)
|
|
||||||
.with(user, anything, anything, 50, anything)
|
|
||||||
|
|
||||||
service.call
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'uses larger radius for neighborhoods' do
|
|
||||||
mock_geocoded_locations[0][:type] = 'neighborhood'
|
|
||||||
|
|
||||||
expect_any_instance_of(LocationSearch::SpatialMatcher)
|
|
||||||
.to receive(:find_points_near)
|
|
||||||
.with(user, anything, anything, 300, anything)
|
|
||||||
|
|
||||||
service.call
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'uses custom radius when override provided' do
|
it 'uses custom radius when override provided' do
|
||||||
service = described_class.new(user, search_params.merge(radius_override: 150))
|
|
||||||
|
|
||||||
expect_any_instance_of(LocationSearch::SpatialMatcher)
|
expect_any_instance_of(LocationSearch::SpatialMatcher)
|
||||||
.to receive(:find_points_near)
|
.to receive(:find_points_near)
|
||||||
.with(user, anything, anything, 150, anything)
|
.with(user, anything, anything, 150, anything)
|
||||||
|
|
@ -141,7 +75,8 @@ RSpec.describe LocationSearch::PointFinder do
|
||||||
context 'with date filtering' do
|
context 'with date filtering' do
|
||||||
let(:search_params) do
|
let(:search_params) do
|
||||||
{
|
{
|
||||||
query: 'Kaufland',
|
latitude: 52.5200,
|
||||||
|
longitude: 13.4050,
|
||||||
date_from: Date.parse('2024-01-01'),
|
date_from: Date.parse('2024-01-01'),
|
||||||
date_to: Date.parse('2024-03-31')
|
date_to: Date.parse('2024-03-31')
|
||||||
}
|
}
|
||||||
|
|
@ -158,62 +93,12 @@ RSpec.describe LocationSearch::PointFinder do
|
||||||
service.call
|
service.call
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
context 'when no geocoding results found' do
|
|
||||||
before do
|
|
||||||
allow(LocationSearch::GeocodingService).to receive(:new).and_return(
|
|
||||||
double('GeocodingService', search: [], provider_name: 'Test Provider')
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns empty result' do
|
|
||||||
result = service.call
|
|
||||||
|
|
||||||
expect(result[:locations]).to be_empty
|
|
||||||
expect(result[:total_locations]).to eq(0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when no matching points found' do
|
|
||||||
before do
|
|
||||||
allow(LocationSearch::GeocodingService).to receive(:new).and_return(
|
|
||||||
double('GeocodingService', search: [{ lat: 52.5200, lon: 13.4050, name: 'Test' }], provider_name: 'Test Provider')
|
|
||||||
)
|
|
||||||
|
|
||||||
allow_any_instance_of(LocationSearch::SpatialMatcher)
|
|
||||||
.to receive(:find_points_near).and_return([])
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'excludes locations with no visits' do
|
|
||||||
result = service.call
|
|
||||||
|
|
||||||
expect(result[:locations]).to be_empty
|
|
||||||
expect(result[:total_locations]).to eq(0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with blank query' do
|
|
||||||
let(:search_params) { { query: '' } }
|
|
||||||
|
|
||||||
it 'returns empty result without calling services' do
|
|
||||||
expect(LocationSearch::GeocodingService).not_to receive(:new)
|
|
||||||
|
|
||||||
result = service.call
|
|
||||||
|
|
||||||
expect(result[:locations]).to be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with limit parameter' do
|
context 'with limit parameter' do
|
||||||
let(:search_params) { { query: 'Kaufland', limit: 10 } }
|
let(:search_params) { { latitude: 52.5200, longitude: 13.4050, limit: 10 } }
|
||||||
let(:many_visits) { Array.new(15) { |i| { timestamp: i, date: "2024-01-#{i+1}T12:00:00Z" } } }
|
let(:many_visits) { Array.new(15) { |i| { timestamp: i, date: "2024-01-#{i+1}T12:00:00Z" } } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(LocationSearch::GeocodingService).to receive(:new).and_return(
|
|
||||||
double('GeocodingService', search: [{ lat: 52.5200, lon: 13.4050, name: 'Test' }], provider_name: 'Test Provider')
|
|
||||||
)
|
|
||||||
|
|
||||||
allow_any_instance_of(LocationSearch::SpatialMatcher)
|
allow_any_instance_of(LocationSearch::SpatialMatcher)
|
||||||
.to receive(:find_points_near).and_return([{}])
|
.to receive(:find_points_near).and_return([{}])
|
||||||
|
|
||||||
|
|
@ -228,4 +113,53 @@ RSpec.describe LocationSearch::PointFinder do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when no matching points found' do
|
||||||
|
let(:search_params) { { latitude: 52.5200, longitude: 13.4050 } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow_any_instance_of(LocationSearch::SpatialMatcher)
|
||||||
|
.to receive(:find_points_near).and_return([])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'excludes locations with no visits' do
|
||||||
|
result = service.call
|
||||||
|
|
||||||
|
expect(result[:locations]).to be_empty
|
||||||
|
expect(result[:total_locations]).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when coordinates are missing' do
|
||||||
|
let(:search_params) { {} }
|
||||||
|
|
||||||
|
it 'returns empty result without calling services' do
|
||||||
|
expect(LocationSearch::SpatialMatcher).not_to receive(:new)
|
||||||
|
|
||||||
|
result = service.call
|
||||||
|
|
||||||
|
expect(result[:locations]).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when only latitude is provided' do
|
||||||
|
let(:search_params) { { latitude: 52.5200 } }
|
||||||
|
|
||||||
|
it 'returns empty result' do
|
||||||
|
result = service.call
|
||||||
|
|
||||||
|
expect(result[:locations]).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when only longitude is provided' do
|
||||||
|
let(:search_params) { { longitude: 13.4050 } }
|
||||||
|
|
||||||
|
it 'returns empty result' do
|
||||||
|
result = service.call
|
||||||
|
|
||||||
|
expect(result[:locations]).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
Loading…
Reference in a new issue